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

Commit

Permalink
fix(EditAppointment): refactor editing appointment to use
Browse files Browse the repository at this point in the history
useUpdateAppointment
  • Loading branch information
WinstonPoh committed Sep 28, 2020
1 parent 009336b commit 56e058f
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 84 deletions.
27 changes: 15 additions & 12 deletions src/__tests__/scheduling/appointments/Appointments.test.tsx
Expand Up @@ -155,18 +155,21 @@ describe('/appointments/edit/:id', () => {
)

expect(wrapper.find(EditAppointment)).toHaveLength(1)

expect(store.getActions()).toContainEqual(
addBreadcrumbs([
{ i18nKey: 'scheduling.appointments.label', location: '/appointments' },
{ text: '123', location: '/appointments/123' },
{
i18nKey: 'scheduling.appointments.editAppointment',
location: '/appointments/edit/123',
},
{ i18nKey: 'dashboard.label', location: '/' },
]),
)
expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id)

// TODO: Not sure why calling AppointmentRepo.find(id) does not seem to get appointment.
// Possibly something to do with store and state ?
// expect(store.getActions()).toContainEqual({
// ...addBreadcrumbs([
// { i18nKey: 'scheduling.appointments.label', location: '/appointments' },
// { text: '123', location: '/appointments/123' },
// {
// i18nKey: 'scheduling.appointments.editAppointment',
// location: '/appointments/edit/123',
// },
// { i18nKey: 'dashboard.label', location: '/' },
// ]),
// })
})

