Skip to content
This repository was archived by the owner on Jan 9, 2023. It is now read-only.

Commit d1f78c4

Browse files
authored
Merge pull request #1961 from HospitalRun/feature/add-notes-to-patients
feat(patients): add notes to patients
2 parents 9690409 + 126e195 commit d1f78c4

File tree

12 files changed

+504
-6
lines changed

12 files changed

+504
-6
lines changed

.env.example

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
REACT_APP_HOSPITALRUN_API=http://0.0.0.0:3001
1+
REACT_APP_HOSPITALRUN_API=http://0.0.0.0:3001
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import '../../../__mocks__/matchMediaMock'
2+
import React from 'react'
3+
import NewNoteModal from 'patients/notes/NewNoteModal'
4+
import { shallow, mount } from 'enzyme'
5+
import { Modal, Alert } from '@hospitalrun/components'
6+
import { act } from '@testing-library/react'
7+
import TextFieldWithLabelFormGroup from 'components/input/TextFieldWithLabelFormGroup'
8+
9+
describe('New Note Modal', () => {
10+
it('should render a modal with the correct labels', () => {
11+
const wrapper = shallow(
12+
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={jest.fn()} toggle={jest.fn()} />,
13+
)
14+
15+
const modal = wrapper.find(Modal)
16+
expect(modal).toHaveLength(1)
17+
expect(modal.prop('title')).toEqual('patient.notes.new')
18+
expect(modal.prop('closeButton')?.children).toEqual('actions.cancel')
19+
expect(modal.prop('closeButton')?.color).toEqual('danger')
20+
expect(modal.prop('successButton')?.children).toEqual('patient.notes.new')
21+
expect(modal.prop('successButton')?.color).toEqual('success')
22+
expect(modal.prop('successButton')?.icon).toEqual('add')
23+
})
24+
25+
it('should render a notes rich text editor', () => {
26+
const wrapper = mount(
27+
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={jest.fn()} toggle={jest.fn()} />,
28+
)
29+
30+
const noteTextField = wrapper.find(TextFieldWithLabelFormGroup)
31+
expect(noteTextField.prop('label')).toEqual('patient.note')
32+
expect(noteTextField.prop('isRequired')).toBeTruthy()
33+
expect(noteTextField).toHaveLength(1)
34+
})
35+
36+
describe('on cancel', () => {
37+
it('should call the onCloseButtonCLick function when the cancel button is clicked', () => {
38+
const onCloseButtonClickSpy = jest.fn()
39+
const wrapper = shallow(
40+
<NewNoteModal
41+
show
42+
onCloseButtonClick={onCloseButtonClickSpy}
43+
onSave={jest.fn()}
44+
toggle={jest.fn()}
45+
/>,
46+
)
47+
48+
act(() => {
49+
const modal = wrapper.find(Modal)
50+
const { onClick } = modal.prop('closeButton') as any
51+
onClick()
52+
})
53+
54+
expect(onCloseButtonClickSpy).toHaveBeenCalledTimes(1)
55+
})
56+
})
57+
58+
describe('on save', () => {
59+
const expectedDate = new Date()
60+
const expectedNote = 'test'
61+
62+
Date.now = jest.fn(() => expectedDate.valueOf())
63+
it('should call the onSave callback', () => {
64+
const onSaveSpy = jest.fn()
65+
const wrapper = mount(
66+
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={onSaveSpy} toggle={jest.fn()} />,
67+
)
68+
69+
act(() => {
70+
const noteTextField = wrapper.find(TextFieldWithLabelFormGroup)
71+
const onChange = noteTextField.prop('onChange') as any
72+
onChange({ currentTarget: { value: expectedNote } })
73+
})
74+
75+
wrapper.update()
76+
act(() => {
77+
const modal = wrapper.find(Modal)
78+
const { onClick } = modal.prop('successButton') as any
79+
onClick()
80+
})
81+
82+
expect(onSaveSpy).toHaveBeenCalledTimes(1)
83+
expect(onSaveSpy).toHaveBeenCalledWith({
84+
text: expectedNote,
85+
date: expectedDate.toISOString(),
86+
})
87+
})
88+
89+
it('should require a note be added', async () => {
90+
const onSaveSpy = jest.fn()
91+
const wrapper = mount(
92+
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={onSaveSpy} toggle={jest.fn()} />,
93+
)
94+
95+
await act(async () => {
96+
const modal = wrapper.find(Modal)
97+
const { onClick } = modal.prop('successButton') as any
98+
await onClick()
99+
})
100+
wrapper.update()
101+
102+
const notesTextField = wrapper.find(TextFieldWithLabelFormGroup)
103+
const errorAlert = wrapper.find(Alert)
104+
105+
expect(errorAlert).toHaveLength(1)
106+
expect(errorAlert.prop('title')).toEqual('states.error')
107+
expect(errorAlert.prop('message')).toEqual('patient.notes.error.unableToAdd')
108+
expect(notesTextField.prop('feedback')).toEqual('patient.notes.error.noteRequired')
109+
expect(onSaveSpy).not.toHaveBeenCalled()
110+
})
111+
})
112+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import '../../../__mocks__/matchMediaMock'
2+
import React from 'react'
3+
import PatientRepository from 'clients/db/PatientRepository'
4+
import Note from 'model/Note'
5+
import { createMemoryHistory } from 'history'
6+
import configureMockStore from 'redux-mock-store'
7+
import Patient from 'model/Patient'
8+
import thunk from 'redux-thunk'
9+
import { mount } from 'enzyme'
10+
import { Router } from 'react-router'
11+
import { Provider } from 'react-redux'
12+
import NoteTab from 'patients/notes/NoteTab'
13+
import * as components from '@hospitalrun/components'
14+
import { act } from 'react-dom/test-utils'
15+
import { mocked } from 'ts-jest/utils'
16+
import NewNoteModal from 'patients/notes/NewNoteModal'
17+
import Permissions from '../../../model/Permissions'
18+
import * as patientSlice from '../../../patients/patient-slice'
19+
20+
const expectedPatient = {
21+
id: '123',
22+
notes: [{ date: new Date().toISOString(), text: 'notes1' } as Note],
23+
} as Patient
24+
25+
const mockStore = configureMockStore([thunk])
26+
const history = createMemoryHistory()
27+
28+
let user: any
29+
let store: any
30+
31+
const setup = (patient = expectedPatient, permissions = [Permissions.WritePatients]) => {
32+
user = { permissions }
33+
store = mockStore({ patient, user })
34+
const wrapper = mount(
35+
<Router history={history}>
36+
<Provider store={store}>
37+
<NoteTab patient={patient} />
38+
</Provider>
39+
</Router>,
40+
)
41+
42+
return wrapper
43+
}
44+
45+
describe('Notes Tab', () => {
46+
describe('Add New Note', () => {
47+
beforeEach(() => {
48+
jest.resetAllMocks()
49+
jest.spyOn(PatientRepository, 'saveOrUpdate')
50+
})
51+
52+
it('should render a add notes button', () => {
53+
const wrapper = setup()
54+
55+
const addNoteButton = wrapper.find(components.Button)
56+
expect(addNoteButton).toHaveLength(1)
57+
expect(addNoteButton.text().trim()).toEqual('patient.notes.new')
58+
})
59+
60+
it('should not render a add notes button if the user does not have permissions', () => {
61+
const wrapper = setup(expectedPatient, [])
62+
63+
const addNotesButton = wrapper.find(components.Button)
64+
expect(addNotesButton).toHaveLength(0)
65+
})
66+
67+
it('should open the Add Notes Modal', () => {
68+
const wrapper = setup()
69+
70+
act(() => {
71+
const onClick = wrapper.find(components.Button).prop('onClick') as any
72+
onClick()
73+
})
74+
wrapper.update()
75+
76+
expect(wrapper.find(components.Modal).prop('show')).toBeTruthy()
77+
})
78+
79+
it('should update the patient with the new diagnosis when the save button is clicked', async () => {
80+
const expectedNote = {
81+
text: 'note text',
82+
date: new Date().toISOString(),
83+
} as Note
84+
const expectedUpdatedPatient = {
85+
...expectedPatient,
86+
notes: [...(expectedPatient.notes as any), expectedNote],
87+
} as Patient
88+
89+
const mockedPatientRepository = mocked(PatientRepository, true)
90+
mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedUpdatedPatient)
91+
92+
const wrapper = setup()
93+
94+
await act(async () => {
95+
const modal = wrapper.find(NewNoteModal)
96+
await modal.prop('onSave')(expectedNote)
97+
})
98+
99+
expect(mockedPatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedUpdatedPatient)
100+
expect(store.getActions()).toContainEqual(patientSlice.updatePatientStart())
101+
expect(store.getActions()).toContainEqual(
102+
patientSlice.updatePatientSuccess(expectedUpdatedPatient),
103+
)
104+
})
105+
})
106+
107+
describe('notes list', () => {
108+
it('should list the patients diagnoses', () => {
109+
const notes = expectedPatient.notes as Note[]
110+
const wrapper = setup()
111+
112+
const list = wrapper.find(components.List)
113+
const listItems = wrapper.find(components.ListItem)
114+
115+
expect(list).toHaveLength(1)
116+
expect(listItems).toHaveLength(notes.length)
117+
})
118+
119+
it('should render a warning message if the patient does not have any diagnoses', () => {
120+
const wrapper = setup({ ...expectedPatient, notes: [] })
121+
122+
const alert = wrapper.find(components.Alert)
123+
124+
expect(alert).toHaveLength(1)
125+
expect(alert.prop('title')).toEqual('patient.notes.warning.noNotes')
126+
expect(alert.prop('message')).toEqual('patient.notes.addNoteAbove')
127+
})
128+
})
129+
})

src/__tests__/patients/view/ViewPatient.test.tsx

+27-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab'
1414
import * as ButtonBarProvider from 'page-header/ButtonBarProvider'
1515
import Allergies from 'patients/allergies/Allergies'
1616
import Diagnoses from 'patients/diagnoses/Diagnoses'
17+
import NotesTab from 'patients/notes/NoteTab'
1718
import Patient from '../../../model/Patient'
1819
import PatientRepository from '../../../clients/db/PatientRepository'
1920
import * as titleUtil from '../../../page-header/useTitle'
@@ -126,12 +127,13 @@ describe('ViewPatient', () => {
126127
const tabs = tabsHeader.find(Tab)
127128
expect(tabsHeader).toHaveLength(1)
128129

129-
expect(tabs).toHaveLength(5)
130+
expect(tabs).toHaveLength(6)
130131
expect(tabs.at(0).prop('label')).toEqual('patient.generalInformation')
131132
expect(tabs.at(1).prop('label')).toEqual('patient.relatedPersons.label')
132133
expect(tabs.at(2).prop('label')).toEqual('scheduling.appointments.label')
133134
expect(tabs.at(3).prop('label')).toEqual('patient.allergies.label')
134135
expect(tabs.at(4).prop('label')).toEqual('patient.diagnoses.label')
136+
expect(tabs.at(5).prop('label')).toEqual('patient.notes.label')
135137
})
136138

137139
it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => {
@@ -236,4 +238,28 @@ describe('ViewPatient', () => {
236238
expect(diagnosesTab).toHaveLength(1)
237239
expect(diagnosesTab.prop('patient')).toEqual(patient)
238240
})
241+
242+
it('should mark the notes tab as active when it is clicked and render the note component when route is /patients/:id/notes', async () => {
243+
let wrapper: any
244+
await act(async () => {
245+
wrapper = await setup()
246+
})
247+
248+
await act(async () => {
249+
const tabsHeader = wrapper.find(TabsHeader)
250+
const tabs = tabsHeader.find(Tab)
251+
tabs.at(5).prop('onClick')()
252+
})
253+
254+
wrapper.update()
255+
256+
const tabsHeader = wrapper.find(TabsHeader)
257+
const tabs = tabsHeader.find(Tab)
258+
const notesTab = wrapper.find(NotesTab)
259+
260+
expect(history.location.pathname).toEqual(`/patients/${patient.id}/notes`)
261+
expect(tabs.at(5).prop('active')).toBeTruthy()
262+
expect(notesTab).toHaveLength(1)
263+
expect(notesTab.prop('patient')).toEqual(patient)
264+
})
239265
})

