From 0baf3af504c1cc0b9512c1d1be4d0b1eeb268f49 Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Wed, 26 Aug 2020 10:24:28 -0400 Subject: [PATCH 1/3] feat(patient): add panel to display useful patient information fix #2259 --- .../view/ImportantPatientInfo.test.tsx | 314 ++++++++++++++++++ .../patients/view/ViewPatient.test.tsx | 4 +- src/patients/care-plans/CarePlanTable.tsx | 2 +- src/patients/view/ImportantPatientInfo.tsx | 247 ++++++++++++++ src/patients/view/ViewPatient.tsx | 168 +++++----- .../enUs/translations/patient/index.ts | 1 + 6 files changed, 646 insertions(+), 90 deletions(-) create mode 100644 src/__tests__/patients/view/ImportantPatientInfo.test.tsx create mode 100644 src/patients/view/ImportantPatientInfo.tsx diff --git a/src/__tests__/patients/view/ImportantPatientInfo.test.tsx b/src/__tests__/patients/view/ImportantPatientInfo.test.tsx new file mode 100644 index 0000000000..f176e0ff9f --- /dev/null +++ b/src/__tests__/patients/view/ImportantPatientInfo.test.tsx @@ -0,0 +1,314 @@ +import * as components from '@hospitalrun/components' +// import { act } from '@testing-library/react' +import format from 'date-fns/format' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +// import { startOfDay, subYears } from 'date-fns' +import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +// import Diagnoses from '../../../patients/diagnoses/Diagnoses' +import NewAllergyModal from '../../../patients/allergies/NewAllergyModal' +import AddDiagnosisModal from '../../../patients/diagnoses/AddDiagnosisModal' +import ImportantPatientInfo from '../../../patients/view/ImportantPatientInfo' +import AddVisitModal from '../../../patients/visits/AddVisitModal' +import PatientRepository from '../../../shared/db/PatientRepository' +import CarePlan from '../../../shared/model/CarePlan' +import Diagnosis from '../../../shared/model/Diagnosis' +// import Allergies from '../../../patients/allergies/Allergies' +// import AllergiesList from '../../../patients/allergies/AllergiesList' +// import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' +// import * as getPatientName from '../../../patients/util/patient-name-util' +// import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' + +const mockStore = createMockStore([thunk]) + +describe('Important Patient Info Panel', () => { + let history: any + let user: any + let store: any + + const expectedPatient = { + id: '123', + sex: 'male', + fullName: 'full Name', + code: 'P-123', + dateOfBirth: format(new Date(), 'MM/dd/yyyy'), + diagnoses: [ + { id: '123', name: 'diagnosis1', diagnosisDate: new Date().toISOString() } as Diagnosis, + ], + allergies: [ + { id: '1', name: 'allergy1' }, + { id: '2', name: 'allergy2' }, + ], + carePlans: [ + { + id: '123', + title: 'title1', + description: 'test', + diagnosisId: '12345', + status: 'status' as string, + intent: 'intent' as string, + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + createdOn: new Date().toISOString(), + note: 'note', + } as CarePlan, + ], + } as Patient + + const setup = async (patient = expectedPatient, permissions: Permissions[]) => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + history = createMemoryHistory() + user = { permissions } + store = mockStore({ patient, user } as any) + + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + return wrapper + } + + describe("patient's full name, patient's code, sex, and date of birth", () => { + it("should render patient's full name", async () => { + const wrapper = await setup(expectedPatient, []) + const code = wrapper.find('.col-2') + expect(code.at(0).text()).toEqual(expectedPatient.fullName) + }) + + it("should render patient's code", async () => { + const wrapper = await setup(expectedPatient, []) + const code = wrapper.find('.col-2') + expect(code.at(1).text()).toEqual(`patient.code${expectedPatient.code}`) + }) + + it("should render patient's sex", async () => { + const wrapper = await setup(expectedPatient, []) + const sex = wrapper.find('.patient-sex') + expect(sex.text()).toEqual(`patient.sex${expectedPatient.sex}`) + }) + + it("should render patient's dateOfDate", async () => { + const wrapper = await setup(expectedPatient, []) + const sex = wrapper.find('.patient-dateOfBirth') + expect(sex.text()).toEqual(`patient.dateOfBirth${expectedPatient.dateOfBirth}`) + }) + }) + + describe('add new visit button', () => { + it('should render an add visit button if user has correct permissions', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddVisit]) + + const addNewButton = wrapper.find(components.Button) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.visits.new') + }) + + it('should open the add visit modal on click', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddVisit]) + + act(() => { + const addNewButton = wrapper.find(components.Button) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + const modal = wrapper.find(AddVisitModal) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should close the modal when the close button is clicked', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddVisit]) + + act(() => { + const addNewButton = wrapper.find(components.Button) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + act(() => { + const modal = wrapper.find(AddVisitModal) + const onClose = modal.prop('onCloseButtonClick') as any + onClose() + }) + wrapper.update() + + expect(wrapper.find(AddVisitModal).prop('show')).toBeFalsy() + }) + + it('should not render new visit button if user does not have permissions', async () => { + const wrapper = await setup(expectedPatient, []) + + expect(wrapper.find(components.Button)).toHaveLength(0) + }) + }) + + describe('add new allergy button', () => { + it('should render an add allergy button if user has correct permissions', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddAllergy]) + + const addNewButton = wrapper.find(components.Button) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.allergies.new') + }) + + it('should open the add allergy modal on click', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddAllergy]) + + act(() => { + const addNewButton = wrapper.find(components.Button) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + const modal = wrapper.find(NewAllergyModal) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should close the modal when the close button is clicked', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddAllergy]) + + act(() => { + const addNewButton = wrapper.find(components.Button) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + act(() => { + const modal = wrapper.find(NewAllergyModal) + const onClose = modal.prop('onCloseButtonClick') as any + onClose() + }) + wrapper.update() + + expect(wrapper.find(NewAllergyModal).prop('show')).toBeFalsy() + }) + + it('should not render new allergy button if user does not have permissions', async () => { + const wrapper = await setup(expectedPatient, []) + + expect(wrapper.find(components.Button)).toHaveLength(0) + }) + }) + + describe('add diagnoses button', () => { + it('should render an add diagnosis button if user has correct permissions', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddDiagnosis]) + + const addNewButton = wrapper.find(components.Button) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.diagnoses.new') + }) + + it('should open the add diagnosis modal on click', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddDiagnosis]) + + act(() => { + const addNewButton = wrapper.find(components.Button) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + const modal = wrapper.find(AddDiagnosisModal) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should close the modal when the close button is clicked', async () => { + const wrapper = await setup(expectedPatient, [Permissions.AddDiagnosis]) + + act(() => { + const addNewButton = wrapper.find(components.Button) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + act(() => { + const modal = wrapper.find(AddDiagnosisModal) + const onClose = modal.prop('onCloseButtonClick') as any + onClose() + }) + wrapper.update() + + expect(wrapper.find(AddDiagnosisModal).prop('show')).toBeFalsy() + }) + + it('should not render new diagnosis button if user does not have permissions', async () => { + const wrapper = await setup(expectedPatient, []) + + expect(wrapper.find(components.Button)).toHaveLength(0) + }) + }) + + // describe('add new care plan button', () => { + // it('should render an add diagnosis button if user has correct permissions', async () => { + // const wrapper = await setup(expectedPatient, [Permissions.AddCarePlan]) + + // const addNewButton = wrapper.find(components.Button) + // expect(addNewButton).toHaveLength(1) + // expect(addNewButton.text().trim()).toEqual('patient.carePlan.new') + // }) + + // it('should open the add care plan modal on click', async () => { + // const wrapper = await setup(expectedPatient, [Permissions.AddCarePlan]) + + // act(() => { + // const addNewButton = wrapper.find(components.Button) + // const onClick = addNewButton.prop('onClick') as any + // onClick() + // }) + // wrapper.update() + + // const modal = wrapper.find(AddCarePlanModal) + // expect(modal.prop('show')).toBeTruthy() + // }) + + // it('should close the modal when the close button is clicked', async () => { + // const wrapper = await setup(expectedPatient, [Permissions.AddCarePlan]) + + // act(() => { + // const addNewButton = wrapper.find(components.Button) + // const onClick = addNewButton.prop('onClick') as any + // onClick() + // }) + // wrapper.update() + + // act(() => { + // const modal = wrapper.find(AddCarePlanModal) + // const onClose = modal.prop('onCloseButtonClick') as any + // onClose() + // }) + // wrapper.update() + + // expect(wrapper.find(AddCarePlanModal).prop('show')).toBeFalsy() + // }) + + // it('should not render new care plan button if user does not have permissions', async () => { + // const wrapper = await setup(expectedPatient, []) + + // expect(wrapper.find(components.Button)).toHaveLength(0) + // }) + // }) +}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 7ce8bf4bf1..39aa5d5e87 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -96,9 +96,7 @@ describe('ViewPatient', () => { await setup() - expect(titleUtil.default).toHaveBeenCalledWith( - `${patient.givenName} ${patient.familyName} ${patient.suffix} (${patient.code})`, - ) + expect(titleUtil.default).toHaveBeenCalledWith(`patient.label`) }) it('should add a "Edit Patient" button to the button tool bar if has WritePatients permissions', async () => { diff --git a/src/patients/care-plans/CarePlanTable.tsx b/src/patients/care-plans/CarePlanTable.tsx index a606af73d6..387961709e 100644 --- a/src/patients/care-plans/CarePlanTable.tsx +++ b/src/patients/care-plans/CarePlanTable.tsx @@ -52,7 +52,7 @@ const CarePlanTable = (props: Props) => { actionsHeaderText={t('actions.label')} actions={[ { - label: 'actions.view', + label: t('actions.view'), action: (row) => history.push(`/patients/${patientId}/care-plans/${row.id}`), }, ]} diff --git a/src/patients/view/ImportantPatientInfo.tsx b/src/patients/view/ImportantPatientInfo.tsx new file mode 100644 index 0000000000..dbee5eb150 --- /dev/null +++ b/src/patients/view/ImportantPatientInfo.tsx @@ -0,0 +1,247 @@ +import { Panel, Container, Row, Table, Button, Typography } from '@hospitalrun/components' +import format from 'date-fns/format' +import React, { CSSProperties, useState } from 'react' +import { useSelector } from 'react-redux' +import { Link, useHistory } from 'react-router-dom' + +import useTranslator from '../../shared/hooks/useTranslator' +import Allergy from '../../shared/model/Allergy' +import Diagnosis from '../../shared/model/Diagnosis' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import NewAllergyModal from '../allergies/NewAllergyModal' +import AddCarePlanModal from '../care-plans/AddCarePlanModal' +import AddDiagnosisModal from '../diagnoses/AddDiagnosisModal' +import AddVisitModal from '../visits/AddVisitModal' +// import {getPatientFullName} from '../util/patient-name-util' + +interface Props { + patient: Patient +} + +const getPatientCode = (p: Patient): string => { + if (p) { + return p.code + } + + return '' +} + +const ImportantPatientInfo = (props: Props) => { + const { patient } = props + const { t } = useTranslator() + const history = useHistory() + const { permissions } = useSelector((state: RootState) => state.user) + const [showNewAllergyModal, setShowNewAllergyModal] = useState(false) + const [showDiagnosisModal, setShowDiagnosisModal] = useState(false) + const [showAddCarePlanModal, setShowAddCarePlanModal] = useState(false) + const [showAddVisitModal, setShowAddVisitModal] = useState(false) + + const patientCodeStyle: CSSProperties = { + position: 'relative', + color: 'black', + backgroundColor: 'rgba(245,245,245,1)', + fontSize: 'small', + textAlign: 'center', + } + + const allergiesSectionStyle: CSSProperties = { + position: 'relative', + color: 'black', + backgroundColor: 'rgba(245,245,245,1)', + fontSize: 'small', + } + + const tableStyle: CSSProperties = { + position: 'relative', + marginLeft: '2px', + marginRight: '2px', + fontSize: 'small', + } + + const addAllergyButtonStyle: CSSProperties = { + fontSize: 'small', + position: 'absolute', + top: '0px', + right: '0px', + } + + return ( +
+ + + +
+

