Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

feat(incidents): add ability to resolve incidents #2222

Merged
merged 17 commits into from
Jul 12, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e97c3ca
fix: add Relates Person search crash if matched person has no DoB
blestab Jul 8, 2020
5a34b8f
feat(incidents): add ability to resolve an incident
blestab Jul 10, 2020
daada44
Merge branch 'master' into master
matteovivona Jul 10, 2020
77c2be5
Merge branch 'master' into master
fox1t Jul 10, 2020
0b4d018
Merge branch 'master' into master
fox1t Jul 10, 2020
d2b935e
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 10, 2020
d43fa37
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 10, 2020
bfecf24
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 10, 2020
6f6bfe0
Merge branch 'master' into master
fox1t Jul 10, 2020
52dc151
Merge branch 'master' into master
matteovivona Jul 11, 2020
6bea698
Merge branch 'master' into master
matteovivona Jul 11, 2020
14ad01a
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 11, 2020
bfe9ccc
Merge branch 'master' into master
matteovivona Jul 11, 2020
2b01be8
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 11, 2020
20c0a71
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 11, 2020
eb9f2b3
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 11, 2020
309a716
Merge branch 'master' of https://github.com/blestab/hospitalrun-frontend
blestab Jul 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/__tests__/incidents/util/extract-username-util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { extractUsername } from '../../../incidents/util/extractUsername'

it('should extract the string after the last : in a given string', () => {
blestab marked this conversation as resolved.
Show resolved Hide resolved
blestab marked this conversation as resolved.
Show resolved Hide resolved
const extractedName = extractUsername('org.couchdb.user:username')
expect(extractedName).toMatch('username')
})
70 changes: 58 additions & 12 deletions src/__tests__/incidents/view/ViewIncident.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Button } from '@hospitalrun/components'
import { act } from '@testing-library/react'
import { mount } from 'enzyme'
import { createMemoryHistory } from 'history'
Expand Down Expand Up @@ -34,7 +35,7 @@ describe('View Incident', () => {
date: expectedDate.toISOString(),
} as Incident

const setup = async (permissions: Permissions[]) => {
const setup = async (mockIncident: Incident, permissions: Permissions[]) => {
jest.resetAllMocks()
jest.spyOn(breadcrumbUtil, 'default')
jest.spyOn(titleUtil, 'default')
Expand All @@ -49,7 +50,7 @@ describe('View Incident', () => {
permissions,
},
incident: {
incident: expectedIncident,
incident: mockIncident,
},
} as any)

Expand All @@ -73,81 +74,126 @@ describe('View Incident', () => {

describe('layout', () => {
it('should set the title', async () => {
await setup([Permissions.ViewIncident])
await setup(expectedIncident, [Permissions.ViewIncident])

expect(titleUtil.default).toHaveBeenCalledWith(expectedIncident.code)
})

it('should set the breadcrumbs properly', async () => {
await setup([Permissions.ViewIncident])
await setup(expectedIncident, [Permissions.ViewIncident])

expect(breadcrumbUtil.default).toHaveBeenCalledWith([
{ i18nKey: expectedIncident.code, location: '/incidents/1234' },
])
})

it('should render the date of incident', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-date')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.dateOfIncident')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual('2020-06-01 07:48 PM')
})

it('should render the status', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-status')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.status')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual(expectedIncident.status)
})

it('should render the reported by', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-reported-by')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.reportedBy')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual(expectedIncident.reportedBy)
})

it('should render the reported on', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-reported-on')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.reportedOn')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual('2020-06-01 07:48 PM')
})

it('should render the completed on if incident status is completed', async () => {
const mockIncident = {
...expectedIncident,
status: 'completed',
completedOn: '2020-07-10 06:33 PM',
} as Incident
const wrapper = await setup(mockIncident, [Permissions.ViewIncident])

const dateOfCompletionFormGroup = wrapper.find('.completed-on')
expect(dateOfCompletionFormGroup.find('h4').text()).toEqual('incidents.reports.completedOn')
expect(dateOfCompletionFormGroup.find('h5').text()).toEqual('2020-07-10 06:33 PM')
})

it('should not render the completed on if incident status is not completed', async () => {
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const completedOn = wrapper.find('.completed-on')
expect(completedOn).toHaveLength(0)
})

it('should render the department', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const departmentInput = wrapper.findWhere((w: any) => w.prop('name') === 'department')
expect(departmentInput.prop('label')).toEqual('incidents.reports.department')
expect(departmentInput.prop('value')).toEqual(expectedIncident.department)
})