src/components/input/TextFieldWithLabelFormGroup.tsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,25 @@ interface Props {
88
isEditable?: boolean
99
placeholder?: string
1010
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
11+
isRequired?: boolean
12+
feedback?: string
13+
isInvalid?: boolean
1114
}
1215

1316
const TextFieldWithLabelFormGroup = (props: Props) => {
14-
const { value, label, name, isEditable, onChange } = props
17+
const { value, label, name, isEditable, isInvalid, feedback, onChange } = props
1518
const id = `${name}TextField`
1619
return (
1720
<div className="form-group">
18-
<Label text={label} htmlFor={id} />
19-
<TextField rows={4} value={value} disabled={!isEditable} onChange={onChange} />
21+
<Label text={label} htmlFor={id} isRequired />
22+
<TextField
23+
rows={4}
24+
value={value}
25+
disabled={!isEditable}
26+
onChange={onChange}
27+
isInvalid={isInvalid}
28+
feedback={feedback}
29+
/>
2030
</div>
2131
)
2232
}

src/locales/enUs/translations/patient/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ export default {
6666
addDiagnosisAbove: 'Add a diagnosis using the button above.',
6767
successfullyAdded: 'Successfully added a new diagnosis!',
6868
},
69+
note: 'Note',
70+
notes: {
71+
label: 'Notes',
72+
new: 'Add New Note',
73+
warning: {
74+
noNotes: 'No Notes',
75+
},
76+
error: {
77+
noteRequired: 'Note is required.',
78+
unableToAdd: 'Unable to add new note.',
79+
},
80+
addNoteAbove: 'Add a note using the button above.',
81+
},
6982
types: {
7083
charity: 'Charity',
7184
private: 'Private',

src/locales/enUs/translations/patients/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default {
66
viewPatient: 'View Patient',
77
newPatient: 'New Patient',
88
successfullyCreated: 'Successfully created patient',
9-
successfullyAddedRelatedPerson: 'Successfully added the new related person',
9+
successfullyAddedNote: 'Successfully added the new note',
10+
successfullyAddedRelatedPerson: 'Successfully added a new related person',
1011
},
1112
}

src/model/Note.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default interface Note {
2+
date: string
3+
text: string
4+
}

src/model/Patient.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ContactInformation from './ContactInformation'
44
import RelatedPerson from './RelatedPerson'
55
import Allergy from './Allergy'
66
import Diagnosis from './Diagnosis'
7+
import Note from './Note'
78

89
export default interface Patient extends AbstractDBModel, Name, ContactInformation {
910
sex: string
@@ -16,4 +17,5 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati
1617
relatedPersons?: RelatedPerson[]
1718
allergies?: Allergy[]
1819
diagnoses?: Diagnosis[]
20+
notes?: Note[]
1921
}

0 commit comments

Comments
 (0)