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 all 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
64 changes: 64 additions & 0 deletions src/__tests__/incidents/incident-slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import incident, {
fetchIncidentStart,
fetchIncidentSuccess,
fetchIncident,
resolveIncident,
resolveIncidentStart,
resolveIncidentSuccess,
} from '../../incidents/incident-slice'
import IncidentRepository from '../../shared/db/IncidentRepository'
import Incident from '../../shared/model/Incident'
Expand Down Expand Up @@ -70,6 +73,11 @@ describe('incident slice', () => {
expect(incidentStore.status).toEqual('loading')
})

it('should handle resolve incident start', () => {
const incidentStore = incident(undefined, resolveIncidentStart())
expect(incidentStore.status).toEqual('loading')
})

it('should handle fetch incident success', () => {
const expectedIncident = {
id: '1234',
Expand All @@ -80,6 +88,18 @@ describe('incident slice', () => {
expect(incidentStore.status).toEqual('completed')
expect(incidentStore.incident).toEqual(expectedIncident)
})

it('should handle resolve incident success', () => {
const expectedIncident = {
id: '1234',
resolvedOn: new Date(Date.now()).toISOString(),
status: 'resolved',
} as Incident

const incidentStore = incident(undefined, resolveIncidentSuccess(expectedIncident))
expect(incidentStore.status).toEqual('completed')
expect(incidentStore.incident).toEqual(expectedIncident)
})
})

describe('report incident', () => {
Expand Down Expand Up @@ -202,4 +222,48 @@ describe('incident slice', () => {
expect(store.getActions()[1]).toEqual(fetchIncidentSuccess(expectedIncident))
})
})

describe('resolve incident', () => {
const expectedDate = new Date()
const mockIncident = {
id: '123',
description: 'description',
date: expectedDate.toISOString(),
department: 'some department',
category: 'category',
categoryItem: 'categoryItem',
status: 'reported',
} as Incident
const expectedResolvedIncident = {
...mockIncident,
resolvedOn: expectedDate.toISOString(),
status: 'resolved',
} as Incident
let incidentRepositorySaveOrUpdateSpy: any

beforeEach(() => {
Date.now = jest.fn().mockReturnValue(expectedDate.valueOf())
incidentRepositorySaveOrUpdateSpy = jest
.spyOn(IncidentRepository, 'saveOrUpdate')
.mockResolvedValue(expectedResolvedIncident)
})

it('should resolve an incident', async () => {
const store = mockStore()

await store.dispatch(resolveIncident(mockIncident))

expect(store.getActions()[0]).toEqual(resolveIncidentStart())
expect(incidentRepositorySaveOrUpdateSpy).toHaveBeenCalledWith(expectedResolvedIncident)
expect(store.getActions()[1]).toEqual(resolveIncidentSuccess(expectedResolvedIncident))
})

it('should call on success callback if provided', async () => {
const store = mockStore()
const onSuccessSpy = jest.fn()
await store.dispatch(resolveIncident(mockIncident, onSuccessSpy))

expect(onSuccessSpy).toHaveBeenCalledWith(expectedResolvedIncident)
})
})
})
102 changes: 90 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 All @@ -20,6 +21,8 @@ const mockStore = createMockStore<RootState, any>([thunk])

describe('View Incident', () => {
const expectedDate = new Date(2020, 5, 1, 19, 48)
const expectedResolveDate = new Date()
let incidentRepositorySaveSpy: any
let history: any
const expectedIncident = {
id: '1234',
Expand All @@ -34,11 +37,15 @@ describe('View Incident', () => {
date: expectedDate.toISOString(),
} as Incident

const setup = async (permissions: Permissions[]) => {
const setup = async (mockIncident: Incident, permissions: Permissions[]) => {
jest.resetAllMocks()
Date.now = jest.fn(() => expectedResolveDate.valueOf())
jest.spyOn(breadcrumbUtil, 'default')
jest.spyOn(titleUtil, 'default')
jest.spyOn(IncidentRepository, 'find').mockResolvedValue(expectedIncident)
incidentRepositorySaveSpy = jest
.spyOn(IncidentRepository, 'saveOrUpdate')
.mockResolvedValue(expectedIncident)

history = createMemoryHistory()
history.push(`/incidents/1234`)
Expand All @@ -49,7 +56,7 @@ describe('View Incident', () => {
permissions,
},
incident: {
incident: expectedIncident,
incident: mockIncident,
},
} as any)

Expand All @@ -73,81 +80,152 @@ 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 resolved on if incident status is resolved', async () => {
const mockIncident = {
...expectedIncident,
status: 'resolved',
resolvedOn: '2020-07-10 06:33 PM',
} as Incident
const wrapper = await setup(mockIncident, [Permissions.ViewIncident])

const dateOfResolutionFormGroup = wrapper.find('.incident-resolved-on')
expect(dateOfResolutionFormGroup.find('h4').text()).toEqual('incidents.reports.resolvedOn')
expect(dateOfResolutionFormGroup.find('h5').text()).toEqual('2020-07-10 06:33 PM')
})

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

const completedOn = wrapper.find('.incident-resolved-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 resolve incident button if the incident is in a reported state', async () => {
const wrapper = await setup(expectedIncident, [
Permissions.ViewIncident,
Permissions.ResolveIncident,
])

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

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

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

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

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

describe('on resolve', () => {
it('should mark the status as resolved and fill in the resolved date with the current time', async () => {
const wrapper = await setup(expectedIncident, [
Permissions.ViewIncident,
Permissions.ResolveIncident,
])

const resolveButton = wrapper.find(Button).at(0)
await act(async () => {
const onClick = resolveButton.prop('onClick')
await onClick()
})
wrapper.update()

expect(incidentRepositorySaveSpy).toHaveBeenCalledTimes(1)
expect(incidentRepositorySaveSpy).toHaveBeenCalledWith(
expect.objectContaining({
...expectedIncident,
status: 'resolved',
resolvedOn: expectedResolveDate.toISOString(),
}),
)
expect(history.location.pathname).toEqual('/incidents')
})
})
})
13 changes: 13 additions & 0 deletions src/__tests__/shared/utils/extractUsername.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { extractUsername } from '../../../shared/util/extractUsername'

describe('extract username util', () => {
it('should extract the string after the last : in a given string', () => {
const extractedName = extractUsername('org.couchdb.user:username')
expect(extractedName).toMatch('username')
})

it('should return the string if string does not contain a : ', () => {
const extractedName = extractUsername('username')
expect(extractedName).toMatch('username')
})
})
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',
resolved = 'resolved',
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,
resolveIncidentStart: start,
resolveIncidentSuccess: finish,
},
})

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

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

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

const resolvedIncident = await IncidentRepository.saveOrUpdate({
...incidentToComplete,
resolvedOn: new Date(Date.now().valueOf()).toISOString(),
status: 'resolved',
})

dispatch(resolveIncidentSuccess(resolvedIncident))

if (onSuccess) {
onSuccess(resolvedIncident)
}
}

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 @@ -11,6 +11,7 @@ import SelectWithLabelFormGroup, {
} from '../../shared/components/input/SelectWithLableFormGroup'
import useTranslator from '../../shared/hooks/useTranslator'
import { RootState } from '../../shared/store'
import { extractUsername } from '../../shared/util/extractUsername'
import IncidentFilter from '../IncidentFilter'
import { searchIncidents } from '../incidents-slice'

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
Loading