From 8eb8f6f0910d8259f15c1878249e630ff3a8be18 Mon Sep 17 00:00:00 2001 From: Rafael Sousa Date: Mon, 7 Sep 2020 14:42:08 -0300 Subject: [PATCH] feat(caregoal): add care goals to patient (#2360) * feat(caregoal): add care goals to patient * feat(caregoal): improve tests * feat(caregoal): applying recommended changes from review Co-authored-by: Maksim Sinik Co-authored-by: Jack Meyer --- .../care-goals/AddCareGoalModal.test.tsx | 101 ++++++ .../patients/care-goals/CareGoalForm.test.tsx | 292 ++++++++++++++++++ .../patients/care-goals/CareGoalTab.test.tsx | 111 +++++++ .../care-goals/CareGoalTable.test.tsx | 96 ++++++ .../patients/care-goals/ViewCareGoal.test.tsx | 48 +++ .../care-goals/ViewCareGoals.test.tsx | 42 +++ .../patients/hooks/useAddCareGoal.test.tsx | 74 +++++ .../patients/util/validate-caregoal.test.ts | 34 ++ .../patients/view/ViewPatient.test.tsx | 26 +- src/patients/care-goals/AddCareGoalModal.tsx | 82 +++++ src/patients/care-goals/CareGoalForm.tsx | 183 +++++++++++ src/patients/care-goals/CareGoalTab.tsx | 61 ++++ src/patients/care-goals/CareGoalTable.tsx | 66 ++++ src/patients/care-goals/ViewCareGoal.tsx | 19 ++ src/patients/care-goals/ViewCareGoals.tsx | 12 + src/patients/hooks/useAddCareGoal.tsx | 47 +++ src/patients/hooks/useCareGoal.tsx | 23 ++ src/patients/hooks/usePatientCareGoals.tsx | 13 + src/patients/util/validate-caregoal.ts | 74 +++++ src/patients/view/ViewPatient.tsx | 9 + .../enUs/translations/patient/index.ts | 33 ++ src/shared/model/CareGoal.ts | 33 ++ src/shared/model/Patient.ts | 2 + src/shared/model/Permissions.ts | 2 + src/user/user-slice.ts | 2 + 25 files changed, 1483 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx create mode 100644 src/__tests__/patients/care-goals/CareGoalForm.test.tsx create mode 100644 src/__tests__/patients/care-goals/CareGoalTab.test.tsx create mode 100644 src/__tests__/patients/care-goals/CareGoalTable.test.tsx create mode 100644 src/__tests__/patients/care-goals/ViewCareGoal.test.tsx create mode 100644 src/__tests__/patients/care-goals/ViewCareGoals.test.tsx create mode 100644 src/__tests__/patients/hooks/useAddCareGoal.test.tsx create mode 100644 src/__tests__/patients/util/validate-caregoal.test.ts create mode 100644 src/patients/care-goals/AddCareGoalModal.tsx create mode 100644 src/patients/care-goals/CareGoalForm.tsx create mode 100644 src/patients/care-goals/CareGoalTab.tsx create mode 100644 src/patients/care-goals/CareGoalTable.tsx create mode 100644 src/patients/care-goals/ViewCareGoal.tsx create mode 100644 src/patients/care-goals/ViewCareGoals.tsx create mode 100644 src/patients/hooks/useAddCareGoal.tsx create mode 100644 src/patients/hooks/useCareGoal.tsx create mode 100644 src/patients/hooks/usePatientCareGoals.tsx create mode 100644 src/patients/util/validate-caregoal.ts create mode 100644 src/shared/model/CareGoal.ts diff --git a/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx b/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx new file mode 100644 index 0000000000..b8b23551b0 --- /dev/null +++ b/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx @@ -0,0 +1,101 @@ +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 { Router } from 'react-router-dom' + +import AddCareGoalModal from '../../../patients/care-goals/AddCareGoalModal' +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('Add Care Goal Modal', () => { + const patient = { + givenName: 'given Name', + fullName: 'full name', + careGoals: [] as CareGoal[], + } as Patient + + const onCloseSpy = jest.fn() + const setup = () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + jest.spyOn(PatientRepository, 'saveOrUpdate') + const history = createMemoryHistory() + const wrapper = mount( + + + , + ) + + wrapper.update() + return { wrapper } + } + + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render a modal', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + const sucessButton = modal.prop('successButton') + const closeButton = modal.prop('closeButton') + + expect(modal).toHaveLength(1) + expect(modal.prop('title')).toEqual('patient.careGoal.new') + expect(sucessButton?.children).toEqual('patient.careGoal.new') + expect(sucessButton?.icon).toEqual('add') + expect(closeButton?.children).toEqual('actions.cancel') + }) + + it('should render a care goal form', () => { + const { wrapper } = setup() + + const careGoalForm = wrapper.find(CareGoalForm) + expect(careGoalForm).toHaveLength(1) + }) + + it('should save care goal when save button is clicked and close', async () => { + const expectedCreatedDate = new Date() + Date.now = jest.fn().mockReturnValue(expectedCreatedDate) + + const expectedCareGoal = { + id: '123', + description: 'some description', + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, + createdOn: expectedCreatedDate, + } + + const { wrapper } = setup() + await act(async () => { + const careGoalForm = wrapper.find(CareGoalForm) + const onChange = careGoalForm.prop('onChange') as any + await onChange(expectedCareGoal) + }) + + wrapper.update() + + await act(async () => { + const modal = wrapper.find(Modal) + const sucessButton = modal.prop('successButton') + const onClick = sucessButton?.onClick as any + await onClick() + }) + + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith({ + ...patient, + careGoals: [expectedCareGoal], + }) + + expect(onCloseSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalForm.test.tsx b/src/__tests__/patients/care-goals/CareGoalForm.test.tsx new file mode 100644 index 0000000000..fc94456ab9 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalForm.test.tsx @@ -0,0 +1,292 @@ +import { Alert } from '@hospitalrun/components' +import { addMonths, addDays } from 'date-fns' +import { mount } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' + +describe('Care Goal Form', () => { + const onCareGoalChangeSpy = jest.fn() + const careGoal = { + description: 'some description', + startDate: new Date().toISOString(), + dueDate: addMonths(new Date(), 1).toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, + } as CareGoal + + const setup = (disabled = false, initializeCareGoal = true, error?: any) => { + const wrapper = mount( + , + ) + + return wrapper + } + + 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('label')).toEqual('patient.careGoal.description') + expect(descriptionInput.prop('isRequired')).toBeTruthy() + expect(descriptionInput.prop('value')).toBe(careGoal.description) + }) + + it('should call onChange handler when description changes', () => { + const expectedDescription = '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: expectedDescription } }) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ description: expectedDescription }) + }) + + it('should render a priority selector', () => { + const wrapper = setup() + + const priority = wrapper.findWhere((w) => w.prop('name') === 'priority') + + expect(priority).toHaveLength(1) + expect(priority.prop('label')).toEqual('patient.careGoal.priority.label') + expect(priority.prop('isRequired')).toBeTruthy() + expect(priority.prop('defaultSelected')[0].value).toBe(careGoal.priority) + expect(priority.prop('options')).toEqual([ + { + label: 'patient.careGoal.priority.low', + value: 'low', + }, + { + label: 'patient.careGoal.priority.medium', + value: 'medium', + }, + { + label: 'patient.careGoal.priority.high', + value: 'high', + }, + ]) + }) + + it('should call onChange handler when priority changes', () => { + const expectedPriority = 'high' + const wrapper = setup(false, false) + + act(() => { + const prioritySelector = wrapper.findWhere((w) => w.prop('name') === 'priority') + const onChange = prioritySelector.prop('onChange') as any + onChange([expectedPriority]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ priority: expectedPriority }) + }) + + it('should render a status selector', () => { + const wrapper = setup() + + const status = wrapper.findWhere((w) => w.prop('name') === 'status') + + expect(status).toHaveLength(1) + expect(status.prop('label')).toEqual('patient.careGoal.status') + expect(status.prop('isRequired')).toBeTruthy() + expect(status.prop('defaultSelected')[0].value).toBe(careGoal.status) + expect(status.prop('options')).toEqual( + Object.values(CareGoalStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call onChange handler when status changes', () => { + const expectedStatus = CareGoalStatus.OnHold + const wrapper = setup(false, false) + + act(() => { + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const onChange = statusSelector.prop('onChange') as any + onChange([expectedStatus]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ status: expectedStatus }) + }) + + it('should render the achievement status selector', () => { + const wrapper = setup() + + const achievementStatus = wrapper.findWhere((w) => w.prop('name') === 'achievementStatus') + expect(achievementStatus).toHaveLength(1) + expect(achievementStatus.prop('label')).toEqual('patient.careGoal.achievementStatus') + expect(achievementStatus.prop('isRequired')).toBeTruthy() + expect(achievementStatus.prop('defaultSelected')[0].value).toBe(careGoal.achievementStatus) + expect(achievementStatus.prop('options')).toEqual( + Object.values(CareGoalAchievementStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call onChange handler when achievement status change', () => { + const expectedAchievementStatus = CareGoalAchievementStatus.Improving + const wrapper = setup(false, false) + + act(() => { + const achievementStatusSelector = wrapper.findWhere( + (w) => w.prop('name') === 'achievementStatus', + ) + const onChange = achievementStatusSelector.prop('onChange') as any + onChange([expectedAchievementStatus]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ + achievementStatus: expectedAchievementStatus, + }) + }) + + 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('label')).toEqual('patient.careGoal.startDate') + expect(startDatePicker.prop('isRequired')).toBeTruthy() + expect(startDatePicker.prop('value')).toEqual(new Date(careGoal.startDate)) + }) + + it('should call onChange handler when start date change', () => { + const expectedStartDate = addDays(1, new Date().getDate()) + const wrapper = setup(false, false) + + act(() => { + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const onChange = startDatePicker.prop('onChange') as any + onChange(expectedStartDate) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ startDate: expectedStartDate.toISOString() }) + }) + + it('should render a due date picker', () => { + const wrapper = setup() + + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + expect(dueDatePicker).toHaveLength(1) + expect(dueDatePicker.prop('label')).toEqual('patient.careGoal.dueDate') + expect(dueDatePicker.prop('isRequired')).toBeTruthy() + expect(dueDatePicker.prop('value')).toEqual(new Date(careGoal.dueDate)) + }) + + it('should call onChange handler when due date change', () => { + const expectedDueDate = addDays(31, new Date().getDate()) + const wrapper = setup(false, false) + + act(() => { + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const onChange = dueDatePicker.prop('onChange') as any + onChange(expectedDueDate) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ dueDate: expectedDueDate.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('label')).toEqual('patient.careGoal.note') + expect(noteInput.prop('isRequired')).toBeFalsy() + expect(noteInput.prop('value')).toEqual(careGoal.note) + }) + + it('should call onChange handler when note change', () => { + const expectedNote = 'some new note' + const wrapper = setup(false, false) + + act(() => { + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + const onChange = noteInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNote } }) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ note: expectedNote }) + }) + + it('should render all the forms fields disabled if the form is disabled', () => { + const wrapper = setup(true) + + const description = wrapper.findWhere((w) => w.prop('name') === 'description') + const priority = wrapper.findWhere((w) => w.prop('name') === 'priority') + const status = wrapper.findWhere((w) => w.prop('name') === 'status') + const achievementStatus = wrapper.findWhere((w) => w.prop('name') === 'achievementStatus') + const startDate = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const dueDate = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const note = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(description.prop('isEditable')).toBeFalsy() + expect(priority.prop('isEditable')).toBeFalsy() + expect(status.prop('isEditable')).toBeFalsy() + expect(achievementStatus.prop('isEditable')).toBeFalsy() + expect(startDate.prop('isEditable')).toBeFalsy() + expect(dueDate.prop('isEditable')).toBeFalsy() + expect(note.prop('isEditable')).toBeFalsy() + }) + + it('should render the forms field in an error state', () => { + const expectedError = { + message: 'some error message', + description: 'some description error', + status: 'some status error', + achievementStatus: 'some achievement status error', + priority: 'some priority error', + startDate: 'some start date error', + dueDate: 'some due date error', + note: 'some note error', + } + + const wrapper = setup(false, false, expectedError) + + const alert = wrapper.find(Alert) + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const prioritySelector = wrapper.findWhere((w) => w.prop('name') === 'priority') + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const achievementStatusSelector = wrapper.findWhere( + (w) => w.prop('name') === 'achievementStatus', + ) + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(alert).toHaveLength(1) + expect(alert.prop('message')).toEqual(expectedError.message) + + expect(descriptionInput.prop('isInvalid')).toBeTruthy() + expect(descriptionInput.prop('feedback')).toEqual(expectedError.description) + + expect(prioritySelector.prop('isInvalid')).toBeTruthy() + // expect(prioritySelector.prop('feedback')).toEqual(expectedError.priority) + + expect(statusSelector.prop('isInvalid')).toBeTruthy() + // expect(statusSelector.prop('feedback')).toEqual(expectedError.status) + + expect(achievementStatusSelector.prop('isInvalid')).toBeTruthy() + // expect(achievementStatusSelector.prop('feedback')).toEqual(expectedError.achievementStatus) + + expect(startDatePicker.prop('isInvalid')).toBeTruthy() + expect(startDatePicker.prop('feedback')).toEqual(expectedError.startDate) + + expect(dueDatePicker.prop('isInvalid')).toBeTruthy() + expect(dueDatePicker.prop('feedback')).toEqual(expectedError.dueDate) + + expect(noteInput.prop('isInvalid')).toBeTruthy() + expect(noteInput.prop('feedback')).toEqual(expectedError.note) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalTab.test.tsx b/src/__tests__/patients/care-goals/CareGoalTab.test.tsx new file mode 100644 index 0000000000..57b0d42877 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalTab.test.tsx @@ -0,0 +1,111 @@ +import { mount, ReactWrapper } 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 AddCareGoalModal from '../../../patients/care-goals/AddCareGoalModal' +import CareGoalTab from '../../../patients/care-goals/CareGoalTab' +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import ViewCareGoal from '../../../patients/care-goals/ViewCareGoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('Care Goals Tab', () => { + const patient = { id: 'patientId' } as Patient + + const setup = async (route: string, permissions: Permissions[]) => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const store = mockStore({ user: { permissions } } as any) + const history = createMemoryHistory() + history.push(route) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + + return wrapper as ReactWrapper + } + + it('should render add care goal button if user has correct permissions', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + const addNewButton = wrapper.find('Button').at(0) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.careGoal.new') + }) + + it('should not render add care goal button if user does not have permissions', async () => { + const wrapper = await setup('patients/123/care-goals', []) + + const addNewButton = wrapper.find('Button') + expect(addNewButton).toHaveLength(0) + }) + + it('should open the add care goal modal on click', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + await act(async () => { + const addNewButton = wrapper.find('Button').at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + const modal = wrapper.find(AddCareGoalModal) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should close the modal when the close button is clicked', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + await act(async () => { + const addNewButton = wrapper.find('Button').at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + await act(async () => { + const modal = wrapper.find(AddCareGoalModal) + const onClose = modal.prop('onCloseButtonClick') as any + onClose() + }) + + wrapper.update() + + const modal = wrapper.find(AddCareGoalModal) + expect(modal.prop('show')).toBeFalsy() + }) + + it('should render care goal table when on patients/123/care-goals', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.ReadCareGoal]) + + const careGoalTable = wrapper.find(CareGoalTable) + expect(careGoalTable).toHaveLength(1) + }) + + it('should render care goal view when on patients/123/care-goals/456', async () => { + const wrapper = await setup('patients/123/care-goals/456', [Permissions.ReadCareGoal]) + + const viewCareGoal = wrapper.find(ViewCareGoal) + expect(viewCareGoal).toHaveLength(1) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalTable.test.tsx b/src/__tests__/patients/care-goals/CareGoalTable.test.tsx new file mode 100644 index 0000000000..0917055e96 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalTable.test.tsx @@ -0,0 +1,96 @@ +import { Table, Alert } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('Care Goal Table', () => { + const careGoal: CareGoal = { + id: '123', + description: 'some description', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.Improving, + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + createdOn: new Date().toISOString(), + note: 'some note', + } + + const patient = { + givenName: 'given Name', + fullName: 'full Name', + careGoals: [careGoal], + } as Patient + + const setup = async (expectedPatient = patient) => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals/${patient.careGoals[0].id}`) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + , + ) + }) + wrapper.update() + return { wrapper: wrapper as ReactWrapper, history } + } + + it('should render a table', async () => { + const { wrapper } = await setup() + + const table = wrapper.find(Table) + const columns = table.prop('columns') + + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.description', key: 'description' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.startDate', key: 'startDate' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.dueDate', key: 'dueDate' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.status', key: 'status' }), + ) + + const actions = table.prop('actions') as any + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(patient.careGoals) + }) + + it('should navigate to the care goal view when the view details button is clicked', async () => { + const { wrapper, history } = await setup() + + const tr = wrapper.find('tr').at(1) + + act(() => { + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-goals/${careGoal.id}`) + }) + + it('should display a warning if there are no care goals', async () => { + const { wrapper } = await setup({ ...patient, careGoals: [] }) + + expect(wrapper.exists(Alert)).toBeTruthy() + const alert = wrapper.find(Alert) + expect(alert.prop('color')).toEqual('warning') + expect(alert.prop('title')).toEqual('patient.careGoals.warning.noCareGoals') + expect(alert.prop('message')).toEqual('patient.careGoals.warning.addCareGoalAbove') + }) +}) diff --git a/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx b/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx new file mode 100644 index 0000000000..380c4f8f20 --- /dev/null +++ b/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx @@ -0,0 +1,48 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router, Route } from 'react-router-dom' + +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import ViewCareGoal from '../../../patients/care-goals/ViewCareGoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' + +describe('View Care Goal', () => { + const patient = { + id: '123', + givenName: 'given Name', + fullName: 'full Name', + careGoals: [{ id: '123', description: 'some description' }], + } as Patient + + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals/${patient.careGoals[0].id}`) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render the care goal form', async () => { + const { wrapper } = await setup() + const careGoalForm = wrapper.find(CareGoalForm) + + expect(careGoalForm).toHaveLength(1) + expect(careGoalForm.prop('careGoal')).toEqual(patient.careGoals[0]) + }) +}) diff --git a/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx b/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx new file mode 100644 index 0000000000..a80f7a7671 --- /dev/null +++ b/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx @@ -0,0 +1,42 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Route, Router } from 'react-router-dom' + +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import ViewCareGoals from '../../../patients/care-goals/ViewCareGoals' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('View Care Goals', () => { + const patient = { id: '123', careGoals: [] as CareGoal[] } as Patient + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals`) + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render a care goals table with the patient id', async () => { + const { wrapper } = await setup() + + expect(wrapper.exists(CareGoalTable)).toBeTruthy() + const careGoalTable = wrapper.find(CareGoalTable) + expect(careGoalTable.prop('patientId')).toEqual(patient.id) + }) +}) diff --git a/src/__tests__/patients/hooks/useAddCareGoal.test.tsx b/src/__tests__/patients/hooks/useAddCareGoal.test.tsx new file mode 100644 index 0000000000..be5885447e --- /dev/null +++ b/src/__tests__/patients/hooks/useAddCareGoal.test.tsx @@ -0,0 +1,74 @@ +import useAddCareGoal from '../../../patients/hooks/useAddCareGoal' +import { CareGoalError } from '../../../patients/util/validate-caregoal' +import * as validateCareGoal from '../../../patients/util/validate-caregoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add care goal', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should add a care goal to the patient', async () => { + const expectedDate = new Date() + Date.now = jest.fn().mockReturnValue(expectedDate) + + const expectedCareGoal: CareGoal = { + id: '123', + description: 'some description', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.Improving, + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + note: 'some note', + createdOn: expectedDate.toISOString(), + } + + const givenPatient = { + id: '123', + givenName: 'given name', + fullName: 'full name', + careGoals: [] as CareGoal[], + } as Patient + + const expectedPatient = { + ...givenPatient, + careGoals: [expectedCareGoal], + } as Patient + + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedCareGoal.id) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + + const result = await executeMutation(() => useAddCareGoal(), { + patientId: givenPatient.id, + careGoal: expectedCareGoal, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(result).toEqual([expectedCareGoal]) + }) + + it('should throw an error if validation fails', async () => { + const expectedError = { + message: 'patient.careGoal.error.unableToAdd', + description: 'some error', + } + jest.spyOn(validateCareGoal, 'default').mockReturnValue(expectedError as CareGoalError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddCareGoal(), { patientId: '123', careGoal: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) +}) diff --git a/src/__tests__/patients/util/validate-caregoal.test.ts b/src/__tests__/patients/util/validate-caregoal.test.ts new file mode 100644 index 0000000000..f0b52ee989 --- /dev/null +++ b/src/__tests__/patients/util/validate-caregoal.test.ts @@ -0,0 +1,34 @@ +import { subDays } from 'date-fns' + +import validateCareGoal from '../../../patients/util/validate-caregoal' +import CareGoal from '../../../shared/model/CareGoal' + +describe('validate care goal', () => { + it('should validate required fields', () => { + const expectedError = { + description: 'patient.careGoal.error.descriptionRequired', + priority: 'patient.careGoal.error.priorityRequired', + status: 'patient.careGoal.error.statusRequired', + achievementStatus: 'patient.careGoal.error.achievementStatusRequired', + startDate: 'patient.careGoal.error.startDate', + dueDate: 'patient.careGoal.error.dueDate', + } + + const actualError = validateCareGoal({} as CareGoal) + + expect(actualError).toEqual(expectedError) + }) + + it('should validate the start date time is before end date time', () => { + const givenCareGoal = { + startDate: new Date().toISOString(), + dueDate: subDays(new Date(), 1).toISOString(), + } as CareGoal + + const actualError = validateCareGoal(givenCareGoal) + + expect(actualError).toEqual( + expect.objectContaining({ dueDate: 'patient.careGoal.error.dueDateMustBeAfterStartDate' }), + ) + }) +}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 5fd316d904..d666764cbf 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -131,7 +131,7 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(9) + expect(tabs).toHaveLength(10) 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,7 +140,8 @@ describe('ViewPatient', () => { 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') - expect(tabs.at(8).prop('label')).toEqual('patient.visits.label') + expect(tabs.at(8).prop('label')).toEqual('patient.careGoal.label') + expect(tabs.at(9).prop('label')).toEqual('patient.visits.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -320,4 +321,25 @@ describe('ViewPatient', () => { expect(tabs.at(7).prop('active')).toBeTruthy() expect(carePlansTab).toHaveLength(1) }) + + it('should mark the care goals tab as active when it is clicked and render the care goal tab component when route is /patients/:id/care-goals', async () => { + const { wrapper } = await setup() + + await act(async () => { + const tabHeader = wrapper.find(TabsHeader) + const tabs = tabHeader.find(Tab) + const onClick = tabs.at(8).prop('onClick') as any + onClick() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const careGoalsTab = tabs.at(8) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-goals`) + expect(careGoalsTab.prop('active')).toBeTruthy() + expect(careGoalsTab).toHaveLength(1) + }) }) diff --git a/src/patients/care-goals/AddCareGoalModal.tsx b/src/patients/care-goals/AddCareGoalModal.tsx new file mode 100644 index 0000000000..ce47b8b767 --- /dev/null +++ b/src/patients/care-goals/AddCareGoalModal.tsx @@ -0,0 +1,82 @@ +import { Modal } from '@hospitalrun/components' +import { addMonths } from 'date-fns' +import React, { useState, useEffect } from 'react' + +import useTranslator from '../../shared/hooks/useTranslator' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../shared/model/CareGoal' +import Patient from '../../shared/model/Patient' +import useAddCareGoal from '../hooks/useAddCareGoal' +import { CareGoalError } from '../util/validate-caregoal' +import CareGoalForm from './CareGoalForm' + +interface Props { + patient: Patient + show: boolean + onCloseButtonClick: () => void +} + +const initialCareGoalState = { + description: '', + startDate: new Date().toISOString(), + dueDate: addMonths(new Date(), 1).toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, +} as CareGoal + +const AddCareGoalModal = (props: Props) => { + const { t } = useTranslator() + const { patient, show, onCloseButtonClick } = props + const [mutate] = useAddCareGoal() + const [careGoal, setCareGoal] = useState(initialCareGoalState) + const [careGoalError, setCareGoalError] = useState(undefined) + + useEffect(() => { + setCareGoal(initialCareGoalState) + }, [show]) + + const onClose = () => { + onCloseButtonClick() + } + + const onCareGoalChange = (newCareGoal: Partial) => { + setCareGoal(newCareGoal as CareGoal) + } + + const onSaveButtonClick = async () => { + try { + await mutate({ patientId: patient.id, careGoal }) + onClose() + } catch (e) { + setCareGoalError(e) + } + } + + const body = ( + + ) + + return ( + + ) +} + +export default AddCareGoalModal diff --git a/src/patients/care-goals/CareGoalForm.tsx b/src/patients/care-goals/CareGoalForm.tsx new file mode 100644 index 0000000000..b53d7c7a38 --- /dev/null +++ b/src/patients/care-goals/CareGoalForm.tsx @@ -0,0 +1,183 @@ +import { Alert, Row, Column } from '@hospitalrun/components' +import React, { useState } from 'react' + +import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import useTranslator from '../../shared/hooks/useTranslator' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../shared/model/CareGoal' + +interface Error { + message?: string + description?: string + status?: string + achievementStatus?: string + priority?: string + startDate?: string + dueDate?: string + note?: string +} + +interface Props { + careGoal: Partial + careGoalError?: Error + onChange?: (newCareGoal: Partial) => void + disabled: boolean +} + +const CareGoalForm = (props: Props) => { + const { careGoal, careGoalError, disabled, onChange } = props + const { t } = useTranslator() + + const [priority, setPriority] = useState(careGoal.priority) + const [status, setStatus] = useState(careGoal.status) + const [achievementStatus, setAchievementStatus] = useState(careGoal.achievementStatus) + + const priorityOptions: Option[] = [ + { label: t('patient.careGoal.priority.low'), value: 'low' }, + { label: t('patient.careGoal.priority.medium'), value: 'medium' }, + { label: t('patient.careGoal.priority.high'), value: 'high' }, + ] + + const statusOptions: Option[] = Object.values(CareGoalStatus).map((v) => ({ label: v, value: v })) + + const achievementsStatusOptions: Option[] = Object.values(CareGoalAchievementStatus).map((v) => ({ + label: v, + value: v, + })) + + const onFieldChange = ( + name: string, + value: string | CareGoalStatus | CareGoalAchievementStatus, + ) => { + if (onChange) { + const newCareGoal = { + ...careGoal, + [name]: value, + } + onChange(newCareGoal) + } + } + + const onPriorityChange = (values: string[]) => { + const value = values[0] as 'low' | 'medium' | 'high' + + onFieldChange('priority', value) + setPriority(value) + } + + return ( +
+ {careGoalError?.message && } + + + onFieldChange('description', event.currentTarget.value)} + /> + + + + + value === priority)} + isEditable={!disabled} + isInvalid={!!careGoalError?.priority} + onChange={onPriorityChange} + /> + + + + + value === status)} + isEditable={!disabled} + isInvalid={!!careGoalError?.status} + onChange={(values) => { + onFieldChange('status', values[0]) + setStatus(values[0] as CareGoalStatus) + }} + /> + + + value === achievementStatus, + )} + isEditable={!disabled} + isInvalid={!!careGoalError?.achievementStatus} + onChange={(values) => { + onFieldChange('achievementStatus', values[0]) + setAchievementStatus(values[0] as CareGoalAchievementStatus) + }} + /> + + + + + (date ? onFieldChange('startDate', date.toISOString()) : null)} + /> + + + (date ? onFieldChange('dueDate', date.toISOString()) : null)} + /> + + + + + onFieldChange('note', event.currentTarget.value)} + /> + + + + ) +} + +CareGoalForm.defaultProps = { + disabled: false, +} + +export default CareGoalForm diff --git a/src/patients/care-goals/CareGoalTab.tsx b/src/patients/care-goals/CareGoalTab.tsx new file mode 100644 index 0000000000..7d6816c1d2 --- /dev/null +++ b/src/patients/care-goals/CareGoalTab.tsx @@ -0,0 +1,61 @@ +import { Button } from '@hospitalrun/components' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { useParams, Route, Switch } from 'react-router-dom' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import usePatient from '../hooks/usePatient' +import AddCareGoalModal from './AddCareGoalModal' +import ViewCareGoal from './ViewCareGoal' +import ViewCareGoals from './ViewCareGoals' + +const CareGoalTab = () => { + const { id: patientId } = useParams() + const { t } = useTranslator() + const { permissions } = useSelector((state: RootState) => state.user) + const { data, status } = usePatient(patientId) + const [showAddCareGoalModal, setShowAddCareGoalModal] = useState(false) + + if (data === undefined || status === 'loading') { + return + } + + return ( + <> +
+
+ {permissions.includes(Permissions.AddCareGoal) && ( + + )} +
+
+
+ + + + + + + + + setShowAddCareGoalModal(false)} + /> + + ) +} + +export default CareGoalTab diff --git a/src/patients/care-goals/CareGoalTable.tsx b/src/patients/care-goals/CareGoalTable.tsx new file mode 100644 index 0000000000..6cf95cf39a --- /dev/null +++ b/src/patients/care-goals/CareGoalTable.tsx @@ -0,0 +1,66 @@ +import { Alert, Table } from '@hospitalrun/components' +import { format } from 'date-fns' +import React from 'react' +import { useHistory } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import usePatientCareGoals from '../hooks/usePatientCareGoals' + +interface Props { + patientId: string +} + +const CareGoalTable = (props: Props) => { + const { patientId } = props + const history = useHistory() + const { t } = useTranslator() + const { data, status } = usePatientCareGoals(patientId) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( + row.id} + data={data} + columns={[ + { label: t('patient.careGoal.description'), key: 'description' }, + { + label: t('patient.careGoal.startDate'), + key: 'startDate', + formatter: (row) => format(new Date(row.startDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.careGoal.dueDate'), + key: 'dueDate', + formatter: (row) => format(new Date(row.dueDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.careGoal.status'), + key: 'status', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: 'actions.view', + action: (row) => history.push(`/patients/${patientId}/care-goals/${row.id}`), + }, + ]} + /> + ) +} + +export default CareGoalTable diff --git a/src/patients/care-goals/ViewCareGoal.tsx b/src/patients/care-goals/ViewCareGoal.tsx new file mode 100644 index 0000000000..83573322b8 --- /dev/null +++ b/src/patients/care-goals/ViewCareGoal.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { useParams } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useCareGoal from '../hooks/useCareGoal' +import CareGoalForm from './CareGoalForm' + +const ViewCareGoal = () => { + const { careGoalId, id: patientId } = useParams() + const { data: careGoal, status } = useCareGoal(patientId, careGoalId) + + if (careGoal === undefined || status === 'loading') { + return + } + + return +} + +export default ViewCareGoal diff --git a/src/patients/care-goals/ViewCareGoals.tsx b/src/patients/care-goals/ViewCareGoals.tsx new file mode 100644 index 0000000000..d0b41ae25c --- /dev/null +++ b/src/patients/care-goals/ViewCareGoals.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { useParams } from 'react-router-dom' + +import CareGoalTable from './CareGoalTable' + +const ViewCareGoals = () => { + const { id } = useParams() + + return +} + +export default ViewCareGoals diff --git a/src/patients/hooks/useAddCareGoal.tsx b/src/patients/hooks/useAddCareGoal.tsx new file mode 100644 index 0000000000..f18bd2a7e9 --- /dev/null +++ b/src/patients/hooks/useAddCareGoal.tsx @@ -0,0 +1,47 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' +import { uuid } from '../../shared/util/uuid' +import validateCareGoal from '../util/validate-caregoal' + +interface AddCareGoalRequest { + patientId: string + careGoal: Omit +} + +async function addCareGoal(request: AddCareGoalRequest): Promise { + const error = validateCareGoal(request.careGoal) + + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const careGoals = patient.careGoals ? [...patient.careGoals] : [] + + const newCareGoal: CareGoal = { + id: uuid(), + createdOn: new Date(Date.now()).toISOString(), + ...request.careGoal, + } + careGoals.push(newCareGoal) + + await PatientRepository.saveOrUpdate({ + ...patient, + careGoals, + }) + + return careGoals + } + + error.message = 'patient.careGoal.error.unableToAdd' + throw error +} + +export default function useAddCareGoal() { + return useMutation(addCareGoal, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['care-goals', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useCareGoal.tsx b/src/patients/hooks/useCareGoal.tsx new file mode 100644 index 0000000000..1cc240cfdd --- /dev/null +++ b/src/patients/hooks/useCareGoal.tsx @@ -0,0 +1,23 @@ +import { useQuery, QueryKey } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' + +async function getCareGoal( + _: QueryKey, + patientId: string, + careGoalId: string, +): Promise { + const patient = await PatientRepository.find(patientId) + const maybeCareGoal = patient.careGoals?.find((c) => c.id === careGoalId) + + if (!maybeCareGoal) { + throw new Error('Care Goal not found') + } + + return maybeCareGoal +} + +export default function useCareGoal(patientId: string, careGoalId: string) { + return useQuery(['care-goals', patientId, careGoalId], getCareGoal) +} diff --git a/src/patients/hooks/usePatientCareGoals.tsx b/src/patients/hooks/usePatientCareGoals.tsx new file mode 100644 index 0000000000..291159b4be --- /dev/null +++ b/src/patients/hooks/usePatientCareGoals.tsx @@ -0,0 +1,13 @@ +import { useQuery, QueryKey } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' + +async function fetchPatientCareGoals(_: QueryKey, patientId: string): Promise { + const patient = await PatientRepository.find(patientId) + return patient.careGoals || [] +} + +export default function usePatientCareGoals(patientId: string) { + return useQuery(['care-goals', patientId], fetchPatientCareGoals) +} diff --git a/src/patients/util/validate-caregoal.ts b/src/patients/util/validate-caregoal.ts new file mode 100644 index 0000000000..75cf848dbc --- /dev/null +++ b/src/patients/util/validate-caregoal.ts @@ -0,0 +1,74 @@ +import { isBefore } from 'date-fns' + +import CareGoal from '../../shared/model/CareGoal' + +export class CareGoalError extends Error { + message: string + + description?: string + + status?: string + + achievementStatus?: string + + priority?: string + + startDate?: string + + dueDate?: string + + constructor( + message: string, + description: string, + status: string, + achievementStatus: string, + priority: string, + startDate: string, + dueDate: string, + ) { + super(message) + this.message = message + this.description = description + this.status = status + this.achievementStatus = achievementStatus + this.priority = priority + this.startDate = startDate + this.dueDate = dueDate + } +} + +export default function validateCareGoal(careGoal: Partial): CareGoalError { + const error = {} as CareGoalError + + if (!careGoal.description) { + error.description = 'patient.careGoal.error.descriptionRequired' + } + + if (!careGoal.status) { + error.status = 'patient.careGoal.error.statusRequired' + } + + if (!careGoal.achievementStatus) { + error.achievementStatus = 'patient.careGoal.error.achievementStatusRequired' + } + + if (!careGoal.priority) { + error.priority = 'patient.careGoal.error.priorityRequired' + } + + if (!careGoal.startDate) { + error.startDate = 'patient.careGoal.error.startDate' + } + + if (!careGoal.dueDate) { + error.dueDate = 'patient.careGoal.error.dueDate' + } + + if (careGoal.startDate && careGoal.dueDate) { + if (isBefore(new Date(careGoal.dueDate), new Date(careGoal.startDate))) { + error.dueDate = 'patient.careGoal.error.dueDateMustBeAfterStartDate' + } + } + + return error +} diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 949912f28d..bc951a4ab6 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -19,6 +19,7 @@ import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import Allergies from '../allergies/Allergies' import AppointmentsList from '../appointments/AppointmentsList' +import CareGoalTab from '../care-goals/CareGoalTab' import CarePlanTab from '../care-plans/CarePlanTab' import Diagnoses from '../diagnoses/Diagnoses' import GeneralInformation from '../GeneralInformation' @@ -134,6 +135,11 @@ const ViewPatient = () => { label={t('patient.carePlan.label')} onClick={() => history.push(`/patients/${patient.id}/care-plans`)} /> + history.push(`/patients/${patient.id}/care-goals`)} + /> { + + + diff --git a/src/shared/locales/enUs/translations/patient/index.ts b/src/shared/locales/enUs/translations/patient/index.ts index 9c40cd1b00..df9098ed62 100644 --- a/src/shared/locales/enUs/translations/patient/index.ts +++ b/src/shared/locales/enUs/translations/patient/index.ts @@ -119,6 +119,39 @@ export default { }, noLabsMessage: 'No labs requests for this person.', }, + careGoal: { + new: 'Add Care Goal', + label: 'Care Goals', + title: 'Title', + description: 'Description', + status: 'Status', + achievementStatus: 'Achievement Status', + priority: { + label: 'Priority', + low: 'low', + medium: 'medium', + high: 'high', + }, + startDate: 'Start Date', + dueDate: 'Due Date', + note: 'Note', + error: { + unableToAdd: 'Unable to add a new care goal.', + descriptionRequired: 'Description is required.', + priorityRequired: 'Priority is required.', + statusRequired: 'Status is required.', + achievementStatusRequired: 'Achievement Status is required.', + startDate: 'Start date is required.', + dueDate: 'Due date is required.', + dueDateMustBeAfterStartDate: 'Due date must be after start date', + }, + }, + careGoals: { + warning: { + noCareGoals: 'No Care Goals', + addCareGoalAbove: 'Add a care goal using the button above.', + }, + }, carePlan: { new: 'Add Care Plan', label: 'Care Plans', diff --git a/src/shared/model/CareGoal.ts b/src/shared/model/CareGoal.ts new file mode 100644 index 0000000000..1513690702 --- /dev/null +++ b/src/shared/model/CareGoal.ts @@ -0,0 +1,33 @@ +export enum CareGoalStatus { + Proposed = 'proposed', + Planned = 'planned', + Accepted = 'accepted', + Active = 'active', + OnHold = 'on hold', + Completed = 'completed', + Cancelled = 'cancelled', + Rejected = 'rejected', +} + +export enum CareGoalAchievementStatus { + InProgress = 'in progress', + Improving = 'improving', + Worsening = 'worsening', + NoChange = 'no change', + Achieved = 'achieved', + NotAchieved = 'not achieved', + NoProgress = 'no progress', + NotAttainable = 'not attainable', +} + +export default interface CareGoal { + id: string + status: CareGoalStatus + achievementStatus: CareGoalAchievementStatus + priority: 'high' | 'medium' | 'low' + description: string + startDate: string + dueDate: string + createdOn: string + note: string +} diff --git a/src/shared/model/Patient.ts b/src/shared/model/Patient.ts index 51ee1d8356..8bbc19825b 100644 --- a/src/shared/model/Patient.ts +++ b/src/shared/model/Patient.ts @@ -1,5 +1,6 @@ import AbstractDBModel from './AbstractDBModel' import Allergy from './Allergy' +import CareGoal from './CareGoal' import CarePlan from './CarePlan' import ContactInformation from './ContactInformation' import Diagnosis from './Diagnosis' @@ -22,6 +23,7 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati notes?: Note[] index: string carePlans: CarePlan[] + careGoals: CareGoal[] bloodType: string visits: Visit[] } diff --git a/src/shared/model/Permissions.ts b/src/shared/model/Permissions.ts index 2b35f5eb5c..d9532b6b36 100644 --- a/src/shared/model/Permissions.ts +++ b/src/shared/model/Permissions.ts @@ -17,6 +17,8 @@ enum Permissions { ResolveIncident = 'resolve:incident', AddCarePlan = 'write:care_plan', ReadCarePlan = 'read:care_plan', + AddCareGoal = 'write:care_goal', + ReadCareGoal = 'read:care_goal', RequestMedication = 'write:medications', CancelMedication = 'cancel:medication', CompleteMedication = 'complete:medication', diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index aeda304d89..7774b1b666 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -39,6 +39,8 @@ const initialState: UserState = { Permissions.ViewIncidentWidgets, Permissions.AddCarePlan, Permissions.ReadCarePlan, + Permissions.AddCareGoal, + Permissions.ReadCareGoal, Permissions.RequestMedication, Permissions.CompleteMedication, Permissions.CancelMedication,