diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 6e9a9b406c..a67923d623 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -4,6 +4,7 @@ import { useSelector } from 'react-redux' import { Redirect, Route, Switch } from 'react-router-dom' import Dashboard from './dashboard/Dashboard' +import Imagings from './imagings/Imagings' import Incidents from './incidents/Incidents' import Labs from './labs/Labs' import Breadcrumbs from './page-header/breadcrumbs/Breadcrumbs' @@ -53,6 +54,7 @@ const HospitalRun = () => { + diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index ef18ef3bb2..15d1b0fdae 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -9,11 +9,13 @@ import thunk from 'redux-thunk' import Dashboard from '../dashboard/Dashboard' import HospitalRun from '../HospitalRun' +import ViewImagings from '../imagings/ViewImagings' import Incidents from '../incidents/Incidents' import ViewLabs from '../labs/ViewLabs' import { addBreadcrumbs } from '../page-header/breadcrumbs/breadcrumbs-slice' import Appointments from '../scheduling/appointments/Appointments' import Settings from '../settings/Settings' +import ImagingRepository from '../shared/db/ImagingRepository' import LabRepository from '../shared/db/LabRepository' import Permissions from '../shared/model/Permissions' import { RootState } from '../shared/store' @@ -170,6 +172,54 @@ describe('HospitalRun', () => { }) }) + describe('/imaging', () => { + it('should render the Imagings component when /imaging is accessed', async () => { + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + const store = mockStore({ + title: 'test', + user: { user: { id: '123' }, permissions: [Permissions.ViewImagings] }, + imagings: { imagings: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + + expect(wrapper.find(ViewImagings)).toHaveLength(1) + }) + + it('should render the dashboard if the user does not have permissions to view imagings', () => { + jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) + const store = mockStore({ + title: 'test', + user: { user: { id: '123' }, permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(ViewImagings)).toHaveLength(0) + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) + }) + describe('/settings', () => { it('should render the Settings component when /settings is accessed', async () => { const store = mockStore({ diff --git a/src/__tests__/imagings/Imagings.test.tsx b/src/__tests__/imagings/Imagings.test.tsx new file mode 100644 index 0000000000..eb3b03db63 --- /dev/null +++ b/src/__tests__/imagings/Imagings.test.tsx @@ -0,0 +1,74 @@ +import { mount } from 'enzyme' +import React from 'react' +import { Provider } from 'react-redux' +import { MemoryRouter } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Imagings from '../../imagings/Imagings' +import NewImagingRequest from '../../imagings/requests/NewImagingRequest' +import ImagingRepository from '../../shared/db/ImagingRepository' +import PatientRepository from '../../shared/db/PatientRepository' +import Imaging from '../../shared/model/Imaging' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('Imagings', () => { + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + jest + .spyOn(ImagingRepository, 'find') + .mockResolvedValue({ id: '1234', requestedOn: new Date().toISOString() } as Imaging) + jest + .spyOn(PatientRepository, 'find') + .mockResolvedValue({ id: '12345', fullName: 'test test' } as Patient) + + describe('routing', () => { + describe('/imaging/new', () => { + it('should render the new imaging request screen when /imaging/new is accessed', () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.RequestImaging] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + imaging: { + imaging: { id: 'imagingId', patient: 'patient' } as Imaging, + patient: { id: 'patientId', fullName: 'some name' }, + error: {}, + }, + } as any) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(NewImagingRequest)).toHaveLength(1) + }) + + it('should not navigate to /imagings/new if the user does not have RequestLab permissions', () => { + const store = mockStore({ + title: 'test', + user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(NewImagingRequest)).toHaveLength(0) + }) + }) + }) +}) diff --git a/src/__tests__/imagings/ViewImagings.test.tsx b/src/__tests__/imagings/ViewImagings.test.tsx new file mode 100644 index 0000000000..94ac8b53fb --- /dev/null +++ b/src/__tests__/imagings/ViewImagings.test.tsx @@ -0,0 +1,125 @@ +import { Table } from '@hospitalrun/components' +import { act } from '@testing-library/react' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Provider } from 'react-redux' +import { Route, Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import ViewImagings from '../../imagings/ViewImagings' +import * as breadcrumbUtil from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../page-header/title/useTitle' +import ImagingRepository from '../../shared/db/ImagingRepository' +import Imaging from '../../shared/model/Imaging' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('View Imagings', () => { + let history: any + let setButtonToolBarSpy: any + const expectedDate = new Date(2020, 5, 3, 19, 48) + const expectedImaging = { + code: 'I-1234', + id: '1234', + type: 'imaging type', + patient: 'patient', + status: 'requested', + requestedOn: expectedDate.toISOString(), + requestedBy: 'some user', + } as Imaging + + const setup = async (permissions: Permissions[], mockImagings?: any) => { + jest.resetAllMocks() + jest.spyOn(breadcrumbUtil, 'default') + setButtonToolBarSpy = jest.fn() + jest.spyOn(titleUtil, 'default') + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue(mockImagings) + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + + history = createMemoryHistory() + history.push(`/imaging`) + + const store = mockStore({ + title: '', + user: { permissions }, + imagings: { imagings: mockImagings }, + } as any) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + + + , + ) + }) + + wrapper.update() + return wrapper as ReactWrapper + } + + describe('title', () => { + it('should have the title', async () => { + await setup([Permissions.ViewImagings], []) + expect(titleUtil.default).toHaveBeenCalledWith('imagings.label') + }) + }) + + describe('button bar', () => { + it('should display button to add new imaging request', async () => { + await setup([Permissions.ViewImagings, Permissions.RequestImaging], []) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('imagings.requests.new') + }) + + it('should not display button to add new imaging request if the user does not have permissions', async () => { + await setup([Permissions.ViewImagings], []) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect(actualButtons).toEqual([]) + }) + }) + + describe('table', () => { + it('should render a table with data', async () => { + const wrapper = await setup( + [Permissions.ViewIncident, Permissions.RequestImaging], + [expectedImaging], + ) + const table = wrapper.find(Table) + const columns = table.prop('columns') + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.code', key: 'code' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.type', key: 'type' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.requestedOn', key: 'requestedOn' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.patient', key: 'patient' }), + ) + expect(columns[4]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.requestedBy', key: 'requestedBy' }), + ) + expect(columns[5]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.status', key: 'status' }), + ) + + expect(table.prop('data')).toEqual([expectedImaging]) + }) + }) +}) diff --git a/src/__tests__/imagings/imaging-slice.test.ts b/src/__tests__/imagings/imaging-slice.test.ts new file mode 100644 index 0000000000..9a674ae194 --- /dev/null +++ b/src/__tests__/imagings/imaging-slice.test.ts @@ -0,0 +1,122 @@ +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import imagingSlice, { + requestImaging, + requestImagingStart, + requestImagingSuccess, + requestImagingError, +} from '../../imagings/imaging-slice' +import ImagingRepository from '../../shared/db/ImagingRepository' +import Imaging from '../../shared/model/Imaging' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('imaging slice', () => { + describe('reducers', () => { + describe('requestImagingStart', () => { + it('should set status to loading', async () => { + const imagingStore = imagingSlice(undefined, requestImagingStart()) + + expect(imagingStore.status).toEqual('loading') + }) + }) + + describe('requestImagingSuccess', () => { + it('should set the imaging and status to success', () => { + const expectedImaging = { id: 'imagingId' } as Imaging + + const imagingStore = imagingSlice(undefined, requestImagingSuccess(expectedImaging)) + + expect(imagingStore.status).toEqual('completed') + expect(imagingStore.imaging).toEqual(expectedImaging) + }) + }) + + describe('requestImagingError', () => { + const expectedError = { message: 'some message', result: 'some result error' } + + const imagingStore = imagingSlice(undefined, requestImagingError(expectedError)) + + expect(imagingStore.status).toEqual('error') + expect(imagingStore.error).toEqual(expectedError) + }) + + describe('request imaging', () => { + const mockImaging = { + id: 'imagingId', + type: 'imagingType', + patient: 'patient', + status: 'requested', + } as Imaging + let imagingRepositorySaveSpy: any + + beforeEach(() => { + jest.restoreAllMocks() + Date.now = jest.fn().mockReturnValue(new Date().valueOf()) + imagingRepositorySaveSpy = jest + .spyOn(ImagingRepository, 'save') + .mockResolvedValue(mockImaging) + }) + + it('should request a new imaging', async () => { + const store = mockStore({ + user: { + user: { + id: '1234', + }, + }, + } as any) + + const expectedRequestedImaging = { + ...mockImaging, + requestedOn: new Date(Date.now()).toISOString(), + requestedBy: store.getState().user.user.id, + } as Imaging + + await store.dispatch(requestImaging(mockImaging)) + + const actions = store.getActions() + + expect(actions[0]).toEqual(requestImagingStart()) + expect(imagingRepositorySaveSpy).toHaveBeenCalledWith(expectedRequestedImaging) + expect(actions[1]).toEqual(requestImagingSuccess(expectedRequestedImaging)) + }) + + it('should execute the onSuccess callback if provided', async () => { + const store = mockStore({ + user: { + user: { + id: 'fake id', + }, + }, + } as any) + const onSuccessSpy = jest.fn() + + await store.dispatch(requestImaging(mockImaging, onSuccessSpy)) + expect(onSuccessSpy).toHaveBeenCalledWith(mockImaging) + }) + + it('should validate that the imaging can be requested', async () => { + const store = mockStore() + const onSuccessSpy = jest.fn() + await store.dispatch(requestImaging({} as Imaging, onSuccessSpy)) + + const actions = store.getActions() + + expect(actions[0]).toEqual(requestImagingStart()) + expect(actions[1]).toEqual( + requestImagingError({ + patient: 'imagings.requests.error.patientRequired', + type: 'imagings.requests.error.typeRequired', + status: 'imagings.requests.error.statusRequired', + message: 'imagings.requests.error.unableToRequest', + }), + ) + expect(imagingRepositorySaveSpy).not.toHaveBeenCalled() + expect(onSuccessSpy).not.toHaveBeenCalled() + }) + }) + }) +}) diff --git a/src/__tests__/imagings/imagings-slice.test.ts b/src/__tests__/imagings/imagings-slice.test.ts new file mode 100644 index 0000000000..e4b5447d59 --- /dev/null +++ b/src/__tests__/imagings/imagings-slice.test.ts @@ -0,0 +1,146 @@ +import { AnyAction } from 'redux' +import { mocked } from 'ts-jest/utils' + +import imagings, { + fetchImagingsStart, + fetchImagingsSuccess, + searchImagings, +} from '../../imagings/imagings-slice' +import ImagingRepository from '../../shared/db/ImagingRepository' +import SortRequest from '../../shared/db/SortRequest' +import Imaging from '../../shared/model/Imaging' + +interface SearchContainer { + text: string + status: 'requested' | 'completed' | 'canceled' | 'all' + defaultSortRequest: SortRequest +} + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +const expectedSearchObject: SearchContainer = { + text: 'search string', + status: 'all', + defaultSortRequest, +} + +describe('imagings slice', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + describe('imagings reducer', () => { + it('should create the proper intial state with empty imagings array', () => { + const imagingsStore = imagings(undefined, {} as AnyAction) + expect(imagingsStore.isLoading).toBeFalsy() + expect(imagingsStore.imagings).toHaveLength(0) + expect(imagingsStore.statusFilter).toEqual('all') + }) + + it('it should handle the FETCH_IMAGINGS_SUCCESS action', () => { + const expectedImagings = [{ id: '1234' }] + const imagingsStore = imagings(undefined, { + type: fetchImagingsSuccess.type, + payload: expectedImagings, + }) + + expect(imagingsStore.isLoading).toBeFalsy() + expect(imagingsStore.imagings).toEqual(expectedImagings) + }) + }) + + describe('searchImagings', () => { + it('should dispatch the FETCH_IMAGINGS_START action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + await searchImagings('search string', 'all')(dispatch, getState, null) + + expect(dispatch).toHaveBeenCalledWith({ type: fetchImagingsStart.type }) + }) + + it('should call the ImagingRepository search method with the correct search criteria', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'search') + + await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(ImagingRepository.search).toHaveBeenCalledWith(expectedSearchObject) + }) + + it('should call the ImagingRepository findAll method if there is no string text and status is set to all', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'findAll') + + await searchImagings('', expectedSearchObject.status)(dispatch, getState, null) + + expect(ImagingRepository.findAll).toHaveBeenCalledTimes(1) + }) + + it('should dispatch the FETCH_IMAGINGS_SUCCESS action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + const expectedImagings = [ + { + type: 'text', + }, + ] as Imaging[] + + const mockedImagingRepository = mocked(ImagingRepository, true) + mockedImagingRepository.search.mockResolvedValue(expectedImagings) + + await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(dispatch).toHaveBeenLastCalledWith({ + type: fetchImagingsSuccess.type, + payload: expectedImagings, + }) + }) + }) + + describe('sort Request', () => { + it('should have called findAll with sort request in searchImagings method', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'findAll') + + await searchImagings('', expectedSearchObject.status)(dispatch, getState, null) + + expect(ImagingRepository.findAll).toHaveBeenCalledWith( + expectedSearchObject.defaultSortRequest, + ) + }) + + it('should include sorts in the search criteria', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'search') + + await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(ImagingRepository.search).toHaveBeenCalledWith(expectedSearchObject) + }) + }) +}) diff --git a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx new file mode 100644 index 0000000000..a05bfde5a4 --- /dev/null +++ b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx @@ -0,0 +1,230 @@ +import { Button, Typeahead, Label, Alert } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router, Route } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import NewImagingRequest from '../../../imagings/requests/NewImagingRequest' +import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/useTitle' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('New Imaging Request', () => { + let history: any + let setButtonToolBarSpy: any + + const setup = async (status: string, error: any = {}) => { + jest.resetAllMocks() + jest.spyOn(breadcrumbUtil, 'default') + setButtonToolBarSpy = jest.fn() + jest.spyOn(titleUtil, 'default') + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + + history = createMemoryHistory() + history.push(`/imaging/new`) + const store = mockStore({ + title: '', + user: { user: { id: '1234' } }, + imaging: { + status, + error, + }, + } as any) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + + + , + ) + }) + wrapper.update() + return wrapper as ReactWrapper + } + + describe('title and breadcrumbs', () => { + it('should have New Imaging Request as the title', async () => { + await setup('loading', {}) + expect(titleUtil.default).toHaveBeenCalledWith('imagings.requests.new') + }) + }) + + describe('form layout', () => { + it('should render a patient typeahead', async () => { + const wrapper = await setup('loading', {}) + const typeaheadDiv = wrapper.find('.patient-typeahead') + + expect(typeaheadDiv).toBeDefined() + + const label = typeaheadDiv.find(Label) + const typeahead = typeaheadDiv.find(Typeahead) + + expect(label).toBeDefined() + expect(label.prop('text')).toEqual('imagings.imaging.patient') + expect(typeahead).toBeDefined() + expect(typeahead.prop('placeholder')).toEqual('imagings.imaging.patient') + expect(typeahead.prop('searchAccessor')).toEqual('fullName') + }) + + it('should render a type input box', async () => { + const wrapper = await setup('loading', {}) + const typeInputBox = wrapper.find(TextInputWithLabelFormGroup) + + expect(typeInputBox).toBeDefined() + expect(typeInputBox.prop('label')).toEqual('imagings.imaging.type') + expect(typeInputBox.prop('isRequired')).toBeTruthy() + expect(typeInputBox.prop('isEditable')).toBeTruthy() + }) + + it('should render a status types select', async () => { + const wrapper = await setup('loading', {}) + const statusTypesSelect = wrapper.find(SelectWithLabelFormGroup) + + expect(statusTypesSelect).toBeDefined() + expect(statusTypesSelect.prop('label')).toEqual('imagings.imaging.status') + expect(statusTypesSelect.prop('isRequired')).toBeTruthy() + expect(statusTypesSelect.prop('isEditable')).toBeTruthy() + expect(statusTypesSelect.prop('options')).toHaveLength(3) + expect(statusTypesSelect.prop('options')[0].label).toEqual('imagings.status.requested') + expect(statusTypesSelect.prop('options')[0].value).toEqual('requested') + expect(statusTypesSelect.prop('options')[1].label).toEqual('imagings.status.completed') + expect(statusTypesSelect.prop('options')[1].value).toEqual('completed') + expect(statusTypesSelect.prop('options')[2].label).toEqual('imagings.status.canceled') + expect(statusTypesSelect.prop('options')[2].value).toEqual('canceled') + }) + + it('should render a notes text field', async () => { + const wrapper = await setup('loading', {}) + const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) + + expect(notesTextField).toBeDefined() + expect(notesTextField.prop('label')).toEqual('imagings.imaging.notes') + expect(notesTextField.prop('isRequired')).toBeFalsy() + expect(notesTextField.prop('isEditable')).toBeTruthy() + }) + + it('should render a save button', async () => { + const wrapper = await setup('loading', {}) + const saveButton = wrapper.find(Button).at(0) + expect(saveButton).toBeDefined() + expect(saveButton.text().trim()).toEqual('actions.save') + }) + + it('should render a cancel button', async () => { + const wrapper = await setup('loading', {}) + const cancelButton = wrapper.find(Button).at(1) + expect(cancelButton).toBeDefined() + expect(cancelButton.text().trim()).toEqual('actions.cancel') + }) + }) + + describe('errors', () => { + const error = { + message: 'some message', + patient: 'some patient message', + type: 'some type error', + status: 'status type error', + } + + it('should display errors', async () => { + const wrapper = await setup('error', error) + const alert = wrapper.find(Alert) + const typeInput = wrapper.find(TextInputWithLabelFormGroup) + const patientTypeahead = wrapper.find(Typeahead) + + expect(alert.prop('message')).toEqual(error.message) + expect(alert.prop('title')).toEqual('states.error') + expect(alert.prop('color')).toEqual('danger') + + expect(patientTypeahead.prop('isInvalid')).toBeTruthy() + + expect(typeInput.prop('feedback')).toEqual(error.type) + expect(typeInput.prop('isInvalid')).toBeTruthy() + }) + }) + + describe('on cancel', () => { + it('should navigate back to /imaging', async () => { + const wrapper = await setup('loading', {}) + const cancelButton = wrapper.find(Button).at(1) + + act(() => { + const onClick = cancelButton.prop('onClick') as any + onClick({} as React.MouseEvent) + }) + + expect(history.location.pathname).toEqual('/imaging') + }) + }) + + describe('on save', () => { + it('should save the imaging request and navigate to "/imaging"', async () => { + const expectedDate = new Date() + const expectedImaging = { + patient: 'patient', + type: 'expected type', + status: 'requested', + notes: 'expected notes', + id: '1234', + requestedOn: expectedDate.toISOString(), + } as Imaging + + const wrapper = await setup('loading', {}) + jest.spyOn(ImagingRepository, 'save').mockResolvedValue({ ...expectedImaging }) + + const patientTypeahead = wrapper.find(Typeahead) + await act(async () => { + const onChange = patientTypeahead.prop('onChange') + await onChange([{ fullName: expectedImaging.patient }] as Patient[]) + }) + + const typeInput = wrapper.find(TextInputWithLabelFormGroup) + act(() => { + const onChange = typeInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedImaging.type } }) + }) + + const statusSelect = wrapper.find(SelectWithLabelFormGroup) + act(() => { + const onChange = statusSelect.prop('onChange') as any + onChange({ currentTarget: { value: expectedImaging.status } }) + }) + + const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) + act(() => { + const onChange = notesTextField.prop('onChange') as any + onChange({ currentTarget: { value: expectedImaging.notes } }) + }) + wrapper.update() + + const saveButton = wrapper.find(Button).at(0) + const onClick = saveButton.prop('onClick') as any + expect(saveButton.text().trim()).toEqual('actions.save') + await act(async () => { + await onClick() + }) + + expect(history.location.pathname).toEqual(`/imaging/new`) + }) + }) +}) diff --git a/src/__tests__/shared/components/Sidebar.test.tsx b/src/__tests__/shared/components/Sidebar.test.tsx index 34e78818fd..fd32f5c6d8 100644 --- a/src/__tests__/shared/components/Sidebar.test.tsx +++ b/src/__tests__/shared/components/Sidebar.test.tsx @@ -32,6 +32,8 @@ describe('Sidebar', () => { Permissions.ViewIncidents, Permissions.ViewIncident, Permissions.ReportIncident, + Permissions.RequestImaging, + Permissions.ViewImagings, ] const store = mockStore({ components: { sidebarCollapsed: false }, @@ -532,4 +534,113 @@ describe('Sidebar', () => { expect(history.location.pathname).toEqual('/incidents') }) }) + + describe('imagings links', () => { + it('should render the main imagings link', () => { + const wrapper = setup('/imaging') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(6).text().trim()).toEqual('imagings.label') + }) + + it('should render the new imaging request link', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(7).text().trim()).toEqual('imagings.requests.new') + }) + + it('should not render the new imaging request link when user does not have the request imaging privileges', () => { + const wrapper = setupNoPermissions('/imagings') + + const listItems = wrapper.find(ListItem) + + listItems.forEach((_, i) => { + expect(listItems.at(i).text().trim()).not.toEqual('imagings.requests.new') + }) + }) + + it('should render the imagings list link', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(8).text().trim()).toEqual('imagings.requests.label') + }) + + it('should not render the imagings list link when user does not have the view imagings privileges', () => { + const wrapper = setupNoPermissions('/imagings') + + const listItems = wrapper.find(ListItem) + + listItems.forEach((_, i) => { + expect(listItems.at(i).text().trim()).not.toEqual('imagings.requests.label') + }) + }) + + it('main imagings link should be active when the current path is /imagings', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(6).prop('active')).toBeTruthy() + }) + + it('should navigate to /imaging when the main imagings link is clicked', () => { + const wrapper = setup('/') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(6).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/imaging') + }) + + it('new imaging request link should be active when the current path is /imagings/new', () => { + const wrapper = setup('/imagings/new') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(7).prop('active')).toBeTruthy() + }) + + it('should navigate to /imaging/new when the new imaging link is clicked', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(7).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/imaging/new') + }) + + it('imagings list link should be active when the current path is /imagings', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(8).prop('active')).toBeTruthy() + }) + + it('should navigate to /imaging when the imagings list link is clicked', () => { + const wrapper = setup('/imagings/new') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(8).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/imaging') + }) + }) }) diff --git a/src/__tests__/shared/components/navbar/Navbar.test.tsx b/src/__tests__/shared/components/navbar/Navbar.test.tsx index d0884a7883..f2afeb206d 100644 --- a/src/__tests__/shared/components/navbar/Navbar.test.tsx +++ b/src/__tests__/shared/components/navbar/Navbar.test.tsx @@ -56,6 +56,8 @@ describe('Navbar', () => { Permissions.ViewIncidents, Permissions.ViewIncident, Permissions.ReportIncident, + Permissions.RequestImaging, + Permissions.ViewImagings, ] describe('hamberger', () => { @@ -78,7 +80,7 @@ describe('Navbar', () => { }) it('should not show an item if user does not have a permission', () => { - // exclude labs and incidents permissions + // exclude labs, incidents, and imagings permissions const wrapper = setup(cloneDeep(allPermissions).slice(0, 6)) const hospitalRunNavbar = wrapper.find(HospitalRunNavbar) const hamberger = hospitalRunNavbar.find('.nav-hamberger') @@ -89,6 +91,8 @@ describe('Navbar', () => { 'labs.requests.label', 'incidents.reports.new', 'incidents.reports.label', + 'imagings.requests.new', + 'imagings.requests.label', ] children.forEach((option: any) => { @@ -151,6 +155,7 @@ describe('Navbar', () => { children.forEach((option: any) => { expect(option.props.children).not.toEqual('labs.requests.new') expect(option.props.children).not.toEqual('incidents.requests.new') + expect(option.props.children).not.toEqual('imagings.requests.new') }) }) }) diff --git a/src/__tests__/shared/db/ImagingRepository.test.ts b/src/__tests__/shared/db/ImagingRepository.test.ts new file mode 100644 index 0000000000..16b980e25f --- /dev/null +++ b/src/__tests__/shared/db/ImagingRepository.test.ts @@ -0,0 +1,84 @@ +import shortid from 'shortid' + +import { relationalDb } from '../../../shared/config/pouchdb' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' + +const uuidV4Regex = /^[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}$/i + +async function removeAllDocs() { + const docs = await relationalDb.rel.find('imaging') + docs.imagings.forEach(async (d: any) => { + await relationalDb.rel.del('imaging', d) + }) +} + +describe('imaging repository', () => { + describe('find', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should return a imaging with the correct data', async () => { + await relationalDb.rel.save('imaging', { _id: 'id1111' }) + const expectedImaging = await relationalDb.rel.save('imaging', { id: 'id2222' }) + + const actualImaging = await ImagingRepository.find('id2222') + + expect(actualImaging).toBeDefined() + expect(actualImaging.id).toEqual(expectedImaging.id) + }) + }) + + describe('save', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should generate an id that is a uuid for the imaging', async () => { + const newImaging = await ImagingRepository.save({ + patient: '123', + type: 'test', + status: 'status' as string, + notes: 'some notes', + } as Imaging) + + expect(uuidV4Regex.test(newImaging.id)).toBeTruthy() + }) + + it('should generate a imaging code', async () => { + const newImaging = await ImagingRepository.save({ + code: 'somecode', + patient: '123', + type: 'test', + status: 'status' as string, + notes: 'some notes', + } as Imaging) + + expect(shortid.isValid(newImaging.code)).toBeTruthy() + }) + + it('should generate a timestamp for created date and last updated date', async () => { + const newImaging = await ImagingRepository.save({ + patient: '123', + type: 'test', + status: 'status' as string, + notes: 'some notes', + } as Imaging) + + expect(newImaging.createdAt).toBeDefined() + expect(newImaging.updatedAt).toBeDefined() + }) + + it('should override the created date and last updated date even if one was passed in', async () => { + const unexpectedTime = new Date(2020, 2, 1).toISOString() + const newImaging = await ImagingRepository.save({ + createdAt: unexpectedTime, + updatedAt: unexpectedTime, + } as Imaging) + + expect(newImaging.createdAt).not.toEqual(unexpectedTime) + expect(newImaging.updatedAt).not.toEqual(unexpectedTime) + }) + }) +}) diff --git a/src/imagings/Imagings.tsx b/src/imagings/Imagings.tsx new file mode 100644 index 0000000000..ce9bc893e3 --- /dev/null +++ b/src/imagings/Imagings.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { Switch } from 'react-router-dom' + +import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' +import PrivateRoute from '../shared/components/PrivateRoute' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' +import NewImagingRequest from './requests/NewImagingRequest' +import ImagingRequests from './ViewImagings' + +const Imagings = () => { + const { permissions } = useSelector((state: RootState) => state.user) + const breadcrumbs = [ + { + i18nKey: 'imagings.imaging.label', + location: '/imaging', + }, + ] + useAddBreadcrumbs(breadcrumbs, true) + + return ( + + + + + ) +} + +export default Imagings diff --git a/src/imagings/ViewImagings.tsx b/src/imagings/ViewImagings.tsx new file mode 100644 index 0000000000..ffc5317566 --- /dev/null +++ b/src/imagings/ViewImagings.tsx @@ -0,0 +1,92 @@ +import { Button, Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React, { useState, useEffect, useCallback } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../page-header/title/useTitle' +import useTranslator from '../shared/hooks/useTranslator' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' +import { extractUsername } from '../shared/util/extractUsername' +import { searchImagings } from './imagings-slice' + +type ImagingFilter = 'requested' | 'completed' | 'canceled' | 'all' + +const ViewImagings = () => { + const { t } = useTranslator() + const history = useHistory() + const setButtons = useButtonToolbarSetter() + useTitle(t('imagings.label')) + + const { permissions } = useSelector((state: RootState) => state.user) + const dispatch = useDispatch() + const { imagings } = useSelector((state: RootState) => state.imagings) + const [searchFilter, setSearchFilter] = useState('all') + + const getButtons = useCallback(() => { + const buttons: React.ReactNode[] = [] + + if (permissions.includes(Permissions.RequestImaging)) { + buttons.push( + , + ) + } + + return buttons + }, [permissions, history, t]) + + useEffect(() => { + setSearchFilter('all' as ImagingFilter) + }, []) + + useEffect(() => { + dispatch(searchImagings(' ', searchFilter)) + }, [dispatch, searchFilter]) + + useEffect(() => { + setButtons(getButtons()) + return () => { + setButtons([]) + } + }, [dispatch, getButtons, setButtons]) + + return ( + <> +
+ row.id} + columns={[ + { label: t('imagings.imaging.code'), key: 'code' }, + { label: t('imagings.imaging.type'), key: 'type' }, + { + label: t('imagings.imaging.requestedOn'), + key: 'requestedOn', + formatter: (row) => + row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { label: t('imagings.imaging.patient'), key: 'patient' }, + { + label: t('imagings.imaging.requestedBy'), + key: 'requestedBy', + formatter: (row) => extractUsername(row.requestedBy), + }, + { label: t('imagings.imaging.status'), key: 'status' }, + ]} + data={imagings} + /> + + + ) +} + +export default ViewImagings diff --git a/src/imagings/imaging-slice.ts b/src/imagings/imaging-slice.ts new file mode 100644 index 0000000000..5be2c06c03 --- /dev/null +++ b/src/imagings/imaging-slice.ts @@ -0,0 +1,103 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +import ImagingRepository from '../shared/db/ImagingRepository' +import Imaging from '../shared/model/Imaging' +import Patient from '../shared/model/Patient' +import { AppThunk } from '../shared/store' + +interface Error { + patient?: string + type?: string + status?: string + message?: string +} + +interface ImagingState { + error: Error + imaging?: Imaging + patient?: Patient + status: 'loading' | 'error' | 'completed' +} + +const statusType: string[] = ['requested', 'completed', 'canceled'] + +const initialState: ImagingState = { + error: {}, + imaging: undefined, + patient: undefined, + status: 'loading', +} + +function start(state: ImagingState) { + state.status = 'loading' +} + +function finish(state: ImagingState, { payload }: PayloadAction) { + state.status = 'completed' + state.imaging = payload + state.error = {} +} + +function error(state: ImagingState, { payload }: PayloadAction) { + state.status = 'error' + state.error = payload +} + +const imagingSlice = createSlice({ + name: 'imaging', + initialState, + reducers: { + requestImagingStart: start, + requestImagingSuccess: finish, + requestImagingError: error, + }, +}) + +export const { + requestImagingStart, + requestImagingSuccess, + requestImagingError, +} = imagingSlice.actions + +const validateImagingRequest = (newImaging: Imaging): Error => { + const imagingRequestError: Error = {} + if (!newImaging.patient) { + imagingRequestError.patient = 'imagings.requests.error.patientRequired' + } + + if (!newImaging.type) { + imagingRequestError.type = 'imagings.requests.error.typeRequired' + } + + if (!newImaging.status) { + imagingRequestError.status = 'imagings.requests.error.statusRequired' + } else if (!statusType.includes(newImaging.status)) { + imagingRequestError.status = 'imagings.requests.error.incorrectStatus' + } + + return imagingRequestError +} + +export const requestImaging = ( + newImaging: Imaging, + onSuccess?: (imaging: Imaging) => void, +): AppThunk => async (dispatch, getState) => { + dispatch(requestImagingStart()) + + const imagingRequestError = validateImagingRequest(newImaging) + if (Object.keys(imagingRequestError).length > 0) { + imagingRequestError.message = 'imagings.requests.error.unableToRequest' + dispatch(requestImagingError(imagingRequestError)) + } else { + newImaging.requestedOn = new Date(Date.now().valueOf()).toISOString() + newImaging.requestedBy = getState().user.user?.id || '' + const requestedImaging = await ImagingRepository.save(newImaging) + dispatch(requestImagingSuccess(requestedImaging)) + + if (onSuccess) { + onSuccess(requestedImaging) + } + } +} + +export default imagingSlice.reducer diff --git a/src/imagings/imagings-slice.ts b/src/imagings/imagings-slice.ts new file mode 100644 index 0000000000..e58fd2fc0d --- /dev/null +++ b/src/imagings/imagings-slice.ts @@ -0,0 +1,66 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +import ImagingRepository from '../shared/db/ImagingRepository' +import SortRequest from '../shared/db/SortRequest' +import Imaging from '../shared/model/Imaging' +import { AppThunk } from '../shared/store' + +interface ImagingsState { + isLoading: boolean + imagings: Imaging[] + statusFilter: status +} + +type status = 'requested' | 'completed' | 'canceled' | 'all' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +const initialState: ImagingsState = { + isLoading: false, + imagings: [], + statusFilter: 'all', +} + +const startLoading = (state: ImagingsState) => { + state.isLoading = true +} + +const imagingsSlice = createSlice({ + name: 'imagings', + initialState, + reducers: { + fetchImagingsStart: startLoading, + fetchImagingsSuccess(state, { payload }: PayloadAction) { + state.isLoading = false + state.imagings = payload + }, + }, +}) +export const { fetchImagingsStart, fetchImagingsSuccess } = imagingsSlice.actions + +export const searchImagings = (text: string, status: status): AppThunk => async (dispatch) => { + dispatch(fetchImagingsStart()) + + let imagings + + if (text.trim() === '' && status === initialState.statusFilter) { + imagings = await ImagingRepository.findAll(defaultSortRequest) + } else { + imagings = await ImagingRepository.search({ + text, + status, + defaultSortRequest, + }) + } + + dispatch(fetchImagingsSuccess(imagings)) +} + +export default imagingsSlice.reducer diff --git a/src/imagings/requests/NewImagingRequest.tsx b/src/imagings/requests/NewImagingRequest.tsx new file mode 100644 index 0000000000..abbb768518 --- /dev/null +++ b/src/imagings/requests/NewImagingRequest.tsx @@ -0,0 +1,153 @@ +import { Typeahead, Label, Button, Alert } from '@hospitalrun/components' +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/title/useTitle' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../shared/db/PatientRepository' +import useTranslator from '../../shared/hooks/useTranslator' +import Imaging from '../../shared/model/Imaging' +import Patient from '../../shared/model/Patient' +import { RootState } from '../../shared/store' +import { requestImaging } from '../imaging-slice' + +const NewImagingRequest = () => { + const { t } = useTranslator() + const dispatch = useDispatch() + const history = useHistory() + useTitle(t('imagings.requests.new')) + const { status, error } = useSelector((state: RootState) => state.imaging) + + const statusOptions: Option[] = [ + { label: t('imagings.status.requested'), value: 'requested' }, + { label: t('imagings.status.completed'), value: 'completed' }, + { label: t('imagings.status.canceled'), value: 'canceled' }, + ] + + const [newImagingRequest, setNewImagingRequest] = useState({ + patient: '', + type: '', + notes: '', + status: '', + }) + + const breadcrumbs = [ + { + i18nKey: 'imagings.requests.new', + location: `/imaging/new`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + const onPatientChange = (patient: Patient) => { + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + patient: patient.fullName as string, + })) + } + + const onImagingTypeChange = (event: React.ChangeEvent) => { + const type = event.currentTarget.value + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + type, + })) + } + + const onStatusChange = (value: string) => { + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + status: value, + })) + } + + const onNoteChange = (event: React.ChangeEvent) => { + const notes = event.currentTarget.value + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + notes, + })) + } + + const onSave = async () => { + const newImaging = newImagingRequest as Imaging + const onSuccess = () => { + history.push(`/imaging`) + } + + dispatch(requestImaging(newImaging, onSuccess)) + } + + const onCancel = () => { + history.push('/imaging') + } + + return ( + <> + {status === 'error' && ( + + )} +
+
+
+ + value === newImagingRequest.status)} + onChange={(values) => onStatusChange(values[0])} + /> +
+ +
+
+
+ + +
+
+ + + ) +} + +export default NewImagingRequest diff --git a/src/shared/components/Sidebar.tsx b/src/shared/components/Sidebar.tsx index 69a4751cf5..204fc28961 100644 --- a/src/shared/components/Sidebar.tsx +++ b/src/shared/components/Sidebar.tsx @@ -44,6 +44,8 @@ const Sidebar = () => { ? 'labs' : splittedPath[1].includes('incidents') ? 'incidents' + : splittedPath[1].includes('imagings') + ? 'imagings' : 'none', ) @@ -297,6 +299,56 @@ const Sidebar = () => { ) + const getImagingLinks = () => ( + <> + { + navigateTo('/imaging') + setExpansion('imagings') + }} + className="nav-item" + style={listItemStyle} + > + + {!sidebarCollapsed && t('imagings.label')} + + {splittedPath[1].includes('imaging') && expandedItem === 'imagings' && ( + + {permissions.includes(Permissions.RequestImaging) && ( + navigateTo('/imaging/new')} + active={splittedPath[1].includes('imaging') && splittedPath.length > 2} + > + + {!sidebarCollapsed && t('imagings.requests.new')} + + )} + {permissions.includes(Permissions.ViewImagings) && ( + navigateTo('/imaging')} + active={splittedPath[1].includes('imaging') && splittedPath.length < 3} + > + + {!sidebarCollapsed && t('imagings.requests.label')} + + )} + + )} + + ) + return ( diff --git a/src/shared/components/navbar/Navbar.tsx b/src/shared/components/navbar/Navbar.tsx index d51390b192..e9c5d79c28 100644 --- a/src/shared/components/navbar/Navbar.tsx +++ b/src/shared/components/navbar/Navbar.tsx @@ -22,6 +22,7 @@ const Navbar = () => { 'scheduling.appointments.new', 'labs.requests.new', 'incidents.reports.new', + 'imagings.requests.new', 'settings.label', ] @@ -43,7 +44,13 @@ const Navbar = () => { const hambergerPages = Object.keys(pageMap).map((key) => pageMap[key]) // For Desktop, add shortcuts menu - const addPages = [pageMap.newPatient, pageMap.newAppointment, pageMap.newLab, pageMap.newIncident] + const addPages = [ + pageMap.newPatient, + pageMap.newAppointment, + pageMap.newLab, + pageMap.newIncident, + pageMap.newImaging, + ] return ( { + constructor() { + super('imaging', relationalDb) + } + + async search(container: SearchContainer): Promise { + const searchValue = { $regex: RegExp(container.text, 'i') } + const selector = { + $and: [ + { + $or: [ + { + 'data.type': searchValue, + }, + { + 'data.code': searchValue, + }, + ], + }, + ...(container.status !== 'all' ? [{ 'data.status': container.status }] : [undefined]), + ].filter((x) => x !== undefined), + sorts: container.defaultSortRequest, + } + + return super.search({ + selector, + }) + } + + async save(entity: Imaging): Promise { + const imagingCode = generateCode('I') + entity.code = imagingCode + return super.save(entity) + } +} + +export default new ImagingRepository() diff --git a/src/shared/locales/enUs/translations/imagings/index.ts b/src/shared/locales/enUs/translations/imagings/index.ts new file mode 100644 index 0000000000..b98e7db19a --- /dev/null +++ b/src/shared/locales/enUs/translations/imagings/index.ts @@ -0,0 +1,36 @@ +export default { + imagings: { + label: 'Imagings', + status: { + requested: 'Requested', + completed: 'Completed', + canceled: 'Canceled', + }, + requests: { + label: 'Imaging Requests', + new: 'New Imaging Request', + view: 'View Imaging', + cancel: 'Cancel Imaging', + complete: 'Complete Imaging', + error: { + unableToRequest: 'Unable to create new imaging request.', + incorrectStatus: 'Incorrect Status', + typeRequired: 'Type is required.', + statusRequired: 'Status is required.', + patientRequired: 'Patient name is required.', + }, + }, + imaging: { + label: 'imaging', + code: 'Imaging Code', + status: 'Status', + type: 'Type', + notes: 'Notes', + requestedOn: 'Requested On', + completedOn: 'Completed On', + canceledOn: 'Canceled On', + requestedBy: 'Requested By', + patient: 'Patient', + }, + }, +} diff --git a/src/shared/locales/enUs/translations/index.ts b/src/shared/locales/enUs/translations/index.ts index ec5eee0dd3..3039d0c06d 100644 --- a/src/shared/locales/enUs/translations/index.ts +++ b/src/shared/locales/enUs/translations/index.ts @@ -1,6 +1,7 @@ import actions from './actions' import bloodType from './blood-type' import dashboard from './dashboard' +import imagings from './imagings' import incidents from './incidents' import labs from './labs' import networkStatus from './network-status' @@ -26,4 +27,5 @@ export default { ...settings, ...user, ...bloodType, + ...imagings, } diff --git a/src/shared/model/Imaging.ts b/src/shared/model/Imaging.ts new file mode 100644 index 0000000000..e423d983fd --- /dev/null +++ b/src/shared/model/Imaging.ts @@ -0,0 +1,13 @@ +import AbstractDBModel from './AbstractDBModel' + +export default interface Imaging extends AbstractDBModel { + code: string + patient: string + type: string + status: 'requested' | 'completed' | 'canceled' + requestedOn: string + requestedBy: string // will be the currently logged in user's id + completedOn?: string + canceledOn?: string + notes?: string +} diff --git a/src/shared/model/Permissions.ts b/src/shared/model/Permissions.ts index f9c6d2ac1f..917ca2d6fd 100644 --- a/src/shared/model/Permissions.ts +++ b/src/shared/model/Permissions.ts @@ -17,6 +17,8 @@ enum Permissions { ResolveIncident = 'resolve:incident', AddCarePlan = 'write:care_plan', ReadCarePlan = 'read:care_plan', + RequestImaging = 'write:imaging', + ViewImagings = 'read:imagings', } export default Permissions diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 4ac0fa9576..68ace1c86f 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -1,6 +1,8 @@ import { configureStore, combineReducers, Action } from '@reduxjs/toolkit' import ReduxThunk, { ThunkAction } from 'redux-thunk' +import imaging from '../../imagings/imaging-slice' +import imagings from '../../imagings/imagings-slice' import incident from '../../incidents/incident-slice' import incidents from '../../incidents/incidents-slice' import lab from '../../labs/lab-slice' @@ -27,6 +29,8 @@ const reducer = combineReducers({ incident, incidents, labs, + imagings, + imaging, }) const store = configureStore({ diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index 660df6262a..388a84ee18 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -38,6 +38,8 @@ const initialState: UserState = { Permissions.ResolveIncident, Permissions.AddCarePlan, Permissions.ReadCarePlan, + Permissions.ViewImagings, + Permissions.RequestImaging, ], }