it('should render the category', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const categoryInput = wrapper.findWhere((w: any) => w.prop('name') === 'category')
expect(categoryInput.prop('label')).toEqual('incidents.reports.category')
expect(categoryInput.prop('value')).toEqual(expectedIncident.category)
})

it('should render the category item', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const categoryItemInput = wrapper.findWhere((w: any) => w.prop('name') === 'categoryItem')
expect(categoryItemInput.prop('label')).toEqual('incidents.reports.categoryItem')
expect(categoryItemInput.prop('value')).toEqual(expectedIncident.categoryItem)
})

it('should render the description', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const descriptionTextInput = wrapper.findWhere((w: any) => w.prop('name') === 'description')
expect(descriptionTextInput.prop('label')).toEqual('incidents.reports.description')
expect(descriptionTextInput.prop('value')).toEqual(expectedIncident.description)
})

it('should display a complete incident button if the incident is in a reported state', async () => {
const wrapper = await setup(expectedIncident, [
Permissions.ViewIncident,
Permissions.CompleteIncident,
])

const buttons = wrapper.find(Button)
expect(buttons.at(0).text().trim()).toEqual('incidents.reports.complete')
})

it('should not display a complete incident button if the user has no access CompleteIncident access', async () => {
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const completeButton = wrapper.find(Button)
expect(completeButton).toHaveLength(0)
})

it('should not display a complete incident button if the incident is completed', async () => {
const mockIncident = { ...expectedIncident, status: 'completed' } as Incident
const wrapper = await setup(mockIncident, [Permissions.ViewIncident])

const completeButton = wrapper.find(Button)
expect(completeButton).toHaveLength(0)
})
})
})
1 change: 1 addition & 0 deletions src/incidents/IncidentFilter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
enum IncidentFilter {
reported = 'reported',
completed = 'completed',
all = 'all',
}

Expand Down
23 changes: 23 additions & 0 deletions src/incidents/incident-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const incidentSlice = createSlice({
reportIncidentStart: start,
reportIncidentSuccess: finish,
reportIncidentError: error,
completeIncidentStart: start,
completeIncidentSuccess: finish,
},
})

Expand All @@ -60,6 +62,8 @@ export const {
reportIncidentStart,
reportIncidentSuccess,
reportIncidentError,
completeIncidentStart,
completeIncidentSuccess,
} = incidentSlice.actions

export const fetchIncident = (id: string): AppThunk => async (dispatch) => {
Expand Down Expand Up @@ -120,4 +124,23 @@ export const reportIncident = (
}
}

export const completeIncident = (
incidentToComplete: Incident,
onSuccess?: (incidentToComplete: Incident) => void,
): AppThunk => async (dispatch) => {
dispatch(completeIncidentStart())

const completedIncident = await IncidentRepository.saveOrUpdate({
...incidentToComplete,
completedOn: new Date(Date.now().valueOf()).toISOString(),
status: 'completed',
})

dispatch(completeIncidentSuccess(completedIncident))

if (onSuccess) {
onSuccess(completedIncident)
}
}
blestab marked this conversation as resolved.
Show resolved Hide resolved

export default incidentSlice.reducer
8 changes: 6 additions & 2 deletions src/incidents/list/ViewIncidents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useTranslator from '../../shared/hooks/useTranslator'
import { RootState } from '../../shared/store'
import IncidentFilter from '../IncidentFilter'
import { searchIncidents } from '../incidents-slice'
import { extractUsername } from '../util/extractUsername'

