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

Commit

Permalink
feat(appointments): adds ability to display appointments on calendar
Browse files Browse the repository at this point in the history
  • Loading branch information
jackcmeyer committed Feb 4, 2020
1 parent fe0fb6d commit 842b9ee
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/__tests__/HospitalRun.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [Permissions.ReadAppointments] },
appointments: { appointments: [] },
})}
>
<MemoryRouter initialEntries={['/appointments']}>
Expand All @@ -131,6 +132,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [] },
appointments: { appointments: [] },
})}
>
<MemoryRouter initialEntries={['/appointments']}>
Expand Down
81 changes: 80 additions & 1 deletion src/__tests__/scheduling/appointments-slice.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { AnyAction } from 'redux'
import { mocked } from 'ts-jest/utils'
import Appointment from 'model/Appointment'
import { createMemoryHistory } from 'history'
import AppointmentRepository from 'clients/db/AppointmentsRepository'
import { mocked } from 'ts-jest/utils'

import appointments, {
createAppointmentStart,
createAppointment,
getAppointmentsStart,
getAppointmentsSuccess,
fetchAppointments,
} from '../../scheduling/appointments/appointments-slice'

describe('appointments slice', () => {
Expand All @@ -22,6 +26,81 @@ describe('appointments slice', () => {

expect(appointmentsStore.isLoading).toBeTruthy()
})

it('should handle the GET_APPOINTMENTS_START action', () => {
const appointmentsStore = appointments(undefined, {
type: getAppointmentsStart.type,
})

expect(appointmentsStore.isLoading).toBeTruthy()
})

it('should handle the GET_APPOINTMENTS_SUCCESS action', () => {
const expectedAppointments = [
{
patientId: '1234',
startDateTime: new Date().toISOString(),
endDateTime: new Date().toISOString(),
},
]
const appointmentsStore = appointments(undefined, {
type: getAppointmentsSuccess.type,
payload: expectedAppointments,
})

expect(appointmentsStore.isLoading).toBeFalsy()
expect(appointmentsStore.appointments).toEqual(expectedAppointments)
})
})

describe('fetchAppointments()', () => {
let findAllSpy = jest.spyOn(AppointmentRepository, 'findAll')
const expectedAppointments: Appointment[] = [
{
id: '1',
rev: '1',
patientId: '123',
startDateTime: new Date().toISOString(),
endDateTime: new Date().toISOString(),
location: 'location',
type: 'type',
reason: 'reason',
},
]

beforeEach(() => {
findAllSpy = jest.spyOn(AppointmentRepository, 'findAll')
mocked(AppointmentRepository, true).findAll.mockResolvedValue(
expectedAppointments as Appointment[],
)
})

it('should dispatch the GET_APPOINTMENTS_START event', async () => {
const dispatch = jest.fn()
const getState = jest.fn()
await fetchAppointments()(dispatch, getState, null)

expect(dispatch).toHaveBeenCalledWith({ type: getAppointmentsStart.type })
})

it('should call the AppointmentsRepository findAll() function', async () => {
const dispatch = jest.fn()
const getState = jest.fn()
await fetchAppointments()(dispatch, getState, null)

expect(findAllSpy).toHaveBeenCalled()
})

it('should dispatch the GET_APPOINTMENTS_SUCCESS event', async () => {
const dispatch = jest.fn()
const getState = jest.fn()
await fetchAppointments()(dispatch, getState, null)

expect(dispatch).toHaveBeenCalledWith({
type: getAppointmentsSuccess.type,
payload: expectedAppointments,
})
})
})

