From e1ce6c95451116e966bee58f7c240c4952a84a32 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 1 Mar 2020 21:11:37 -0600 Subject: [PATCH] feat(diagnoses): adds ability to add a diagnosis --- .../patients/allergies/Allergies.test.tsx | 3 +- .../diagnoses/AddDiagnosisModal.test.tsx | 95 +++++++++++++ .../patients/diagnoses/Diagnoses.test.tsx | 129 ++++++++++++++++++ src/locales/en-US/translation.json | 14 +- src/model/Diagnosis.ts | 5 + src/model/Patient.ts | 2 + src/model/Permissions.ts | 1 + src/patients/diagnoses/AddDiagnosisModal.tsx | 106 ++++++++++++++ src/patients/diagnoses/Diagnoses.tsx | 73 +++++++++- src/user/user-slice.ts | 1 + 10 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx create mode 100644 src/__tests__/patients/diagnoses/Diagnoses.test.tsx create mode 100644 src/model/Diagnosis.ts create mode 100644 src/patients/diagnoses/AddDiagnosisModal.tsx diff --git a/src/__tests__/patients/allergies/Allergies.test.tsx b/src/__tests__/patients/allergies/Allergies.test.tsx index c2d0ec7392..f3ed5e39e0 100644 --- a/src/__tests__/patients/allergies/Allergies.test.tsx +++ b/src/__tests__/patients/allergies/Allergies.test.tsx @@ -9,7 +9,6 @@ import thunk from 'redux-thunk' import { Router } from 'react-router' import { Provider } from 'react-redux' import Patient from 'model/Patient' -import User from 'model/User' import { Button, Modal, List, ListItem, Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mocked } from 'ts-jest/utils' @@ -33,7 +32,7 @@ let user: any let store: any const setup = (patient = expectedPatient, permissions = [Permissions.AddAllergy]) => { - user = { permissions } as User + user = { permissions } store = mockStore({ patient, user }) const wrapper = mount( diff --git a/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx new file mode 100644 index 0000000000..fb10a3970b --- /dev/null +++ b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx @@ -0,0 +1,95 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { shallow, mount } from 'enzyme' +import { Modal, Alert } from '@hospitalrun/components' +import { act } from '@testing-library/react' +import AddDiagnosisModal from 'patients/diagnoses/AddDiagnosisModal' +import Diagnosis from 'model/Diagnosis' + +describe('Add Diagnosis Modal', () => { + it('should render a modal with the correct labels', () => { + const wrapper = shallow( + , + ) + + const modal = wrapper.find(Modal) + expect(modal).toHaveLength(1) + expect(modal.prop('title')).toEqual('patient.diagnoses.new') + expect(modal.prop('closeButton')?.children).toEqual('actions.cancel') + expect(modal.prop('closeButton')?.color).toEqual('danger') + expect(modal.prop('successButton')?.children).toEqual('patient.diagnoses.new') + expect(modal.prop('successButton')?.color).toEqual('success') + expect(modal.prop('successButton')?.icon).toEqual('add') + }) + + describe('cancel', () => { + it('should call the onCloseButtonClick function when the close button is clicked', () => { + const onCloseButtonClickSpy = jest.fn() + const wrapper = shallow( + , + ) + + act(() => { + const modal = wrapper.find(Modal) + const { onClick } = modal.prop('closeButton') as any + onClick() + }) + + expect(onCloseButtonClickSpy).toHaveBeenCalledTimes(1) + }) + }) + + describe('save', () => { + it('should call the onSave function with the correct data when the save button is clicked', () => { + const expectedName = 'expected name' + const expectedDate = new Date() + const onSaveSpy = jest.fn() + const wrapper = mount( + , + ) + + act(() => { + const input = wrapper.findWhere((c: any) => c.prop('name') === 'name') + const onChange = input.prop('onChange') + onChange({ target: { value: expectedName } }) + }) + wrapper.update() + + act(() => { + const input = wrapper.findWhere((c: any) => c.prop('name') === 'diagnosisDate') + const onChange = input.prop('onChange') + onChange(expectedDate) + }) + wrapper.update() + + act(() => { + const modal = wrapper.find(Modal) + const onSave = (modal.prop('successButton') as any).onClick + onSave({} as React.MouseEvent) + }) + + expect(onSaveSpy).toHaveBeenCalledTimes(1) + expect(onSaveSpy).toHaveBeenCalledWith({ + name: expectedName, + diagnosisDate: expectedDate.toISOString(), + } as Diagnosis) + }) + + it('should display an error message if the name field is not filled out', () => { + const wrapper = mount( + , + ) + + act(() => { + const modal = wrapper.find(Modal) + const onSave = (modal.prop('successButton') as any).onClick + onSave({} as React.MouseEvent) + }) + wrapper.update() + + expect(wrapper.find(Alert)).toHaveLength(1) + expect(wrapper.find(Alert).prop('title')).toEqual('states.error') + expect(wrapper.find(Alert).prop('message')).toContain('patient.diagnoses.error.nameRequired') + }) + }) +}) diff --git a/src/__tests__/patients/diagnoses/Diagnoses.test.tsx b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx new file mode 100644 index 0000000000..5660325a5a --- /dev/null +++ b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx @@ -0,0 +1,129 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import Patient from 'model/Patient' +import Diagnosis from 'model/Diagnosis' +import Permissions from 'model/Permissions' +import { Router } from 'react-router' +import { Provider } from 'react-redux' +import Diagnoses from 'patients/diagnoses/Diagnoses' +import { Button, Modal, List, ListItem, Alert } from '@hospitalrun/components' +import { act } from 'react-dom/test-utils' +import { mocked } from 'ts-jest/utils' +import PatientRepository from 'clients/db/PatientRepository' +import AddDiagnosisModal from 'patients/diagnoses/AddDiagnosisModal' +import * as patientSlice from '../../../patients/patient-slice' + +const expectedPatient = { + id: '123', + diagnoses: [ + { id: '123', name: 'diagnosis1', diagnosisDate: new Date().toISOString() } as Diagnosis, + ], +} as Patient + +const mockStore = configureMockStore([thunk]) +const history = createMemoryHistory() + +let user: any +let store: any + +const setup = (patient = expectedPatient, permissions = [Permissions.AddDiagnosis]) => { + user = { permissions } + store = mockStore({ patient, user }) + const wrapper = mount( + + + + + , + ) + + return wrapper +} +describe('Diagnoses', () => { + describe('add diagnoses button', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'saveOrUpdate') + }) + + it('should render a add diagnoses button', () => { + const wrapper = setup() + + const addDiagnosisButton = wrapper.find(Button) + expect(addDiagnosisButton).toHaveLength(1) + expect(addDiagnosisButton.text().trim()).toEqual('patient.diagnoses.new') + }) + + it('should not render a diagnoses button if the user does not have permissions', () => { + const wrapper = setup(expectedPatient, []) + + const addDiagnosisButton = wrapper.find(Button) + expect(addDiagnosisButton).toHaveLength(0) + }) + + it('should open the Add Diagnosis Modal', () => { + const wrapper = setup() + + act(() => { + wrapper.find(Button).prop('onClick')() + }) + wrapper.update() + + expect(wrapper.find(Modal).prop('show')).toBeTruthy() + }) + + it('should update the patient with the new diagnosis when the save button is clicked', async () => { + const expectedDiagnosis = { + name: 'name', + diagnosisDate: new Date().toISOString(), + } as Diagnosis + const expectedUpdatedPatient = { + ...expectedPatient, + diagnoses: [...(expectedPatient.diagnoses as any), expectedDiagnosis], + } as Patient + + const mockedPatientRepository = mocked(PatientRepository, true) + mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedUpdatedPatient) + + const wrapper = setup() + + await act(async () => { + const modal = wrapper.find(AddDiagnosisModal) + await modal.prop('onSave')(expectedDiagnosis) + }) + + expect(mockedPatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedUpdatedPatient) + expect(store.getActions()).toContainEqual(patientSlice.updatePatientStart()) + expect(store.getActions()).toContainEqual( + patientSlice.updatePatientSuccess(expectedUpdatedPatient), + ) + }) + }) + + describe('diagnoses list', () => { + it('should list the patients diagnoses', () => { + const diagnoses = expectedPatient.diagnoses as Diagnosis[] + const wrapper = setup() + + const list = wrapper.find(List) + const listItems = wrapper.find(ListItem) + + expect(list).toHaveLength(1) + expect(listItems).toHaveLength(diagnoses.length) + }) + + it('should render a warning message if the patient does not have any diagnoses', () => { + const wrapper = setup({ ...expectedPatient, diagnoses: [] }) + + const alert = wrapper.find(Alert) + + expect(alert).toHaveLength(1) + expect(alert.prop('title')).toEqual('patient.diagnoses.warning.noDiagnoses') + expect(alert.prop('message')).toEqual('patient.diagnoses.addDiagnosisAbove') + }) + }) +}) diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 0734c209ca..046fb00da5 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -65,7 +65,19 @@ "addAllergyAbove": "Add an allergy using the button above." }, "diagnoses": { - "label": "Diagnoses" + "label": "Diagnoses", + "new": "Add Diagnoses", + "diagnosisName": "Diagnosis Name", + "diagnosisDate": "Diagnosis Date", + "warning": { + "noDiagnoses": "No Diagnoses" + }, + "error": { + "nameRequired": "Diagnosis Name is required.", + "dateRequired": "Diagnosis Date is required." + }, + "addDiagnosisAbove": "Add a diagnosis using the button above." + } }, "sex": { diff --git a/src/model/Diagnosis.ts b/src/model/Diagnosis.ts new file mode 100644 index 0000000000..ad259ef50c --- /dev/null +++ b/src/model/Diagnosis.ts @@ -0,0 +1,5 @@ +export default interface Diagnosis { + id: string + name: string + diagnosisDate: string +} diff --git a/src/model/Patient.ts b/src/model/Patient.ts index 52db49648f..11ae80f29d 100644 --- a/src/model/Patient.ts +++ b/src/model/Patient.ts @@ -3,6 +3,7 @@ import Name from './Name' import ContactInformation from './ContactInformation' import RelatedPerson from './RelatedPerson' import Allergy from './Allergy' +import Diagnosis from './Diagnosis' export default interface Patient extends AbstractDBModel, Name, ContactInformation { sex: string @@ -14,4 +15,5 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati friendlyId: string relatedPersons?: RelatedPerson[] allergies?: Allergy[] + diagnoses?: Diagnosis[] } diff --git a/src/model/Permissions.ts b/src/model/Permissions.ts index b7403fb69e..90884d6037 100644 --- a/src/model/Permissions.ts +++ b/src/model/Permissions.ts @@ -5,6 +5,7 @@ enum Permissions { WriteAppointments = 'write:appointments', DeleteAppointment = 'delete:appointment', AddAllergy = 'write:allergy', + AddDiagnosis = 'write:diagnosis', } export default Permissions diff --git a/src/patients/diagnoses/AddDiagnosisModal.tsx b/src/patients/diagnoses/AddDiagnosisModal.tsx new file mode 100644 index 0000000000..311ebf7460 --- /dev/null +++ b/src/patients/diagnoses/AddDiagnosisModal.tsx @@ -0,0 +1,106 @@ +import React, { useState, useEffect } from 'react' +import { Modal, Alert } from '@hospitalrun/components' +import { useTranslation } from 'react-i18next' +import Diagnosis from 'model/Diagnosis' +import TextInputWithLabelFormGroup from 'components/input/TextInputWithLabelFormGroup' +import DatePickerWithLabelFormGroup from 'components/input/DatePickerWithLabelFormGroup' + +interface Props { + show: boolean + onCloseButtonClick: () => void + onSave: (diagnosis: Diagnosis) => void +} + +const AddDiagnosisModal = (props: Props) => { + const { show, onCloseButtonClick, onSave } = props + const [diagnosis, setDiagnosis] = useState({ name: '', diagnosisDate: new Date().toISOString() }) + const [errorMessage, setErrorMessage] = useState('') + + const { t } = useTranslation() + + useEffect(() => { + setErrorMessage('') + setDiagnosis({ name: '', diagnosisDate: new Date().toISOString() }) + }, [show]) + + const onSaveButtonClick = () => { + let newErrorMessage = '' + if (!diagnosis.name) { + newErrorMessage += t('patient.diagnoses.error.nameRequired') + } + setErrorMessage(newErrorMessage) + + if (!newErrorMessage) { + onSave(diagnosis as Diagnosis) + } + } + + const onNameChange = (event: React.ChangeEvent) => { + const name = event.target.value + setDiagnosis((prevDiagnosis) => ({ ...prevDiagnosis, name })) + } + + const onDiagnosisDateChange = (diagnosisDate: Date) => { + if (diagnosisDate) { + setDiagnosis((prevDiagnosis) => ({ + ...prevDiagnosis, + diagnosisDate: diagnosisDate.toISOString(), + })) + } + } + + const body = ( + <> +
+ {errorMessage && } +
+
+
+ +
+
+
+
+
+ +
+
+ + + ) + return ( + + ) +} + +export default AddDiagnosisModal diff --git a/src/patients/diagnoses/Diagnoses.tsx b/src/patients/diagnoses/Diagnoses.tsx index 41854a4072..6b117f794c 100644 --- a/src/patients/diagnoses/Diagnoses.tsx +++ b/src/patients/diagnoses/Diagnoses.tsx @@ -1,6 +1,16 @@ -import React from 'react' +import React, { useState } from 'react' +import { RootState } from 'store' import Patient from 'model/Patient' import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' +import { useSelector, useDispatch } from 'react-redux' +import Permissions from 'model/Permissions' +import { Button, List, ListItem, Alert } from '@hospitalrun/components' +import { useTranslation } from 'react-i18next' +import Diagnosis from 'model/Diagnosis' +import { getTimestampId } from 'patients/util/timestamp-id-generator' +import { updatePatient } from 'patients/patient-slice' +import { useHistory } from 'react-router' +import AddDiagnosisModal from './AddDiagnosisModal' interface Props { patient: Patient @@ -8,6 +18,13 @@ interface Props { const Diagnoses = (props: Props) => { const { patient } = props + const [showDiagnosisModal, setShowDiagnosisModal] = useState(false) + + const history = useHistory() + const dispatch = useDispatch() + const { t } = useTranslation() + const { permissions } = useSelector((state: RootState) => state.user) + const breadcrumbs = [ { i18nKey: 'patient.diagnoses.label', @@ -16,7 +33,59 @@ const Diagnoses = (props: Props) => { ] useAddBreadcrumbs(breadcrumbs) - return

Diagnoses

+ const onAddDiagnosisModalClose = () => { + setShowDiagnosisModal(false) + } + + const onDiagnosisSave = (diagnosis: Diagnosis) => { + diagnosis.id = getTimestampId() + const diagnoses = [] + if (patient.diagnoses) { + diagnoses.push(...patient.diagnoses) + } + diagnoses.push(diagnosis) + const patientToUpdate = { ...patient, diagnoses } + dispatch(updatePatient(patientToUpdate, history)) + setShowDiagnosisModal(false) + } + + return ( + <> +
+
+ {permissions.includes(Permissions.AddDiagnosis) && ( + + )} +
+
+
+ {(!patient.diagnoses || patient.diagnoses.length === 0) && ( + + )} + + {patient.diagnoses?.map((a: Diagnosis) => ( + {a.name} + ))} + + + + ) } export default Diagnoses diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index 80089b0b02..81bae4cde1 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -13,6 +13,7 @@ const initialState: UserState = { Permissions.WriteAppointments, Permissions.DeleteAppointment, Permissions.AddAllergy, + Permissions.AddDiagnosis, ], }