diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 3af081530d..31ab6b9990 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -5,6 +5,7 @@ import { Toaster } from '@hospitalrun/components' import Appointments from 'scheduling/appointments/Appointments' import NewAppointment from 'scheduling/appointments/new/NewAppointment' 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' @@ -34,6 +35,7 @@ const HospitalRun = () => {

{title}

+
diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 569e241c02..d3cc0e40f7 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -7,10 +7,12 @@ 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 { addBreadcrumbs } from 'breadcrumbs/breadcrumbs-slice' import NewPatient from '../patients/new/NewPatient' import EditPatient from '../patients/edit/EditPatient' import ViewPatient from '../patients/view/ViewPatient' @@ -25,13 +27,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( - + @@ -39,6 +42,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', () => { @@ -47,6 +58,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -74,14 +86,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( - + @@ -89,6 +102,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', () => { @@ -97,6 +119,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.WritePatients] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -114,6 +137,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.ReadPatients] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -141,14 +165,15 @@ describe('HospitalRun', () => { mockedPatientRepository.find.mockResolvedValue(patient) + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.ReadPatients] }, + patient: { patient }, + breadcrumbs: { breadcrumbs: [] }, + }) + const wrapper = mount( - + @@ -156,6 +181,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', () => { @@ -164,6 +197,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -178,14 +212,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( - + @@ -197,6 +232,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', () => { @@ -205,7 +247,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, - appointments: { appointments: [] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -221,13 +263,14 @@ 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( - + @@ -235,6 +278,14 @@ describe('HospitalRun', () => { ) expect(wrapper.find(NewAppointment)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { i18nKey: 'scheduling.appointments.new', location: '/appointments/new' }, + { i18nKey: 'dashboard.label', location: '/' }, + ]), + ) }) it('should render the Dashboard when the user does not have read appointment privileges', () => { @@ -243,6 +294,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -262,6 +314,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/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/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/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 b47de7491c..2e3db2da99 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 070470b7eb..5c80cdc0b6 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..80358d8562 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -12,6 +12,7 @@ import { getPatientFullName } from '../util/patient-name-util' 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 => { @@ -47,6 +48,12 @@ const ViewPatient = () => { , ]) + const breadcrumbs = [ + { i18nKey: 'patients.label', location: '/patients' }, + { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, + ] + useAddBreadcrumbs(breadcrumbs, true) + const { id } = useParams() useEffect(() => { if (id) { diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 55d618d98a..f152f49507 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.new')} , ]) + useAddBreadcrumbs(breadcrumbs, true) useEffect(() => { dispatch(fetchAppointments()) diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index 538ae33f98..487948f5eb 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, Alert } from '@hospitalrun/components' +import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' import { createAppointment } from '../appointments-slice' import AppointmentDetailForm from '../AppointmentDetailForm' +const breadcrumbs = [ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { i18nKey: 'scheduling.appointments.new', location: '/appointments/new' }, +] + const NewAppointment = () => { const { t } = useTranslation() const history = useHistory() const dispatch = useDispatch() useTitle(t('scheduling.appointments.new')) + useAddBreadcrumbs(breadcrumbs, true) const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) const endDateTime = addMinutes(startDateTime, 60) diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index c2e78947ac..5744041875 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -5,8 +5,18 @@ import { RootState } from 'store' import { useParams } from 'react-router' import { Spinner } from '@hospitalrun/components' import { useTranslation } from 'react-i18next' +import Appointment from 'model/Appointment' import { fetchAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' +import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' + +function getAppointmentLabel(appointment: Appointment) { + const { id, startDateTime, endDateTime } = appointment + + return startDateTime && endDateTime + ? `${new Date(startDateTime).toLocaleString()} - ${new Date(endDateTime).toLocaleString()}` + : id +} const ViewAppointment = () => { const { t } = useTranslation() @@ -15,6 +25,12 @@ const ViewAppointment = () => { const { id } = useParams() const { appointment, patient, isLoading } = useSelector((state: RootState) => state.appointment) + const breadcrumbs = [ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { text: getAppointmentLabel(appointment), location: `/patients/${appointment.id}` }, + ] + useAddBreadcrumbs(breadcrumbs, true) + useEffect(() => { if (id) { dispatch(fetchAppointment(id)) 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({