describe('createAppointments()', () => {
Expand Down
40 changes: 35 additions & 5 deletions src/__tests__/scheduling/appointments/Appointments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@ import Appointments from 'scheduling/appointments/Appointments'
import createMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import { Calendar } from '@hospitalrun/components'
import { act } from '@testing-library/react'
import * as titleUtil from '../../../page-header/useTitle'

describe('Appointments', () => {
const setup = () => {
const expectedAppointments = [
{
id: '123',
rev: '1',
patientId: '1234',
startDateTime: new Date().toISOString(),
endDateTime: new Date().toISOString(),
location: 'location',
reason: 'reason',
},
]

const setup = async () => {
const mockStore = createMockStore([thunk])
return mount(
<Provider store={mockStore({})}>
<Provider store={mockStore({ appointments: { appointments: expectedAppointments } })}>
<MemoryRouter initialEntries={['/appointments']}>
<Appointments />
</MemoryRouter>
Expand All @@ -27,8 +40,25 @@ describe('Appointments', () => {
expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label')
})

it('should render a calendar', () => {
const wrapper = setup()
expect(wrapper.find(Calendar)).toHaveLength(1)
it('should render a calendar with the proper events', async () => {
let wrapper: any
await act(async () => {
wrapper = await setup()
})
wrapper.update()

const expectedEvents = [
{
id: expectedAppointments[0].id,
start: new Date(expectedAppointments[0].startDateTime),
end: new Date(expectedAppointments[0].endDateTime),
title: expectedAppointments[0].patientId,
allDay: false,
},
]

const calendar = wrapper.find(Calendar)
expect(calendar).toHaveLength(1)
expect(calendar.prop('events')).toEqual(expectedEvents)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import '../../../../__mocks__/matchMediaMock'
import React from 'react'
import { mount } from 'enzyme'
import { Provider } from 'react-redux'
import createMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import Appointment from 'model/Appointment'
import ViewAppointment from 'scheduling/appointments/view/ViewAppointment'
import * as titleUtil from '../../../../page-header/useTitle'
import { Router, Route } from 'react-router'
import { createMemoryHistory } from 'history'

describe('View Appointment', () => {
describe('header', () => {
it('should use the correct title', () => {
jest.spyOn(titleUtil, 'default')
const history = createMemoryHistory()
const mockStore = createMockStore([thunk])
const store = mockStore({
appointment: {
appointment: {
id: '123',
patientId: 'patient',
} as Appointment,
isLoading: false,
},
})
mount(
<Provider store={store}>
<Router history={history}>
<Route path="appointments/:id">
<ViewAppointment />
</Route>
</Router>
</Provider>,
)

expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.view.label')
})
})

it('should render a loading spinner', () => {})

it('should render a AppointmentDetailForm with the correct data', () => {})
})
115 changes: 115 additions & 0 deletions src/scheduling/appointments/AppointmentDetailForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react'
import Appointment from 'model/Appointment'
import DateTimePickerWithLabelFormGroup from 'components/input/DateTimePickerWithLabelFormGroup'
import { Typeahead, Label } from '@hospitalrun/components'
import Patient from 'model/Patient'
import PatientRepository from 'clients/db/PatientRepository'
import TextInputWithLabelFormGroup from 'components/input/TextInputWithLabelFormGroup'
import TextFieldWithLabelFormGroup from 'components/input/TextFieldWithLabelFormGroup'
import SelectWithLabelFormGroup from 'components/input/SelectWithLableFormGroup'
import { useTranslation } from 'react-i18next'
interface Props {
appointment: Appointment
onAppointmentChange: (appointment: Appointment) => void
}

const AppointmentDetailForm = (props: Props) => {
const { onAppointmentChange, appointment } = props
const { t } = useTranslation()
return (
<>
<div className="row">
<div className="col">
<div className="form-group">
<Label htmlFor="patientTypeahead" text={t('scheduling.appointment.patient')} />
<Typeahead
id="patientTypeahead"
placeholder={t('scheduling.appointment.patient')}
onChange={(patient: Patient[]) => {
onAppointmentChange({ ...appointment, patientId: patient[0].id })
}}
onSearch={async (query: string) => PatientRepository.search(query)}
searchAccessor="fullName"
renderMenuItemChildren={(patient: Patient) => (
<div>{`${patient.fullName} (${patient.friendlyId})`}</div>
)}
/>
</div>
</div>
</div>
<div className="row">
<div className="col">
<DateTimePickerWithLabelFormGroup
name="startDate"
label={t('scheduling.appointment.startDate')}
value={new Date(appointment.startDateTime)}
isEditable
onChange={(date) => {
onAppointmentChange({ ...appointment, startDateTime: date.toISOString() })
}}
/>
</div>
<div className="col">
<DateTimePickerWithLabelFormGroup
name="endDate"
label={t('scheduling.appointment.endDate')}
value={new Date(appointment.endDateTime)}
isEditable
onChange={(date) => {
onAppointmentChange({ ...appointment, endDateTime: date.toISOString() })
}}
/>
</div>
</div>
<div className="row">
<div className="col">
<TextInputWithLabelFormGroup
name="location"
label={t('scheduling.appointment.location')}
value={appointment.location}
isEditable
onChange={(event) => {
onAppointmentChange({ ...appointment, location: event?.target.value })
}}
/>
</div>
</div>
<div className="row">
<div className="col">
<SelectWithLabelFormGroup
name="type"
label={t('scheduling.appointment.type')}
value={appointment.type}
options={[
{ label: t('scheduling.appointment.types.checkup'), value: 'checkup' },
{ label: t('scheduling.appointment.types.emergency'), value: 'emergency' },
{ label: t('scheduling.appointment.types.followUp'), value: 'follow up' },
{ label: t('scheduling.appointment.types.routine'), value: 'routine' },
{ label: t('scheduling.appointment.types.walkUp'), value: 'walk up' },
]}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => {
onAppointmentChange({ ...appointment, type: event.currentTarget.value })
}}
/>
</div>
</div>
<div className="row">
<div className="col">
<div className="form-group">
<TextFieldWithLabelFormGroup
name="reason"
label={t('scheduling.appointment.reason')}
value={appointment.reason}
isEditable
onChange={(event) => {
onAppointmentChange({ ...appointment, reason: event?.target.value })
}}
/>
</div>
</div>
</div>
</>
)
}

export default AppointmentDetailForm
53 changes: 51 additions & 2 deletions src/scheduling/appointments/Appointments.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,61 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { Calendar } from '@hospitalrun/components'
import useTitle from 'page-header/useTitle'
import { useTranslation } from 'react-i18next'
import { useSelector, useDispatch } from 'react-redux'
import { RootState } from 'store'
import { useHistory } from 'react-router'
import { fetchAppointments } from './appointments-slice'

interface Event {
id: string
start: Date
end: Date
title: string
allDay: boolean
}

const Appointments = () => {
const { t } = useTranslation()
const history = useHistory()
useTitle(t('scheduling.appointments.label'))
return <Calendar />
const dispatch = useDispatch()
const { appointments } = useSelector((state: RootState) => state.appointments)
const [events, setEvents] = useState<Event[]>([])

useEffect(() => {
dispatch(fetchAppointments())
}, [dispatch])

useEffect(() => {
if (appointments) {
const newEvents: Event[] = []
appointments.forEach((a) => {
const event = {
id: a.id,
start: new Date(a.startDateTime),
end: new Date(a.endDateTime),
title: a.patientId,
allDay: false,
}

newEvents.push(event)
})

setEvents(newEvents)
}
}, [appointments])

return (
<div>
<Calendar
events={events}
onEventClick={(event) => {
history.push(`/appointments/${event.id}`)
}}
/>
</div>
)
}

export default Appointments
Loading

0 comments on commit 842b9ee

Please sign in to comment.