diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx
index 206b4f8c07..b54011daa6 100644
--- a/src/HospitalRun.tsx
+++ b/src/HospitalRun.tsx
@@ -6,6 +6,7 @@ import Appointments from 'scheduling/appointments/Appointments'
import NewAppointment from 'scheduling/appointments/new/NewAppointment'
import EditAppointment from 'scheduling/appointments/edit/EditAppointment'
import ViewAppointment from 'scheduling/appointments/view/ViewAppointment'
+import Breadcrumbs from 'breadcrumbs/Breadcrumbs'
import { ButtonBarProvider } from 'page-header/ButtonBarProvider'
import ButtonToolBar from 'page-header/ButtonToolBar'
import Sidebar from './components/Sidebar'
@@ -35,6 +36,7 @@ const HospitalRun = () => {
{title}
+
diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx
index 9cd65ce7e3..f352146852 100644
--- a/src/__tests__/HospitalRun.test.tsx
+++ b/src/__tests__/HospitalRun.test.tsx
@@ -7,11 +7,13 @@ import { mocked } from 'ts-jest/utils'
import thunk from 'redux-thunk'
import configureMockStore from 'redux-mock-store'
import { Toaster } from '@hospitalrun/components'
+
import { act } from 'react-dom/test-utils'
import Dashboard from 'dashboard/Dashboard'
import Appointments from 'scheduling/appointments/Appointments'
import NewAppointment from 'scheduling/appointments/new/NewAppointment'
import EditAppointment from 'scheduling/appointments/edit/EditAppointment'
+import { addBreadcrumbs } from 'breadcrumbs/breadcrumbs-slice'
import NewPatient from '../patients/new/NewPatient'
import EditPatient from '../patients/edit/EditPatient'
import ViewPatient from '../patients/view/ViewPatient'
@@ -28,13 +30,14 @@ describe('HospitalRun', () => {
describe('routing', () => {
describe('/patients/new', () => {
it('should render the new patient screen when /patients/new is accessed', () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.WritePatients] },
+ breadcrumbs: { breadcrumbs: [] },
+ })
+
const wrapper = mount(
-
+
@@ -42,6 +45,14 @@ describe('HospitalRun', () => {
)
expect(wrapper.find(NewPatient)).toHaveLength(1)
+
+ expect(store.getActions()).toContainEqual(
+ addBreadcrumbs([
+ { i18nKey: 'patients.label', location: '/patients' },
+ { i18nKey: 'patients.newPatient', location: '/patients/new' },
+ { i18nKey: 'dashboard.label', location: '/' },
+ ]),
+ )
})
it('should render the Dashboard if the user does not have write patient privileges', () => {
@@ -50,6 +61,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -77,14 +89,15 @@ describe('HospitalRun', () => {
mockedPatientRepository.find.mockResolvedValue(patient)
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.WritePatients, Permissions.ReadPatients] },
+ patient: { patient },
+ breadcrumbs: { breadcrumbs: [] },
+ })
+
const wrapper = mount(
-
+
@@ -92,6 +105,15 @@ describe('HospitalRun', () => {
)
expect(wrapper.find(EditPatient)).toHaveLength(1)
+
+ expect(store.getActions()).toContainEqual(
+ addBreadcrumbs([
+ { i18nKey: 'patients.label', location: '/patients' },
+ { text: 'test test test', location: `/patients/${patient.id}` },
+ { i18nKey: 'patients.editPatient', location: `/patients/${patient.id}/edit` },
+ { i18nKey: 'dashboard.label', location: '/' },
+ ]),
+ )
})
it('should render the Dashboard when the user does not have read patient privileges', () => {
@@ -100,6 +122,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [Permissions.WritePatients] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -117,6 +140,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [Permissions.ReadPatients] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -144,14 +168,15 @@ describe('HospitalRun', () => {
mockedPatientRepository.find.mockResolvedValue(patient)
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.ReadPatients] },
+ patient: { patient },
+ breadcrumbs: { breadcrumbs: [] },
+ })
+
const wrapper = mount(
-
+
@@ -159,6 +184,14 @@ describe('HospitalRun', () => {
)
expect(wrapper.find(ViewPatient)).toHaveLength(1)
+
+ expect(store.getActions()).toContainEqual(
+ addBreadcrumbs([
+ { i18nKey: 'patients.label', location: '/patients' },
+ { text: 'test test test', location: `/patients/${patient.id}` },
+ { i18nKey: 'dashboard.label', location: '/' },
+ ]),
+ )
})
it('should render the Dashboard when the user does not have read patient privileges', () => {
@@ -167,6 +200,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -181,14 +215,15 @@ describe('HospitalRun', () => {
describe('/appointments', () => {
it('should render the appointments screen when /appointments is accessed', async () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.ReadAppointments] },
+ appointments: { appointments: [] },
+ breadcrumbs: { breadcrumbs: [] },
+ })
+
const wrapper = mount(
-
+
@@ -200,6 +235,13 @@ describe('HospitalRun', () => {
})
expect(wrapper.find(Appointments)).toHaveLength(1)
+
+ expect(store.getActions()).toContainEqual(
+ addBreadcrumbs([
+ { i18nKey: 'scheduling.appointments.label', location: '/appointments' },
+ { i18nKey: 'dashboard.label', location: '/' },
+ ]),
+ )
})
it('should render the Dashboard when the user does not have read appointment privileges', () => {
@@ -208,7 +250,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [] },
- appointments: { appointments: [] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -223,20 +265,30 @@ describe('HospitalRun', () => {
describe('/appointments/new', () => {
it('should render the new appointment screen when /appointments/new is accessed', async () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.WriteAppointments] },
+ breadcrumbs: { breadcrumbs: [] },
+ })
+
const wrapper = mount(
-
+
,
)
+ wrapper.update()
+
expect(wrapper.find(NewAppointment)).toHaveLength(1)
+ expect(store.getActions()).toContainEqual(
+ addBreadcrumbs([
+ { i18nKey: 'scheduling.appointments.label', location: '/appointments' },
+ { i18nKey: 'scheduling.appointments.newAppointment', location: '/appointments/new' },
+ { i18nKey: 'dashboard.label', location: '/' },
+ ]),
+ )
})
it('should render the Dashboard when the user does not have read appointment privileges', () => {
@@ -245,6 +297,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -280,6 +333,7 @@ describe('HospitalRun', () => {
title: 'test',
user: { permissions: [Permissions.WriteAppointments, Permissions.ReadAppointments] },
appointment: { appointment: {} as Appointment, patient: {} as Patient },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -297,6 +351,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [Permissions.WriteAppointments] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -314,6 +369,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [Permissions.ReadAppointments] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
@@ -334,6 +390,7 @@ describe('HospitalRun', () => {
store={mockStore({
title: 'test',
user: { permissions: [Permissions.WritePatients] },
+ breadcrumbs: { breadcrumbs: [] },
})}
>
diff --git a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx
new file mode 100644
index 0000000000..f14bc46b9f
--- /dev/null
+++ b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx
@@ -0,0 +1,58 @@
+import '../../__mocks__/matchMediaMock'
+import React from 'react'
+import { Provider } from 'react-redux'
+import { mount } from 'enzyme'
+import { createMemoryHistory } from 'history'
+import { Router } from 'react-router-dom'
+import configureMockStore from 'redux-mock-store'
+import {
+ Breadcrumb as HRBreadcrumb,
+ BreadcrumbItem as HRBreadcrumbItem,
+} from '@hospitalrun/components'
+
+import Breadcrumbs from 'breadcrumbs/Breadcrumbs'
+import Breadcrumb from 'model/Breadcrumb'
+
+const mockStore = configureMockStore()
+
+describe('Breadcrumbs', () => {
+ const setup = (breadcrumbs: Breadcrumb[]) => {
+ const history = createMemoryHistory()
+ const store = mockStore({
+ breadcrumbs: { breadcrumbs },
+ })
+
+ const wrapper = mount(
+
+
+
+
+ ,
+ )
+
+ return wrapper
+ }
+
+ it('should not render the breadcrumb when there are no items in the store', () => {
+ const wrapper = setup([])
+
+ expect(wrapper.find(HRBreadcrumb)).toHaveLength(0)
+ expect(wrapper.find(HRBreadcrumbItem)).toHaveLength(0)
+ })
+
+ it('should render breadcrumbs items', () => {
+ const breadcrumbs = [
+ { i18nKey: 'patient.label', location: '/patient' },
+ { text: 'Bob', location: '/patient/1' },
+ { text: 'Edit Patient', location: '/patient/1/edit' },
+ ]
+ const wrapper = setup(breadcrumbs)
+
+ const items = wrapper.find(HRBreadcrumbItem)
+
+ expect(items).toHaveLength(3)
+ expect(items.at(0).text()).toEqual('patient.label')
+ expect(items.at(1).text()).toEqual('Bob')
+ expect(items.at(2).text()).toEqual('Edit Patient')
+ })
+})
diff --git a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts
new file mode 100644
index 0000000000..10f73f19ff
--- /dev/null
+++ b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts
@@ -0,0 +1,82 @@
+import '../../__mocks__/matchMediaMock'
+import { AnyAction } from 'redux'
+import breadcrumbs, { addBreadcrumbs, removeBreadcrumbs } from '../../breadcrumbs/breadcrumbs-slice'
+
+describe('breadcrumbs slice', () => {
+ describe('breadcrumbs reducer', () => {
+ it('should create the proper initial state with empty patients array', () => {
+ const breadcrumbsStore = breadcrumbs(undefined, {} as AnyAction)
+
+ expect(breadcrumbsStore.breadcrumbs).toEqual([])
+ })
+
+ it('should handle the ADD_BREADCRUMBS action', () => {
+ const breadcrumbsToAdd = [
+ { text: 'user', location: '/user' },
+ { text: 'Bob', location: '/user/1' },
+ ]
+
+ const breadcrumbsStore = breadcrumbs(undefined, {
+ type: addBreadcrumbs.type,
+ payload: breadcrumbsToAdd,
+ })
+
+ expect(breadcrumbsStore.breadcrumbs).toEqual(breadcrumbsToAdd)
+ })
+
+ it('should handle the ADD_BREADCRUMBS action with existing breadcrumbs', () => {
+ const breadcrumbsToAdd = [{ text: 'Bob', location: '/user/1' }]
+
+ const state = {
+ breadcrumbs: [{ text: 'user', location: '/user' }],
+ }
+
+ const breadcrumbsStore = breadcrumbs(state, {
+ type: addBreadcrumbs.type,
+ payload: breadcrumbsToAdd,
+ })
+
+ expect(breadcrumbsStore.breadcrumbs).toEqual([...state.breadcrumbs, ...breadcrumbsToAdd])
+ })
+
+ it('should handle the ADD_BREADCRUMBS action and sort the breadcrumbs by their location', () => {
+ const breadcrumbsToAdd = [{ text: 'Bob', location: '/user/1/' }]
+
+ const state = {
+ breadcrumbs: [
+ { text: 'user', location: '/user' },
+ { text: 'edit user', location: '/user/1/edit' },
+ ],
+ }
+
+ const breadcrumbsStore = breadcrumbs(state, {
+ type: addBreadcrumbs.type,
+ payload: breadcrumbsToAdd,
+ })
+
+ expect(breadcrumbsStore.breadcrumbs).toEqual([
+ { text: 'user', location: '/user' },
+ { text: 'Bob', location: '/user/1/' },
+ { text: 'edit user', location: '/user/1/edit' },
+ ])
+ })
+
+ it('should handle the REMOVE_BREADCRUMBS action', () => {
+ const breadcrumbsToRemove = [{ text: 'Bob', location: '/user/1' }]
+
+ const state = {
+ breadcrumbs: [
+ { text: 'user', location: '/user' },
+ { text: 'Bob', location: '/user/1' },
+ ],
+ }
+
+ const breadcrumbsStore = breadcrumbs(state, {
+ type: removeBreadcrumbs.type,
+ payload: breadcrumbsToRemove,
+ })
+
+ expect(breadcrumbsStore.breadcrumbs).toEqual([{ text: 'user', location: '/user' }])
+ })
+ })
+})
diff --git a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx
new file mode 100644
index 0000000000..452f76ebdc
--- /dev/null
+++ b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react'
+import { renderHook } from '@testing-library/react-hooks'
+import configureMockStore from 'redux-mock-store'
+import { Provider } from 'react-redux'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
+import * as breadcrumbsSlice from '../../breadcrumbs/breadcrumbs-slice'
+
+const store = configureMockStore()
+
+describe('useAddBreadcrumbs', () => {
+ beforeEach(() => jest.clearAllMocks())
+
+ it('should call addBreadcrumbs with the correct data', () => {
+ const wrapper = ({ children }: any) => {children}
+
+ jest.spyOn(breadcrumbsSlice, 'addBreadcrumbs')
+ const breadcrumbs = [
+ {
+ text: 'Patients',
+ location: '/patients',
+ },
+ ]
+
+ renderHook(() => useAddBreadcrumbs(breadcrumbs), { wrapper } as any)
+ expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledTimes(1)
+ expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledWith(breadcrumbs)
+ })
+
+ it('should call addBreadcrumbs with an additional dashboard breadcrumb', () => {
+ const wrapper = ({ children }: any) => {children}
+
+ jest.spyOn(breadcrumbsSlice, 'addBreadcrumbs')
+ const breadcrumbs = [
+ {
+ text: 'Patients',
+ location: '/patients',
+ },
+ ]
+
+ renderHook(() => useAddBreadcrumbs(breadcrumbs, true), { wrapper } as any)
+ expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledTimes(1)
+ expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledWith([
+ ...breadcrumbs,
+ { i18nKey: 'dashboard.label', location: '/' },
+ ])
+ })
+
+ it('should call removeBreadcrumbs with the correct data after unmount', () => {
+ const wrapper = ({ children }: any) => {children}
+
+ jest.spyOn(breadcrumbsSlice, 'addBreadcrumbs')
+ jest.spyOn(breadcrumbsSlice, 'removeBreadcrumbs')
+ const breadcrumbs = [
+ {
+ text: 'Patients',
+ location: '/patients',
+ },
+ ]
+
+ const { unmount } = renderHook(() => useAddBreadcrumbs(breadcrumbs), { wrapper } as any)
+ unmount()
+ expect(breadcrumbsSlice.removeBreadcrumbs).toHaveBeenCalledTimes(1)
+ expect(breadcrumbsSlice.removeBreadcrumbs).toHaveBeenCalledWith(breadcrumbs)
+ })
+})
diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx
index 5b56f8df0b..4c0d9ad99d 100644
--- a/src/__tests__/patients/view/ViewPatient.test.tsx
+++ b/src/__tests__/patients/view/ViewPatient.test.tsx
@@ -42,7 +42,7 @@ describe('ViewPatient', () => {
let history: any
let store: MockStore
- const setup = () => {
+ const setup = (permissions = [Permissions.ReadPatients]) => {
jest.spyOn(PatientRepository, 'find')
const mockedPatientRepository = mocked(PatientRepository, true)
mockedPatientRepository.find.mockResolvedValue(patient)
@@ -50,7 +50,7 @@ describe('ViewPatient', () => {
history = createMemoryHistory()
store = mockStore({
patient: { patient },
- user: { permissions: [Permissions.ReadPatients] },
+ user: { permissions },
})
history.push('/patients/123')
@@ -92,17 +92,29 @@ describe('ViewPatient', () => {
)
})
- it('should add a "Edit Patient" button to the button tool bar', () => {
+ it('should add a "Edit Patient" button to the button tool bar if has WritePatients permissions', () => {
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
const setButtonToolBarSpy = jest.fn()
mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
- setup()
+ setup([Permissions.WritePatients])
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect((actualButtons[0] as any).props.children).toEqual('actions.edit')
})
+ it('button toolbar empty if only has ReadPatients permission', () => {
+ jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
+ const setButtonToolBarSpy = jest.fn()
+ mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
+
+ setup()
+
+ const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
+ console.log(actualButtons)
+ expect(actualButtons.length).toEqual(0)
+ })
+
it('should render a tabs header with the correct tabs', async () => {
let wrapper: any
await act(async () => {
diff --git a/src/__tests__/scheduling/appointments/appointment-slice.test.ts b/src/__tests__/scheduling/appointments/appointment-slice.test.ts
index 8375c746e9..ba46f6404d 100644
--- a/src/__tests__/scheduling/appointments/appointment-slice.test.ts
+++ b/src/__tests__/scheduling/appointments/appointment-slice.test.ts
@@ -1,6 +1,8 @@
+import '../../../__mocks__/matchMediaMock'
import { AnyAction } from 'redux'
import Appointment from 'model/Appointment'
import AppointmentRepository from 'clients/db/AppointmentRepository'
+import * as components from '@hospitalrun/components'
import { mocked } from 'ts-jest/utils'
import { createMemoryHistory } from 'history'
import PatientRepository from 'clients/db/PatientRepository'
@@ -15,6 +17,9 @@ import appointment, {
updateAppointmentStart,
updateAppointmentSuccess,
updateAppointment,
+ deleteAppointment,
+ deleteAppointmentStart,
+ deleteAppointmentSuccess,
} from '../../../scheduling/appointments/appointment-slice'
describe('appointment slice', () => {
@@ -97,6 +102,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('createAppointment()', () => {
@@ -221,11 +242,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 })
})
})
diff --git a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx
index 8f3c1b8965..b02fe24623 100644
--- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx
+++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx
@@ -142,6 +142,7 @@ describe('New Appointment', () => {
wrapper.update()
const saveButton = wrapper.find(Button).at(0)
+ expect(saveButton.text().trim()).toEqual('actions.save')
const onClick = saveButton.prop('onClick') as any
await act(async () => {
diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx
index 85e475bb5a..d1f69af573 100644
--- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx
+++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx
@@ -11,11 +11,12 @@ import { createMemoryHistory } from 'history'
import AppointmentRepository from 'clients/db/AppointmentRepository'
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'
@@ -38,10 +39,12 @@ describe('View Appointment', () => {
let history: any
let store: MockStore
- const setup = (isLoading: boolean) => {
+ const setup = (isLoading: boolean, permissions = [Permissions.ReadAppointments]) => {
jest.spyOn(AppointmentRepository, 'find')
+ jest.spyOn(AppointmentRepository, 'delete')
const mockedAppointmentRepository = mocked(AppointmentRepository, true)
mockedAppointmentRepository.find.mockResolvedValue(appointment)
+ mockedAppointmentRepository.delete.mockResolvedValue(appointment)
jest.spyOn(PatientRepository, 'find')
const mockedPatientRepository = mocked(PatientRepository, true)
@@ -51,6 +54,9 @@ describe('View Appointment', () => {
history.push('/appointments/123')
store = mockStore({
+ user: {
+ permissions,
+ },
appointment: {
appointment,
isLoading,
@@ -85,17 +91,41 @@ describe('View Appointment', () => {
expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.viewAppointment')
})
- it('should add a "Edit Appointment" button to the button tool bar', () => {
+ it('should add a "Edit Appointment" button to the button tool bar if has WriteAppointment permissions', () => {
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
const setButtonToolBarSpy = jest.fn()
mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
- setup(true)
+ setup(true, [Permissions.WriteAppointments, Permissions.ReadAppointments])
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect((actualButtons[0] as any).props.children).toEqual('actions.edit')
})
+ it('should add a "Delete Appointment" button to the button tool bar if has DeleteAppointment permissions', () => {
+ jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
+ const setButtonToolBarSpy = jest.fn()
+ mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
+
+ setup(true, [Permissions.DeleteAppointment, Permissions.ReadAppointments])
+
+ const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
+ expect((actualButtons[0] as any).props.children).toEqual(
+ 'scheduling.appointments.deleteAppointment',
+ )
+ })
+
+ it('button toolbar empty if has only ReadAppointments permission', () => {
+ jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
+ const setButtonToolBarSpy = jest.fn()
+ mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
+
+ setup(true)
+
+ const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
+ expect(actualButtons.length).toEqual(0)
+ })
+
it('should dispatch getAppointment if id is present', async () => {
await act(async () => {
await setup(true)
@@ -127,4 +157,109 @@ 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, [Permissions.ReadAppointments, Permissions.DeleteAppointment])
+ })
+
+ expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
+ const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
+ expect((actualButtons[0] as any).props.children).toEqual(
+ 'scheduling.appointments.deleteAppointment',
+ )
+ })
+
+ it('should pop up the modal when on delete appointment click', async () => {
+ let wrapper: any
+ await act(async () => {
+ wrapper = await setup(false, [Permissions.ReadAppointments, Permissions.DeleteAppointment])
+ })
+
+ 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, [Permissions.ReadAppointments, Permissions.DeleteAppointment])
+ })
+
+ 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, [Permissions.ReadAppointments, Permissions.DeleteAppointment])
+ })
+
+ 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())
+ })
+ })
})
diff --git a/src/breadcrumbs/Breadcrumbs.tsx b/src/breadcrumbs/Breadcrumbs.tsx
new file mode 100644
index 0000000000..263f8e9476
--- /dev/null
+++ b/src/breadcrumbs/Breadcrumbs.tsx
@@ -0,0 +1,33 @@
+import React from 'react'
+import { useHistory } from 'react-router'
+import { useSelector } from 'react-redux'
+import { useTranslation } from 'react-i18next'
+import { Breadcrumb, BreadcrumbItem } from '@hospitalrun/components'
+import { RootState } from '../store'
+
+const Breadcrumbs = () => {
+ const history = useHistory()
+ const { t } = useTranslation()
+ const { breadcrumbs } = useSelector((state: RootState) => state.breadcrumbs)
+
+ if (breadcrumbs.length === 0) {
+ return null
+ }
+
+ return (
+
+ {breadcrumbs.map(({ i18nKey, text, location }, index) => {
+ const isLast = index === breadcrumbs.length - 1
+ const onClick = !isLast ? () => history.push(location) : undefined
+
+ return (
+
+ {i18nKey ? t(i18nKey) : text}
+
+ )
+ })}
+
+ )
+}
+
+export default Breadcrumbs
diff --git a/src/breadcrumbs/breadcrumbs-slice.ts b/src/breadcrumbs/breadcrumbs-slice.ts
new file mode 100644
index 0000000000..86a689e0d8
--- /dev/null
+++ b/src/breadcrumbs/breadcrumbs-slice.ts
@@ -0,0 +1,32 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+import Breadcrumb from 'model/Breadcrumb'
+
+interface BreadcrumbsState {
+ breadcrumbs: Breadcrumb[]
+}
+
+const initialState: BreadcrumbsState = {
+ breadcrumbs: [],
+}
+
+const breadcrumbsSlice = createSlice({
+ name: 'breadcrumbs',
+ initialState,
+ reducers: {
+ addBreadcrumbs(state, { payload }: PayloadAction) {
+ state.breadcrumbs = [...state.breadcrumbs, ...payload].sort(
+ (b1, b2) => b1.location.length - b2.location.length,
+ )
+ },
+ removeBreadcrumbs(state, { payload }: PayloadAction) {
+ const locations = payload.map((b) => b.location)
+ state.breadcrumbs = state.breadcrumbs.filter(
+ (breadcrumb) => !locations.includes(breadcrumb.location),
+ )
+ },
+ },
+})
+
+export const { addBreadcrumbs, removeBreadcrumbs } = breadcrumbsSlice.actions
+
+export default breadcrumbsSlice.reducer
diff --git a/src/breadcrumbs/useAddBreadcrumbs.ts b/src/breadcrumbs/useAddBreadcrumbs.ts
new file mode 100644
index 0000000000..76d68e80ce
--- /dev/null
+++ b/src/breadcrumbs/useAddBreadcrumbs.ts
@@ -0,0 +1,21 @@
+import { useEffect } from 'react'
+import { useDispatch } from 'react-redux'
+import Breadcrumb from 'model/Breadcrumb'
+import { addBreadcrumbs, removeBreadcrumbs } from './breadcrumbs-slice'
+
+export default function useAddBreadcrumbs(breadcrumbs: Breadcrumb[], withDashboard = false): void {
+ const dispatch = useDispatch()
+
+ const breadcrumbsStringified = withDashboard
+ ? JSON.stringify([...breadcrumbs, { i18nKey: 'dashboard.label', location: '/' }])
+ : JSON.stringify(breadcrumbs)
+
+ useEffect(() => {
+ const breadcrumbsParsed: Breadcrumb[] = JSON.parse(breadcrumbsStringified)
+ dispatch(addBreadcrumbs(breadcrumbsParsed))
+
+ return () => {
+ dispatch(removeBreadcrumbs(breadcrumbsParsed))
+ }
+ }, [breadcrumbsStringified, dispatch])
+}
diff --git a/src/index.css b/src/index.css
index 49f3c5114d..417dbe2316 100644
--- a/src/index.css
+++ b/src/index.css
@@ -88,3 +88,8 @@ code {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}
+
+.breadcrumb {
+ padding: 0;
+ background-color: white;
+}
diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json
index 0aada8be38..9f6ea58b29 100644
--- a/src/locales/en-US/translation.json
+++ b/src/locales/en-US/translation.json
@@ -66,7 +66,9 @@
"cancel": "Cancel",
"new": "New",
"list": "List",
- "search": "Search"
+ "search": "Search",
+ "delete": "Delete",
+ "confirmDelete": "Confirm Delete"
},
"states": {
"success": "Success!",
@@ -78,7 +80,9 @@
"label": "Appointments",
"newAppointment": "New Appointment",
"editAppointment": "Edit Appointment",
- "viewAppointment": "View Appointment"
+ "viewAppointment": "View Appointment",
+ "deleteAppointment": "Delete Appointment",
+ "successfullyDeleted": "Successfully deleted appointment!"
},
"appointment": {
"startDate": "Start Date",
@@ -97,7 +101,8 @@
"startDateMustBeBeforeEndDate": "Start Time must be before End Time."
},
"reason": "Reason",
- "patient": "Patient"
+ "patient": "Patient",
+ "deleteConfirmationMessage": "Are you sure you want to delete this appointment?"
}
}
}
diff --git a/src/model/Breadcrumb.ts b/src/model/Breadcrumb.ts
new file mode 100644
index 0000000000..b93f41ed41
--- /dev/null
+++ b/src/model/Breadcrumb.ts
@@ -0,0 +1,5 @@
+export default interface Breadcrumb {
+ i18nKey?: string
+ text?: string
+ location: string
+}
diff --git a/src/model/Permissions.ts b/src/model/Permissions.ts
index 5640a99292..fdb910cc63 100644
--- a/src/model/Permissions.ts
+++ b/src/model/Permissions.ts
@@ -3,6 +3,7 @@ enum Permissions {
WritePatients = 'write:patients',
ReadAppointments = 'read:appointments',
WriteAppointments = 'write:appointments',
+ DeleteAppointment = 'delete:appointment',
}
export default Permissions
diff --git a/src/patients/appointments/AppointmentsList.tsx b/src/patients/appointments/AppointmentsList.tsx
index feb0f9189c..7224bce5f8 100644
--- a/src/patients/appointments/AppointmentsList.tsx
+++ b/src/patients/appointments/AppointmentsList.tsx
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { TextInput, Button, List, ListItem, Container, Row } from '@hospitalrun/components'
import { RootState } from '../../store'
import { fetchPatientAppointments } from '../../scheduling/appointments/appointments-slice'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
interface Props {
patientId: string
@@ -19,6 +20,14 @@ const AppointmentsList = (props: Props) => {
const { appointments } = useSelector((state: RootState) => state.appointments)
const [searchText, setSearchText] = useState('')
+ const breadcrumbs = [
+ {
+ i18nKey: 'scheduling.appointments.label',
+ location: `/patients/${patientId}/appointments`,
+ },
+ ]
+ useAddBreadcrumbs(breadcrumbs)
+
useEffect(() => {
dispatch(fetchPatientAppointments(patientId))
}, [dispatch, patientId])
diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx
index 5ce7b57957..5dc801c3f5 100644
--- a/src/patients/edit/EditPatient.tsx
+++ b/src/patients/edit/EditPatient.tsx
@@ -10,6 +10,7 @@ import Patient from '../../model/Patient'
import { updatePatient, fetchPatient } from '../patient-slice'
import { RootState } from '../../store'
import { getPatientFullName, getPatientName } from '../util/patient-name-util'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
const getFriendlyId = (p: Patient): string => {
if (p) {
@@ -34,6 +35,13 @@ const EditPatient = () => {
)})`,
)
+ const breadcrumbs = [
+ { i18nKey: 'patients.label', location: '/patients' },
+ { text: getPatientFullName(reduxPatient), location: `/patients/${reduxPatient.id}` },
+ { i18nKey: 'patients.editPatient', location: `/patients/${reduxPatient.id}/edit` },
+ ]
+ useAddBreadcrumbs(breadcrumbs, true)
+
useEffect(() => {
setPatient(reduxPatient)
}, [reduxPatient])
diff --git a/src/patients/list/Patients.tsx b/src/patients/list/Patients.tsx
index 1a8373eee4..2a428793ee 100644
--- a/src/patients/list/Patients.tsx
+++ b/src/patients/list/Patients.tsx
@@ -7,11 +7,15 @@ import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider'
import { RootState } from '../../store'
import { fetchPatients, searchPatients } from '../patients-slice'
import useTitle from '../../page-header/useTitle'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
+
+const breadcrumbs = [{ i18nKey: 'patients.label', location: '/patients' }]
const Patients = () => {
const { t } = useTranslation()
const history = useHistory()
useTitle(t('patients.label'))
+ useAddBreadcrumbs(breadcrumbs, true)
const dispatch = useDispatch()
const { patients, isLoading } = useSelector((state: RootState) => state.patients)
diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx
index f80930511b..527d3f764d 100644
--- a/src/patients/new/NewPatient.tsx
+++ b/src/patients/new/NewPatient.tsx
@@ -9,6 +9,12 @@ import useTitle from '../../page-header/useTitle'
import Patient from '../../model/Patient'
import { createPatient } from '../patient-slice'
import { getPatientName } from '../util/patient-name-util'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
+
+const breadcrumbs = [
+ { i18nKey: 'patients.label', location: '/patients' },
+ { i18nKey: 'patients.newPatient', location: '/patients/new' },
+]
const NewPatient = () => {
const { t } = useTranslation()
@@ -19,6 +25,7 @@ const NewPatient = () => {
const [errorMessage, setErrorMessage] = useState('')
useTitle(t('patients.newPatient'))
+ useAddBreadcrumbs(breadcrumbs, true)
const onCancel = () => {
history.push('/patients')
diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx
index 8eecc152da..f3451dae19 100644
--- a/src/patients/related-persons/RelatedPersonTab.tsx
+++ b/src/patients/related-persons/RelatedPersonTab.tsx
@@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'store'
import Permissions from 'model/Permissions'
import PatientRepository from 'clients/db/PatientRepository'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
interface Props {
patient: Patient
@@ -28,6 +29,14 @@ const RelatedPersonTab = (props: Props) => {
const [showNewRelatedPersonModal, setShowRelatedPersonModal] = useState(false)
const [relatedPersons, setRelatedPersons] = useState(undefined)
+ const breadcrumbs = [
+ {
+ i18nKey: 'patient.relatedPersons.label',
+ location: `/patients/${patient.id}/relatedpersons`,
+ },
+ ]
+ useAddBreadcrumbs(breadcrumbs)
+
useEffect(() => {
const fetchRelatedPersons = async () => {
const fetchedRelatedPersons: Patient[] = []
diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx
index 1d221e1ce0..5d4ad45cce 100644
--- a/src/patients/view/ViewPatient.tsx
+++ b/src/patients/view/ViewPatient.tsx
@@ -9,9 +9,11 @@ import useTitle from '../../page-header/useTitle'
import { fetchPatient } from '../patient-slice'
import { RootState } from '../../store'
import { getPatientFullName } from '../util/patient-name-util'
+import Permissions from '../../model/Permissions'
import Patient from '../../model/Patient'
import GeneralInformation from '../GeneralInformation'
import RelatedPerson from '../related-persons/RelatedPersonTab'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
import AppointmentsList from '../appointments/AppointmentsList'
const getFriendlyId = (p: Patient): string => {
@@ -29,23 +31,36 @@ const ViewPatient = () => {
const location = useLocation()
const { patient, isLoading } = useSelector((state: RootState) => state.patient)
+ const { permissions } = useSelector((state: RootState) => state.user)
useTitle(`${getPatientFullName(patient)} (${getFriendlyId(patient)})`)
const setButtonToolBar = useButtonToolbarSetter()
- setButtonToolBar([
- ,
- ])
+
+ const buttons = []
+ if (permissions.includes(Permissions.WritePatients)) {
+ buttons.push(
+ ,
+ )
+ }
+
+ setButtonToolBar(buttons)
+
+ const breadcrumbs = [
+ { i18nKey: 'patients.label', location: '/patients' },
+ { text: getPatientFullName(patient), location: `/patients/${patient.id}` },
+ ]
+ useAddBreadcrumbs(breadcrumbs, true)
const { id } = useParams()
useEffect(() => {
diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx
index 4490ff7960..6d40df2996 100644
--- a/src/scheduling/appointments/Appointments.tsx
+++ b/src/scheduling/appointments/Appointments.tsx
@@ -6,6 +6,7 @@ import { useSelector, useDispatch } from 'react-redux'
import { RootState } from 'store'
import { useHistory } from 'react-router'
import PatientRepository from 'clients/db/PatientRepository'
+import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs'
import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider'
import { fetchAppointments } from './appointments-slice'
@@ -17,6 +18,8 @@ interface Event {
allDay: boolean
}
+const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/appointments' }]
+
const Appointments = () => {
const { t } = useTranslation()
const history = useHistory()
@@ -36,6 +39,7 @@ const Appointments = () => {
{t('scheduling.appointments.newAppointment')}
,
])
+ useAddBreadcrumbs(breadcrumbs, true)
useEffect(() => {
dispatch(fetchAppointments())
diff --git a/src/scheduling/appointments/appointment-slice.ts b/src/scheduling/appointments/appointment-slice.ts
index 7966ae3bc8..16ff88d4cf 100644
--- a/src/scheduling/appointments/appointment-slice.ts
+++ b/src/scheduling/appointments/appointment-slice.ts
@@ -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/AppointmentRepository'
import Patient from 'model/Patient'
import PatientRepository from 'clients/db/PatientRepository'
+import il8n from '../../i18n'
interface AppointmentState {
appointment: Appointment
@@ -28,6 +30,10 @@ const appointmentSlice = createSlice({
fetchAppointmentStart: startLoading,
createAppointmentStart: startLoading,
updateAppointmentStart: startLoading,
+ deleteAppointmentStart: startLoading,
+ deleteAppointmentSuccess: (state: AppointmentState) => {
+ state.isLoading = false
+ },
fetchAppointmentSuccess: (
state,
{ payload }: PayloadAction<{ appointment: Appointment; patient: Patient }>,
@@ -53,6 +59,8 @@ export const {
updateAppointmentSuccess,
fetchAppointmentStart,
fetchAppointmentSuccess,
+ deleteAppointmentStart,
+ deleteAppointmentSuccess,
} = appointmentSlice.actions
export const fetchAppointment = (id: string): AppThunk => async (dispatch) => {
@@ -81,4 +89,18 @@ export const updateAppointment = (appointment: Appointment, history: any): AppTh
history.push(`/appointments/${updatedAppointment.id}`)
}
+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
diff --git a/src/scheduling/appointments/appointments-slice.ts b/src/scheduling/appointments/appointments-slice.ts
index 7290275aac..f902684d03 100644
--- a/src/scheduling/appointments/appointments-slice.ts
+++ b/src/scheduling/appointments/appointments-slice.ts
@@ -21,7 +21,6 @@ const appointmentsSlice = createSlice({
name: 'appointments',
initialState,
reducers: {
- createAppointmentStart: startLoading,
fetchAppointmentsStart: startLoading,
fetchAppointmentsSuccess: (state, { payload }: PayloadAction) => {
state.isLoading = false
@@ -30,11 +29,7 @@ const appointmentsSlice = createSlice({
},
})
-export const {
- createAppointmentStart,
- fetchAppointmentsStart,
- fetchAppointmentsSuccess,
-} = appointmentsSlice.actions
+export const { fetchAppointmentsStart, fetchAppointmentsSuccess } = appointmentsSlice.actions
export const fetchAppointments = (): AppThunk => async (dispatch) => {
dispatch(fetchAppointmentsStart())
@@ -59,12 +54,4 @@ export const fetchPatientAppointments = (
dispatch(fetchAppointmentsSuccess(appointments))
}
-export const createAppointment = (appointment: Appointment, history: any): AppThunk => async (
- dispatch,
-) => {
- dispatch(createAppointmentStart())
- await AppointmentRepository.save(appointment)
- history.push('/appointments')
-}
-
export default appointmentsSlice.reducer
diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx
index 4adc7e2982..df32be9fad 100644
--- a/src/scheduling/appointments/new/NewAppointment.tsx
+++ b/src/scheduling/appointments/new/NewAppointment.tsx
@@ -1,7 +1,6 @@
import React, { useState } from 'react'
import useTitle from 'page-header/useTitle'
import { useTranslation } from 'react-i18next'
-
import roundToNearestMinutes from 'date-fns/roundToNearestMinutes'
import { useHistory } from 'react-router'
import { useDispatch } from 'react-redux'
@@ -9,14 +8,21 @@ import Appointment from 'model/Appointment'
import addMinutes from 'date-fns/addMinutes'
import { isBefore } from 'date-fns'
import { Button } from '@hospitalrun/components'
+import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs'
import { createAppointment } from '../appointment-slice'
import AppointmentDetailForm from '../AppointmentDetailForm'
+const breadcrumbs = [
+ { i18nKey: 'scheduling.appointments.label', location: '/appointments' },
+ { i18nKey: 'scheduling.appointments.newAppointment', location: '/appointments/new' },
+]
+
const NewAppointment = () => {
const { t } = useTranslation()
const history = useHistory()
const dispatch = useDispatch()
useTitle(t('scheduling.appointments.newAppointment'))
+ useAddBreadcrumbs(breadcrumbs, true)
const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 })
const endDateTime = addMinutes(startDateTime, 60)
@@ -66,7 +72,6 @@ const NewAppointment = () => {
errorMessage={errorMessage}
onFieldChange={onFieldChange}
/>
-