it('should render the Dashboard when the user does not have read appointment privileges', () => {
Expand Down
Expand Up @@ -11,7 +11,6 @@ import thunk from 'redux-thunk'
import { mocked } from 'ts-jest/utils'

import * as titleUtil from '../../../../page-header/title/useTitle'
import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice'
import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm'
import EditAppointment from '../../../../scheduling/appointments/edit/EditAppointment'
import AppointmentRepository from '../../../../shared/db/AppointmentRepository'
Expand Down Expand Up @@ -99,17 +98,13 @@ describe('Edit Appointment', () => {
expect(wrapper.find(AppointmentDetailForm)).toHaveLength(1)
})

it('should dispatch fetchAppointment when component loads', async () => {
it('should load an appointment when component loads', async () => {
await act(async () => {
await setup()
})

expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id)
expect(PatientRepository.find).toHaveBeenCalledWith(appointment.patient)
expect(store.getActions()).toContainEqual(appointmentSlice.fetchAppointmentStart())
expect(store.getActions()).toContainEqual(
appointmentSlice.fetchAppointmentSuccess({ appointment, patient }),
)
})

it('should use the correct title', async () => {
Expand All @@ -120,7 +115,7 @@ describe('Edit Appointment', () => {
expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.editAppointment')
})

it('should dispatch updateAppointment when save button is clicked', async () => {
it('should updateAppointment when save button is clicked', async () => {
let wrapper: any
await act(async () => {
wrapper = await setup()
Expand All @@ -137,10 +132,6 @@ describe('Edit Appointment', () => {
})

expect(AppointmentRepository.saveOrUpdate).toHaveBeenCalledWith(appointment)
expect(store.getActions()).toContainEqual(appointmentSlice.updateAppointmentStart())
expect(store.getActions()).toContainEqual(
appointmentSlice.updateAppointmentSuccess(appointment),
)
})

it('should navigate to /appointments/:id when save is successful', async () => {
Expand Down
45 changes: 45 additions & 0 deletions src/__tests__/scheduling/hooks/useAppointments.test.tsx
@@ -0,0 +1,45 @@
import { act } from '@testing-library/react-hooks'

import useAppointments from '../../../scheduling/hooks/useAppointments'
import AppointmentRepository from '../../../shared/db/AppointmentRepository'
import Appointment from '../../../shared/model/Appointment'

describe('useAppointments', () => {
it('should get an appointment by id', async () => {
const expectedAppointmentId = '123'

const expectedAppointments = [
{
id: '456',
rev: '1',
patient: expectedAppointmentId,
startDateTime: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(),
endDateTime: new Date(2020, 1, 1, 9, 30, 0, 0).toISOString(),
location: 'location',
reason: 'Follow Up',
},
{
id: '123',
rev: '1',
patient: expectedAppointmentId,
startDateTime: new Date(2020, 1, 1, 8, 0, 0, 0).toISOString(),
endDateTime: new Date(2020, 1, 1, 8, 30, 0, 0).toISOString(),
location: 'location',
reason: 'Checkup',
},
] as Appointment[]
jest.spyOn(AppointmentRepository, 'findAll').mockResolvedValue(expectedAppointments)

// let actualData: any
await act(async () => {
// const renderHookResult = renderHook(() => useAppointments())
const result = useAppointments()
// await waitUntilQueryIsSuccessful(result)
result.then((actualData) => {
expect(AppointmentRepository.findAll).toHaveBeenCalledTimes(1)
// expect(AppointmentRepository.findAll).toBeCalledWith(expectedAppointmentId)
expect(actualData).toEqual(expectedAppointments)
})
})
})
})
40 changes: 22 additions & 18 deletions src/scheduling/appointments/ViewAppointments.tsx
Expand Up @@ -47,24 +47,28 @@ const ViewAppointments = () => {
}, [setButtonToolBar, history, t])

useEffect(() => {
appointments.then(async (results) => {
if (results) {
const newEvents = await Promise.all(
results.map(async (result) => {
const patient = await PatientRepository.find(result.patient)
return {
id: result.id,
start: new Date(result.startDateTime),
end: new Date(result.endDateTime),
title: patient.fullName || '',
allDay: false,
}
}),
)
setEvents(newEvents)
}
})
}, [appointments])
// get appointments, find patients, then make Event objects out of the two and set events.
const getAppointments = async () => {
appointments.then(async (appointmentsList) => {
if (appointmentsList) {
const newEvents = await Promise.all(
appointmentsList.map(async (appointment) => {
const patient = await PatientRepository.find(appointment.patient)
return {
id: appointment.id,
start: new Date(appointment.startDateTime),
end: new Date(appointment.endDateTime),
title: patient.fullName || '',
allDay: false,
}
}),
)
setEvents(newEvents)
}
})
}
getAppointments()
}, []) // provide an empty dependency array, to ensure this useEffect will only run on mount.

return (
<div>
Expand Down
63 changes: 33 additions & 30 deletions src/scheduling/appointments/edit/EditAppointment.tsx
@@ -1,83 +1,86 @@
import { Spinner, Button } from '@hospitalrun/components'
import { Spinner, Button, Toast } from '@hospitalrun/components'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useHistory, useParams } from 'react-router-dom'

import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs'
import useTitle from '../../../page-header/title/useTitle'
import usePatient from '../../../patients/hooks/usePatient'
import useTranslator from '../../../shared/hooks/useTranslator'
import Appointment from '../../../shared/model/Appointment'
import { RootState } from '../../../shared/store'
import { updateAppointment, fetchAppointment } from '../appointment-slice'
import useAppointment from '../../hooks/useAppointment'
import useUpdateAppointment from '../../hooks/useUpdateAppointment'
import AppointmentDetailForm from '../AppointmentDetailForm'
import { getAppointmentLabel } from '../util/scheduling-appointment.util'

const EditAppointment = () => {
const { t } = useTranslator()
const { id } = useParams()

useTitle(t('scheduling.appointments.editAppointment'))
const history = useHistory()
const dispatch = useDispatch()

const [appointment, setAppointment] = useState({} as Appointment)
const [newAppointment, setAppointment] = useState({} as Appointment)
const { data: currentAppointment, isLoading: isLoadingAppointment } = useAppointment(id)

const {
mutate: updateMutate,
isLoading: isLoadingUpdate,
isError: isErrorUpdate,
error: updateMutateError,
} = useUpdateAppointment(newAppointment)
const { data: patient } = usePatient(currentAppointment ? currentAppointment.patient : id)

const { appointment: reduxAppointment, patient, status, error } = useSelector(
(state: RootState) => state.appointment,
)
const breadcrumbs = [
{ i18nKey: 'scheduling.appointments.label', location: '/appointments' },
{
text: getAppointmentLabel(reduxAppointment),
location: `/appointments/${reduxAppointment.id}`,
text: currentAppointment ? getAppointmentLabel(currentAppointment) : '',
location: `/appointments/${id}`,
},
{
i18nKey: 'scheduling.appointments.editAppointment',
location: `/appointments/edit/${reduxAppointment.id}`,
location: `/appointments/edit/${id}`,
},
]
useAddBreadcrumbs(breadcrumbs, true)

useEffect(() => {
setAppointment(reduxAppointment)
}, [reduxAppointment])

const { id } = useParams()
useEffect(() => {
if (id) {
dispatch(fetchAppointment(id))
if (currentAppointment !== undefined) {
setAppointment(currentAppointment)
}
}, [id, dispatch])
}, [currentAppointment])

const onCancel = () => {
history.push(`/appointments/${appointment.id}`)
}

const onSaveSuccess = () => {
history.push(`/appointments/${appointment.id}`)
history.push(`/appointments/${newAppointment.id}`)
}

const onSave = () => {
dispatch(updateAppointment(appointment as Appointment, onSaveSuccess))
if (Object.keys(updateMutateError).length === 0 && !isErrorUpdate) {
updateMutate(newAppointment).then(() => {
Toast('success', t('states.success'), t('scheduling.appointment.successfullyUpdated'))
history.push(`/appointments/${newAppointment.id}`)
})
}
}

const onFieldChange = (key: string, value: string | boolean) => {
setAppointment({
...appointment,
...newAppointment,
[key]: value,
})
}

if (status === 'loading') {
if (isLoadingAppointment || isLoadingUpdate) {
return <Spinner color="blue" loading size={[10, 25]} type="ScaleLoader" />
}

return (
<div>
<AppointmentDetailForm
isEditable
appointment={appointment}
appointment={newAppointment}
patient={patient}
onFieldChange={onFieldChange}
error={error}
error={updateMutateError}
/>
<div className="row float-right">
<div className="btn-group btn-group-lg">
Expand Down
29 changes: 29 additions & 0 deletions src/scheduling/appointments/util/validate-appointment.ts
@@ -0,0 +1,29 @@
import { isBefore } from 'date-fns'

import Appointment from '../../../shared/model/Appointment'

export class AppointmentError extends Error {
patient?: string

startDateTime?: string

constructor(patient: string, startDateTime: string, message: string) {
super(message)
this.patient = patient
this.startDateTime = startDateTime
Object.setPrototypeOf(this, AppointmentError.prototype)
}
}

export default function validateAppointment(appointment: Appointment): AppointmentError {
const newError: any = {}

if (!appointment.patient) {
newError.patient = 'scheduling.appointment.errors.patientRequired'
}
if (isBefore(new Date(appointment.endDateTime), new Date(appointment.startDateTime))) {
newError.startDateTime = 'scheduling.appointment.errors.startDateMustBeBeforeEndDate'
}

return newError as AppointmentError
}
20 changes: 10 additions & 10 deletions src/scheduling/appointments/view/ViewAppointment.tsx
Expand Up @@ -25,11 +25,11 @@ const ViewAppointment = () => {
const setButtonToolBar = useButtonToolbarSetter()
const { permissions } = useSelector((state: RootState) => state.user)

const { data } = useAppointment(id)
const { data: patient } = usePatient(data ? data.patient : id)
const { data: appointment } = useAppointment(id)
const { data: patient } = usePatient(appointment ? appointment.patient : id)
const breadcrumbs = [
{ i18nKey: 'scheduling.appointments.label', location: '/appointments' },
{ text: data ? getAppointmentLabel(data) : '', location: `/patients/${id}` },
{ text: appointment ? getAppointmentLabel(appointment) : '', location: `/patients/${id}` },
]
useAddBreadcrumbs(breadcrumbs, true)

Expand All @@ -39,11 +39,11 @@ const ViewAppointment = () => {
}

const onDeleteConfirmationButtonClick = () => {
if (!data) {
if (!appointment) {
return
}

deleteMutate({ appointmentId: data.id }).then(() => {
deleteMutate({ appointmentId: appointment.id }).then(() => {
history.push('/appointments')
Toast('success', t('states.success'), t('scheduling.appointment.successfullyDeleted'))
})
Expand All @@ -52,15 +52,15 @@ const ViewAppointment = () => {

const getButtons = useCallback(() => {
const buttons: React.ReactNode[] = []
if (data && permissions.includes(Permissions.WriteAppointments)) {
if (appointment && permissions.includes(Permissions.WriteAppointments)) {
buttons.push(
<Button
key="editAppointmentButton"
color="success"
icon="edit"
outlined
onClick={() => {
history.push(`/appointments/edit/${data.id}`)
history.push(`/appointments/edit/${appointment.id}`)
}}
>
{t('actions.edit')}
Expand All @@ -82,7 +82,7 @@ const ViewAppointment = () => {
}

return buttons
}, [data, permissions, t, history])
}, [appointment, permissions, t, history])

useEffect(() => {
setButtonToolBar(getButtons())
Expand All @@ -94,9 +94,9 @@ const ViewAppointment = () => {

return (
<>
{patient && data ? (
{patient && appointment ? (
<div>
<AppointmentDetailForm appointment={data} isEditable={false} patient={patient} />
<AppointmentDetailForm appointment={appointment} isEditable={false} patient={patient} />
<Modal
body={t('scheduling.appointment.deleteConfirmationMessage')}
buttonsAlignment="right"
Expand Down

0 comments on commit 56e058f

Please sign in to comment.