diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx
index 199e4be740..320195d4f2 100644
--- a/src/HospitalRun.tsx
+++ b/src/HospitalRun.tsx
@@ -17,6 +17,7 @@ import { RootState } from './store'
import Navbar from './components/Navbar'
import PrivateRoute from './components/PrivateRoute'
import Patients from './patients/Patients'
+import Incidents from './incidents/Incidents'
const HospitalRun = () => {
const { title } = useSelector((state: RootState) => state.title)
@@ -74,6 +75,7 @@ const HospitalRun = () => {
/>
+
diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx
index 737a42f61c..edffdd7dd5 100644
--- a/src/__tests__/HospitalRun.test.tsx
+++ b/src/__tests__/HospitalRun.test.tsx
@@ -20,6 +20,7 @@ import Patient from '../model/Patient'
import Appointment from '../model/Appointment'
import HospitalRun from '../HospitalRun'
import Permissions from '../model/Permissions'
+import Incidents from '../incidents/Incidents'
const mockStore = configureMockStore([thunk])
@@ -256,6 +257,52 @@ describe('HospitalRun', () => {
expect(wrapper.find(Dashboard)).toHaveLength(1)
})
})
+
+ describe('/incidents', () => {
+ it('should render the Incidents component when /incidents is accessed', async () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.ViewIncidents] },
+ breadcrumbs: { breadcrumbs: [] },
+ components: { sidebarCollapsed: false },
+ })
+
+ let wrapper: any
+ await act(async () => {
+ wrapper = await mount(
+
+
+
+
+ ,
+ )
+ })
+ wrapper.update()
+
+ expect(wrapper.find(Incidents)).toHaveLength(1)
+ })
+
+ it('should render the dashboard if the user does not have permissions to view incidents', () => {
+ jest.spyOn(LabRepository, 'findAll').mockResolvedValue([])
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [] },
+ breadcrumbs: { breadcrumbs: [] },
+ components: { sidebarCollapsed: false },
+ })
+
+ const wrapper = mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper.find(Incidents)).toHaveLength(0)
+ expect(wrapper.find(Dashboard)).toHaveLength(1)
+ })
+ })
})
describe('layout', () => {
diff --git a/src/__tests__/components/Sidebar.test.tsx b/src/__tests__/components/Sidebar.test.tsx
index 11bf2804af..5aaf695060 100644
--- a/src/__tests__/components/Sidebar.test.tsx
+++ b/src/__tests__/components/Sidebar.test.tsx
@@ -326,4 +326,93 @@ describe('Sidebar', () => {
expect(history.location.pathname).toEqual('/labs')
})
})
+
+ describe('incident links', () => {
+ it('should render the main incidents link', () => {
+ const wrapper = setup('/incidents')
+
+ const listItems = wrapper.find(ListItem)
+
+ expect(listItems.at(5).text().trim()).toEqual('incidents.label')
+ })
+
+ it('should render the new incident report link', () => {
+ const wrapper = setup('/incidents')
+
+ const listItems = wrapper.find(ListItem)
+
+ expect(listItems.at(6).text().trim()).toEqual('incidents.reports.new')
+ })
+
+ it('should render the incidents list link', () => {
+ const wrapper = setup('/incidents')
+
+ const listItems = wrapper.find(ListItem)
+
+ expect(listItems.at(7).text().trim()).toEqual('incidents.reports.label')
+ })
+
+ it('main incidents link should be active when the current path is /incidents', () => {
+ const wrapper = setup('/incidents')
+
+ const listItems = wrapper.find(ListItem)
+
+ expect(listItems.at(5).prop('active')).toBeTruthy()
+ })
+
+ it('should navigate to /incidents when the main incident link is clicked', () => {
+ const wrapper = setup('/')
+
+ const listItems = wrapper.find(ListItem)
+
+ act(() => {
+ const onClick = listItems.at(5).prop('onClick') as any
+ onClick()
+ })
+
+ expect(history.location.pathname).toEqual('/incidents')
+ })
+
+ it('new incident report link should be active when the current path is /incidents/new', () => {
+ const wrapper = setup('/incidents/new')
+
+ const listItems = wrapper.find(ListItem)
+
+ expect(listItems.at(6).prop('active')).toBeTruthy()
+ })
+
+ it('should navigate to /incidents/new when the new labs link is clicked', () => {
+ const wrapper = setup('/incidents')
+
+ const listItems = wrapper.find(ListItem)
+
+ act(() => {
+ const onClick = listItems.at(6).prop('onClick') as any
+ onClick()
+ })
+
+ expect(history.location.pathname).toEqual('/incidents/new')
+ })
+
+ it('incidents list link should be active when the current path is /incidents', () => {
+ const wrapper = setup('/incidents')
+
+ const listItems = wrapper.find(ListItem)
+
+ expect(listItems.at(7).prop('active')).toBeTruthy()
+ })
+
+ it('should navigate to /labs when the labs list link is clicked', () => {
+ const wrapper = setup('/incidents/new')
+
+ const listItems = wrapper.find(ListItem)
+
+ act(() => {
+ const onClick = listItems.at(7).prop('onClick') as any
+ onClick()
+ })
+
+ expect(history.location.pathname).toEqual('/incidents')
+ })
+ })
})
diff --git a/src/__tests__/incidents/Incidents.test.tsx b/src/__tests__/incidents/Incidents.test.tsx
new file mode 100644
index 0000000000..2646be6a36
--- /dev/null
+++ b/src/__tests__/incidents/Incidents.test.tsx
@@ -0,0 +1,102 @@
+import '../../__mocks__/matchMediaMock'
+import React from 'react'
+import { mount } from 'enzyme'
+import { MemoryRouter } from 'react-router'
+import { Provider } from 'react-redux'
+import thunk from 'redux-thunk'
+import configureMockStore from 'redux-mock-store'
+import { act } from '@testing-library/react'
+import Permissions from 'model/Permissions'
+import ViewIncident from '../../incidents/view/ViewIncident'
+import Incidents from '../../incidents/Incidents'
+import ReportIncident from '../../incidents/report/ReportIncident'
+
+const mockStore = configureMockStore([thunk])
+
+describe('Incidents', () => {
+ describe('routing', () => {
+ describe('/incidents/new', () => {
+ it('should render the new lab request screen when /incidents/new is accessed', () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.ReportIncident] },
+ breadcrumbs: { breadcrumbs: [] },
+ components: { sidebarCollapsed: false },
+ })
+
+ const wrapper = mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper.find(ReportIncident)).toHaveLength(1)
+ })
+
+ it('should not navigate to /incidents/new if the user does not have RequestLab permissions', () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [] },
+ breadcrumbs: { breadcrumbs: [] },
+ components: { sidebarCollapsed: false },
+ })
+
+ const wrapper = mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper.find(ReportIncident)).toHaveLength(0)
+ })
+ })
+
+ describe('/incidents/:id', () => {
+ it('should render the view lab screen when /incidents/:id is accessed', async () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [Permissions.ViewIncident] },
+ breadcrumbs: { breadcrumbs: [] },
+ components: { sidebarCollapsed: false },
+ })
+
+ let wrapper: any
+
+ await act(async () => {
+ wrapper = await mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper.find(ViewIncident)).toHaveLength(1)
+ })
+ })
+
+ it('should not navigate to /incidents/:id if the user does not have ViewIncident permissions', async () => {
+ const store = mockStore({
+ title: 'test',
+ user: { permissions: [] },
+ breadcrumbs: { breadcrumbs: [] },
+ components: { sidebarCollapsed: false },
+ })
+
+ const wrapper = await mount(
+
+
+
+
+ ,
+ )
+
+ expect(wrapper.find(ViewIncident)).toHaveLength(0)
+ })
+ })
+ })
+})
diff --git a/src/__tests__/incidents/list/ViewIncidents.test.tsx b/src/__tests__/incidents/list/ViewIncidents.test.tsx
new file mode 100644
index 0000000000..6c408f6572
--- /dev/null
+++ b/src/__tests__/incidents/list/ViewIncidents.test.tsx
@@ -0,0 +1,61 @@
+import '../../../__mocks__/matchMediaMock'
+import React from 'react'
+import { mount } from 'enzyme'
+import { createMemoryHistory } from 'history'
+import { act } from '@testing-library/react'
+import { Provider } from 'react-redux'
+import { Route, Router } from 'react-router'
+import createMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import Permissions from '../../../model/Permissions'
+import * as titleUtil from '../../../page-header/useTitle'
+import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider'
+import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs'
+import ViewIncidents from '../../../incidents/list/ViewIncidents'
+
+const mockStore = createMockStore([thunk])
+
+describe('View Incidents', () => {
+ let history: any
+
+ let setButtonToolBarSpy: any
+ const setup = async (permissions: Permissions[]) => {
+ jest.resetAllMocks()
+ jest.spyOn(breadcrumbUtil, 'default')
+ setButtonToolBarSpy = jest.fn()
+ jest.spyOn(titleUtil, 'default')
+ jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy)
+
+ history = createMemoryHistory()
+ history.push(`/incidents`)
+ const store = mockStore({
+ title: '',
+ user: {
+ permissions,
+ },
+ })
+
+ let wrapper: any
+ await act(async () => {
+ wrapper = await mount(
+
+
+
+
+
+
+
+
+ ,
+ )
+ })
+ wrapper.update()
+ return wrapper
+ }
+
+ it('should set the title', async () => {
+ await setup([Permissions.ViewIncidents])
+
+ expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.label')
+ })
+})
diff --git a/src/__tests__/incidents/report/ReportIncident.test.tsx b/src/__tests__/incidents/report/ReportIncident.test.tsx
new file mode 100644
index 0000000000..5cad7b542c
--- /dev/null
+++ b/src/__tests__/incidents/report/ReportIncident.test.tsx
@@ -0,0 +1,69 @@
+import '../../../__mocks__/matchMediaMock'
+import React from 'react'
+import { mount } from 'enzyme'
+import { createMemoryHistory } from 'history'
+import { act } from '@testing-library/react'
+import { Provider } from 'react-redux'
+import { Route, Router } from 'react-router'
+import createMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import Permissions from '../../../model/Permissions'
+import * as titleUtil from '../../../page-header/useTitle'
+import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider'
+import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs'
+import ReportIncident from '../../../incidents/report/ReportIncident'
+
+const mockStore = createMockStore([thunk])
+
+describe('Report Incident', () => {
+ let history: any
+
+ let setButtonToolBarSpy: any
+ const setup = async (permissions: Permissions[]) => {
+ jest.resetAllMocks()
+ jest.spyOn(breadcrumbUtil, 'default')
+ setButtonToolBarSpy = jest.fn()
+ jest.spyOn(titleUtil, 'default')
+ jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy)
+
+ history = createMemoryHistory()
+ history.push(`/incidents/new`)
+ const store = mockStore({
+ title: '',
+ user: {
+ permissions,
+ },
+ })
+
+ let wrapper: any
+ await act(async () => {
+ wrapper = await mount(
+
+
+
+
+
+
+
+
+ ,
+ )
+ })
+ wrapper.update()
+ return wrapper
+ }
+
+ it('should set the title', async () => {
+ await setup([Permissions.ReportIncident])
+
+ expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.new')
+ })
+
+ it('should set the breadcrumbs properly', async () => {
+ await setup([Permissions.ReportIncident])
+
+ expect(breadcrumbUtil.default).toHaveBeenCalledWith([
+ { i18nKey: 'incidents.reports.new', location: '/incidents/new' },
+ ])
+ })
+})
diff --git a/src/__tests__/incidents/view/ViewIncident.test.tsx b/src/__tests__/incidents/view/ViewIncident.test.tsx
new file mode 100644
index 0000000000..78daea2f77
--- /dev/null
+++ b/src/__tests__/incidents/view/ViewIncident.test.tsx
@@ -0,0 +1,66 @@
+import '../../../__mocks__/matchMediaMock'
+import React from 'react'
+import { mount } from 'enzyme'
+import { createMemoryHistory } from 'history'
+import { act } from '@testing-library/react'
+import { Provider } from 'react-redux'
+import { Route, Router } from 'react-router'
+import createMockStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import Permissions from '../../../model/Permissions'
+import * as titleUtil from '../../../page-header/useTitle'
+import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider'
+import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs'
+import ViewIncident from '../../../incidents/view/ViewIncident'
+
+const mockStore = createMockStore([thunk])
+
+describe('View Incident', () => {
+ let history: any
+
+ const setup = async (permissions: Permissions[]) => {
+ jest.resetAllMocks()
+ jest.spyOn(breadcrumbUtil, 'default')
+ jest.spyOn(titleUtil, 'default')
+
+ history = createMemoryHistory()
+ history.push(`/incidents/1234`)
+ const store = mockStore({
+ title: '',
+ user: {
+ permissions,
+ },
+ })
+
+ let wrapper: any
+ await act(async () => {
+ wrapper = await mount(
+
+
+
+
+
+
+
+
+ ,
+ )
+ })
+ wrapper.update()
+ return wrapper
+ }
+
+ it('should set the title', async () => {
+ await setup([Permissions.ViewIncident])
+
+ expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.view')
+ })
+
+ it('should set the breadcrumbs properly', async () => {
+ await setup([Permissions.ViewIncident])
+
+ expect(breadcrumbUtil.default).toHaveBeenCalledWith([
+ { i18nKey: 'incidents.reports.view', location: '/incidents/1234' },
+ ])
+ })
+})
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index e70dfcd5e3..8468043fc2 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -39,6 +39,8 @@ const Sidebar = () => {
? 'appointment'
: splittedPath[1].includes('labs')
? 'labs'
+ : splittedPath[1].includes('incidents')
+ ? 'incidents'
: 'none',
)
@@ -230,6 +232,52 @@ const Sidebar = () => {
>
)
+ const getIncidentLinks = () => (
+ <>
+ {
+ navigateTo('/incidents')
+ setExpansion('incidents')
+ }}
+ className="nav-item"
+ style={listItemStyle}
+ >
+
+ {!sidebarCollapsed && t('incidents.label')}
+
+ {splittedPath[1].includes('incidents') && expandedItem === 'incidents' && (
+
+ navigateTo('/incidents/new')}
+ active={splittedPath[1].includes('incidents') && splittedPath.length > 2}
+ >
+
+ {!sidebarCollapsed && t('incidents.reports.new')}
+
+ navigateTo('/incidents')}
+ active={splittedPath[1].includes('incidents') && splittedPath.length < 3}
+ >
+
+ {!sidebarCollapsed && t('incidents.reports.label')}
+
+
+ )}
+ >
+ )
+
return (
diff --git a/src/incidents/Incidents.tsx b/src/incidents/Incidents.tsx
new file mode 100644
index 0000000000..fef38887e8
--- /dev/null
+++ b/src/incidents/Incidents.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import { Switch } from 'react-router'
+import { useSelector } from 'react-redux'
+import PrivateRoute from '../components/PrivateRoute'
+import { RootState } from '../store'
+import Permissions from '../model/Permissions'
+import ViewIncidents from './list/ViewIncidents'
+import ReportIncident from './report/ReportIncident'
+import ViewIncident from './view/ViewIncident'
+import useAddBreadcrumbs from '../breadcrumbs/useAddBreadcrumbs'
+
+const Incidents = () => {
+ const { permissions } = useSelector((state: RootState) => state.user)
+ const breadcrumbs = [
+ {
+ i18nKey: 'incidents.label',
+ location: `/incidents`,
+ },
+ ]
+ useAddBreadcrumbs(breadcrumbs, true)
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default Incidents
diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx
new file mode 100644
index 0000000000..2acf9090a1
--- /dev/null
+++ b/src/incidents/list/ViewIncidents.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import useTitle from '../../page-header/useTitle'
+
+const ViewIncidents = () => {
+ const { t } = useTranslation()
+ useTitle(t('incidents.reports.label'))
+
+ return
Reported Incidents
+}
+
+export default ViewIncidents
diff --git a/src/incidents/report/ReportIncident.tsx b/src/incidents/report/ReportIncident.tsx
new file mode 100644
index 0000000000..70f22c861a
--- /dev/null
+++ b/src/incidents/report/ReportIncident.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import useTitle from '../../page-header/useTitle'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
+
+const ReportIncident = () => {
+ const { t } = useTranslation()
+ useTitle(t('incidents.reports.new'))
+ const breadcrumbs = [
+ {
+ i18nKey: 'incidents.reports.new',
+ location: `/incidents/new`,
+ },
+ ]
+ useAddBreadcrumbs(breadcrumbs)
+
+ return Report Incident
+}
+
+export default ReportIncident
diff --git a/src/incidents/view/ViewIncident.tsx b/src/incidents/view/ViewIncident.tsx
new file mode 100644
index 0000000000..17b4c87a68
--- /dev/null
+++ b/src/incidents/view/ViewIncident.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { useParams } from 'react-router'
+import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
+import useTitle from '../../page-header/useTitle'
+
+const ViewIncident = () => {
+ const { t } = useTranslation()
+ const { id } = useParams()
+ useTitle(t('incidents.reports.view'))
+ const breadcrumbs = [
+ {
+ i18nKey: 'incidents.reports.view',
+ location: `/incidents/${id}`,
+ },
+ ]
+ useAddBreadcrumbs(breadcrumbs)
+
+ return View Incident
+}
+
+export default ViewIncident
diff --git a/src/locales/enUs/translations/incidents/index.ts b/src/locales/enUs/translations/incidents/index.ts
new file mode 100644
index 0000000000..97cf559d13
--- /dev/null
+++ b/src/locales/enUs/translations/incidents/index.ts
@@ -0,0 +1,10 @@
+export default {
+ incidents: {
+ label: 'Incidents',
+ reports: {
+ label: 'Reported Incidents',
+ new: 'Report Incident',
+ view: 'View Incident',
+ },
+ },
+}
diff --git a/src/locales/enUs/translations/index.ts b/src/locales/enUs/translations/index.ts
index d6bf2a1db2..674001f887 100644
--- a/src/locales/enUs/translations/index.ts
+++ b/src/locales/enUs/translations/index.ts
@@ -6,6 +6,7 @@ import scheduling from './scheduling'
import states from './states'
import sex from './sex'
import labs from './labs'
+import incidents from './incidents'
export default {
...actions,
@@ -16,4 +17,5 @@ export default {
...states,
...sex,
...labs,
+ ...incidents,
}
diff --git a/src/model/Permissions.ts b/src/model/Permissions.ts
index 170fb173ba..91a407cd2d 100644
--- a/src/model/Permissions.ts
+++ b/src/model/Permissions.ts
@@ -11,6 +11,9 @@ enum Permissions {
CompleteLab = 'complete:lab',
ViewLab = 'read:lab',
ViewLabs = 'read:labs',
+ ViewIncidents = 'read:incidents',
+ ViewIncident = 'read:incident',
+ ReportIncident = 'write:incident',
}
export default Permissions
diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts
index 98c163429c..22af27f53e 100644
--- a/src/user/user-slice.ts
+++ b/src/user/user-slice.ts
@@ -19,6 +19,9 @@ const initialState: UserState = {
Permissions.RequestLab,
Permissions.CompleteLab,
Permissions.CancelLab,
+ Permissions.ViewIncident,
+ Permissions.ViewIncidents,
+ Permissions.ReportIncident,
],
}