From 0aa0cf93b3f9e3153592684ce05babee2e0c5379 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Tue, 19 May 2020 23:15:49 -0500 Subject: [PATCH 1/6] feat(careplan): adds ability to add a new care plan --- .../input/SelectWithLableFormGroup.tsx | 25 ++- .../enUs/translations/patient/index.ts | 12 ++ src/model/CarePlan.ts | 28 +++ src/model/Patient.ts | 2 + src/model/Permissions.ts | 2 + src/patients/care-plans/AddCarePlanModal.tsx | 197 ++++++++++++++++++ src/patients/care-plans/CarePlanTab.tsx | 70 +++++++ src/patients/patient-slice.ts | 31 +++ src/patients/view/ViewPatient.tsx | 9 + src/user/user-slice.ts | 2 + 10 files changed, 375 insertions(+), 3 deletions(-) create mode 100644 src/model/CarePlan.ts create mode 100644 src/patients/care-plans/AddCarePlanModal.tsx create mode 100644 src/patients/care-plans/CarePlanTab.tsx diff --git a/src/components/input/SelectWithLableFormGroup.tsx b/src/components/input/SelectWithLableFormGroup.tsx index be9874aca1..fff067037c 100644 --- a/src/components/input/SelectWithLableFormGroup.tsx +++ b/src/components/input/SelectWithLableFormGroup.tsx @@ -10,18 +10,37 @@ interface Props { value: string label: string name: string + isRequired?: boolean isEditable?: boolean options: Option[] onChange?: (event: React.ChangeEvent) => void + feedback?: string + isInvalid?: boolean } const SelectWithLabelFormGroup = (props: Props) => { - const { value, label, name, isEditable, options, onChange } = props + const { + value, + label, + name, + isEditable, + options, + onChange, + isRequired, + feedback, + isInvalid, + } = props const id = `${name}Select` return (
-
) diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index afcb489b76..c48dd0bc2a 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -25,6 +25,8 @@ const initialState: UserState = { Permissions.ViewIncident, Permissions.ViewIncidents, Permissions.ReportIncident, + Permissions.AddCarePlan, + Permissions.ReadCarePlan ], user: { id: 'some-hardcoded-id', From e96eb835c37c0d4174ee8a8878286fbabda309c8 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 1 Jun 2020 23:04:47 -0500 Subject: [PATCH 2/6] feat(care-plan): add care plan form and tests --- .../patients/care-plans/CarePlanForm.test.tsx | 313 ++++++++++++++++++ src/patients/care-plans/CarePlanForm.tsx | 163 +++++++++ 2 files changed, 476 insertions(+) create mode 100644 src/__tests__/patients/care-plans/CarePlanForm.test.tsx create mode 100644 src/patients/care-plans/CarePlanForm.tsx diff --git a/src/__tests__/patients/care-plans/CarePlanForm.test.tsx b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx new file mode 100644 index 0000000000..8778ff8751 --- /dev/null +++ b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx @@ -0,0 +1,313 @@ +import '../../../__mocks__/matchMediaMock' +import { addDays } from 'date-fns' +import { mount } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' +import Diagnosis from '../../../model/Diagnosis' +import Patient from '../../../model/Patient' +import CarePlanForm from '../../../patients/care-plans/CarePlanForm' + +describe('Care Plan Form', () => { + let onCarePlanChangeSpy: any + const diagnosis: Diagnosis = { + id: '123', + name: 'some diagnosis name', + diagnosisDate: new Date().toISOString(), + } + const carePlan: CarePlan = { + id: 'id', + title: 'title', + description: 'description', + status: CarePlanStatus.Active, + intent: CarePlanIntent.Option, + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + diagnosisId: diagnosis.id, + createdOn: new Date().toISOString(), + note: 'note', + } + const setup = (disabled = false, initializeCarePlan = true, error?: any) => { + onCarePlanChangeSpy = jest.fn() + const mockPatient = { id: '123', diagnoses: [diagnosis] } as Patient + const wrapper = mount( + , + ) + return { wrapper } + } + + it('should render a title input', () => { + const { wrapper } = setup() + + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + + expect(titleInput).toHaveLength(1) + expect(titleInput.prop('patient.carePlan.title')) + expect(titleInput.prop('isRequired')).toBeTruthy() + expect(titleInput.prop('value')).toEqual(carePlan.title) + }) + + it('should call the on change handler when condition changes', () => { + const expectedNewTitle = 'some new title' + const { wrapper } = setup(false, false) + act(() => { + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + const onChange = titleInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewTitle } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ title: expectedNewTitle }) + }) + + it('should render a description input', () => { + const { wrapper } = setup() + + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + + expect(descriptionInput).toHaveLength(1) + expect(descriptionInput.prop('patient.carePlan.description')) + expect(descriptionInput.prop('isRequired')).toBeTruthy() + expect(descriptionInput.prop('value')).toEqual(carePlan.description) + }) + + it('should call the on change handler when condition changes', () => { + const expectedNewDescription = 'some new description' + const { wrapper } = setup(false, false) + act(() => { + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const onChange = descriptionInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewDescription } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ description: expectedNewDescription }) + }) + + it('should render a condition selector with the diagnoses from the patient', () => { + const { wrapper } = setup() + + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + + expect(conditionSelector).toHaveLength(1) + expect(conditionSelector.prop('patient.carePlan.condition')) + expect(conditionSelector.prop('isRequired')).toBeTruthy() + expect(conditionSelector.prop('value')).toEqual(carePlan.diagnosisId) + expect(conditionSelector.prop('options')).toEqual([ + { value: diagnosis.id, label: diagnosis.name }, + ]) + }) + + it('should call the on change handler when condition changes', () => { + const expectedNewCondition = 'some new condition' + const { wrapper } = setup(false, false) + act(() => { + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + const onChange = conditionSelector.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewCondition } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ diagnosisId: expectedNewCondition }) + }) + + it('should render a status selector', () => { + const { wrapper } = setup() + + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + + expect(statusSelector).toHaveLength(1) + expect(statusSelector.prop('patient.carePlan.status')) + expect(statusSelector.prop('isRequired')).toBeTruthy() + expect(statusSelector.prop('value')).toEqual(carePlan.status) + expect(statusSelector.prop('options')).toEqual( + Object.values(CarePlanStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call the on change handler when status changes', () => { + const expectedNewStatus = CarePlanStatus.Revoked + const { wrapper } = setup(false, false) + act(() => { + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const onChange = statusSelector.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewStatus } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ status: expectedNewStatus }) + }) + + it('should render an intent selector', () => { + const { wrapper } = setup() + + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + + expect(intentSelector).toHaveLength(1) + expect(intentSelector.prop('patient.carePlan.intent')) + expect(intentSelector.prop('isRequired')).toBeTruthy() + expect(intentSelector.prop('value')).toEqual(carePlan.intent) + expect(intentSelector.prop('options')).toEqual( + Object.values(CarePlanIntent).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call the on change handler when intent changes', () => { + const newIntent = CarePlanIntent.Proposal + const { wrapper } = setup(false, false) + act(() => { + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + const onChange = intentSelector.prop('onChange') as any + onChange({ currentTarget: { value: newIntent } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ intent: newIntent }) + }) + + it('should render a start date picker', () => { + const { wrapper } = setup() + + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + + expect(startDatePicker).toHaveLength(1) + expect(startDatePicker.prop('patient.carePlan.startDate')) + expect(startDatePicker.prop('isRequired')).toBeTruthy() + expect(startDatePicker.prop('value')).toEqual(new Date(carePlan.startDate)) + }) + + it('should call the on change handler when start date changes', () => { + const expectedNewStartDate = addDays(1, new Date().getDate()) + const { wrapper } = setup(false, false) + + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + act(() => { + const onChange = startDatePicker.prop('onChange') as any + onChange(expectedNewStartDate) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ + startDate: expectedNewStartDate.toISOString(), + }) + }) + + it('should render an end date picker', () => { + const { wrapper } = setup() + + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + + expect(endDatePicker).toHaveLength(1) + expect(endDatePicker.prop('patient.carePlan.endDate')) + expect(endDatePicker.prop('isRequired')).toBeTruthy() + expect(endDatePicker.prop('value')).toEqual(new Date(carePlan.endDate)) + }) + + it('should call the on change handler when end date changes', () => { + const expectedNewEndDate = addDays(1, new Date().getDate()) + const { wrapper } = setup(false, false) + + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + act(() => { + const onChange = endDatePicker.prop('onChange') as any + onChange(expectedNewEndDate) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ + endDate: expectedNewEndDate.toISOString(), + }) + }) + + it('should render a note input', () => { + const { wrapper } = setup() + + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + expect(noteInput).toHaveLength(1) + expect(noteInput.prop('patient.carePlan.note')) + expect(noteInput.prop('isRequired')).toBeTruthy() + expect(noteInput.prop('value')).toEqual(carePlan.note) + }) + + it('should call the on change handler when note changes', () => { + const expectedNewNote = 'some new note' + const { wrapper } = setup(false, false) + + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + act(() => { + const onChange = noteInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewNote } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ note: expectedNewNote }) + }) + + it('should render all of the fields as disabled if the form is disabled', () => { + const { wrapper } = setup(true) + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(titleInput.prop('isEditable')).toBeFalsy() + expect(descriptionInput.prop('isEditable')).toBeFalsy() + expect(conditionSelector.prop('isEditable')).toBeFalsy() + expect(statusSelector.prop('isEditable')).toBeFalsy() + expect(intentSelector.prop('isEditable')).toBeFalsy() + expect(startDatePicker.prop('isEditable')).toBeFalsy() + expect(endDatePicker.prop('isEditable')).toBeFalsy() + expect(noteInput.prop('isEditable')).toBeFalsy() + }) + + it('should render the form fields in an error state', () => { + const expectedError = { + title: 'some title error', + description: 'some description error', + status: 'some status error', + intent: 'some intent error', + startDate: 'some start date error', + endDate: 'some end date error', + note: 'some note error', + condition: 'some condition error', + } + + const { wrapper } = setup(false, false, expectedError) + + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(titleInput.prop('isInvalid')).toBeTruthy() + expect(titleInput.prop('feedback')).toEqual(expectedError.title) + + expect(descriptionInput.prop('isInvalid')).toBeTruthy() + expect(descriptionInput.prop('feedback')).toEqual(expectedError.description) + + expect(conditionSelector.prop('isInvalid')).toBeTruthy() + expect(conditionSelector.prop('feedback')).toEqual(expectedError.condition) + + expect(statusSelector.prop('isInvalid')).toBeTruthy() + expect(statusSelector.prop('feedback')).toEqual(expectedError.status) + + expect(intentSelector.prop('isInvalid')).toBeTruthy() + expect(intentSelector.prop('feedback')).toEqual(expectedError.intent) + + expect(startDatePicker.prop('isInvalid')).toBeTruthy() + expect(startDatePicker.prop('feedback')).toEqual(expectedError.startDate) + + expect(endDatePicker.prop('isInvalid')).toBeTruthy() + expect(endDatePicker.prop('feedback')).toEqual(expectedError.endDate) + + expect(noteInput.prop('isInvalid')).toBeTruthy() + expect(noteInput.prop('feedback')).toEqual(expectedError.note) + }) +}) diff --git a/src/patients/care-plans/CarePlanForm.tsx b/src/patients/care-plans/CarePlanForm.tsx new file mode 100644 index 0000000000..cf7bac0044 --- /dev/null +++ b/src/patients/care-plans/CarePlanForm.tsx @@ -0,0 +1,163 @@ +import { Column, Row } from '@hospitalrun/components' +import React from 'react' +import { useTranslation } from 'react-i18next' + +import DatePickerWithLabelFormGroup from '../../components/input/DatePickerWithLabelFormGroup' +import SelectWithLabelFormGroup from '../../components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../model/CarePlan' +import Patient from '../../model/Patient' + +interface Error { + title?: string + description?: string + status?: string + intent?: string + startDate?: string + endDate?: string + note?: string + condition?: string +} +interface Props { + patient: Patient + carePlan: Partial + carePlanError?: Error + onChange: (newCarePlan: Partial) => void + disabled: boolean +} + +const CarePlanForm = (props: Props) => { + const { t } = useTranslation() + const { patient, carePlan, carePlanError, disabled, onChange } = props + + const onFieldChange = (name: string, value: string | CarePlanStatus | CarePlanIntent) => { + const newCarePlan = { + ...carePlan, + [name]: value, + } + onChange(newCarePlan) + } + + return ( +
+ + + onFieldChange('title', event.currentTarget.value)} + /> + + + + + onFieldChange('description', event.currentTarget.value)} + /> + + + + + onFieldChange('diagnosisId', event.currentTarget.value)} + options={patient.diagnoses?.map((d) => ({ label: d.name, value: d.id })) || []} + /> + + + + + ({ label: v, value: v }))} + onChange={(event) => onFieldChange('status', event.currentTarget.value)} + /> + + + ({ label: v, value: v }))} + onChange={(event) => onFieldChange('intent', event.currentTarget.value)} + /> + + + + + onFieldChange('startDate', date.toISOString())} + /> + + + onFieldChange('endDate', date.toISOString())} + /> + + + + + onFieldChange('note', event.currentTarget.value)} + /> + + +
+ ) +} + +CarePlanForm.defaultProps = { + disabled: false, +} + +export default CarePlanForm From 066e1427a733a9138816c3d45fab0df5ecd311aa Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Wed, 3 Jun 2020 21:58:53 -0500 Subject: [PATCH 3/6] test(care plan): add tests for care plans --- .../care-plans/AddCarePlanModal.test.tsx | 120 +++++++++++++++ .../patients/care-plans/CarePlanForm.test.tsx | 6 + .../patients/care-plans/CarePlanTab.test.tsx | 101 +++++++++++++ .../care-plans/CarePlanTable.test.tsx | 89 +++++++++++ .../patients/care-plans/ViewCarePlan.test.tsx | 55 +++++++ src/__tests__/patients/patient-slice.test.ts | 119 +++++++++++++-- .../patients/view/ViewPatient.test.tsx | 53 ++++++- src/patients/care-plans/AddCarePlanModal.tsx | 140 ++---------------- src/patients/care-plans/CarePlanForm.tsx | 16 +- src/patients/care-plans/CarePlanTab.tsx | 43 ++---- src/patients/care-plans/CarePlanTable.tsx | 50 +++++++ src/patients/care-plans/ViewCarePlan.tsx | 34 +++++ src/patients/patient-slice.ts | 90 ++++++++--- src/patients/view/ViewPatient.tsx | 6 +- src/user/user-slice.ts | 2 +- 15 files changed, 721 insertions(+), 203 deletions(-) create mode 100644 src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx create mode 100644 src/__tests__/patients/care-plans/CarePlanTab.test.tsx create mode 100644 src/__tests__/patients/care-plans/CarePlanTable.test.tsx create mode 100644 src/__tests__/patients/care-plans/ViewCarePlan.test.tsx create mode 100644 src/patients/care-plans/CarePlanTable.tsx create mode 100644 src/patients/care-plans/ViewCarePlan.tsx diff --git a/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx b/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx new file mode 100644 index 0000000000..e62b486275 --- /dev/null +++ b/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx @@ -0,0 +1,120 @@ +import '../../../__mocks__/matchMediaMock' +import { Modal } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +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 { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' +import Patient from '../../../model/Patient' +import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' +import CarePlanForm from '../../../patients/care-plans/CarePlanForm' +import * as patientSlice from '../../../patients/patient-slice' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('Add Care Plan Modal', () => { + const patient = { + id: 'patientId', + diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + carePlans: [ + { + id: '123', + title: 'some title', + description: 'some description', + diagnosisId: '123', + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + status: CarePlanStatus.Active, + intent: CarePlanIntent.Proposal, + }, + ], + } as Patient + + const carePlanError = { + title: 'some care plan error', + } + + const onCloseSpy = jest.fn() + const setup = () => { + const store = mockStore({ patient: { patient, carePlanError } } as any) + const history = createMemoryHistory() + const wrapper = mount( + + + + + , + ) + + wrapper.update() + return { wrapper } + } + + it('should render a modal', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + + expect(modal).toHaveLength(1) + + const successButton = modal.prop('successButton') + const cancelButton = modal.prop('closeButton') + expect(modal.prop('title')).toEqual('patient.carePlan.new') + expect(successButton?.children).toEqual('patient.carePlan.new') + expect(successButton?.icon).toEqual('add') + expect(cancelButton?.children).toEqual('actions.cancel') + }) + + it('should render the care plan form', () => { + const { wrapper } = setup() + + const carePlanForm = wrapper.find(CarePlanForm) + expect(carePlanForm).toHaveLength(1) + expect(carePlanForm.prop('carePlanError')).toEqual(carePlanError) + expect(carePlanForm.prop('patient')).toEqual(patient) + }) + + it('should dispatch add care plan when the save button is clicked', async () => { + const { wrapper } = setup() + jest.spyOn(patientSlice, 'addCarePlan') + + act(() => { + const carePlanForm = wrapper.find(CarePlanForm) + const onChange = carePlanForm.prop('onChange') as any + onChange(patient.carePlans[0]) + }) + wrapper.update() + + await act(async () => { + const modal = wrapper.find(Modal) + const successButton = modal.prop('successButton') + const onClick = successButton?.onClick as any + await onClick() + }) + + expect(patientSlice.addCarePlan).toHaveBeenCalledTimes(1) + expect(patientSlice.addCarePlan).toHaveBeenCalledWith(patient.id, patient.carePlans[0]) + }) + + it('should call the on close function when the cancel button is clicked', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + + expect(modal).toHaveLength(1) + + act(() => { + const cancelButton = modal.prop('closeButton') + const onClick = cancelButton?.onClick as any + onClick() + }) + + expect(onCloseSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/__tests__/patients/care-plans/CarePlanForm.test.tsx b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx index 8778ff8751..20f83dd4c8 100644 --- a/src/__tests__/patients/care-plans/CarePlanForm.test.tsx +++ b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx @@ -1,4 +1,5 @@ import '../../../__mocks__/matchMediaMock' +import { Alert } from '@hospitalrun/components' import { addDays } from 'date-fns' import { mount } from 'enzyme' import React from 'react' @@ -265,6 +266,7 @@ describe('Care Plan Form', () => { it('should render the form fields in an error state', () => { const expectedError = { + message: 'some error message', title: 'some title error', description: 'some description error', status: 'some status error', @@ -277,6 +279,7 @@ describe('Care Plan Form', () => { const { wrapper } = setup(false, false, expectedError) + const alert = wrapper.find(Alert) const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') @@ -286,6 +289,9 @@ describe('Care Plan Form', () => { const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + expect(alert).toHaveLength(1) + expect(alert.prop('message')).toEqual(expectedError.message) + expect(titleInput.prop('isInvalid')).toBeTruthy() expect(titleInput.prop('feedback')).toEqual(expectedError.title) diff --git a/src/__tests__/patients/care-plans/CarePlanTab.test.tsx b/src/__tests__/patients/care-plans/CarePlanTab.test.tsx new file mode 100644 index 0000000000..1a202bc841 --- /dev/null +++ b/src/__tests__/patients/care-plans/CarePlanTab.test.tsx @@ -0,0 +1,101 @@ +import '../../../__mocks__/matchMediaMock' +import { Button } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +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 Permissions from '../../../model/Permissions' +import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' +import CarePlanTab from '../../../patients/care-plans/CarePlanTab' +import CarePlanTable from '../../../patients/care-plans/CarePlanTable' +import ViewCarePlan from '../../../patients/care-plans/ViewCarePlan' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('Care Plan Tab', () => { + const patient = { + id: 'patientId', + } + + const setup = (route: string, permissions: Permissions[]) => { + const store = mockStore({ patient: { patient }, user: { permissions } } as any) + const history = createMemoryHistory() + history.push(route) + const wrapper = mount( + + + + + , + ) + + wrapper.update() + return { wrapper, history } + } + + it('should render an add care plan button if user has correct permissions', () => { + const { wrapper } = setup('/patients/123/care-plans', [Permissions.AddCarePlan]) + + const addNewButton = wrapper.find(Button).at(0) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.carePlan.new') + }) + + it('should open the add care plan modal on click', () => { + const { wrapper } = setup('/patients/123/care-plans', [Permissions.AddCarePlan]) + + act(() => { + const addNewButton = wrapper.find(Button).at(0) + 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', () => { + const { wrapper } = setup('/patients/123/care-plans', [Permissions.AddCarePlan]) + + act(() => { + const addNewButton = wrapper.find(Button).at(0) + 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 care plan button if user does not have permissions', () => { + const { wrapper } = setup('/patients/123/care-plans', []) + + expect(wrapper.find(Button)).toHaveLength(0) + }) + + it('should render the care plans table when on /patient/:id/care-plans', () => { + const { wrapper } = setup('/patients/123/care-plans', []) + + expect(wrapper.find(CarePlanTable)).toHaveLength(1) + }) + + it('should render the care plan view when on /patient/:id/care-plans/:carePlanId', () => { + const { wrapper } = setup('/patients/123/care-plans/456', []) + + expect(wrapper.find(ViewCarePlan)).toHaveLength(1) + }) +}) diff --git a/src/__tests__/patients/care-plans/CarePlanTable.test.tsx b/src/__tests__/patients/care-plans/CarePlanTable.test.tsx new file mode 100644 index 0000000000..ce705f2a59 --- /dev/null +++ b/src/__tests__/patients/care-plans/CarePlanTable.test.tsx @@ -0,0 +1,89 @@ +import '../../../__mocks__/matchMediaMock' +import { Button } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +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 CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' +import Patient from '../../../model/Patient' +import CarePlanTable from '../../../patients/care-plans/CarePlanTable' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('Care Plan Table', () => { + const carePlan: CarePlan = { + id: 'id', + title: 'title', + description: 'description', + status: CarePlanStatus.Active, + intent: CarePlanIntent.Option, + startDate: new Date(2020, 6, 3).toISOString(), + endDate: new Date(2020, 6, 5).toISOString(), + diagnosisId: 'some id', + createdOn: new Date().toISOString(), + note: 'note', + } + const patient = { + id: 'patientId', + diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + carePlans: [carePlan], + } as Patient + + const setup = () => { + const store = mockStore({ patient: { patient } } as any) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-plans/${patient.carePlans[0].id}`) + const wrapper = mount( + + + + + , + ) + + return { wrapper, history } + } + + it('should render a table', () => { + const { wrapper } = setup() + + const table = wrapper.find('table') + const tableHeader = table.find('thead') + const headers = tableHeader.find('th') + const body = table.find('tbody') + const columns = body.find('tr').find('td') + + expect(headers.at(0).text()).toEqual('patient.carePlan.title') + expect(headers.at(1).text()).toEqual('patient.carePlan.startDate') + expect(headers.at(2).text()).toEqual('patient.carePlan.endDate') + expect(headers.at(3).text()).toEqual('patient.carePlan.status') + expect(headers.at(4).text()).toEqual('actions.label') + + expect(columns.at(0).text()).toEqual(carePlan.title) + expect(columns.at(1).text()).toEqual('2020-07-03') + expect(columns.at(2).text()).toEqual('2020-07-05') + expect(columns.at(3).text()).toEqual(carePlan.status) + expect(columns.at(4).find('button')).toHaveLength(1) + }) + + it('should navigate to the care plan view when the view details button is clicked', () => { + const { wrapper, history } = setup() + + const table = wrapper.find('table') + const body = table.find('tbody') + const columns = body.find('tr').find('td') + + act(() => { + const onClick = columns.at(4).find(Button).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-plans/${carePlan.id}`) + }) +}) diff --git a/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx b/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx new file mode 100644 index 0000000000..558b2773cf --- /dev/null +++ b/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx @@ -0,0 +1,55 @@ +import '../../../__mocks__/matchMediaMock' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Provider } from 'react-redux' +import { Route, Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Patient from '../../../model/Patient' +import CarePlanForm from '../../../patients/care-plans/CarePlanForm' +import ViewCarePlan from '../../../patients/care-plans/ViewCarePlan' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('View Care Plan', () => { + const patient = { + id: 'patientId', + diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + carePlans: [{ id: '123', title: 'some title' }], + } as Patient + + const setup = () => { + const store = mockStore({ patient: { patient } } as any) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-plans/${patient.carePlans[0].id}`) + const wrapper = mount( + + + + + + + , + ) + + return { wrapper } + } + + it('should render the care plan title', () => { + const { wrapper } = setup() + + expect(wrapper.find('h2').text()).toEqual(patient.carePlans[0].title) + }) + + it('should render a care plan form with the correct data', () => { + const { wrapper } = setup() + + const carePlanForm = wrapper.find(CarePlanForm) + expect(carePlanForm).toHaveLength(1) + expect(carePlanForm.prop('carePlan')).toEqual(patient.carePlans[0]) + expect(carePlanForm.prop('patient')).toEqual(patient) + }) +}) diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index f49ca7caa1..1d7d4b426b 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -1,34 +1,37 @@ import '../../__mocks__/matchMediaMock' -import { addDays } from 'date-fns' +import { addDays, subDays } from 'date-fns' import { AnyAction } from 'redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import PatientRepository from '../../clients/db/PatientRepository' import Allergy from '../../model/Allergy' +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../model/CarePlan' import Diagnosis from '../../model/Diagnosis' import Patient from '../../model/Patient' import RelatedPerson from '../../model/RelatedPerson' import patient, { - fetchPatientStart, - fetchPatientSuccess, - fetchPatient, - createPatientStart, - createPatientSuccess, - createPatient, - updatePatientStart, - updatePatientSuccess, - updatePatient, - addRelatedPerson, - addDiagnosis, addAllergy, - removeRelatedPerson, - updatePatientError, - createPatientError, addAllergyError, + addCarePlan, + addDiagnosis, addDiagnosisError, + addRelatedPerson, addRelatedPersonError, + createPatient, + createPatientError, + createPatientStart, + createPatientSuccess, + fetchPatient, + fetchPatientStart, + fetchPatientSuccess, + removeRelatedPerson, + updatePatient, + updatePatientError, + updatePatientStart, + updatePatientSuccess, + addCarePlanError, } from '../../patients/patient-slice' import { RootState } from '../../store' import * as uuid from '../../util/uuid' @@ -637,4 +640,90 @@ describe('patients slice', () => { expect(onSuccessSpy).not.toHaveBeenCalled() }) }) + + describe('add care plan', () => { + it('should add a care plan', async () => { + const expectedCarePlanId = 'expected id' + const store = mockStore() + const expectedPatientId = '123' + + const expectedPatient = { + id: expectedPatientId, + givenName: 'some name', + } as Patient + + const expectedCarePlan = { + id: 'some id', + title: 'care plan title', + description: 'care plan description', + status: CarePlanStatus.Completed, + intent: CarePlanIntent.Proposal, + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + createdOn: new Date(Date.now()).toISOString(), + diagnosisId: 'some diagnosis id', + note: 'care plan note', + } as CarePlan + + const expectedUpdatedPatient = { + ...expectedPatient, + carePlans: [{ ...expectedCarePlan, id: expectedCarePlanId }], + } as Patient + + const findPatientSpy = jest + .spyOn(PatientRepository, 'find') + .mockResolvedValue(expectedPatient) + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedCarePlanId) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedUpdatedPatient) + const onSuccessSpy = jest.fn() + + await store.dispatch(addCarePlan(expectedPatientId, expectedCarePlan, onSuccessSpy)) + + expect(findPatientSpy).toHaveBeenCalledWith(expectedPatientId) + expect(store.getActions()[1]).toEqual(updatePatientSuccess(expectedUpdatedPatient)) + expect(onSuccessSpy).toHaveBeenCalledWith(expectedUpdatedPatient) + }) + + it('should validate the required fields', async () => { + const expectedError = { + message: 'patient.carePlans.error.unableToAdd', + title: 'patient.carePlans.error.titleRequired', + description: 'patient.carePlans.error.descriptionRequired', + status: 'patient.carePlans.error.statusRequired', + intent: 'patient.carePlans.error.intentRequired', + startDate: 'patient.carePlans.error.startDateRequired', + endDate: 'patient.carePlans.error.endDateRequired', + condition: 'patient.carePlans.error.conditionRequired', + note: 'patient.carePlans.error.noteRequired', + } + const store = mockStore() + const expectedCarePlan = {} as CarePlan + const onSuccessSpy = jest.fn() + + await store.dispatch(addCarePlan('some id', expectedCarePlan, onSuccessSpy)) + + expect(store.getActions()[0]).toEqual(addCarePlanError(expectedError)) + expect(onSuccessSpy).not.toHaveBeenCalled() + }) + + it('should validate that start date is before end date', async () => { + const store = mockStore() + const expectedCarePlan = { + startDate: new Date().toISOString(), + endDate: subDays(new Date(), 1).toISOString(), + } as CarePlan + const onSuccessSpy = jest.fn() + + await store.dispatch(addCarePlan('some id', expectedCarePlan, onSuccessSpy)) + + expect(store.getActions()[0]).toEqual( + addCarePlanError( + expect.objectContaining({ + endDate: 'patient.carePlans.error.endDateMustBeAfterStartDate', + }), + ), + ) + expect(onSuccessSpy).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 71207f49bd..3cc651b1e3 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -18,6 +18,8 @@ import Permissions from '../../../model/Permissions' import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' import * as titleUtil from '../../../page-header/useTitle' import Allergies from '../../../patients/allergies/Allergies' +import AppointmentsList from '../../../patients/appointments/AppointmentsList' +import CarePlanTab from '../../../patients/care-plans/CarePlanTab' import Diagnoses from '../../../patients/diagnoses/Diagnoses' import GeneralInformation from '../../../patients/GeneralInformation' import LabsTab from '../../../patients/labs/LabsTab' @@ -59,6 +61,7 @@ describe('ViewPatient', () => { store = mockStore({ patient: { patient }, user: { permissions }, + appointments: { appointments: [] }, } as any) history.push('/patients/123') @@ -132,7 +135,7 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(7) + expect(tabs).toHaveLength(8) 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') @@ -140,6 +143,7 @@ describe('ViewPatient', () => { 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') + expect(tabs.at(7).prop('label')).toEqual('patient.carePlan.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -197,6 +201,30 @@ describe('ViewPatient', () => { expect(relatedPersonTab.prop('patient')).toEqual(patient) }) + it('should mark the appointments tab as active when it is clicked and render the appointments tab component when route is /patients/:id/appointments', 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(2).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const appointmentsTab = wrapper.find(AppointmentsList) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/appointments`) + expect(tabs.at(2).prop('active')).toBeTruthy() + expect(appointmentsTab).toHaveLength(1) + expect(appointmentsTab.prop('patientId')).toEqual(patient.id) + }) + it('should mark the allergies tab as active when it is clicked and render the allergies component when route is /patients/:id/allergies', async () => { let wrapper: any await act(async () => { @@ -292,4 +320,27 @@ describe('ViewPatient', () => { expect(labsTab).toHaveLength(1) expect(labsTab.prop('patientId')).toEqual(patient.id) }) + + it('should mark the care plans tab as active when it is clicked and render the care plan tab component when route is /patients/:id/care-plans', 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(7).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const carePlansTab = wrapper.find(CarePlanTab) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-plans`) + expect(tabs.at(7).prop('active')).toBeTruthy() + expect(carePlansTab).toHaveLength(1) + }) }) diff --git a/src/patients/care-plans/AddCarePlanModal.tsx b/src/patients/care-plans/AddCarePlanModal.tsx index e2021bb204..923652775b 100644 --- a/src/patients/care-plans/AddCarePlanModal.tsx +++ b/src/patients/care-plans/AddCarePlanModal.tsx @@ -1,16 +1,13 @@ -import { Modal, Column, Row } from '@hospitalrun/components' +import { Modal } from '@hospitalrun/components' import { addMonths } from 'date-fns' import React, { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import DatePickerWithLabelFormGroup from '../../components/input/DatePickerWithLabelFormGroup' -import SelectWithLabelFormGroup from '../../components/input/SelectWithLableFormGroup' -import TextFieldWithLabelFormGroup from '../../components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../components/input/TextInputWithLabelFormGroup' -import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../model/CarePlan' +import CarePlan from '../../model/CarePlan' import { RootState } from '../../store' import { addCarePlan } from '../patient-slice' +import CarePlanForm from './CarePlanForm' interface Props { show: boolean @@ -23,8 +20,6 @@ const initialCarePlanState = { startDate: new Date().toISOString(), endDate: addMonths(new Date(), 1).toISOString(), note: '', - intent: '', - status: '', diagnosisId: '', } @@ -39,11 +34,8 @@ const AddCarePlanModal = (props: Props) => { setCarePlan(initialCarePlanState) }, [show]) - const onFieldChange = (name: string, value: string) => { - setCarePlan((previousCarePlan) => ({ - ...previousCarePlan, - [name]: value, - })) + const onCarePlanChange = (newCarePlan: Partial) => { + setCarePlan(newCarePlan as CarePlan) } const onSaveButtonClick = () => { @@ -55,123 +47,13 @@ const AddCarePlanModal = (props: Props) => { } const body = ( - <> -
- - - onFieldChange('title', event.currentTarget.value)} - /> - - - - - onFieldChange('description', event.currentTarget.value)} - /> - - - - - onFieldChange('diagnosisId', event.currentTarget.value)} - options={patient.diagnoses?.map((d) => ({ label: d.name, value: d.id })) || []} - /> - - - - - ({ label: v, value: v }))} - onChange={(event) => onFieldChange('status', event.currentTarget.value)} - /> - - - ({ label: v, value: v }))} - onChange={(event) => onFieldChange('intent', event.currentTarget.value)} - /> - - - - - onFieldChange('startDate', date.toISOString())} - /> - - - onFieldChange('endDate', date.toISOString())} - /> - - - - - onFieldChange('note', event.currentTarget.value)} - /> - - -
- + ) - return ( carePlanError?: Error - onChange: (newCarePlan: Partial) => void + onChange?: (newCarePlan: Partial) => void disabled: boolean } @@ -32,15 +33,18 @@ const CarePlanForm = (props: Props) => { const { patient, carePlan, carePlanError, disabled, onChange } = props const onFieldChange = (name: string, value: string | CarePlanStatus | CarePlanIntent) => { - const newCarePlan = { - ...carePlan, - [name]: value, + if (onChange) { + const newCarePlan = { + ...carePlan, + [name]: value, + } + onChange(newCarePlan) } - onChange(newCarePlan) } return (
+ {carePlanError?.message && } { +const CarePlanTab = () => { const { t } = useTranslation() - const { permissions } = useSelector((state: RootState) => state.user) - const { patient } = useSelector((state: RootState) => state.patient) const [showAddCarePlanModal, setShowAddCarePlanModal] = useState(false) - const { patientId } = props - console.log(patientId) - console.log(patient.carePlans) return ( <>
@@ -39,26 +32,14 @@ export const CarePlanTab = (props: Props) => {

- - - - - - - - - - - {patient.carePlans?.map((carePlan) => ( - - - - - - - ))} - -
{t('patient.carePlan.title')}{t('patient.carePlan.startDate')}{t('patient.carePlan.endDate')}{t('patient.carePlan.status')}
{carePlan.title}{format(new Date(carePlan.startDate), 'yyyy-MM-dd')}{format(new Date(carePlan.endDate), 'yyyy-MM-dd')}{carePlan.status}
+ + + + + + + + setShowAddCarePlanModal(false)} diff --git a/src/patients/care-plans/CarePlanTable.tsx b/src/patients/care-plans/CarePlanTable.tsx new file mode 100644 index 0000000000..d621315078 --- /dev/null +++ b/src/patients/care-plans/CarePlanTable.tsx @@ -0,0 +1,50 @@ +import { Button } from '@hospitalrun/components' +import format from 'date-fns/format' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import CarePlan from '../../model/CarePlan' +import { RootState } from '../../store' + +const CarePlanTable = () => { + const history = useHistory() + const { t } = useTranslation() + const { patient } = useSelector((state: RootState) => state.patient) + + const onViewClick = (carePlan: CarePlan) => { + history.push(`/patients/${patient.id}/care-plans/${carePlan.id}`) + } + + return ( + + + + + + + + + + + + {patient.carePlans?.map((carePlan) => ( + + + + + + + + ))} + +
{t('patient.carePlan.title')}{t('patient.carePlan.startDate')}{t('patient.carePlan.endDate')}{t('patient.carePlan.status')}{t('actions.label')}
{carePlan.title}{format(new Date(carePlan.startDate), 'yyyy-MM-dd')}{format(new Date(carePlan.endDate), 'yyyy-MM-dd')}{carePlan.status} + +
+ ) +} + +export default CarePlanTable diff --git a/src/patients/care-plans/ViewCarePlan.tsx b/src/patients/care-plans/ViewCarePlan.tsx new file mode 100644 index 0000000000..e94297e08b --- /dev/null +++ b/src/patients/care-plans/ViewCarePlan.tsx @@ -0,0 +1,34 @@ +import findLast from 'lodash/findLast' +import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' +import { useParams } from 'react-router' + +import CarePlan from '../../model/CarePlan' +import { RootState } from '../../store' +import CarePlanForm from './CarePlanForm' + +const ViewCarePlan = () => { + const { patient } = useSelector((root: RootState) => root.patient) + const { carePlanId } = useParams() + + const [carePlan, setCarePlan] = useState() + + useEffect(() => { + if (patient && carePlanId) { + const currentCarePlan = findLast(patient.carePlans, (c: CarePlan) => c.id === carePlanId) + setCarePlan(currentCarePlan) + } + }, [setCarePlan, carePlanId, patient]) + + if (carePlan) { + return ( + <> +

{carePlan?.title}

+ + + ) + } + return <> +} + +export default ViewCarePlan diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index e9857d2a37..3f8b80b5ee 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { isAfter, parseISO } from 'date-fns' -import _ from 'lodash' +import { isAfter, isBefore, parseISO } from 'date-fns' +import { isEmpty } from 'lodash' import validator from 'validator' import PatientRepository from '../clients/db/PatientRepository' @@ -62,6 +62,7 @@ interface AddNoteError { } interface AddCarePlanError { + message?: string title?: string description?: string status?: string @@ -133,6 +134,10 @@ const patientSlice = createSlice({ state.status = 'error' state.noteError = payload }, + addCarePlanError(state, { payload }: PayloadAction) { + state.status = 'error' + state.carePlanError = payload + }, }, }) @@ -149,6 +154,7 @@ export const { addDiagnosisError, addRelatedPersonError, addNoteError, + addCarePlanError, } = patientSlice.actions export const fetchPatient = (id: string): AppThunk => async (dispatch) => { @@ -221,7 +227,7 @@ export const createPatient = ( const newPatientError = validatePatient(patient) - if (_.isEmpty(newPatientError)) { + if (isEmpty(newPatientError)) { const newPatient = await PatientRepository.save(patient) dispatch(createPatientSuccess()) @@ -240,7 +246,7 @@ export const updatePatient = ( ): AppThunk => async (dispatch) => { dispatch(updatePatientStart()) const updateError = validatePatient(patient) - if (_.isEmpty(updateError)) { + if (isEmpty(updateError)) { const updatedPatient = await PatientRepository.saveOrUpdate(patient) dispatch(updatePatientSuccess(updatedPatient)) @@ -274,7 +280,7 @@ export const addRelatedPerson = ( ): AppThunk => async (dispatch) => { const newRelatedPersonError = validateRelatedPerson(relatedPerson) - if (_.isEmpty(newRelatedPersonError)) { + if (isEmpty(newRelatedPersonError)) { const patient = await PatientRepository.find(patientId) const relatedPersons = patient.relatedPersons || [] relatedPersons.push({ id: uuid(), ...relatedPerson }) @@ -319,7 +325,7 @@ export const addDiagnosis = ( ): AppThunk => async (dispatch) => { const newDiagnosisError = validateDiagnosis(diagnosis) - if (_.isEmpty(newDiagnosisError)) { + if (isEmpty(newDiagnosisError)) { const patient = await PatientRepository.find(patientId) const diagnoses = patient.diagnoses || [] diagnoses.push({ id: uuid(), ...diagnosis }) @@ -349,7 +355,7 @@ export const addAllergy = ( ): AppThunk => async (dispatch) => { const newAllergyError = validateAllergy(allergy) - if (_.isEmpty(newAllergyError)) { + if (isEmpty(newAllergyError)) { const patient = await PatientRepository.find(patientId) const allergies = patient.allergies || [] allergies.push({ id: uuid(), ...allergy }) @@ -378,7 +384,7 @@ export const addNote = ( ): AppThunk => async (dispatch) => { const newNoteError = validateNote(note) - if (_.isEmpty(newNoteError)) { + if (isEmpty(newNoteError)) { const patient = await PatientRepository.find(patientId) const notes = patient.notes || [] notes.push({ id: uuid(), date: new Date().toISOString(), ...note }) @@ -391,21 +397,71 @@ export const addNote = ( } } +function validateCarePlan(carePlan: CarePlan): AddCarePlanError { + const error: AddCarePlanError = {} + + if (!carePlan.title) { + error.title = 'patient.carePlans.error.titleRequired' + } + + if (!carePlan.description) { + error.description = 'patient.carePlans.error.descriptionRequired' + } + + if (!carePlan.status) { + error.status = 'patient.carePlans.error.statusRequired' + } + + if (!carePlan.intent) { + error.intent = 'patient.carePlans.error.intentRequired' + } + + if (!carePlan.startDate) { + error.startDate = 'patient.carePlans.error.startDateRequired' + } + + if (!carePlan.endDate) { + error.endDate = 'patient.carePlans.error.endDateRequired' + } + + if (carePlan.startDate && carePlan.endDate) { + if (isBefore(new Date(carePlan.endDate), new Date(carePlan.startDate))) { + error.endDate = 'patient.carePlans.error.endDateMustBeAfterStartDate' + } + } + + if (!carePlan.diagnosisId) { + error.condition = 'patient.carePlans.error.conditionRequired' + } + + if (!carePlan.note) { + error.note = 'patient.carePlans.error.noteRequired' + } + + return error +} + export const addCarePlan = ( patientId: string, carePlan: CarePlan, onSuccess?: (patient: Patient) => void, ): AppThunk => async (dispatch) => { - const patient = await PatientRepository.find(patientId) - const carePlans = patient.carePlans || ([] as CarePlan[]) - carePlans.push({ - id: uuid(), - createdOn: new Date(Date.now().valueOf()).toISOString(), - ...carePlan, - }) - patient.carePlans = carePlans + const carePlanError = validateCarePlan(carePlan) + if (isEmpty(carePlanError)) { + const patient = await PatientRepository.find(patientId) + const carePlans = patient.carePlans || ([] as CarePlan[]) + carePlans.push({ + id: uuid(), + createdOn: new Date(Date.now().valueOf()).toISOString(), + ...carePlan, + }) + patient.carePlans = carePlans - await dispatch(updatePatient(patient, onSuccess)) + await dispatch(updatePatient(patient, onSuccess)) + } else { + carePlanError.message = 'patient.carePlans.error.unableToAdd' + dispatch(addCarePlanError(carePlanError)) + } } export default patientSlice.reducer diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 3e6432868b..8f4270d9d4 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -12,6 +12,7 @@ import useTitle from '../../page-header/useTitle' import { RootState } from '../../store' import Allergies from '../allergies/Allergies' import AppointmentsList from '../appointments/AppointmentsList' +import CarePlanTab from '../care-plans/CarePlanTab' import Diagnoses from '../diagnoses/Diagnoses' import GeneralInformation from '../GeneralInformation' import Labs from '../labs/LabsTab' @@ -19,7 +20,6 @@ import Note from '../notes/NoteTab' import { fetchPatient } from '../patient-slice' import RelatedPerson from '../related-persons/RelatedPersonTab' import { getPatientFullName } from '../util/patient-name-util' -import CarePlanTab from "../care-plans/CarePlanTab"; const getPatientCode = (p: Patient): string => { if (p) { @@ -148,8 +148,8 @@ const ViewPatient = () => { - - + + diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index c48dd0bc2a..cd25a963c6 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -26,7 +26,7 @@ const initialState: UserState = { Permissions.ViewIncidents, Permissions.ReportIncident, Permissions.AddCarePlan, - Permissions.ReadCarePlan + Permissions.ReadCarePlan, ], user: { id: 'some-hardcoded-id', From 72a5bede7862c72eecb068c0c20141ef619a8f27 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Wed, 3 Jun 2020 22:15:58 -0500 Subject: [PATCH 4/6] feat(care plan): fix internationalization --- src/__tests__/patients/patient-slice.test.ts | 20 +++++++++---------- .../enUs/translations/patient/index.ts | 10 ++++++++++ src/patients/care-plans/CarePlanForm.tsx | 16 +++++++-------- src/patients/patient-slice.ts | 20 +++++++++---------- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index 1d7d4b426b..9ce0c2ddf6 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -686,15 +686,15 @@ describe('patients slice', () => { it('should validate the required fields', async () => { const expectedError = { - message: 'patient.carePlans.error.unableToAdd', - title: 'patient.carePlans.error.titleRequired', - description: 'patient.carePlans.error.descriptionRequired', - status: 'patient.carePlans.error.statusRequired', - intent: 'patient.carePlans.error.intentRequired', - startDate: 'patient.carePlans.error.startDateRequired', - endDate: 'patient.carePlans.error.endDateRequired', - condition: 'patient.carePlans.error.conditionRequired', - note: 'patient.carePlans.error.noteRequired', + message: 'patient.carePlan.error.unableToAdd', + title: 'patient.carePlan.error.titleRequired', + description: 'patient.carePlan.error.descriptionRequired', + status: 'patient.carePlan.error.statusRequired', + intent: 'patient.carePlan.error.intentRequired', + startDate: 'patient.carePlan.error.startDateRequired', + endDate: 'patient.carePlan.error.endDateRequired', + condition: 'patient.carePlan.error.conditionRequired', + note: 'patient.carePlan.error.noteRequired', } const store = mockStore() const expectedCarePlan = {} as CarePlan @@ -719,7 +719,7 @@ describe('patients slice', () => { expect(store.getActions()[0]).toEqual( addCarePlanError( expect.objectContaining({ - endDate: 'patient.carePlans.error.endDateMustBeAfterStartDate', + endDate: 'patient.carePlan.error.endDateMustBeAfterStartDate', }), ), ) diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index 9f331021a8..e8ca987bd9 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -104,6 +104,16 @@ export default { startDate: 'Start Date', endDate: 'End Date', note: 'Note', + error: { + unableToAdd: 'Unable to add a new care plan.', + titleRequired: 'Title is required.', + descriptionRequired: 'Description is required.', + conditionRequired: 'Condition is required.', + statusRequired: 'Status is required.', + intentRequired: 'Intent is required.', + startDate: 'Start date is required.', + endDate: 'End date is required', + } }, types: { charity: 'Charity', diff --git a/src/patients/care-plans/CarePlanForm.tsx b/src/patients/care-plans/CarePlanForm.tsx index 95d8ecebee..0845250a40 100644 --- a/src/patients/care-plans/CarePlanForm.tsx +++ b/src/patients/care-plans/CarePlanForm.tsx @@ -44,7 +44,7 @@ const CarePlanForm = (props: Props) => { return ( - {carePlanError?.message && } + {carePlanError?.message && } { value={carePlan.title} label={t('patient.carePlan.title')} name="title" - feedback={carePlanError?.title} + feedback={t(carePlanError?.title || '')} isInvalid={!!carePlanError?.title} isEditable={!disabled} onChange={(event) => onFieldChange('title', event.currentTarget.value)} @@ -66,7 +66,7 @@ const CarePlanForm = (props: Props) => { value={carePlan.description} label={t('patient.carePlan.description')} name="description" - feedback={carePlanError?.description} + feedback={t(carePlanError?.description || '')} isInvalid={!!carePlanError?.description} isEditable={!disabled} onChange={(event) => onFieldChange('description', event.currentTarget.value)} @@ -80,7 +80,7 @@ const CarePlanForm = (props: Props) => { value={carePlan.diagnosisId} label={t('patient.carePlan.condition')} name="condition" - feedback={carePlanError?.condition} + feedback={t(carePlanError?.condition || '')} isInvalid={!!carePlanError?.condition} isEditable={!disabled} onChange={(event) => onFieldChange('diagnosisId', event.currentTarget.value)} @@ -95,7 +95,7 @@ const CarePlanForm = (props: Props) => { value={carePlan.status} label={t('patient.carePlan.status')} name="status" - feedback={carePlanError?.status} + feedback={t(carePlanError?.status || '')} isInvalid={!!carePlanError?.status} isEditable={!disabled} options={Object.values(CarePlanStatus).map((v) => ({ label: v, value: v }))} @@ -108,7 +108,7 @@ const CarePlanForm = (props: Props) => { value={carePlan.intent} label={t('patient.carePlan.intent')} name="intent" - feedback={carePlanError?.intent} + feedback={t(carePlanError?.intent || '')} isInvalid={!!carePlanError?.intent} isEditable={!disabled} options={Object.values(CarePlanIntent).map((v) => ({ label: v, value: v }))} @@ -123,7 +123,7 @@ const CarePlanForm = (props: Props) => { value={carePlan.startDate ? new Date(carePlan.startDate) : new Date()} label={t('patient.carePlan.startDate')} name="startDate" - feedback={carePlanError?.startDate} + feedback={t(carePlanError?.startDate || '')} isInvalid={!!carePlanError?.startDate} isEditable={!disabled} onChange={(date) => onFieldChange('startDate', date.toISOString())} @@ -135,7 +135,7 @@ const CarePlanForm = (props: Props) => { value={carePlan.endDate ? new Date(carePlan.endDate) : new Date()} label={t('patient.carePlan.endDate')} name="endDate" - feedback={carePlanError?.endDate} + feedback={t(carePlanError?.endDate || '')} isInvalid={!!carePlanError?.endDate} isEditable={!disabled} onChange={(date) => onFieldChange('endDate', date.toISOString())} diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index 3f8b80b5ee..2e98e75b89 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -401,41 +401,41 @@ function validateCarePlan(carePlan: CarePlan): AddCarePlanError { const error: AddCarePlanError = {} if (!carePlan.title) { - error.title = 'patient.carePlans.error.titleRequired' + error.title = 'patient.carePlan.error.titleRequired' } if (!carePlan.description) { - error.description = 'patient.carePlans.error.descriptionRequired' + error.description = 'patient.carePlan.error.descriptionRequired' } if (!carePlan.status) { - error.status = 'patient.carePlans.error.statusRequired' + error.status = 'patient.carePlan.error.statusRequired' } if (!carePlan.intent) { - error.intent = 'patient.carePlans.error.intentRequired' + error.intent = 'patient.carePlan.error.intentRequired' } if (!carePlan.startDate) { - error.startDate = 'patient.carePlans.error.startDateRequired' + error.startDate = 'patient.carePlan.error.startDateRequired' } if (!carePlan.endDate) { - error.endDate = 'patient.carePlans.error.endDateRequired' + error.endDate = 'patient.carePlan.error.endDateRequired' } if (carePlan.startDate && carePlan.endDate) { if (isBefore(new Date(carePlan.endDate), new Date(carePlan.startDate))) { - error.endDate = 'patient.carePlans.error.endDateMustBeAfterStartDate' + error.endDate = 'patient.carePlan.error.endDateMustBeAfterStartDate' } } if (!carePlan.diagnosisId) { - error.condition = 'patient.carePlans.error.conditionRequired' + error.condition = 'patient.carePlan.error.conditionRequired' } if (!carePlan.note) { - error.note = 'patient.carePlans.error.noteRequired' + error.note = 'patient.carePlan.error.noteRequired' } return error @@ -459,7 +459,7 @@ export const addCarePlan = ( await dispatch(updatePatient(patient, onSuccess)) } else { - carePlanError.message = 'patient.carePlans.error.unableToAdd' + carePlanError.message = 'patient.carePlan.error.unableToAdd' dispatch(addCarePlanError(carePlanError)) } } From 8821c9c571593f4b52ea3e1dae70be670551587f Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Wed, 3 Jun 2020 22:33:43 -0500 Subject: [PATCH 5/6] style(lint): fix lint --- src/locales/enUs/translations/patient/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index e8ca987bd9..dc46024ba2 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -113,7 +113,7 @@ export default { intentRequired: 'Intent is required.', startDate: 'Start date is required.', endDate: 'End date is required', - } + }, }, types: { charity: 'Charity', From 080e286429a09adb6612c72f18328ae373d50e75 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2020 12:29:43 +0000 Subject: [PATCH 6/6] chore(deps-dev): bump @testing-library/react-hooks from 3.2.1 to 3.3.0 Bumps [@testing-library/react-hooks](https://github.com/testing-library/react-hooks-testing-library) from 3.2.1 to 3.3.0. - [Release notes](https://github.com/testing-library/react-hooks-testing-library/releases) - [Commits](https://github.com/testing-library/react-hooks-testing-library/compare/v3.2.1...v3.3.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ceeba1c71..4afcda7bcc 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@commitlint/core": "~8.3.5", "@commitlint/prompt": "~8.3.5", "@testing-library/react": "~10.1.0", - "@testing-library/react-hooks": "~3.2.1", + "@testing-library/react-hooks": "~3.3.0", "@types/enzyme": "^3.10.5", "@types/jest": "~25.2.0", "@types/lodash": "^4.14.150",