diff --git a/src/__tests__/patients/related-persons/NewRelatedPersonModal.test.tsx b/src/__tests__/patients/related-persons/NewRelatedPersonModal.test.tsx new file mode 100644 index 0000000000..6d3de3a546 --- /dev/null +++ b/src/__tests__/patients/related-persons/NewRelatedPersonModal.test.tsx @@ -0,0 +1,210 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { ReactWrapper, mount } from 'enzyme' +import { Modal, Button } from '@hospitalrun/components' +import { act } from '@testing-library/react' +import NewRelatedPersonModal from '../../../patients/related-persons/NewRelatedPersonModal' +import TextInputWithLabelFormGroup from '../../../components/input/TextInputWithLabelFormGroup' +import TextFieldWithLabelFormGroup from '../../../components/input/TextFieldWithLabelFormGroup' + +describe('New Related Person Modal', () => { + describe('layout', () => { + let wrapper: ReactWrapper + beforeEach(() => { + wrapper = mount( + , + ) + }) + + it('should render a modal', () => { + const modal = wrapper.find(Modal) + expect(modal).toHaveLength(1) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should render a prefix name text input', () => { + const prefixTextInput = wrapper.findWhere((w) => w.prop('name') === 'prefix') + + expect(prefixTextInput).toHaveLength(1) + expect(prefixTextInput.type()).toBe(TextInputWithLabelFormGroup) + expect(prefixTextInput.prop('name')).toEqual('prefix') + expect(prefixTextInput.prop('isEditable')).toBeTruthy() + expect(prefixTextInput.prop('label')).toEqual('patient.prefix') + }) + + it('should render a given name text input', () => { + const givenNameTextInput = wrapper.findWhere((w) => w.prop('name') === 'givenName') + + expect(givenNameTextInput).toHaveLength(1) + expect(givenNameTextInput.type()).toBe(TextInputWithLabelFormGroup) + expect(givenNameTextInput.prop('name')).toEqual('givenName') + expect(givenNameTextInput.prop('isEditable')).toBeTruthy() + expect(givenNameTextInput.prop('label')).toEqual('patient.givenName') + }) + + it('should render a family name text input', () => { + const familyNameTextInput = wrapper.findWhere((w) => w.prop('name') === 'familyName') + + expect(familyNameTextInput).toHaveLength(1) + expect(familyNameTextInput.type()).toBe(TextInputWithLabelFormGroup) + expect(familyNameTextInput.prop('name')).toEqual('familyName') + expect(familyNameTextInput.prop('isEditable')).toBeTruthy() + expect(familyNameTextInput.prop('label')).toEqual('patient.familyName') + }) + + it('should render a suffix text input', () => { + const suffixTextInput = wrapper.findWhere((w) => w.prop('name') === 'suffix') + + expect(suffixTextInput).toHaveLength(1) + expect(suffixTextInput.type()).toBe(TextInputWithLabelFormGroup) + expect(suffixTextInput.prop('name')).toEqual('suffix') + expect(suffixTextInput.prop('isEditable')).toBeTruthy() + expect(suffixTextInput.prop('label')).toEqual('patient.suffix') + }) + + it('should render a relationship type text input', () => { + const relationshipTypeTextInput = wrapper.findWhere((w) => w.prop('name') === 'type') + + expect(relationshipTypeTextInput).toHaveLength(1) + expect(relationshipTypeTextInput.type()).toBe(TextInputWithLabelFormGroup) + expect(relationshipTypeTextInput.prop('name')).toEqual('type') + expect(relationshipTypeTextInput.prop('isEditable')).toBeTruthy() + expect(relationshipTypeTextInput.prop('label')).toEqual( + 'patient.relatedPersons.relationshipType', + ) + }) + + it('should render a phone number text input', () => { + const phoneNumberTextInput = wrapper.findWhere((w) => w.prop('name') === 'phoneNumber') + + expect(phoneNumberTextInput).toHaveLength(1) + expect(phoneNumberTextInput.type()).toBe(TextInputWithLabelFormGroup) + expect(phoneNumberTextInput.prop('name')).toEqual('phoneNumber') + expect(phoneNumberTextInput.prop('isEditable')).toBeTruthy() + expect(phoneNumberTextInput.prop('label')).toEqual('patient.phoneNumber') + }) + + it('should render a email text input', () => { + const emailTextInput = wrapper.findWhere((w) => w.prop('name') === 'email') + + expect(emailTextInput).toHaveLength(1) + expect(emailTextInput.type()).toBe(TextInputWithLabelFormGroup) + expect(emailTextInput.prop('name')).toEqual('email') + expect(emailTextInput.prop('isEditable')).toBeTruthy() + expect(emailTextInput.prop('label')).toEqual('patient.email') + }) + + it('should render a address text input', () => { + const addressTextField = wrapper.findWhere((w) => w.prop('name') === 'address') + + expect(addressTextField).toHaveLength(1) + expect(addressTextField.type()).toBe(TextFieldWithLabelFormGroup) + expect(addressTextField.prop('name')).toEqual('address') + expect(addressTextField.prop('isEditable')).toBeTruthy() + expect(addressTextField.prop('label')).toEqual('patient.address') + }) + + it('should render a cancel button', () => { + const cancelButton = wrapper.findWhere((w) => w.text() === 'actions.cancel') + + expect(cancelButton).toHaveLength(1) + }) + + it('should render an add new related person button button', () => { + const addNewButton = wrapper.findWhere((w) => w.text() === 'patient.relatedPersons.new') + + expect(addNewButton).toHaveLength(1) + }) + }) + + describe('save', () => { + let wrapper: ReactWrapper + let onSaveSpy = jest.fn() + beforeEach(() => { + onSaveSpy = jest.fn() + wrapper = mount( + , + ) + }) + + it('should call the save function with the correct data', () => { + act(() => { + const prefixTextInput = wrapper.findWhere((w) => w.prop('name') === 'prefix') + prefixTextInput.prop('onChange')({ target: { value: 'prefix' } }) + }) + wrapper.update() + + act(() => { + const givenNameTextInput = wrapper.findWhere((w) => w.prop('name') === 'givenName') + givenNameTextInput.prop('onChange')({ target: { value: 'given' } }) + }) + wrapper.update() + + act(() => { + const familyNameTextInput = wrapper.findWhere((w) => w.prop('name') === 'familyName') + familyNameTextInput.prop('onChange')({ target: { value: 'family' } }) + }) + wrapper.update() + + act(() => { + const suffixTextInput = wrapper.findWhere((w) => w.prop('name') === 'suffix') + suffixTextInput.prop('onChange')({ target: { value: 'suffix' } }) + }) + wrapper.update() + + act(() => { + const relationshipTypeTextInput = wrapper.findWhere((w) => w.prop('name') === 'type') + relationshipTypeTextInput.prop('onChange')({ target: { value: 'relationship' } }) + }) + wrapper.update() + + act(() => { + const phoneNumberTextInput = wrapper.findWhere((w) => w.prop('name') === 'phoneNumber') + phoneNumberTextInput.prop('onChange')({ target: { value: 'phone number' } }) + }) + wrapper.update() + + act(() => { + const emailTextInput = wrapper.findWhere((w) => w.prop('name') === 'email') + emailTextInput.prop('onChange')({ target: { value: 'email' } }) + }) + wrapper.update() + + act(() => { + const addressTextField = wrapper.findWhere((w) => w.prop('name') === 'address') + addressTextField.prop('onChange')({ currentTarget: { value: 'address' } }) + }) + wrapper.update() + + const addNewButton = wrapper.findWhere((w) => w.text() === 'patient.relatedPersons.new') + act(() => { + wrapper + .find(Modal) + .prop('successButton') + .onClick({} as React.MouseEvent) + }) + + expect(onSaveSpy).toHaveBeenCalledTimes(1) + expect(onSaveSpy).toHaveBeenCalledWith({ + prefix: 'prefix', + givenName: 'given', + familyName: 'family', + suffix: 'suffix', + type: 'relationship', + phoneNumber: 'phone number', + email: 'email', + address: 'address', + }) + }) + }) +}) diff --git a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx new file mode 100644 index 0000000000..08e64d248b --- /dev/null +++ b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx @@ -0,0 +1,137 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount, ReactWrapper } from 'enzyme' +import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab' +import { Button, List, ListItem } from '@hospitalrun/components' +import NewRelatedPersonModal from 'patients/related-persons/NewRelatedPersonModal' +import { act } from '@testing-library/react' +import PatientRepository from 'clients/db/PatientRepository' +import Patient from 'model/Patient' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { Provider } from 'react-redux' +import * as patientSlice from '../../../patients/patient-slice' + +const mockStore = createMockStore([thunk]) + +describe('Related Persons Tab', () => { + let wrapper: ReactWrapper + + describe('Add New Related Person', () => { + const patient = { + id: '123', + rev: '123', + } as Patient + + beforeEach(() => { + wrapper = mount( + + + , + ) + }) + + it('should render a New Related Person button', () => { + const newRelatedPersonButton = wrapper.find(Button) + + expect(newRelatedPersonButton).toHaveLength(1) + expect(newRelatedPersonButton.text().trim()).toEqual('patient.relatedPersons.new') + }) + + it('should render a New Related Person modal', () => { + const newRelatedPersonModal = wrapper.find(NewRelatedPersonModal) + + expect(newRelatedPersonModal.prop('show')).toBeFalsy() + expect(newRelatedPersonModal).toHaveLength(1) + }) + + it('should show the New Related Person modal when the New Related Person button is clicked', () => { + const newRelatedPersonButton = wrapper.find(Button) + + act(() => { + ;(newRelatedPersonButton.prop('onClick') as any)() + }) + + wrapper.update() + + const newRelatedPersonModal = wrapper.find(NewRelatedPersonModal) + expect(newRelatedPersonModal.prop('show')).toBeTruthy() + }) + + it('should call update patient with the data from the modal', () => { + jest.spyOn(patientSlice, 'updatePatient') + jest.spyOn(PatientRepository, 'saveOrUpdate') + const expectedRelatedPerson = { givenName: 'test', fullName: 'test' } + const expectedPatient = { + ...patient, + relatedPersons: [expectedRelatedPerson], + } + + act(() => { + const newRelatedPersonButton = wrapper.find(Button) + const onClick = newRelatedPersonButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + act(() => { + const newRelatedPersonModal = wrapper.find(NewRelatedPersonModal) + + const onSave = newRelatedPersonModal.prop('onSave') as any + onSave({ givenName: 'test' }) + }) + + wrapper.update() + + expect(patientSlice.updatePatient).toHaveBeenCalledTimes(1) + expect(patientSlice.updatePatient).toHaveBeenCalledWith(expectedPatient) + }) + + it('should close the modal when the save button is clicked', () => { + act(() => { + const newRelatedPersonButton = wrapper.find(Button) + const onClick = newRelatedPersonButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + act(() => { + const newRelatedPersonModal = wrapper.find(NewRelatedPersonModal) + const onSave = newRelatedPersonModal.prop('onSave') as any + onSave({ givenName: 'test' }) + }) + + wrapper.update() + + const newRelatedPersonModal = wrapper.find(NewRelatedPersonModal) + expect(newRelatedPersonModal.prop('show')).toBeFalsy() + }) + }) + + describe('List', () => { + const patient = { + id: '123', + rev: '123', + relatedPersons: [{ fullName: 'test' }], + } as Patient + + beforeEach(() => { + wrapper = mount( + + + , + ) + }) + + it('should render a list of of related persons with their full name being displayed', () => { + const list = wrapper.find(List) + const listItems = wrapper.find(ListItem) + + expect(list).toHaveLength(1) + expect(listItems).toHaveLength(1) + expect(listItems.at(0).text()).toEqual(patient.relatedPersons[0].fullName) + }) + }) +}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index dfa163961c..4fb4c251c0 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -4,10 +4,11 @@ import { Provider } from 'react-redux' import { mount } from 'enzyme' import { mocked } from 'ts-jest/utils' import { act } from 'react-dom/test-utils' -import { MemoryRouter, Route, BrowserRouter, Router } from 'react-router-dom' +import { Route, Router } from 'react-router-dom' import { TabsHeader, Tab } from '@hospitalrun/components' import GeneralInformation from 'patients/view/GeneralInformation' import { createMemoryHistory } from 'history' +import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab' import Patient from '../../../model/Patient' import PatientRepository from '../../../clients/db/PatientRepository' import * as titleUtil from '../../../page-header/useTitle' @@ -83,8 +84,9 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(1) + expect(tabs).toHaveLength(2) expect(tabs.at(0).prop('label')).toEqual('patient.generalInformation') + expect(tabs.at(1).prop('label')).toEqual('patient.relatedPersons.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -117,4 +119,28 @@ describe('ViewPatient', () => { expect(history.location.pathname).toEqual('/patients/123') }) + + it('should mark the related persons tab as active when it is clicked and render the Related Person Tab component when route is /patients/:id/relatedpersons', 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(1).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const relatedPersonTab = wrapper.find(RelatedPersonTab) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/relatedpersons`) + expect(tabs.at(1).prop('active')).toBeTruthy() + expect(relatedPersonTab).toHaveLength(1) + expect(relatedPersonTab.prop('patient')).toEqual(patient) + }) }) diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index 0b7301a7cb..708a4f36d7 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -3,11 +3,11 @@ import React from 'react' import { mount } from 'enzyme' import { MemoryRouter } from 'react-router-dom' import { Provider } from 'react-redux' -import * as titleUtil from '../../../page-header/useTitle' import Appointments from 'scheduling/appointments/Appointments' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { Calendar } from '@hospitalrun/components' +import * as titleUtil from '../../../page-header/useTitle' describe('Appointments', () => { const setup = () => { diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 2ab2ae532b..03bba25fcc 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -30,6 +30,11 @@ "generalInformation": "General Information", "contactInformation": "Contact Information", "unknownDateOfBirth": "Unknown", + "relatedPersons": { + "label": "Related Persons", + "new": "New Related Person", + "relationshipType": "Relationship Type" + }, "types": { "charity": "Charity", "private": "Private" diff --git a/src/model/Patient.ts b/src/model/Patient.ts index 0f4e8bef1d..acd670be80 100644 --- a/src/model/Patient.ts +++ b/src/model/Patient.ts @@ -1,6 +1,7 @@ import AbstractDBModel from './AbstractDBModel' import Name from './Name' import ContactInformation from './ContactInformation' +import RelatedPerson from './RelatedPerson' export default interface Patient extends AbstractDBModel, Name, ContactInformation { sex: string @@ -10,4 +11,5 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati occupation?: string type?: string friendlyId: string + relatedPersons: RelatedPerson[] } diff --git a/src/model/RelatedPerson.ts b/src/model/RelatedPerson.ts new file mode 100644 index 0000000000..8122e6c8b1 --- /dev/null +++ b/src/model/RelatedPerson.ts @@ -0,0 +1,6 @@ +import Name from './Name' +import ContactInformation from './ContactInformation' + +export default interface RelatedPerson extends Name, ContactInformation { + type: string +} diff --git a/src/patients/new/NewPatientForm.tsx b/src/patients/new/NewPatientForm.tsx index fc07f3632b..2387ff97c0 100644 --- a/src/patients/new/NewPatientForm.tsx +++ b/src/patients/new/NewPatientForm.tsx @@ -139,7 +139,6 @@ const NewPatientForm = (props: Props) => { /> -
void + onCloseButtonClick: () => void + onSave: (relatedPerson: RelatedPerson) => void +} + +const NewRelatedPersonModal = (props: Props) => { + const { show, toggle, onCloseButtonClick, onSave } = props + const { t } = useTranslation() + const [relatedPerson, setRelatedPerson] = useState({ + prefix: '', + givenName: '', + familyName: '', + suffix: '', + type: '', + phoneNumber: '', + email: '', + address: '', + }) + + const onFieldChange = (key: string, value: string) => { + setRelatedPerson({ + ...relatedPerson, + [key]: value, + }) + } + + const onInputElementChange = (event: React.ChangeEvent, fieldName: string) => { + onFieldChange(fieldName, event.target.value) + } + + const body = ( +
+
+
+ ) => { + onInputElementChange(event, 'prefix') + }} + /> +
+
+ ) => { + onInputElementChange(event, 'givenName') + }} + /> +
+
+ ) => { + onInputElementChange(event, 'familyName') + }} + /> +
+
+ ) => { + onInputElementChange(event, 'suffix') + }} + /> +
+
+
+
+ ) => { + onInputElementChange(event, 'type') + }} + /> +
+
+
+
+ ) => { + onInputElementChange(event, 'phoneNumber') + }} + /> +
+
+ ) => { + onInputElementChange(event, 'email') + }} + /> +
+
+
+
+ ) => { + onFieldChange('address', event.currentTarget.value) + }} + /> +
+
+
+ ) + + return ( + onSave(relatedPerson as RelatedPerson), + }} + /> + ) +} + +export default NewRelatedPersonModal diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx new file mode 100644 index 0000000000..5cf6271516 --- /dev/null +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react' +import { Button, Panel, List, ListItem } from '@hospitalrun/components' +import NewRelatedPersonModal from 'patients/related-persons/NewRelatedPersonModal' +import RelatedPerson from 'model/RelatedPerson' +import { useTranslation } from 'react-i18next' +import Patient from 'model/Patient' +import { updatePatient } from 'patients/patient-slice' +import { getPatientName } from 'patients/util/patient-name-util' +import { useDispatch } from 'react-redux' + +interface Props { + patient: Patient +} + +const RelatedPersonTab = (props: Props) => { + const dispatch = useDispatch() + const { patient } = props + const { t } = useTranslation() + const [showNewRelatedPersonModal, setShowRelatedPersonModal] = useState(false) + + const onNewRelatedPersonClick = () => { + setShowRelatedPersonModal(true) + } + + const closeNewRelatedPersonModal = () => { + setShowRelatedPersonModal(false) + } + + const onRelatedPersonSave = (relatedPerson: RelatedPerson) => { + console.log('on related person save') + const patientToUpdate = { + ...patient, + } + relatedPerson.fullName = getPatientName( + relatedPerson.givenName, + relatedPerson.familyName, + relatedPerson.suffix, + ) + if (!patientToUpdate.relatedPersons) { + patientToUpdate.relatedPersons = [] + } + + patientToUpdate.relatedPersons.push(relatedPerson) + dispatch(updatePatient(patientToUpdate)) + closeNewRelatedPersonModal() + } + + let relatedPersonsList + if (patient.relatedPersons) { + relatedPersonsList = patient.relatedPersons.map((p) => {p.fullName}) + } + + return ( +
+
+
+ +
+
+
+
+
+ + {relatedPersonsList} + +
+
+ + +
+ ) +} + +export default RelatedPersonTab diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 0a90e9e8f7..6fec74aacd 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next' import useTitle from '../../page-header/useTitle' import { fetchPatient } from '../patient-slice' import { RootState } from '../../store' - import { getPatientFullName } from '../util/patient-name-util' import Patient from '../../model/Patient' import GeneralInformation from './GeneralInformation' +import RelatedPerson from '../related-persons/RelatedPersonTab' const getFriendlyId = (p: Patient): string => { if (p) { @@ -46,11 +46,19 @@ const ViewPatient = () => { label={t('patient.generalInformation')} onClick={() => history.push(`/patients/${patient.id}`)} /> + history.push(`/patients/${patient.id}/relatedpersons`)} + /> + + +
)