diff --git a/package.json b/package.json index 105ca34417..5a14ebd333 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dependencies": { "@hospitalrun/components": "^1.4.0", "@reduxjs/toolkit": "~1.3.0", + "@types/escape-string-regexp": "~2.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.4.1", "date-fns": "~2.12.0", diff --git a/src/__tests__/patients/labs/LabsTab.test.tsx b/src/__tests__/patients/labs/LabsTab.test.tsx new file mode 100644 index 0000000000..cc5f5c0518 --- /dev/null +++ b/src/__tests__/patients/labs/LabsTab.test.tsx @@ -0,0 +1,94 @@ +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', +} as Patient + +const labs = [ + { + id: 'labId', + patientId: '123', + type: 'type', + status: 'requested', + requestedOn: new Date().toISOString(), + } as Lab, +] + +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, 'findAllByPatientId').mockResolvedValue(labs) + const wrapper = mount( + + + + + , + ) + + return wrapper +} + +describe('Labs Tab', () => { + it('should list the patients labs', async () => { + const expectedLabs = labs + 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', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup({ ...expectedPatient }) + }) + + 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..3080e73384 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 LabRepository from '../../../clients/db/LabRepository' const mockStore = configureMockStore([thunk]) @@ -47,9 +49,9 @@ describe('ViewPatient', () => { const setup = (permissions = [Permissions.ReadPatients]) => { jest.spyOn(PatientRepository, 'find') + jest.spyOn(LabRepository, 'findAllByPatientId').mockResolvedValue([]) const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.find.mockResolvedValue(patient) - history = createMemoryHistory() store = mockStore({ patient: { patient }, @@ -127,13 +129,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 +265,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/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx index 4b502dcd41..7d79ec1b67 100644 --- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx @@ -18,6 +18,8 @@ import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm import * as components from '@hospitalrun/components' import * as titleUtil from '../../../../page-header/useTitle' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' +import LabRepository from '../../../../clients/db/LabRepository' +import Lab from '../../../../model/Lab' const mockStore = configureMockStore([thunk]) const mockedComponents = mocked(components, true) @@ -32,6 +34,7 @@ describe('New Appointment', () => { mocked(AppointmentRepository, true).save.mockResolvedValue( expectedNewAppointment as Appointment, ) + jest.spyOn(LabRepository, 'findAllByPatientId').mockResolvedValue([] as Lab[]) history = createMemoryHistory() store = mockStore({ diff --git a/src/clients/db/LabRepository.ts b/src/clients/db/LabRepository.ts index 6ffbfdd6d3..9ddfa74df0 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 findAllByPatientId(patientId: string): Promise { + return super.search({ + selector: { + $and: [ + { + patientId, + }, + ], + }, + }) + } } export default new LabRepository() diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index c404f9e229..5bbbedc04d 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -85,6 +85,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/patients/labs/LabsTab.tsx b/src/patients/labs/LabsTab.tsx new file mode 100644 index 0000000000..6b40d81353 --- /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.findAllByPatientId(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 fab29a157c..6119142e0b 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 = () => { + + + )