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

Commit

Permalink
Merge pull request #1961 from HospitalRun/feature/add-notes-to-patients
Browse files Browse the repository at this point in the history
feat(patients): add notes to patients
  • Loading branch information
fox1t committed Apr 3, 2020
2 parents 9690409 + 126e195 commit d1f78c4
Show file tree
Hide file tree
Showing 12 changed files with 504 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1 @@
REACT_APP_HOSPITALRUN_API=http://0.0.0.0:3001
REACT_APP_HOSPITALRUN_API=http://0.0.0.0:3001
112 changes: 112 additions & 0 deletions src/__tests__/patients/notes/NewNoteModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import '../../../__mocks__/matchMediaMock'
import React from 'react'
import NewNoteModal from 'patients/notes/NewNoteModal'
import { shallow, mount } from 'enzyme'
import { Modal, Alert } from '@hospitalrun/components'
import { act } from '@testing-library/react'
import TextFieldWithLabelFormGroup from 'components/input/TextFieldWithLabelFormGroup'

describe('New Note Modal', () => {
it('should render a modal with the correct labels', () => {
const wrapper = shallow(
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={jest.fn()} toggle={jest.fn()} />,
)

const modal = wrapper.find(Modal)
expect(modal).toHaveLength(1)
expect(modal.prop('title')).toEqual('patient.notes.new')
expect(modal.prop('closeButton')?.children).toEqual('actions.cancel')
expect(modal.prop('closeButton')?.color).toEqual('danger')
expect(modal.prop('successButton')?.children).toEqual('patient.notes.new')
expect(modal.prop('successButton')?.color).toEqual('success')
expect(modal.prop('successButton')?.icon).toEqual('add')
})

it('should render a notes rich text editor', () => {
const wrapper = mount(
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={jest.fn()} toggle={jest.fn()} />,
)

const noteTextField = wrapper.find(TextFieldWithLabelFormGroup)
expect(noteTextField.prop('label')).toEqual('patient.note')
expect(noteTextField.prop('isRequired')).toBeTruthy()
expect(noteTextField).toHaveLength(1)
})

describe('on cancel', () => {
it('should call the onCloseButtonCLick function when the cancel button is clicked', () => {
const onCloseButtonClickSpy = jest.fn()
const wrapper = shallow(
<NewNoteModal
show
onCloseButtonClick={onCloseButtonClickSpy}
onSave={jest.fn()}
toggle={jest.fn()}
/>,
)

act(() => {
const modal = wrapper.find(Modal)
const { onClick } = modal.prop('closeButton') as any
onClick()
})

expect(onCloseButtonClickSpy).toHaveBeenCalledTimes(1)
})
})

describe('on save', () => {
const expectedDate = new Date()
const expectedNote = 'test'

Date.now = jest.fn(() => expectedDate.valueOf())
it('should call the onSave callback', () => {
const onSaveSpy = jest.fn()
const wrapper = mount(
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={onSaveSpy} toggle={jest.fn()} />,
)

act(() => {
const noteTextField = wrapper.find(TextFieldWithLabelFormGroup)
const onChange = noteTextField.prop('onChange') as any
onChange({ currentTarget: { value: expectedNote } })
})

wrapper.update()
act(() => {
const modal = wrapper.find(Modal)
const { onClick } = modal.prop('successButton') as any
onClick()
})

expect(onSaveSpy).toHaveBeenCalledTimes(1)
expect(onSaveSpy).toHaveBeenCalledWith({
text: expectedNote,
date: expectedDate.toISOString(),
})
})

it('should require a note be added', async () => {
const onSaveSpy = jest.fn()
const wrapper = mount(
<NewNoteModal show onCloseButtonClick={jest.fn()} onSave={onSaveSpy} toggle={jest.fn()} />,
)

await act(async () => {
const modal = wrapper.find(Modal)
const { onClick } = modal.prop('successButton') as any
await onClick()
})
wrapper.update()

const notesTextField = wrapper.find(TextFieldWithLabelFormGroup)
const errorAlert = wrapper.find(Alert)

expect(errorAlert).toHaveLength(1)
expect(errorAlert.prop('title')).toEqual('states.error')
expect(errorAlert.prop('message')).toEqual('patient.notes.error.unableToAdd')
expect(notesTextField.prop('feedback')).toEqual('patient.notes.error.noteRequired')
expect(onSaveSpy).not.toHaveBeenCalled()
})
})
})
129 changes: 129 additions & 0 deletions src/__tests__/patients/notes/NotesTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import '../../../__mocks__/matchMediaMock'
import React from 'react'
import PatientRepository from 'clients/db/PatientRepository'
import Note from 'model/Note'
import { createMemoryHistory } from 'history'
import configureMockStore from 'redux-mock-store'
import Patient from 'model/Patient'
import thunk from 'redux-thunk'
import { mount } from 'enzyme'
import { Router } from 'react-router'
import { Provider } from 'react-redux'
import NoteTab from 'patients/notes/NoteTab'
import * as components from '@hospitalrun/components'
import { act } from 'react-dom/test-utils'
import { mocked } from 'ts-jest/utils'
import NewNoteModal from 'patients/notes/NewNoteModal'
import Permissions from '../../../model/Permissions'
import * as patientSlice from '../../../patients/patient-slice'

