From 69eb7a1260ffffcdf6f1808ae0c8617f8558a5d8 Mon Sep 17 00:00:00 2001 From: ruben Date: Wed, 15 Apr 2020 00:10:19 +0200 Subject: [PATCH] feat(viewpatient): added labs tab to ViewPatient A patient can see all his requests for labs in the Labs tab fix #1972 --- src/__tests__/labs/Labs.test.tsx | 1 + .../labs/requests/NewLabRequest.test.tsx | 39 ++++++-- src/__tests__/patients/labs/LabsTab.test.tsx | 88 +++++++++++++++++++ .../patients/view/ViewPatient.test.tsx | 32 ++++++- src/clients/db/LabRepository.ts | 12 +++ src/labs/requests/NewLabRequest.tsx | 20 ++++- .../enUs/translations/patient/index.ts | 8 ++ src/model/Patient.ts | 2 + src/patients/labs/LabsTab.tsx | 66 ++++++++++++++ src/patients/view/ViewPatient.tsx | 9 ++ 10 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 src/__tests__/patients/labs/LabsTab.test.tsx create mode 100644 src/patients/labs/LabsTab.tsx diff --git a/src/__tests__/labs/Labs.test.tsx b/src/__tests__/labs/Labs.test.tsx index 82d13bf0c7..01a7ae3b8b 100644 --- a/src/__tests__/labs/Labs.test.tsx +++ b/src/__tests__/labs/Labs.test.tsx @@ -34,6 +34,7 @@ describe('Labs', () => { user: { permissions: [Permissions.RequestLab] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, + patient: '', }) const wrapper = mount( diff --git a/src/__tests__/labs/requests/NewLabRequest.test.tsx b/src/__tests__/labs/requests/NewLabRequest.test.tsx index 87575dbc8e..c227663bfe 100644 --- a/src/__tests__/labs/requests/NewLabRequest.test.tsx +++ b/src/__tests__/labs/requests/NewLabRequest.test.tsx @@ -25,7 +25,7 @@ describe('New Lab Request', () => { const history = createMemoryHistory() beforeEach(() => { - const store = mockStore({ title: '' }) + const store = mockStore({ title: '', patient: '' }) titleSpy = jest.spyOn(titleUtil, 'default') history.push('/labs/new') @@ -48,7 +48,7 @@ describe('New Lab Request', () => { const history = createMemoryHistory() beforeEach(() => { - const store = mockStore({ title: '' }) + const store = mockStore({ title: '', patient: '' }) history.push('/labs/new') wrapper = mount( @@ -112,7 +112,7 @@ describe('New Lab Request', () => { beforeEach(() => { history.push('/labs/new') - const store = mockStore({ title: '' }) + const store = mockStore({ title: '', patient: '' }) wrapper = mount( @@ -138,6 +138,7 @@ describe('New Lab Request', () => { let wrapper: ReactWrapper const history = createMemoryHistory() let labRepositorySaveSpy: any + let patientRepositorySaveOrUpdateSpy: any const expectedDate = new Date() const expectedLab = { patientId: '12345', @@ -147,18 +148,21 @@ describe('New Lab Request', () => { id: '1234', requestedOn: expectedDate.toISOString(), } as Lab + const patient = { id: expectedLab.patientId, fullName: 'some full name' } beforeEach(() => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) labRepositorySaveSpy = jest.spyOn(LabRepository, 'save').mockResolvedValue(expectedLab as Lab) - jest - .spyOn(PatientRepository, 'search') - .mockResolvedValue([{ id: expectedLab.patientId, fullName: 'some full name' }] as Patient[]) + patientRepositorySaveOrUpdateSpy = jest + .spyOn(PatientRepository, 'saveOrUpdate') + .mockResolvedValue(patient as Patient) + + jest.spyOn(PatientRepository, 'search').mockResolvedValue([patient] as Patient[]) history.push('/labs/new') - const store = mockStore({ title: '' }) + const store = mockStore({ title: '', patient: '' }) wrapper = mount( @@ -168,11 +172,11 @@ describe('New Lab Request', () => { ) }) - it('should save the lab request and navigate to "/labs/:id"', async () => { + it('should save the lab request link new lab to patient and navigate to "/labs/:id"', async () => { const patientTypeahead = wrapper.find(Typeahead) await act(async () => { const onChange = patientTypeahead.prop('onChange') - await onChange([{ id: expectedLab.patientId }] as Patient[]) + await onChange([patient] as Patient[]) }) const typeInput = wrapper.find(TextInputWithLabelFormGroup) @@ -204,6 +208,23 @@ describe('New Lab Request', () => { requestedOn: expectedDate.toISOString(), }), ) + + expect(patientRepositorySaveOrUpdateSpy).toHaveBeenCalledTimes(1) + expect(patientRepositorySaveOrUpdateSpy).toHaveBeenCalledWith( + expect.objectContaining({ + id: expectedLab.patientId, + fullName: patient.fullName, + labs: [ + { + patientId: '12345', + type: 'expected type', + status: 'requested', + notes: 'expected notes', + requestedOn: expectedLab.requestedOn, + }, + ], + }), + ) expect(history.location.pathname).toEqual(`/labs/${expectedLab.id}`) }) diff --git a/src/__tests__/patients/labs/LabsTab.test.tsx b/src/__tests__/patients/labs/LabsTab.test.tsx new file mode 100644 index 0000000000..69544b83ad --- /dev/null +++ b/src/__tests__/patients/labs/LabsTab.test.tsx @@ -0,0 +1,88 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { Router } from 'react-router' +import { Provider } from 'react-redux' +import * as components from '@hospitalrun/components' +import format from 'date-fns/format' +import { act } from 'react-dom/test-utils' +import LabsTab from '../../../patients/labs/LabsTab' +import Patient from '../../../model/Patient' +import Lab from '../../../model/Lab' +import Permissions from '../../../model/Permissions' +import LabRepository from '../../../clients/db/LabRepository' + +const expectedPatient = { + id: '123', + labs: [ + { + patientId: '123', + type: 'type', + status: 'requested', + requestedOn: new Date().toISOString(), + } as Lab, + ], +} as Patient + +const mockStore = configureMockStore([thunk]) +const history = createMemoryHistory() + +let user: any +let store: any + +const setup = (patient = expectedPatient, permissions = [Permissions.WritePatients]) => { + user = { permissions } + store = mockStore({ patient, user }) + jest.spyOn(LabRepository, 'findLabsByPatientId').mockResolvedValue(expectedPatient.labs as Lab[]) + const wrapper = mount( + + + + + , + ) + + return wrapper +} + +describe('Labs Tab', () => { + it('should list the patients labs', async () => { + const expectedLabs = expectedPatient.labs as Lab[] + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + wrapper.update() + + const table = wrapper.find('table') + const tableHeader = wrapper.find('thead') + const tableHeaders = wrapper.find('th') + const tableBody = wrapper.find('tbody') + const tableData = wrapper.find('td') + + expect(table).toHaveLength(1) + expect(tableHeader).toHaveLength(1) + expect(tableBody).toHaveLength(1) + expect(tableHeaders.at(0).text()).toEqual('labs.lab.type') + expect(tableHeaders.at(1).text()).toEqual('labs.lab.requestedOn') + expect(tableHeaders.at(2).text()).toEqual('labs.lab.status') + expect(tableData.at(0).text()).toEqual(expectedLabs[0].type) + expect(tableData.at(1).text()).toEqual( + format(new Date(expectedLabs[0].requestedOn), 'yyyy-MM-dd hh:mm a'), + ) + expect(tableData.at(2).text()).toEqual(expectedLabs[0].status) + }) + + it('should render a warning message if the patient does not have any labs', () => { + const wrapper = setup({ ...expectedPatient, labs: [] }) + + const alert = wrapper.find(components.Alert) + + expect(alert).toHaveLength(1) + expect(alert.prop('title')).toEqual('patient.labs.warning.noLabs') + expect(alert.prop('message')).toEqual('patient.labs.noLabsMessage') + }) +}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index e24c07fcdf..1b39b91b0b 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -21,6 +21,8 @@ import * as titleUtil from '../../../page-header/useTitle' import ViewPatient from '../../../patients/view/ViewPatient' import * as patientSlice from '../../../patients/patient-slice' import Permissions from '../../../model/Permissions' +import LabsTab from '../../../patients/labs/LabsTab' +import Lab from '../../../model/Lab' const mockStore = configureMockStore([thunk]) @@ -49,11 +51,12 @@ describe('ViewPatient', () => { jest.spyOn(PatientRepository, 'find') const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.find.mockResolvedValue(patient) - + const labs: Lab[] = [] history = createMemoryHistory() store = mockStore({ patient: { patient }, user: { permissions }, + labs: { labs }, }) history.push('/patients/123') @@ -127,13 +130,14 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(6) + expect(tabs).toHaveLength(7) expect(tabs.at(0).prop('label')).toEqual('patient.generalInformation') expect(tabs.at(1).prop('label')).toEqual('patient.relatedPersons.label') expect(tabs.at(2).prop('label')).toEqual('scheduling.appointments.label') expect(tabs.at(3).prop('label')).toEqual('patient.allergies.label') expect(tabs.at(4).prop('label')).toEqual('patient.diagnoses.label') expect(tabs.at(5).prop('label')).toEqual('patient.notes.label') + expect(tabs.at(6).prop('label')).toEqual('patient.labs.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -262,4 +266,28 @@ describe('ViewPatient', () => { expect(notesTab).toHaveLength(1) expect(notesTab.prop('patient')).toEqual(patient) }) + + it('should mark the labs tab as active when it is clicked and render the lab component when route is /patients/:id/labs', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + await act(async () => { + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + tabs.at(6).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const labsTab = wrapper.find(LabsTab) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/labs`) + expect(tabs.at(6).prop('active')).toBeTruthy() + expect(labsTab).toHaveLength(1) + expect(labsTab.prop('patientId')).toEqual(patient.id) + }) }) diff --git a/src/clients/db/LabRepository.ts b/src/clients/db/LabRepository.ts index 6ffbfdd6d3..bd89d04c29 100644 --- a/src/clients/db/LabRepository.ts +++ b/src/clients/db/LabRepository.ts @@ -9,6 +9,18 @@ export class LabRepository extends Repository { index: { fields: ['requestedOn'] }, }) } + + async findLabsByPatientId(patientId: string): Promise { + return super.search({ + selector: { + $and: [ + { + patientId, + }, + ], + }, + }) + } } export default new LabRepository() diff --git a/src/labs/requests/NewLabRequest.tsx b/src/labs/requests/NewLabRequest.tsx index affd974c73..900a6233dd 100644 --- a/src/labs/requests/NewLabRequest.tsx +++ b/src/labs/requests/NewLabRequest.tsx @@ -10,16 +10,19 @@ import LabRepository from 'clients/db/LabRepository' import Lab from 'model/Lab' import TextFieldWithLabelFormGroup from 'components/input/TextFieldWithLabelFormGroup' import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' +import { useDispatch } from 'react-redux' +import { updatePatient } from '../../patients/patient-slice' const NewLabRequest = () => { const { t } = useTranslation() const history = useHistory() + const dispatch = useDispatch() useTitle(t('labs.requests.new')) const [isPatientInvalid, setIsPatientInvalid] = useState(false) const [isTypeInvalid, setIsTypeInvalid] = useState(false) const [typeFeedback, setTypeFeedback] = useState() - + const [searchedPatient, setSearchedPatient] = useState({} as Patient) const [newLabRequest, setNewLabRequest] = useState({ patientId: '', type: '', @@ -40,6 +43,7 @@ const NewLabRequest = () => { ...previousNewLabRequest, patientId: patient.id, })) + setSearchedPatient(patient) } const onLabTypeChange = (event: React.ChangeEvent) => { @@ -74,7 +78,19 @@ const NewLabRequest = () => { newLab.requestedOn = new Date(Date.now().valueOf()).toISOString() const createdLab = await LabRepository.save(newLab) - history.push(`/labs/${createdLab.id}`) + + const newLabs: Lab[] = [] + if (searchedPatient.labs) { + newLabs.push(...searchedPatient.labs) + } + newLabs.push(newLab) + + const patientToUpdate = { + ...searchedPatient, + labs: newLabs, + } + + dispatch(updatePatient(patientToUpdate, () => history.push(`/labs/${createdLab.id}`))) } const onCancel = () => { history.push('/labs') diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index cbdce7cb5b..25c0c9de8f 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -84,6 +84,14 @@ export default { }, addNoteAbove: 'Add a note using the button above.', }, + labs: { + label: 'Labs', + new: 'Add New Lab', + warning: { + noLabs: 'No Labs', + }, + noLabsMessage: 'No labs requests for this person.', + }, types: { charity: 'Charity', private: 'Private', diff --git a/src/model/Patient.ts b/src/model/Patient.ts index 27fde51a46..784eac3af9 100644 --- a/src/model/Patient.ts +++ b/src/model/Patient.ts @@ -5,6 +5,7 @@ import RelatedPerson from './RelatedPerson' import Allergy from './Allergy' import Diagnosis from './Diagnosis' import Note from './Note' +import Lab from './Lab' export default interface Patient extends AbstractDBModel, Name, ContactInformation { sex: string @@ -18,4 +19,5 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati allergies?: Allergy[] diagnoses?: Diagnosis[] notes?: Note[] + labs?: Lab[] } diff --git a/src/patients/labs/LabsTab.tsx b/src/patients/labs/LabsTab.tsx new file mode 100644 index 0000000000..c71e6ef83e --- /dev/null +++ b/src/patients/labs/LabsTab.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from 'react' +import { Alert } from '@hospitalrun/components' +import { useTranslation } from 'react-i18next' +import format from 'date-fns/format' +import { useHistory } from 'react-router' +import Lab from '../../model/Lab' +import LabRepository from '../../clients/db/LabRepository' + +interface Props { + patientId: string +} + +const LabsTab = (props: Props) => { + const history = useHistory() + const { patientId } = props + const { t } = useTranslation() + + const [labs, setLabs] = useState([]) + + useEffect(() => { + const fetch = async () => { + const fetchedLabs = await LabRepository.findLabsByPatientId(patientId) + setLabs(fetchedLabs) + } + + fetch() + }, [patientId]) + + const onTableRowClick = (lab: Lab) => { + history.push(`/labs/${lab.id}`) + } + + return ( +
+ {(!labs || labs.length === 0) && ( + + )} + {labs && labs.length > 0 && ( + + + + + + + + + + {labs.map((lab) => ( + onTableRowClick(lab)} key={lab.id}> + + + + + ))} + +
{t('labs.lab.type')}{t('labs.lab.requestedOn')}{t('labs.lab.status')}
{lab.type}{format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')}{lab.status}
+ )} +
+ ) +} + +export default LabsTab diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 27703a5e28..8f5ce3029b 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -18,6 +18,7 @@ import RelatedPerson from '../related-persons/RelatedPersonTab' import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' import AppointmentsList from '../appointments/AppointmentsList' import Note from '../notes/NoteTab' +import Labs from '../labs/LabsTab' const getPatientCode = (p: Patient): string => { if (p) { @@ -113,6 +114,11 @@ const ViewPatient = () => { label={t('patient.notes.label')} onClick={() => history.push(`/patients/${patient.id}/notes`)} /> + history.push(`/patients/${patient.id}/labs`)} + /> @@ -133,6 +139,9 @@ const ViewPatient = () => { + + + )