diff --git a/.gitignore b/.gitignore index 769faaf7e5..d67cb06f91 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ typings/ package-lock.json .DS_Store yarn.lock +.vscode/launch.json +.vscode/tasks.json diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 4b5f9d9617..3179d925ca 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -23,6 +23,7 @@ describe('HospitalRun', () => { const store = mockStore({ title: 'test', user: { permissions: [Permissions.ViewLabs] }, + labs: { labs: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, }) diff --git a/src/__tests__/clients/db/LabRepository.test.ts b/src/__tests__/clients/db/LabRepository.test.ts index e7f24726d5..f9ed08130e 100644 --- a/src/__tests__/clients/db/LabRepository.test.ts +++ b/src/__tests__/clients/db/LabRepository.test.ts @@ -1,14 +1,170 @@ +/* eslint "@typescript-eslint/camelcase": "off" */ import shortid from 'shortid' -import LabRepository from '../../../clients/db/LabRepository' +import { labs } from 'config/pouchdb' +import LabRepository from 'clients/db/LabRepository' +import SortRequest from 'clients/db/SortRequest' import Lab from '../../../model/Lab' +interface SearchContainer { + text: string + status: 'requested' | 'completed' | 'canceled' | 'all' + defaultSortRequest: SortRequest +} + +const removeAllDocs = async () => { + const allDocs = await labs.allDocs({ include_docs: true }) + await Promise.all( + allDocs.rows.map(async (row) => { + if (row.doc) { + await labs.remove(row.doc) + } + }), + ) +} + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +const searchObject: SearchContainer = { + text: '', + status: 'all', + defaultSortRequest, +} + describe('lab repository', () => { - it('should generate a lab code', async () => { - const newLab = await LabRepository.save({ - patientId: '123', - type: 'test', - } as Lab) + describe('find', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should return a lab with the correct data', async () => { + // first lab to store is to have mock data to make sure we are getting the expected + await labs.put({ _id: 'id1111' }) + const expectedLab = await labs.put({ _id: 'id2222' }) + + const actualLab = await LabRepository.find('id2222') + + expect(actualLab).toBeDefined() + expect(actualLab.id).toEqual(expectedLab.id) + }) + + it('should generate a lab code', async () => { + const newLab = await LabRepository.save({ + patientId: '123', + type: 'test', + } as Lab) + + expect(shortid.isValid(newLab.code)).toBeTruthy() + }) + }) + + describe('search', () => { + it('should return all records that lab type matches search text', async () => { + const expectedLabType = 'more tests' + await labs.put({ _id: 'someId1', type: expectedLabType, status: 'requested' }) + await labs.put({ _id: 'someId2', type: 'P00002', status: 'requested' }) + + searchObject.text = expectedLabType + + const result = await LabRepository.search(searchObject) + + expect(result).toHaveLength(1) + expect(result[0].type).toEqual(expectedLabType) + }) + + it('should return all records that contains search text in the type', async () => { + const expectedLabType = 'Labs Tests' + await labs.put({ _id: 'someId3', type: expectedLabType }) + await labs.put({ _id: 'someId4', type: 'Sencond Lab labs tests' }) + await labs.put({ _id: 'someId5', type: 'not found' }) + + searchObject.text = expectedLabType + + const result = await LabRepository.search(searchObject) + + expect(result).toHaveLength(2) + expect(result[0].id).toEqual('someId3') + expect(result[1].id).toEqual('someId4') + }) + + it('should return all records that contains search text in code', async () => { + const expectedLabCode = 'L-CODE-sam445Pl' + await labs.put({ _id: 'theID13', type: 'Expected', code: 'L-CODE-sam445Pl' }) + await labs.put({ _id: 'theID14', type: 'Sencond Lab labs tests', code: 'L-4XXX' }) + await labs.put({ _id: 'theID15', type: 'not found', code: 'L-775YYxc' }) + + searchObject.text = expectedLabCode + + const result = await LabRepository.search(searchObject) + + expect(result).toHaveLength(1) + expect(result[0].id).toEqual('theID13') + }) + + it('should match search criteria with case insesitive match', async () => { + await labs.put({ _id: 'id3333', type: 'lab tests' }) + await labs.put({ _id: 'id4444', type: 'not found' }) + + searchObject.text = 'LAB TESTS' + + const result = await LabRepository.search(searchObject) + + expect(result).toHaveLength(1) + expect(result[0].id).toEqual('id3333') + }) + + it('should return all records that matches an specific status', async () => { + await labs.put({ _id: 'id5555', type: 'lab tests', status: 'requested' }) + await labs.put({ _id: 'id6666', type: 'lab tests', status: 'requested' }) + await labs.put({ _id: 'id7777', type: 'lab tests', status: 'completed' }) + await labs.put({ _id: 'id8888', type: 'not found', status: 'completed' }) + + searchObject.text = '' + searchObject.status = 'completed' + + const result = await LabRepository.search(searchObject) + + expect(result).toHaveLength(2) + expect(result[0].id).toEqual('id7777') + expect(result[1].id).toEqual('id8888') + }) + + it('should return records with search text and specific status', async () => { + await labs.put({ _id: 'theID09', type: 'the specific lab', status: 'requested' }) + await labs.put({ _id: 'theID10', type: 'not found', status: 'cancelled' }) + + searchObject.text = 'the specific lab' + searchObject.status = 'requested' + + const result = await LabRepository.search(searchObject) + + expect(result).toHaveLength(1) + expect(result[0].id).toEqual('theID09') + }) + }) + + describe('findAll', () => { + afterEach(async () => { + await removeAllDocs() + }) + it('should find all labs in the database sorted by their requestedOn', async () => { + const expectedLab1 = await labs.put({ _id: 'theID11' }) + + setTimeout(async () => { + const expectedLab2 = await labs.put({ _id: 'theID12' }) + + const result = await LabRepository.findAll() - expect(shortid.isValid(newLab.code)).toBeTruthy() + expect(result).toHaveLength(2) + expect(result[0].id).toEqual(expectedLab1.id) + expect(result[1].id).toEqual(expectedLab2.id) + }, 1000) + }) }) }) diff --git a/src/__tests__/labs/ViewLabs.test.tsx b/src/__tests__/labs/ViewLabs.test.tsx index a1c22b4c12..0cf1eaaf64 100644 --- a/src/__tests__/labs/ViewLabs.test.tsx +++ b/src/__tests__/labs/ViewLabs.test.tsx @@ -4,16 +4,17 @@ import { Provider } from 'react-redux' import { Router } from 'react-router' import ViewLabs from 'labs/ViewLabs' import { mount, ReactWrapper } from 'enzyme' +import { TextInput, Select } from '@hospitalrun/components' import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { createMemoryHistory } from 'history' import Permissions from 'model/Permissions' import { act } from '@testing-library/react' import LabRepository from 'clients/db/LabRepository' +import * as labsSlice from 'labs/labs-slice' import Lab from 'model/Lab' import format from 'date-fns/format' import * as ButtonBarProvider from 'page-header/ButtonBarProvider' -import SortRequest from 'clients/db/SortRequest' import * as titleUtil from '../../page-header/useTitle' const mockStore = configureMockStore([thunk]) @@ -25,6 +26,7 @@ describe('View Labs', () => { const store = mockStore({ title: '', user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, + labs: { labs: [] }, }) titleSpy = jest.spyOn(titleUtil, 'default') jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) @@ -49,6 +51,7 @@ describe('View Labs', () => { const store = mockStore({ title: '', user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, + labs: { labs: [] }, }) const setButtonToolBarSpy = jest.fn() jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) @@ -71,6 +74,7 @@ describe('View Labs', () => { const store = mockStore({ title: '', user: { permissions: [Permissions.ViewLabs] }, + labs: { labs: [] }, }) const setButtonToolBarSpy = jest.fn() jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) @@ -106,6 +110,7 @@ describe('View Labs', () => { const store = mockStore({ title: '', user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, + labs: { labs: [expectedLab] }, }) history = createMemoryHistory() @@ -167,36 +172,113 @@ describe('View Labs', () => { }) }) - describe('sort Request', () => { - let findAllSpy: any - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - }) - findAllSpy = jest.spyOn(LabRepository, 'findAll') - findAllSpy.mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) + describe('dropdown', () => { + it('should search for labs when dropdown changes', () => { + const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') + let wrapper: ReactWrapper + let history: any + const expectedLab = { + id: '1234', + type: 'lab type', + patientId: 'patientId', + status: 'requested', + requestedOn: '2020-03-30T04:43:20.102Z', + } as Lab + + beforeEach(async () => { + const store = mockStore({ + title: '', + user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, + labs: { labs: [expectedLab] }, + }) + history = createMemoryHistory() + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + searchLabsSpy.mockClear() + + act(() => { + const onChange = wrapper.find(Select).prop('onChange') as any + onChange({ + target: { + value: 'requested', + }, + preventDefault: jest.fn(), + }) + }) + + wrapper.update() + expect(searchLabsSpy).toHaveBeenCalledTimes(1) }) }) + }) + + describe('search functionality', () => { + beforeEach(() => jest.useFakeTimers()) + + afterEach(() => jest.useRealTimers()) + + it('should search for labs after the search text has not changed for 500 milliseconds', () => { + const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') + let wrapper: ReactWrapper + let history: any + const expectedLab = { + id: '1234', + type: 'lab type', + patientId: 'patientId', + status: 'requested', + requestedOn: '2020-03-30T04:43:20.102Z', + } as Lab - it('should have called findAll with sort request', () => { - const sortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], - } - expect(findAllSpy).toHaveBeenCalledWith(sortRequest) + beforeEach(async () => { + const store = mockStore({ + title: '', + user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, + labs: { labs: [expectedLab] }, + }) + history = createMemoryHistory() + + jest.spyOn(LabRepository, 'findAll').mockResolvedValue([expectedLab]) + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + searchLabsSpy.mockClear() + const expectedSearchText = 'search text' + + act(() => { + const onClick = wrapper.find(TextInput).prop('onChange') as any + onClick({ + target: { + value: expectedSearchText, + }, + preventDefault: jest.fn(), + }) + }) + + act(() => { + jest.advanceTimersByTime(500) + }) + + wrapper.update() + + expect(searchLabsSpy).toHaveBeenCalledTimes(1) + expect(searchLabsSpy).toHaveBeenLastCalledWith(expectedSearchText) + }) }) }) }) diff --git a/src/__tests__/labs/labs.slice.test.ts b/src/__tests__/labs/labs.slice.test.ts new file mode 100644 index 0000000000..a380d0beb3 --- /dev/null +++ b/src/__tests__/labs/labs.slice.test.ts @@ -0,0 +1,139 @@ +import { AnyAction } from 'redux' +import { mocked } from 'ts-jest/utils' +import SortRequest from 'clients/db/SortRequest' +import labs, { fetchLabsStart, fetchLabsSuccess, searchLabs } from '../../labs/labs-slice' +import Lab from '../../model/Lab' +import LabRepository from '../../clients/db/LabRepository' + +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('labs slice', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + describe('labs reducer', () => { + it('should create the proper intial state with empty labs array', () => { + const labsStore = labs(undefined, {} as AnyAction) + expect(labsStore.isLoading).toBeFalsy() + expect(labsStore.labs).toHaveLength(0) + expect(labsStore.statusFilter).toEqual('all') + }) + + it('it should handle the FETCH_LABS_SUCCESS action', () => { + const expectedLabs = [{ id: '1234' }] + const labsStore = labs(undefined, { + type: fetchLabsSuccess.type, + payload: expectedLabs, + }) + + expect(labsStore.isLoading).toBeFalsy() + expect(labsStore.labs).toEqual(expectedLabs) + }) + }) + + describe('searchLabs', () => { + it('should dispatch the FETCH_LABS_START action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + await searchLabs('search string', 'all')(dispatch, getState, null) + + expect(dispatch).toHaveBeenCalledWith({ type: fetchLabsStart.type }) + }) + + it('should call the LabRepository search method with the correct search criteria', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(LabRepository, 'search') + + await searchLabs(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(LabRepository.search).toHaveBeenCalledWith(expectedSearchObject) + }) + + it('should call the LabRepository 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(LabRepository, 'findAll') + + await searchLabs('', expectedSearchObject.status)(dispatch, getState, null) + + expect(LabRepository.findAll).toHaveBeenCalledTimes(1) + }) + + it('should dispatch the FETCH_LABS_SUCCESS action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + const expectedLabs = [ + { + type: 'text', + }, + ] as Lab[] + + const mockedLabRepository = mocked(LabRepository, true) + mockedLabRepository.search.mockResolvedValue(expectedLabs) + + await searchLabs(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(dispatch).toHaveBeenLastCalledWith({ + type: fetchLabsSuccess.type, + payload: expectedLabs, + }) + }) + }) + + describe('sort Request', () => { + it('should have called findAll with sort request in searchLabs method', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(LabRepository, 'findAll') + + await searchLabs('', expectedSearchObject.status)(dispatch, getState, null) + + expect(LabRepository.findAll).toHaveBeenCalledWith(expectedSearchObject.defaultSortRequest) + }) + + it('should include sorts in the search criteria', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(LabRepository, 'search') + + await searchLabs(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(LabRepository.search).toHaveBeenCalledWith(expectedSearchObject) + }) + }) +}) diff --git a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx index ba8112caef..49da165c9c 100644 --- a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx +++ b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx @@ -14,7 +14,6 @@ import thunk from 'redux-thunk' import { Provider } from 'react-redux' import Permissions from 'model/Permissions' import RelatedPerson from 'model/RelatedPerson' -import { Button } from '@hospitalrun/components' import * as patientSlice from '../../../patients/patient-slice' const mockStore = configureMockStore([thunk]) @@ -137,7 +136,7 @@ describe('Related Persons Tab', () => { const tableHeaders = wrapper.find('th') const tableBody = wrapper.find('tbody') const tableData = wrapper.find('td') - const deleteButton = tableData.at(3).find(Button) + const deleteButton = tableData.at(3).find(components.Button) expect(table).toHaveLength(1) expect(tableHeader).toHaveLength(1) expect(tableBody).toHaveLength(1) @@ -160,7 +159,7 @@ describe('Related Persons Tab', () => { const table = wrapper.find('table') const tableBody = table.find('tbody') const tableData = tableBody.find('td') - const deleteButton = tableData.at(3).find(Button) + const deleteButton = tableData.at(3).find(components.Button) await act(async () => { const onClick = deleteButton.prop('onClick') diff --git a/src/clients/db/LabRepository.ts b/src/clients/db/LabRepository.ts index 6748ab1e7d..3581c96e12 100644 --- a/src/clients/db/LabRepository.ts +++ b/src/clients/db/LabRepository.ts @@ -2,12 +2,39 @@ import Lab from 'model/Lab' import generateCode from '../../util/generateCode' import Repository from './Repository' import { labs } from '../../config/pouchdb' +import SortRequest from './SortRequest' +interface SearchContainer { + text: string + status: 'requested' | 'completed' | 'canceled' | 'all' + defaultSortRequest: SortRequest +} export class LabRepository extends Repository { constructor() { super(labs) - labs.createIndex({ - index: { fields: ['requestedOn'] }, + } + + async search(container: SearchContainer): Promise { + const searchValue = { $regex: RegExp(container.text, 'i') } + const selector = { + $and: [ + { + $or: [ + { + type: searchValue, + }, + { + code: searchValue, + }, + ], + }, + ...(container.status !== 'all' ? [{ status: container.status }] : [undefined]), + ].filter((x) => x !== undefined), + sorts: container.defaultSortRequest, + } + + return super.search({ + selector, }) } diff --git a/src/clients/db/Repository.ts b/src/clients/db/Repository.ts index a1d6972403..df56a1d3a9 100644 --- a/src/clients/db/Repository.ts +++ b/src/clients/db/Repository.ts @@ -1,7 +1,7 @@ /* eslint "@typescript-eslint/camelcase": "off" */ import { v4 as uuidv4 } from 'uuid' import AbstractDBModel from '../../model/AbstractDBModel' -import { Unsorted } from './SortRequest' +import SortRequest, { Unsorted } from './SortRequest' function mapDocument(document: any): any { const { _id, _rev, ...values } = document @@ -33,6 +33,23 @@ export default class Repository { selector[s.field] = { $gt: null } }) + // Adds an index to each of the fields coming from the sorting object + // allowing the algorithm to sort by any given SortRequest, by avoiding the default index error (lack of index) + + await Promise.all( + sort.sorts.map( + async (s): Promise => { + await this.db.createIndex({ + index: { + fields: [s.field], + }, + }) + + return sort + }, + ), + ) + const result = await this.db.find({ selector, sort: sort.sorts.length > 0 ? sort.sorts.map((s) => ({ [s.field]: s.direction })) : undefined, diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e70dfcd5e3..06134b07f8 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -51,7 +51,7 @@ const Sidebar = () => { setExpandedItem(item.toString()) } - const listSubItemStyleNew: CSSProperties = { + const listSubItemStyle: CSSProperties = { cursor: 'pointer', fontSize: 'small', borderBottomWidth: 0, @@ -61,7 +61,7 @@ const Sidebar = () => { backgroundColor: 'rgba(245,245,245,1)', } - const listSubItemStyle: CSSProperties = { + const listSubItemStyleNew: CSSProperties = { cursor: 'pointer', fontSize: 'small', borderBottomWidth: 0, diff --git a/src/labs/ViewLabs.tsx b/src/labs/ViewLabs.tsx index 42909a9537..012b26a836 100644 --- a/src/labs/ViewLabs.tsx +++ b/src/labs/ViewLabs.tsx @@ -1,16 +1,20 @@ import React, { useState, useEffect, useCallback } from 'react' +import { useSelector, useDispatch } from 'react-redux' import useTitle from 'page-header/useTitle' import { useTranslation } from 'react-i18next' import format from 'date-fns/format' import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' -import { Button } from '@hospitalrun/components' +import { Spinner, Button } from '@hospitalrun/components' import { useHistory } from 'react-router' -import LabRepository from 'clients/db/LabRepository' -import SortRequest from 'clients/db/SortRequest' import Lab from 'model/Lab' -import { useSelector } from 'react-redux' import Permissions from 'model/Permissions' +import SelectWithLabelFormGroup from 'components/input/SelectWithLableFormGroup' +import TextInputWithLabelFormGroup from 'components/input/TextInputWithLabelFormGroup' import { RootState } from '../store' +import { searchLabs } from './labs-slice' +import useDebounce from '../hooks/debounce' + +type filter = 'requested' | 'completed' | 'canceled' | 'all' const ViewLabs = () => { const { t } = useTranslation() @@ -19,7 +23,11 @@ const ViewLabs = () => { useTitle(t('labs.label')) const { permissions } = useSelector((state: RootState) => state.user) - const [labs, setLabs] = useState([]) + const dispatch = useDispatch() + const { labs, isLoading } = useSelector((state: RootState) => state.labs) + const [searchFilter, setSearchFilter] = useState('all') + const [searchText, setSearchText] = useState('') + const debouncedSearchText = useDebounce(searchText, 500) const getButtons = useCallback(() => { const buttons: React.ReactNode[] = [] @@ -41,54 +49,97 @@ const ViewLabs = () => { return buttons }, [permissions, history, t]) + const setFilter = (filter: string) => + filter === 'requested' + ? 'requested' + : filter === 'completed' + ? 'completed' + : filter === 'canceled' + ? 'canceled' + : 'all' + useEffect(() => { - const fetch = async () => { - const sortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], - } - const fetchedLabs = await LabRepository.findAll(sortRequest) - setLabs(fetchedLabs) - } + dispatch(searchLabs(debouncedSearchText, searchFilter)) + }, [dispatch, debouncedSearchText, searchFilter]) + useEffect(() => { setButtons(getButtons()) - fetch() - return () => { setButtons([]) } - }, [getButtons, setButtons]) + }, [dispatch, getButtons, setButtons]) + + const loadingIndicator = const onTableRowClick = (lab: Lab) => { history.push(`/labs/${lab.id}`) } + const onSelectChange = (event: React.ChangeEvent) => { + setSearchFilter(setFilter(event.target.value)) + } + + const onSearchBoxChange = (event: React.ChangeEvent) => { + setSearchText(event.target.value) + } + + const listBody = ( + + {labs.map((lab) => ( + onTableRowClick(lab)} key={lab.id}> + {lab.code} + {lab.type} + {format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')} + {lab.status} + + ))} + + ) + return ( <> - - - - - - - - - - - {labs.map((lab) => ( - onTableRowClick(lab)} key={lab.id}> - - - - +
+
+ ) => { + onSelectChange(event) + }} + /> +
+
+ +
+
+
+
{t('labs.lab.code')}{t('labs.lab.type')}{t('labs.lab.requestedOn')}{t('labs.lab.status')}
{lab.code}{lab.type}{format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')}{lab.status}
+ + + + + + - ))} - -
{t('labs.lab.code')}{t('labs.lab.type')}{t('labs.lab.requestedOn')}{t('labs.lab.status')}
+ + {isLoading ? loadingIndicator : listBody} + + ) } diff --git a/src/labs/labs-slice.ts b/src/labs/labs-slice.ts new file mode 100644 index 0000000000..db956992da --- /dev/null +++ b/src/labs/labs-slice.ts @@ -0,0 +1,65 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import Lab from '../model/Lab' +import LabRepository from '../clients/db/LabRepository' +import SortRequest from '../clients/db/SortRequest' +import { AppThunk } from '../store' + +interface LabsState { + isLoading: boolean + labs: Lab[] + statusFilter: status +} + +type status = 'requested' | 'completed' | 'canceled' | 'all' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +const initialState: LabsState = { + isLoading: false, + labs: [], + statusFilter: 'all', +} + +const startLoading = (state: LabsState) => { + state.isLoading = true +} + +const labsSlice = createSlice({ + name: 'labs', + initialState, + reducers: { + fetchLabsStart: startLoading, + fetchLabsSuccess(state, { payload }: PayloadAction) { + state.isLoading = false + state.labs = payload + }, + }, +}) +export const { fetchLabsStart, fetchLabsSuccess } = labsSlice.actions + +export const searchLabs = (text: string, status: status): AppThunk => async (dispatch) => { + dispatch(fetchLabsStart()) + + let labs + + if (text.trim() === '' && status === initialState.statusFilter) { + labs = await LabRepository.findAll(defaultSortRequest) + } else { + labs = await LabRepository.search({ + text, + status, + defaultSortRequest, + }) + } + + dispatch(fetchLabsSuccess(labs)) +} + +export default labsSlice.reducer diff --git a/src/locales/enUs/translations/labs/index.ts b/src/locales/enUs/translations/labs/index.ts index ae22c88c02..d5027ed844 100644 --- a/src/locales/enUs/translations/labs/index.ts +++ b/src/locales/enUs/translations/labs/index.ts @@ -1,6 +1,16 @@ export default { labs: { label: 'Labs', + filterTitle: 'Filter by status', + search: 'Search labs', + status: { + requested: 'Requested', + completed: 'Completed', + canceled: 'Canceled', + }, + filter: { + all: 'All statuses', + }, requests: { label: 'Lab Requests', new: 'New Lab Request', diff --git a/src/store/index.ts b/src/store/index.ts index 77ec4087cc..1a39af5ff3 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -7,6 +7,7 @@ import appointments from '../scheduling/appointments/appointments-slice' import title from '../page-header/title-slice' import user from '../user/user-slice' import lab from '../labs/lab-slice' +import labs from '../labs/labs-slice' import breadcrumbs from '../breadcrumbs/breadcrumbs-slice' import components from '../components/component-slice' @@ -20,6 +21,7 @@ const reducer = combineReducers({ breadcrumbs, components, lab, + labs, }) const store = configureStore({