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 #1847 from HospitalRun/feature/delete-appointment
Browse files Browse the repository at this point in the history
feat(delete appointment): adds ability to delete appointment
  • Loading branch information
matteovivona authored Feb 23, 2020
2 parents 878684c + 73c662a commit 779bc10
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 15 deletions.
103 changes: 99 additions & 4 deletions src/__tests__/scheduling/appointments/appointment-slice.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import '../../../__mocks__/matchMediaMock'
import { AnyAction } from 'redux'
import Appointment from 'model/Appointment'
import AppointmentRepository from 'clients/db/AppointmentsRepository'
import * as components from '@hospitalrun/components'
import { mocked } from 'ts-jest/utils'
import PatientRepository from 'clients/db/PatientRepository'
import Patient from 'model/Patient'
import { createMemoryHistory } from 'history'
import appointment, {
fetchAppointmentStart,
fetchAppointmentSuccess,
fetchAppointment,
deleteAppointment,
deleteAppointmentStart,
deleteAppointmentSuccess,
} from '../../../scheduling/appointments/appointment-slice'

describe('appointment slice', () => {
Expand Down Expand Up @@ -46,6 +52,22 @@ describe('appointment slice', () => {
expect(appointmentStore.appointment).toEqual(expectedAppointment)
expect(appointmentStore.patient).toEqual(expectedPatient)
})

it('should handle the DELETE_APPOINTMENT_START action', () => {
const appointmentStore = appointment(undefined, {
type: deleteAppointmentStart.type,
})

expect(appointmentStore.isLoading).toBeTruthy()
})

it('should handle the DELETE_APPOINTMENT_SUCCESS action', () => {
const appointmentStore = appointment(undefined, {
type: deleteAppointmentSuccess.type,
})

expect(appointmentStore.isLoading).toBeFalsy()
})
})

describe('fetchAppointment()', () => {
Expand Down Expand Up @@ -107,11 +129,84 @@ describe('appointment slice', () => {
const dispatch = jest.fn()
const getState = jest.fn()
await fetchAppointment('id')(dispatch, getState, null)
})
})

expect(dispatch).toHaveBeenCalledWith({
type: fetchAppointmentSuccess.type,
payload: { appointment: expectedAppointment, patient: expectedPatient },
})
describe('deleteAppointment()', () => {
let deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete')
let toastSpy = jest.spyOn(components, 'Toast')
beforeEach(() => {
jest.resetAllMocks()
deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete')
toastSpy = jest.spyOn(components, 'Toast')
})

it('should dispatch the DELETE_APPOINTMENT_START action', async () => {
const dispatch = jest.fn()
const getState = jest.fn()

await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())(
dispatch,
getState,
null,
)

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

it('should call the AppointmentRepository delete function with the appointment', async () => {
const expectedAppointment = { id: 'appointmentId1' } as Appointment

const dispatch = jest.fn()
const getState = jest.fn()

await deleteAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null)

expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1)
expect(deleteAppointmentSpy).toHaveBeenCalledWith(expectedAppointment)
})

it('should navigate to /appointments after deleting', async () => {
const history = createMemoryHistory()
const expectedAppointment = { id: 'appointmentId1' } as Appointment

const dispatch = jest.fn()
const getState = jest.fn()

await deleteAppointment(expectedAppointment, history)(dispatch, getState, null)

expect(history.location.pathname).toEqual('/appointments')
})

it('should create a toast with a success message', async () => {
const dispatch = jest.fn()
const getState = jest.fn()

await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())(
dispatch,
getState,
null,
)

expect(toastSpy).toHaveBeenCalledTimes(1)
expect(toastSpy).toHaveBeenLastCalledWith(
'success',
'states.success',
'scheduling.appointments.successfullyDeleted',
)
})

