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} /> -
, - ]) + + const onAppointmentDeleteButtonClick = (event: React.MouseEvent) => { + event.preventDefault() + setShowDeleteConfirmation(true) + } + + const onDeleteConfirmationButtonClick = () => { + dispatch(deleteAppointment(appointment, history)) + setShowDeleteConfirmation(false) + } + + const buttons = [] + if (permissions.includes(Permissions.WriteAppointments)) { + buttons.push( + , + ) + } + + if (permissions.includes(Permissions.DeleteAppointment)) { + buttons.push( + , + ) + } + + setButtonToolBar(buttons) + + const breadcrumbs = [ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { text: getAppointmentLabel(appointment), location: `/patients/${appointment.id}` }, + ] + useAddBreadcrumbs(breadcrumbs, true) useEffect(() => { if (id) { @@ -49,7 +94,23 @@ const ViewAppointment = () => { return } - return + return ( +
+ + setShowDeleteConfirmation(false)} + /> +
+ ) } export default ViewAppointment diff --git a/src/store/index.ts b/src/store/index.ts index 60176dfc5c..b226815116 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -6,6 +6,7 @@ import appointment from '../scheduling/appointments/appointment-slice' import appointments from '../scheduling/appointments/appointments-slice' import title from '../page-header/title-slice' import user from '../user/user-slice' +import breadcrumbs from '../breadcrumbs/breadcrumbs-slice' const reducer = combineReducers({ patient, @@ -14,6 +15,7 @@ const reducer = combineReducers({ user, appointment, appointments, + breadcrumbs, }) const store = configureStore({ diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index dce26fc088..ea24efd27a 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -11,6 +11,7 @@ const initialState: UserState = { Permissions.WritePatients, Permissions.ReadAppointments, Permissions.WriteAppointments, + Permissions.DeleteAppointment, ], }