const ViewIncidents = () => {
const { t } = useTranslator()
Expand All @@ -21,7 +22,10 @@ const ViewIncidents = () => {
useTitle(t('incidents.reports.label'))
const [searchFilter, setSearchFilter] = useState(IncidentFilter.reported)
const { incidents } = useSelector((state: RootState) => state.incidents)

const viewIncidents = incidents.map((row) => ({
...row,
reportedBy: extractUsername(row.reportedBy),
}))
const setButtonToolBar = useButtonToolbarSetter()
useEffect(() => {
setButtonToolBar([
Expand Down Expand Up @@ -67,7 +71,7 @@ const ViewIncidents = () => {
<div className="row">
<Table
getID={(row) => row.id}
data={incidents}
data={viewIncidents}
columns={[
{ label: t('incidents.reports.code'), key: 'code' },
{
Expand Down
2 changes: 2 additions & 0 deletions src/incidents/util/extractUsername.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const extractUsername = (username: string) =>
blestab marked this conversation as resolved.
Show resolved Hide resolved
username ? username.slice(username.lastIndexOf(':') + 1) : ''
66 changes: 62 additions & 4 deletions src/incidents/view/ViewIncident.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { Column, Row, Spinner } from '@hospitalrun/components'
import { Button, Column, Row, Spinner } from '@hospitalrun/components'
import format from 'date-fns/format'
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useParams } from 'react-router-dom'
import { useParams, useHistory } from 'react-router-dom'

import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs'
import useTitle from '../../page-header/title/useTitle'
import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup'
import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup'
import useTranslator from '../../shared/hooks/useTranslator'
import Permissions from '../../shared/model/Permissions'
import { RootState } from '../../shared/store'
import { fetchIncident } from '../incident-slice'
import { fetchIncident, completeIncident } from '../incident-slice'
import { extractUsername } from '../util/extractUsername'

const ViewIncident = () => {
const dispatch = useDispatch()
const { t } = useTranslator()
const history = useHistory()
const { id } = useParams()
const { incident } = useSelector((state: RootState) => state.incident)
const { permissions } = useSelector((state: RootState) => state.user)
const isIncomplete = incident?.status !== 'completed'
useTitle(incident ? incident.code : '')
const breadcrumbs = [
{
Expand All @@ -31,7 +36,54 @@ const ViewIncident = () => {
dispatch(fetchIncident(id))
}
}, [dispatch, id])

const onComplete = async () => {
const onSuccess = () => {
history.push('/incidents')
}

if (incident) {
dispatch(completeIncident(incident, onSuccess))
}
}

const getButtons = () => {
const buttons: React.ReactNode[] = []
if (incident?.status === 'completed') {
return buttons
}

if (permissions.includes(Permissions.CompleteIncident)) {
buttons.push(
<Button
className="mr-2"
onClick={onComplete}
color="primary"
key="incidents.reports.complete"
>
{t('incidents.reports.complete')}
</Button>,
)
}

return buttons
}

if (incident) {
const getCompletedOnDate = () => {
if (incident.status === 'completed' && incident.completedOn) {
return (
<Column>
<div className="form-group completed-on">
<h4>{t('incidents.reports.completedOn')}</h4>
<h5>{format(new Date(incident.completedOn), 'yyyy-MM-dd hh:mm a')}</h5>
</div>
</Column>
)
}
return <></>
}

return (
<>
<Row>
Expand All @@ -50,7 +102,7 @@ const ViewIncident = () => {
<Column>
<div className="form-group incident-reported-by">
<h4>{t('incidents.reports.reportedBy')}</h4>
<h5>{incident.reportedBy}</h5>
<h5>{extractUsername(incident.reportedBy)}</h5>
</div>
</Column>
<Column>
Expand All @@ -59,6 +111,7 @@ const ViewIncident = () => {
<h5>{format(new Date(incident.reportedOn || ''), 'yyyy-MM-dd hh:mm a')}</h5>
</div>
</Column>
{getCompletedOnDate()}
</Row>
<div className="border-bottom mb-2" />
<Row>
Expand Down Expand Up @@ -95,6 +148,11 @@ const ViewIncident = () => {
/>
</Column>
</Row>
{isIncomplete && (
<div className="row float-right">
<div className="btn-group btn-group-lg mt-3">{getButtons()}</div>
</div>
)}
</>
)
}
Expand Down
3 changes: 3 additions & 0 deletions src/shared/locales/enUs/translations/incidents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ export default {
},
status: {
reported: 'reported',
completed: 'completed',
all: 'all',
},
reports: {
label: 'Reported Incidents',
new: 'Report Incident',
view: 'View Incident',
complete: 'Complete Incident',
dateOfIncident: 'Date of Incident',
department: 'Department',
category: 'Category',
Expand All @@ -21,6 +23,7 @@ export default {
code: 'Code',
reportedBy: 'Reported By',
reportedOn: 'Reported On',
completedOn: 'Completed On',
status: 'Status',
error: {
dateRequired: 'Date is required.',
Expand Down
3 changes: 2 additions & 1 deletion src/shared/model/Incident.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export default interface Incident extends AbstractDBModel {
category: string
categoryItem: string
description: string
status: 'reported'
status: 'reported' | 'completed'
completedOn: string
}
Loading