it('should dispatch the DELETE_APPOINTMENT_SUCCESS action', async () => {
const dispatch = jest.fn()
const getState = jest.fn()

await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())(
dispatch,
getState,
null,
)

expect(dispatch).toHaveBeenCalledWith({ type: deleteAppointmentSuccess.type })
})
})
})
125 changes: 123 additions & 2 deletions src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { createMemoryHistory } from 'history'
import AppointmentRepository from 'clients/db/AppointmentsRepository'
import { mocked } from 'ts-jest/utils'
import { act } from 'react-dom/test-utils'
import { Spinner } from '@hospitalrun/components'
import { Spinner, Modal } from '@hospitalrun/components'
import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm'
import PatientRepository from 'clients/db/PatientRepository'
import Patient from 'model/Patient'
import * as ButtonBarProvider from 'page-header/ButtonBarProvider'
import Permissions from 'model/Permissions'
import * as titleUtil from '../../../../page-header/useTitle'
import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice'

Expand All @@ -37,7 +39,7 @@ describe('View Appointment', () => {
let history: any
let store: MockStore

const setup = (isLoading: boolean) => {
const setup = (isLoading: boolean, permissions = [Permissions.DeleteAppointment]) => {
jest.spyOn(AppointmentRepository, 'find')
const mockedAppointmentRepository = mocked(AppointmentRepository, true)
mockedAppointmentRepository.find.mockResolvedValue(appointment)
Expand All @@ -50,6 +52,9 @@ describe('View Appointment', () => {
history.push('/appointments/123')

store = mockStore({
user: {
permissions,
},
appointment: {
appointment,
isLoading,
Expand Down Expand Up @@ -115,4 +120,120 @@ describe('View Appointment', () => {
expect(appointmentDetailForm.prop('appointment')).toEqual(appointment)
expect(appointmentDetailForm.prop('isEditable')).toBeFalsy()
})

it('should render a modal for delete confirmation', async () => {
let wrapper: any
await act(async () => {
wrapper = await setup(false)
})

const deleteAppointmentConfirmationModal = wrapper.find(Modal)
expect(deleteAppointmentConfirmationModal).toHaveLength(1)
expect(deleteAppointmentConfirmationModal.prop('closeButton').children).toEqual(
'actions.delete',
)
expect(deleteAppointmentConfirmationModal.prop('body')).toEqual(
'scheduling.appointment.deleteConfirmationMessage',
)
expect(deleteAppointmentConfirmationModal.prop('title')).toEqual('actions.confirmDelete')
})

describe('delete appointment', () => {
let setButtonToolBarSpy = jest.fn()
let deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete')
beforeEach(() => {
jest.resetAllMocks()
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete')
setButtonToolBarSpy = jest.fn()
mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
})

it('should render a delete appointment button in the button toolbar', async () => {
await act(async () => {
await setup(false)
})

expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect((actualButtons[0] as any).props.children).toEqual('scheduling.appointment.delete')
})

it('should pop up the modal when on delete appointment click', async () => {
let wrapper: any
await act(async () => {
wrapper = await setup(false)
})

expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]

act(() => {
const { onClick } = (actualButtons[0] as any).props
onClick({ preventDefault: jest.fn() })
})
wrapper.update()

const deleteConfirmationModal = wrapper.find(Modal)
expect(deleteConfirmationModal.prop('show')).toEqual(true)
})

it('should close the modal when the toggle button is clicked', async () => {
let wrapper: any
await act(async () => {
wrapper = await setup(false)
})

expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]

act(() => {
const { onClick } = (actualButtons[0] as any).props
onClick({ preventDefault: jest.fn() })
})
wrapper.update()

act(() => {
const deleteConfirmationModal = wrapper.find(Modal)
deleteConfirmationModal.prop('toggle')()
})
wrapper.update()

const deleteConfirmationModal = wrapper.find(Modal)
expect(deleteConfirmationModal.prop('show')).toEqual(false)
})

it('should dispatch DELETE_APPOINTMENT action when modal confirmation button is clicked', async () => {
let wrapper: any
await act(async () => {
wrapper = await setup(false)
})

const deleteConfirmationModal = wrapper.find(Modal)

await act(async () => {
await deleteConfirmationModal.prop('closeButton').onClick()
})
wrapper.update()

expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1)
expect(deleteAppointmentSpy).toHaveBeenCalledWith(appointment)

expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentStart())
expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentSuccess())
})

it('should not add delete appointment button to toolbar if the user does not have delete appointment permissions', async () => {
await act(async () => {
await setup(false, [])
})

expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]

expect(
actualButtons.filter((b: any) => b.props.children === 'scheduling.appointment.delete'),
).toHaveLength(0)
})
})
})
11 changes: 8 additions & 3 deletions src/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@
"cancel": "Cancel",
"new": "New",
"list": "List",
"search": "Search"
"search": "Search",
"delete": "Delete",
"confirmDelete": "Confirm Delete"
},
"states": {
"success": "Success!",
Expand All @@ -77,7 +79,8 @@
"appointments": {
"label": "Appointments",
"new": "New Appointment",
"view": "View Appointment"
"view": "View Appointment",
"successfullyDeleted": "Successfully deleted appointment!"
},
"appointment": {
"startDate": "Start Date",
Expand All @@ -97,7 +100,9 @@
"startDateMustBeBeforeEndDate": "Start Time must be before End Time."
},
"reason": "Reason",
"patient": "Patient"
"patient": "Patient",
"delete": "Delete Appointment",
"deleteConfirmationMessage": "Are you sure you want to delete this appointment?"
}
}
}
1 change: 1 addition & 0 deletions src/model/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ enum Permissions {
WritePatients = 'write:patients',
ReadAppointments = 'read:appointments',
WriteAppointments = 'write:appointments',
DeleteAppointment = 'delete:appointment',
}