{patient.fullName}

+
+
+
+ {t('patient.code')} +
{getPatientCode(patient)}
+
+
+
+ {permissions.includes(Permissions.AddVisit) && ( + + )} +
+
+ +
+
+ {t('patient.sex')} +
{patient.sex}
+
+
+ {t('patient.dateOfBirth')} +
+ {patient.dateOfBirth + ? format(new Date(patient.dateOfBirth), 'MM/dd/yyyy') + : t('patient.unknownDateOfBirth')} +
+
+ {/* + Sex + + {patient.sex} + + DateOfBirth + + + {patient.dateOfBirth + ? format(new Date(patient.dateOfBirth), 'MM/dd/yyyy') + : 'Unknown'} + */} +
+ +
+ {t('patient.allergies.label')} + {patient.allergies ? ( + patient.allergies?.map((a: Allergy) => ( +
  • + {a.name} +
  • + )) + ) : ( + <> + )} + {permissions.includes(Permissions.AddAllergy) && ( + + )} +
    +
    + +
    + {t('patient.diagnoses.label')} +
    + row.id} + columns={[ + { label: t('patient.diagnoses.diagnosisName'), key: 'name' }, + { + label: t('patient.diagnoses.diagnosisDate'), + key: 'diagnosisDate', + formatter: (row) => + row.diagnosisDate + ? format(new Date(row.diagnosisDate), 'yyyy-MM-dd hh:mm a') + : '', + }, + ]} + data={patient.diagnoses ? (patient.diagnoses as Diagnosis[]) : []} + /> + + {permissions.includes(Permissions.AddDiagnosis) && ( + + )} + +
    + {t('patient.carePlan.label')} +
    + {/* */} +
    history.push(`/patients/${patient.id}/care-plans/${row.id}`)} + getID={(row) => row.id} + data={patient.carePlans || []} + columns={[ + { label: t('patient.carePlan.title'), key: 'title' }, + { + label: t('patient.carePlan.startDate'), + key: 'startDate', + formatter: (row) => format(new Date(row.startDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.carePlan.endDate'), + key: 'endDate', + formatter: (row) => format(new Date(row.endDate), 'yyyy-MM-dd'), + }, + { label: t('patient.carePlan.status'), key: 'status' }, + ]} + /> + + {permissions.includes(Permissions.AddCarePlan) && ( + + )} + + + + + + setShowNewAllergyModal(false)} + patientId={patient.id} + /> + + setShowDiagnosisModal(false)} + /> + + setShowAddCarePlanModal(false)} + patient={patient} + /> + + setShowAddVisitModal(false)} + /> +
    + + ) +} + +export default ImportantPatientInfo diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index fc9adf6761..8fef173cce 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -14,7 +14,6 @@ import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' import useTitle from '../../page-header/title/useTitle' import useTranslator from '../../shared/hooks/useTranslator' -import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import Allergies from '../allergies/Allergies' @@ -28,14 +27,7 @@ import { fetchPatient } from '../patient-slice' import RelatedPerson from '../related-persons/RelatedPersonTab' import { getPatientFullName } from '../util/patient-name-util' import VisitTab from '../visits/VisitTab' - -const getPatientCode = (p: Patient): string => { - if (p) { - return p.code - } - - return '' -} +import ImportantPatientInfo from './ImportantPatientInfo' const ViewPatient = () => { const { t } = useTranslator() @@ -47,7 +39,7 @@ const ViewPatient = () => { const { patient, status } = useSelector((state: RootState) => state.patient) const { permissions } = useSelector((state: RootState) => state.user) - useTitle(`${getPatientFullName(patient)} (${getPatientCode(patient)})`) + useTitle(t('patient.label')) const setButtonToolBar = useButtonToolbarSetter() @@ -93,82 +85,86 @@ const ViewPatient = () => { return (
    - - history.push(`/patients/${patient.id}`)} - /> - history.push(`/patients/${patient.id}/relatedpersons`)} - /> - history.push(`/patients/${patient.id}/appointments`)} - /> - history.push(`/patients/${patient.id}/allergies`)} - /> - history.push(`/patients/${patient.id}/diagnoses`)} - /> - history.push(`/patients/${patient.id}/notes`)} - /> - history.push(`/patients/${patient.id}/labs`)} - /> - history.push(`/patients/${patient.id}/care-plans`)} - /> - history.push(`/patients/${patient.id}/visits`)} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {' '} + +
    + + history.push(`/patients/${patient.id}`)} + /> + history.push(`/patients/${patient.id}/relatedpersons`)} + /> + history.push(`/patients/${patient.id}/appointments`)} + /> + history.push(`/patients/${patient.id}/allergies`)} + /> + history.push(`/patients/${patient.id}/diagnoses`)} + /> + history.push(`/patients/${patient.id}/notes`)} + /> + history.push(`/patients/${patient.id}/labs`)} + /> + history.push(`/patients/${patient.id}/care-plans`)} + /> + history.push(`/patients/${patient.id}/visits`)} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ) } diff --git a/src/shared/locales/enUs/translations/patient/index.ts b/src/shared/locales/enUs/translations/patient/index.ts index 9c40cd1b00..82b8c071a0 100644 --- a/src/shared/locales/enUs/translations/patient/index.ts +++ b/src/shared/locales/enUs/translations/patient/index.ts @@ -1,5 +1,6 @@ export default { patient: { + label: 'Patient', code: 'Patient Code', firstName: 'First Name', lastName: 'Last Name', From 4b234a1968802e45038197f1e765020e9ca15bff Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Mon, 28 Sep 2020 19:31:58 -0400 Subject: [PATCH 2/3] feat(patient): refactor add allergy button fix #2259 --- src/patients/view/ImportantPatientInfo.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/patients/view/ImportantPatientInfo.tsx b/src/patients/view/ImportantPatientInfo.tsx index 95a4625b10..fe89793f50 100644 --- a/src/patients/view/ImportantPatientInfo.tsx +++ b/src/patients/view/ImportantPatientInfo.tsx @@ -50,6 +50,7 @@ const ImportantPatientInfo = (props: Props) => { color: 'black', backgroundColor: 'rgba(245,245,245,1)', fontSize: 'small', + padding: '10px', } const tableStyle: CSSProperties = { @@ -61,9 +62,9 @@ const ImportantPatientInfo = (props: Props) => { const addAllergyButtonStyle: CSSProperties = { fontSize: 'small', - position: 'absolute', - top: '0px', - right: '0px', + position: 'relative', + top: '5px', + bottom: '5px', } return ( @@ -138,6 +139,7 @@ const ImportantPatientInfo = (props: Props) => { {t('patient.diagnoses.label')}
    history.push(`/patients/${patient.id}/diagnoses`)} getID={(row) => row.id} columns={[ { label: t('patient.diagnoses.diagnosisName'), key: 'name' }, @@ -149,6 +151,7 @@ const ImportantPatientInfo = (props: Props) => { ? format(new Date(row.diagnosisDate), 'yyyy-MM-dd hh:mm a') : '', }, + { label: t('patient.diagnoses.status'), key: 'status' }, ]} data={patient.diagnoses ? (patient.diagnoses as Diagnosis[]) : []} /> From eba6074adb0a52be03cc5e4b6c2945f4d7d24f9c Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Mon, 28 Sep 2020 19:41:26 -0400 Subject: [PATCH 3/3] feat(patient): remove unused function fix #2259 --- src/patients/view/ViewPatient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 927b10a52f..cc905aa827 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -26,7 +26,7 @@ import usePatient from '../hooks/usePatient' import Labs from '../labs/Labs' import Note from '../notes/NoteTab' import RelatedPerson from '../related-persons/RelatedPersonTab' -import { getPatientCode, getPatientFullName } from '../util/patient-util' +import { getPatientFullName } from '../util/patient-util' import VisitTab from '../visits/VisitTab' import ImportantPatientInfo from './ImportantPatientInfo'