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
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
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
}