export default Permissions
29 changes: 28 additions & 1 deletion src/scheduling/appointments/appointment-slice.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import Appointment from 'model/Appointment'
import { AppThunk } from 'store'
import { Toast } from '@hospitalrun/components'
import AppointmentRepository from 'clients/db/AppointmentsRepository'
import Patient from 'model/Patient'
import PatientRepository from 'clients/db/PatientRepository'
import il8n from '../../i18n'

interface AppointmentState {
appointment: Appointment
Expand All @@ -24,6 +26,12 @@ const appointmentSlice = createSlice({
fetchAppointmentStart: (state: AppointmentState) => {
state.isLoading = true
},
deleteAppointmentStart: (state: AppointmentState) => {
state.isLoading = true
},
deleteAppointmentSuccess: (state: AppointmentState) => {
state.isLoading = false
},
fetchAppointmentSuccess: (
state,
{ payload }: PayloadAction<{ appointment: Appointment; patient: Patient }>,
Expand All @@ -35,7 +43,12 @@ const appointmentSlice = createSlice({
},
})

export const { fetchAppointmentStart, fetchAppointmentSuccess } = appointmentSlice.actions
export const {
fetchAppointmentStart,
fetchAppointmentSuccess,
deleteAppointmentStart,
deleteAppointmentSuccess,
} = appointmentSlice.actions

export const fetchAppointment = (id: string): AppThunk => async (dispatch) => {
dispatch(fetchAppointmentStart())
Expand All @@ -45,4 +58,18 @@ export const fetchAppointment = (id: string): AppThunk => async (dispatch) => {
dispatch(fetchAppointmentSuccess({ appointment, patient }))
}

export const deleteAppointment = (appointment: Appointment, history: any): AppThunk => async (
dispatch,
) => {
dispatch(deleteAppointmentStart())
await AppointmentRepository.delete(appointment)
history.push('/appointments')
Toast(
'success',
il8n.t('states.success'),
`${il8n.t('scheduling.appointments.successfullyDeleted')}`,
)
dispatch(deleteAppointmentSuccess())
}

export default appointmentSlice.reducer
Loading

1 comment on commit 779bc10

@vercel
Copy link

@vercel vercel bot commented on 779bc10 Feb 23, 2020

Choose a reason for hiding this comment

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

Please sign in to comment.