const expectedPatient = {
id: '123',
notes: [{ date: new Date().toISOString(), text: 'notes1' } as Note],
} as Patient

const mockStore = configureMockStore([thunk])
const history = createMemoryHistory()

let user: any
let store: any

const setup = (patient = expectedPatient, permissions = [Permissions.WritePatients]) => {
user = { permissions }
store = mockStore({ patient, user })
const wrapper = mount(
<Router history={history}>
<Provider store={store}>
<NoteTab patient={patient} />
</Provider>
</Router>,
)

return wrapper
}

describe('Notes Tab', () => {
describe('Add New Note', () => {
beforeEach(() => {
jest.resetAllMocks()
jest.spyOn(PatientRepository, 'saveOrUpdate')
})

it('should render a add notes button', () => {
const wrapper = setup()

const addNoteButton = wrapper.find(components.Button)
expect(addNoteButton).toHaveLength(1)
expect(addNoteButton.text().trim()).toEqual('patient.notes.new')
})

it('should not render a add notes button if the user does not have permissions', () => {
const wrapper = setup(expectedPatient, [])

const addNotesButton = wrapper.find(components.Button)
expect(addNotesButton).toHaveLength(0)
})

it('should open the Add Notes Modal', () => {
const wrapper = setup()

act(() => {
const onClick = wrapper.find(components.Button).prop('onClick') as any
onClick()
})
wrapper.update()

expect(wrapper.find(components.Modal).prop('show')).toBeTruthy()
})

it('should update the patient with the new diagnosis when the save button is clicked', async () => {
const expectedNote = {
text: 'note text',
date: new Date().toISOString(),
} as Note
const expectedUpdatedPatient = {
...expectedPatient,
notes: [...(expectedPatient.notes as any), expectedNote],
} as Patient

const mockedPatientRepository = mocked(PatientRepository, true)
mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedUpdatedPatient)

const wrapper = setup()

await act(async () => {
const modal = wrapper.find(NewNoteModal)
await modal.prop('onSave')(expectedNote)
})

expect(mockedPatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedUpdatedPatient)
expect(store.getActions()).toContainEqual(patientSlice.updatePatientStart())
expect(store.getActions()).toContainEqual(
patientSlice.updatePatientSuccess(expectedUpdatedPatient),
)
})
})

describe('notes list', () => {
it('should list the patients diagnoses', () => {
const notes = expectedPatient.notes as Note[]
const wrapper = setup()

const list = wrapper.find(components.List)
const listItems = wrapper.find(components.ListItem)

expect(list).toHaveLength(1)
expect(listItems).toHaveLength(notes.length)
})

it('should render a warning message if the patient does not have any diagnoses', () => {
const wrapper = setup({ ...expectedPatient, notes: [] })

const alert = wrapper.find(components.Alert)

expect(alert).toHaveLength(1)
expect(alert.prop('title')).toEqual('patient.notes.warning.noNotes')
expect(alert.prop('message')).toEqual('patient.notes.addNoteAbove')
})
})
})
28 changes: 27 additions & 1 deletion src/__tests__/patients/view/ViewPatient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab'
import * as ButtonBarProvider from 'page-header/ButtonBarProvider'
import Allergies from 'patients/allergies/Allergies'
import Diagnoses from 'patients/diagnoses/Diagnoses'
import NotesTab from 'patients/notes/NoteTab'
import Patient from '../../../model/Patient'
import PatientRepository from '../../../clients/db/PatientRepository'
import * as titleUtil from '../../../page-header/useTitle'
Expand Down Expand Up @@ -126,12 +127,13 @@ describe('ViewPatient', () => {
const tabs = tabsHeader.find(Tab)
expect(tabsHeader).toHaveLength(1)

expect(tabs).toHaveLength(5)
expect(tabs).toHaveLength(6)
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')
expect(tabs.at(3).prop('label')).toEqual('patient.allergies.label')
expect(tabs.at(4).prop('label')).toEqual('patient.diagnoses.label')
expect(tabs.at(5).prop('label')).toEqual('patient.notes.label')
})

