diff --git a/src/__tests__/incidents/incidents-slice.test.ts b/src/__tests__/incidents/incidents-slice.test.ts new file mode 100644 index 0000000000..fd869a9116 --- /dev/null +++ b/src/__tests__/incidents/incidents-slice.test.ts @@ -0,0 +1,49 @@ +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import { AnyAction } from 'redux' +import { RootState } from '../../store' +import incidents, { + fetchIncidents, + fetchIncidentsStart, + fetchIncidentsSuccess, +} from '../../incidents/incidents-slice' +import IncidentRepository from '../../clients/db/IncidentRepository' +import Incident from '../../model/Incident' + +const mockStore = createMockStore([thunk]) + +describe('Incidents Slice', () => { + describe('actions', () => { + it('should setup the default state correctly', () => { + const incidentsStore = incidents(undefined, {} as AnyAction) + expect(incidentsStore.status).toEqual('loading') + expect(incidentsStore.incidents).toEqual([]) + }) + + it('should handle fetch incidents start', () => { + const incidentsStore = incidents(undefined, fetchIncidentsStart()) + expect(incidentsStore.status).toEqual('loading') + }) + + it('should handle fetch incidents success', () => { + const expectedIncidents = [{ id: '123' }] as Incident[] + const incidentsStore = incidents(undefined, fetchIncidentsSuccess(expectedIncidents)) + expect(incidentsStore.status).toEqual('completed') + expect(incidentsStore.incidents).toEqual(expectedIncidents) + }) + + describe('fetch incidents', () => { + it('should fetch all of the incidents', async () => { + const expectedIncidents = [{ id: '123' }] as Incident[] + jest.spyOn(IncidentRepository, 'findAll').mockResolvedValue(expectedIncidents) + const store = mockStore() + + await store.dispatch(fetchIncidents()) + + expect(store.getActions()[0]).toEqual(fetchIncidentsStart()) + expect(IncidentRepository.findAll).toHaveBeenCalledTimes(1) + expect(store.getActions()[1]).toEqual(fetchIncidentsSuccess(expectedIncidents)) + }) + }) + }) +}) diff --git a/src/__tests__/incidents/list/ViewIncidents.test.tsx b/src/__tests__/incidents/list/ViewIncidents.test.tsx index 6c408f6572..e29b5e88ef 100644 --- a/src/__tests__/incidents/list/ViewIncidents.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidents.test.tsx @@ -12,11 +12,24 @@ 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' +import Incident from '../../../model/Incident' +import IncidentRepository from '../../../clients/db/IncidentRepository' const mockStore = createMockStore([thunk]) describe('View Incidents', () => { let history: any + const expectedDate = new Date(2020, 5, 3, 19, 48) + const expectedIncidents = [ + { + id: '123', + code: 'some code', + status: 'reported', + reportedBy: 'some user id', + date: expectedDate.toISOString(), + reportedOn: expectedDate.toISOString(), + }, + ] as Incident[] let setButtonToolBarSpy: any const setup = async (permissions: Permissions[]) => { @@ -25,6 +38,7 @@ describe('View Incidents', () => { setButtonToolBarSpy = jest.fn() jest.spyOn(titleUtil, 'default') jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + jest.spyOn(IncidentRepository, 'findAll').mockResolvedValue(expectedIncidents) history = createMemoryHistory() history.push(`/incidents`) @@ -33,6 +47,9 @@ describe('View Incidents', () => { user: { permissions, }, + incidents: { + incidents: expectedIncidents, + }, }) let wrapper: any @@ -53,9 +70,48 @@ describe('View Incidents', () => { return wrapper } - it('should set the title', async () => { - await setup([Permissions.ViewIncidents]) + describe('layout', () => { + it('should set the title', async () => { + await setup([Permissions.ViewIncidents]) + + expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.label') + }) + + it('should render a table with the incidents', async () => { + const wrapper = await setup([Permissions.ViewIncidents]) - expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.label') + const table = wrapper.find('table') + const tableHeader = table.find('thead') + const tableBody = table.find('tbody') + const tableHeaders = tableHeader.find('th') + const tableColumns = tableBody.find('td') + + expect(tableHeaders.at(0).text()).toEqual('incidents.reports.code') + expect(tableHeaders.at(1).text()).toEqual('incidents.reports.dateOfIncident') + expect(tableHeaders.at(2).text()).toEqual('incidents.reports.reportedBy') + expect(tableHeaders.at(3).text()).toEqual('incidents.reports.reportedOn') + expect(tableHeaders.at(4).text()).toEqual('incidents.reports.status') + + expect(tableColumns.at(0).text()).toEqual(expectedIncidents[0].code) + expect(tableColumns.at(1).text()).toEqual('2020-06-03 07:48 PM') + expect(tableColumns.at(2).text()).toEqual(expectedIncidents[0].reportedBy) + expect(tableColumns.at(3).text()).toEqual('2020-06-03 07:48 PM') + expect(tableColumns.at(4).text()).toEqual(expectedIncidents[0].status) + }) + }) + + describe('on table row click', () => { + it('should navigate to the incident when the table row is clicked', async () => { + const wrapper = await setup([Permissions.ViewIncidents]) + + const tr = wrapper.find('tr').at(1) + + act(() => { + const onClick = tr.prop('onClick') + onClick() + }) + + expect(history.location.pathname).toEqual(`/incidents/${expectedIncidents[0].id}`) + }) }) }) diff --git a/src/incidents/incidents-slice.ts b/src/incidents/incidents-slice.ts new file mode 100644 index 0000000000..377af0faab --- /dev/null +++ b/src/incidents/incidents-slice.ts @@ -0,0 +1,44 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { AppThunk } from 'store' +import Incident from '../model/Incident' +import IncidentRepository from '../clients/db/IncidentRepository' + +interface IncidentsState { + incidents: Incident[] + status: 'loading' | 'completed' +} + +const initialState: IncidentsState = { + incidents: [], + status: 'loading', +} + +function start(state: IncidentsState) { + state.status = 'loading' +} + +function finish(state: IncidentsState, { payload }: PayloadAction) { + state.status = 'completed' + state.incidents = payload +} + +const incidentSlice = createSlice({ + name: 'lab', + initialState, + reducers: { + fetchIncidentsStart: start, + fetchIncidentsSuccess: finish, + }, +}) + +export const { fetchIncidentsStart, fetchIncidentsSuccess } = incidentSlice.actions + +export const fetchIncidents = (): AppThunk => async (dispatch) => { + dispatch(fetchIncidentsStart()) + + const incidents = await IncidentRepository.findAll() + + dispatch(fetchIncidentsSuccess(incidents)) +} + +export default incidentSlice.reducer diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx index 2acf9090a1..464e1f769d 100644 --- a/src/incidents/list/ViewIncidents.tsx +++ b/src/incidents/list/ViewIncidents.tsx @@ -1,12 +1,53 @@ -import React from 'react' +import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import format from 'date-fns/format' +import { useHistory } from 'react-router' import useTitle from '../../page-header/useTitle' +import { RootState } from '../../store' +import { fetchIncidents } from '../incidents-slice' +import Incident from '../../model/Incident' const ViewIncidents = () => { const { t } = useTranslation() + const history = useHistory() + const dispatch = useDispatch() useTitle(t('incidents.reports.label')) - return

Reported Incidents

+ const { incidents } = useSelector((state: RootState) => state.incidents) + + useEffect(() => { + dispatch(fetchIncidents()) + }, [dispatch]) + + const onTableRowClick = (incident: Incident) => { + history.push(`incidents/${incident.id}`) + } + + return ( + + + + + + + + + + + + {incidents.map((incident: Incident) => ( + onTableRowClick(incident)} key={incident.id}> + + + + + + + ))} + +
{t('incidents.reports.code')}{t('incidents.reports.dateOfIncident')}{t('incidents.reports.reportedBy')}{t('incidents.reports.reportedOn')}{t('incidents.reports.status')}
{incident.code}{format(new Date(incident.date), 'yyyy-MM-dd hh:mm a')}{incident.reportedBy}{format(new Date(incident.reportedOn), 'yyyy-MM-dd hh:mm a')}{incident.status}
+ ) } export default ViewIncidents diff --git a/src/locales/enUs/translations/incidents/index.ts b/src/locales/enUs/translations/incidents/index.ts index c62b6897f9..d6cadc5994 100644 --- a/src/locales/enUs/translations/incidents/index.ts +++ b/src/locales/enUs/translations/incidents/index.ts @@ -13,6 +13,10 @@ export default { category: 'Category', categoryItem: 'Category Item', description: 'Description of Incident', + code: 'Code', + reportedBy: 'Reported By', + reportedOn: 'Reported On', + status: 'Status', error: { dateRequired: 'Date is required.', dateMustBeInThePast: 'Date must be in the past.', diff --git a/src/model/Incident.ts b/src/model/Incident.ts index 194949f63b..ecc8f66cdc 100644 --- a/src/model/Incident.ts +++ b/src/model/Incident.ts @@ -9,4 +9,5 @@ export default interface Incident extends AbstractDBModel { category: string categoryItem: string description: string + status: 'reported' } diff --git a/src/store/index.ts b/src/store/index.ts index 036746fdd5..e9c9710899 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -8,6 +8,7 @@ import title from '../page-header/title-slice' import user from '../user/user-slice' import lab from '../labs/lab-slice' import incident from '../incidents/incident-slice' +import incidents from '../incidents/incidents-slice' import breadcrumbs from '../breadcrumbs/breadcrumbs-slice' import components from '../components/component-slice' @@ -22,6 +23,7 @@ const reducer = combineReducers({ components, lab, incident, + incidents, }) const store = configureStore({