it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => {
Expand Down Expand Up @@ -236,4 +238,28 @@ describe('ViewPatient', () => {
expect(diagnosesTab).toHaveLength(1)
expect(diagnosesTab.prop('patient')).toEqual(patient)
})

it('should mark the notes tab as active when it is clicked and render the note component when route is /patients/:id/notes', 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(5).prop('onClick')()
})

wrapper.update()

const tabsHeader = wrapper.find(TabsHeader)
const tabs = tabsHeader.find(Tab)
const notesTab = wrapper.find(NotesTab)

expect(history.location.pathname).toEqual(`/patients/${patient.id}/notes`)
expect(tabs.at(5).prop('active')).toBeTruthy()
expect(notesTab).toHaveLength(1)
expect(notesTab.prop('patient')).toEqual(patient)
})
})
16 changes: 13 additions & 3 deletions src/components/input/TextFieldWithLabelFormGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,25 @@ interface Props {
isEditable?: boolean
placeholder?: string
onChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
isRequired?: boolean
feedback?: string
isInvalid?: boolean
}

const TextFieldWithLabelFormGroup = (props: Props) => {
const { value, label, name, isEditable, onChange } = props
const { value, label, name, isEditable, isInvalid, feedback, onChange } = props
const id = `${name}TextField`
return (
<div className="form-group">
<Label text={label} htmlFor={id} />
<TextField rows={4} value={value} disabled={!isEditable} onChange={onChange} />
<Label text={label} htmlFor={id} isRequired />
<TextField
rows={4}
value={value}
disabled={!isEditable}
onChange={onChange}
isInvalid={isInvalid}
feedback={feedback}
/>
</div>
)
}
Expand Down
13 changes: 13 additions & 0 deletions src/locales/enUs/translations/patient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ export default {
addDiagnosisAbove: 'Add a diagnosis using the button above.',
successfullyAdded: 'Successfully added a new diagnosis!',
},
note: 'Note',
notes: {
label: 'Notes',
new: 'Add New Note',
warning: {
noNotes: 'No Notes',
},
error: {
noteRequired: 'Note is required.',
unableToAdd: 'Unable to add new note.',
},
addNoteAbove: 'Add a note using the button above.',
},
types: {
charity: 'Charity',
private: 'Private',
Expand Down
3 changes: 2 additions & 1 deletion src/locales/enUs/translations/patients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
viewPatient: 'View Patient',
newPatient: 'New Patient',
successfullyCreated: 'Successfully created patient',
successfullyAddedRelatedPerson: 'Successfully added the new related person',
successfullyAddedNote: 'Successfully added the new note',
successfullyAddedRelatedPerson: 'Successfully added a new related person',
},
}
4 changes: 4 additions & 0 deletions src/model/Note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface Note {
date: string
text: string
}
2 changes: 2 additions & 0 deletions src/model/Patient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ContactInformation from './ContactInformation'
import RelatedPerson from './RelatedPerson'
import Allergy from './Allergy'
import Diagnosis from './Diagnosis'
import Note from './Note'

export default interface Patient extends AbstractDBModel, Name, ContactInformation {
sex: string
Expand All @@ -16,4 +17,5 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati
relatedPersons?: RelatedPerson[]
allergies?: Allergy[]
diagnoses?: Diagnosis[]
notes?: Note[]
}
Loading

1 comment on commit d1f78c4

@vercel
Copy link

@vercel vercel bot commented on d1f78c4 Apr 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed to assign a domain to your deployment due to the following error:

We could not create a certificate for staging.hospitalrun.io because HTTP pretest failed. Please ensure the CNAME for staging.hospitalrun.io points to "alias.zeit.co". You can find more information at https://err.sh/now-cli/cant-solve-challenge.

(Learn more or visit the non-aliased deployment)

Please sign in to comment.