From 1a4ece9d4a1a4b05a7166f34a741c57595a550c0 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 12 Apr 2020 16:50:49 -0500 Subject: [PATCH] refactor labs to use redux --- src/labs/ViewLab.tsx | 123 +++++++++---------- src/labs/lab-slice.ts | 183 ++++++++++++++++++++++++++++ src/labs/requests/NewLabRequest.tsx | 41 +++---- src/store/index.ts | 2 + 4 files changed, 254 insertions(+), 95 deletions(-) create mode 100644 src/labs/lab-slice.ts diff --git a/src/labs/ViewLab.tsx b/src/labs/ViewLab.tsx index b8b217fef6..b4d8e2d52e 100644 --- a/src/labs/ViewLab.tsx +++ b/src/labs/ViewLab.tsx @@ -1,18 +1,17 @@ import React, { useEffect, useState } from 'react' import { useParams, useHistory } from 'react-router' import format from 'date-fns/format' -import LabRepository from 'clients/db/LabRepository' import Lab from 'model/Lab' import Patient from 'model/Patient' -import PatientRepository from 'clients/db/PatientRepository' import useTitle from 'page-header/useTitle' import { useTranslation } from 'react-i18next' import { Row, Column, Badge, Button, Alert } from '@hospitalrun/components' import TextFieldWithLabelFormGroup from 'components/input/TextFieldWithLabelFormGroup' import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' -import { useSelector } from 'react-redux' +import { useSelector, useDispatch } from 'react-redux' import Permissions from 'model/Permissions' import { RootState } from '../store' +import { cancelLab, completeLab, updateLab, fetchLab } from './lab-slice' const getTitle = (patient: Patient | undefined, lab: Lab | undefined) => patient && lab ? `${lab.type} for ${patient.fullName}` : '' @@ -21,94 +20,82 @@ const ViewLab = () => { const { id } = useParams() const { t } = useTranslation() const history = useHistory() + const dispatch = useDispatch() const { permissions } = useSelector((state: RootState) => state.user) - const [patient, setPatient] = useState() - const [lab, setLab] = useState() + const { lab, patient, status, error } = useSelector((state: RootState) => state.lab) + + const [labToView, setLabToView] = useState() const [isEditable, setIsEditable] = useState(true) - const [isResultInvalid, setIsResultInvalid] = useState(false) - const [resultFeedback, setResultFeedback] = useState() - const [errorMessage, setErrorMessage] = useState() - useTitle(getTitle(patient, lab)) + useTitle(getTitle(patient, labToView)) const breadcrumbs = [ { i18nKey: 'labs.requests.view', - location: `/labs/${lab?.id}`, + location: `/labs/${labToView?.id}`, }, ] useAddBreadcrumbs(breadcrumbs) useEffect(() => { - const fetchLab = async () => { - if (id) { - const fetchedLab = await LabRepository.find(id) - setLab(fetchedLab) - setIsEditable(fetchedLab.status === 'requested') - } + if (id) { + dispatch(fetchLab(id)) } - fetchLab() - }, [id]) + }, [id, dispatch]) useEffect(() => { - const fetchPatient = async () => { - if (lab) { - const fetchedPatient = await PatientRepository.find(lab.patientId) - setPatient(fetchedPatient) - } + if (lab) { + setLabToView({ ...lab }) + setIsEditable(lab.status === 'requested') } - - fetchPatient() + console.log('lab change') + console.log(lab) }, [lab]) const onResultChange = (event: React.ChangeEvent) => { const result = event.currentTarget.value - const newLab = lab as Lab - setLab({ ...newLab, result }) + const newLab = labToView as Lab + setLabToView({ ...newLab, result }) } const onNotesChange = (event: React.ChangeEvent) => { const notes = event.currentTarget.value - const newLab = lab as Lab - setLab({ ...newLab, notes }) + const newLab = labToView as Lab + setLabToView({ ...newLab, notes }) } const onUpdate = async () => { - await LabRepository.saveOrUpdate(lab as Lab) - history.push('/labs') + const onSuccess = () => { + history.push('/labs') + } + if (labToView) { + dispatch(updateLab(labToView, onSuccess)) + } } const onComplete = async () => { - const newLab = lab as Lab - - if (!newLab.result) { - setIsResultInvalid(true) - setResultFeedback(t('labs.requests.error.resultRequiredToComplete')) - setErrorMessage(t('labs.requests.error.unableToComplete')) - return + const onSuccess = () => { + history.push('/labs') } - await LabRepository.saveOrUpdate({ - ...newLab, - completedOn: new Date(Date.now().valueOf()).toISOString(), - status: 'completed', - }) - history.push('/labs') + if (labToView) { + dispatch(completeLab(labToView, onSuccess)) + } } const onCancel = async () => { - const newLab = lab as Lab - await LabRepository.saveOrUpdate({ - ...newLab, - canceledOn: new Date(Date.now().valueOf()).toISOString(), - status: 'canceled', - }) - history.push('/labs') + const onSuccess = () => { + history.push('/labs') + } + + if (labToView) { + dispatch(cancelLab(labToView, onSuccess)) + } } const getButtons = () => { const buttons: React.ReactNode[] = [] - if (lab?.status === 'completed' || lab?.status === 'canceled') { + if (labToView?.status === 'completed' || labToView?.status === 'canceled') { return buttons } @@ -137,34 +124,34 @@ const ViewLab = () => { return buttons } - if (lab && patient) { + if (labToView && patient) { const getBadgeColor = () => { - if (lab.status === 'completed') { + if (labToView.status === 'completed') { return 'primary' } - if (lab.status === 'canceled') { + if (labToView.status === 'canceled') { return 'danger' } return 'warning' } const getCanceledOnOrCompletedOnDate = () => { - if (lab.status === 'completed' && lab.completedOn) { + if (labToView.status === 'completed' && labToView.completedOn) { return (

{t('labs.lab.completedOn')}

-
{format(new Date(lab.completedOn), 'yyyy-MM-dd hh:mm a')}
+
{format(new Date(labToView.completedOn), 'yyyy-MM-dd hh:mm a')}
) } - if (lab.status === 'canceled' && lab.canceledOn) { + if (labToView.status === 'canceled' && labToView.canceledOn) { return (

{t('labs.lab.canceledOn')}

-
{format(new Date(lab.canceledOn), 'yyyy-MM-dd hh:mm a')}
+
{format(new Date(labToView.canceledOn), 'yyyy-MM-dd hh:mm a')}
) @@ -174,15 +161,15 @@ const ViewLab = () => { return ( <> - {isResultInvalid && ( - + {status === 'error' && ( + )}

{t('labs.lab.status')}

-
{lab.status}
+
{labToView.status}
@@ -195,13 +182,13 @@ const ViewLab = () => {

{t('labs.lab.type')}

-
{lab.type}
+
{labToView.type}

{t('labs.lab.requestedOn')}

-
{format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')}
+
{format(new Date(labToView.requestedOn), 'yyyy-MM-dd hh:mm a')}
{getCanceledOnOrCompletedOnDate()} @@ -211,16 +198,16 @@ const ViewLab = () => { diff --git a/src/labs/lab-slice.ts b/src/labs/lab-slice.ts new file mode 100644 index 0000000000..5e4b26be3f --- /dev/null +++ b/src/labs/lab-slice.ts @@ -0,0 +1,183 @@ +import Lab from 'model/Lab' +import Patient from 'model/Patient' +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { AppThunk } from 'store' +import LabRepository from 'clients/db/LabRepository' +import PatientRepository from 'clients/db/PatientRepository' + +interface Error { + result?: string + patient?: string + type?: string + message?: string +} + +interface LabState { + error: Error + lab?: Lab + patient?: Patient + status: 'loading' | 'error' | 'success' +} + +const initialState: LabState = { + error: {}, + lab: undefined, + patient: undefined, + status: 'loading', +} + +function start(state: LabState) { + state.status = 'loading' +} + +function finish(state: LabState, { payload }: PayloadAction) { + state.status = 'success' + state.lab = payload + state.error = {} +} + +function error(state: LabState, { payload }: PayloadAction) { + state.status = 'error' + state.error = payload +} + +const labSlice = createSlice({ + name: 'lab', + initialState, + reducers: { + fetchLabStart: start, + fetchLabSuccess: ( + state: LabState, + { payload }: PayloadAction<{ lab: Lab; patient: Patient }>, + ) => { + state.status = 'success' + state.lab = payload.lab + state.patient = payload.patient + }, + updateLabStart: start, + updateLabSuccess: finish, + requestLabStart: start, + requestLabSuccess: finish, + requestLabError: error, + cancelLabStart: start, + cancelLabSuccess: finish, + completeLabStart: start, + completeLabSuccess: finish, + completeLabError: error, + }, +}) + +export const { + fetchLabStart, + fetchLabSuccess, + updateLabStart, + updateLabSuccess, + requestLabStart, + requestLabSuccess, + requestLabError, + cancelLabStart, + cancelLabSuccess, + completeLabStart, + completeLabSuccess, + completeLabError, +} = labSlice.actions + +export const fetchLab = (labId: string): AppThunk => async (dispatch) => { + dispatch(fetchLabStart()) + const fetchedLab = await LabRepository.find(labId) + const fetchedPatient = await PatientRepository.find(fetchedLab.patientId) + dispatch(fetchLabSuccess({ lab: fetchedLab, patient: fetchedPatient })) +} + +const validateLabRequest = (newLab: Lab): Error => { + const labRequestError: Error = {} + if (!newLab.patientId) { + labRequestError.patient = 'labs.requests.error.patientRequired' + } + + if (!newLab.type) { + labRequestError.type = 'labs.requests.error.typeRequired' + } + + return labRequestError +} + +export const requestLab = (newLab: Lab, onSuccess?: (lab: Lab) => void): AppThunk => async ( + dispatch, +) => { + dispatch(requestLabStart()) + + const labRequestError = validateLabRequest(newLab) + if (Object.keys(labRequestError).length > 0) { + labRequestError.message = 'labs.requests.error.unableToRequest' + dispatch(requestLabError(labRequestError)) + } else { + newLab.requestedOn = new Date(Date.now().valueOf()).toISOString() + const requestedLab = await LabRepository.saveOrUpdate(newLab) + dispatch(requestLabSuccess(requestedLab)) + + if (onSuccess) { + onSuccess(newLab) + } + } +} + +export const cancelLab = (labToCancel: Lab, onSuccess?: (lab: Lab) => void): AppThunk => async ( + dispatch, +) => { + dispatch(cancelLabStart()) + + labToCancel.canceledOn = new Date(Date.now().valueOf()).toISOString() + labToCancel.status = 'canceled' + const canceledLab = await LabRepository.saveOrUpdate(labToCancel) + dispatch(cancelLabSuccess(canceledLab)) + + if (onSuccess) { + onSuccess(canceledLab) + } +} + +const validateCompleteLab = (labToComplete: Lab): Error => { + const completeError: Error = {} + + if (!labToComplete.result) { + completeError.result = 'labs.requests.error.resultRequiredToComplete' + } + + return completeError +} + +export const completeLab = (labToComplete: Lab, onSuccess?: (lab: Lab) => void): AppThunk => async ( + dispatch, +) => { + dispatch(cancelLabStart()) + + const completeLabErrors = validateCompleteLab(labToComplete) + if (Object.keys(completeLabErrors).length > 0) { + completeLabErrors.message = 'labs.requests.error.unableToComplete' + dispatch(completeLabError(completeLabErrors)) + } else { + labToComplete.completedOn = new Date(Date.now().valueOf()).toISOString() + labToComplete.status = 'completed' + const completedLab = await LabRepository.saveOrUpdate(labToComplete) + dispatch(cancelLabSuccess(completedLab)) + + if (onSuccess) { + onSuccess(completedLab) + } + } +} + +export const updateLab = (labToUpdate: Lab, onSuccess?: (lab: Lab) => void): AppThunk => async ( + dispatch, +) => { + dispatch(updateLabStart()) + const updatedLab = await LabRepository.saveOrUpdate(labToUpdate) + dispatch(updateLabSuccess(updatedLab)) + + if (onSuccess) { + onSuccess(updatedLab) + } +} + +export default labSlice.reducer diff --git a/src/labs/requests/NewLabRequest.tsx b/src/labs/requests/NewLabRequest.tsx index affd974c73..5f9f03fe5a 100644 --- a/src/labs/requests/NewLabRequest.tsx +++ b/src/labs/requests/NewLabRequest.tsx @@ -6,19 +6,19 @@ import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' import TextInputWithLabelFormGroup from 'components/input/TextInputWithLabelFormGroup' import { useHistory } from 'react-router' -import LabRepository from 'clients/db/LabRepository' import Lab from 'model/Lab' import TextFieldWithLabelFormGroup from 'components/input/TextFieldWithLabelFormGroup' import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' +import { useDispatch, useSelector } from 'react-redux' +import { requestLab } from 'labs/lab-slice' +import { RootState } from 'store' const NewLabRequest = () => { const { t } = useTranslation() + const dispatch = useDispatch() const history = useHistory() useTitle(t('labs.requests.new')) - - const [isPatientInvalid, setIsPatientInvalid] = useState(false) - const [isTypeInvalid, setIsTypeInvalid] = useState(false) - const [typeFeedback, setTypeFeedback] = useState() + const { status, error } = useSelector((state: RootState) => state.lab) const [newLabRequest, setNewLabRequest] = useState({ patientId: '', @@ -60,34 +60,21 @@ const NewLabRequest = () => { const onSave = async () => { const newLab = newLabRequest as Lab - - if (!newLab.patientId) { - setIsPatientInvalid(true) - return - } - - if (!newLab.type) { - setIsTypeInvalid(true) - setTypeFeedback(t('labs.requests.error.typeRequired')) - return + const onSuccess = (createdLab: Lab) => { + history.push(`/labs/${createdLab.id}`) } - newLab.requestedOn = new Date(Date.now().valueOf()).toISOString() - const createdLab = await LabRepository.save(newLab) - history.push(`/labs/${createdLab.id}`) + dispatch(requestLab(newLab, onSuccess)) } + const onCancel = () => { history.push('/labs') } return ( <> - {(isTypeInvalid || isPatientInvalid) && ( - + {status === 'error' && ( + )}
@@ -99,7 +86,7 @@ const NewLabRequest = () => { onSearch={async (query: string) => PatientRepository.search(query)} searchAccessor="fullName" renderMenuItemChildren={(p: Patient) =>
{`${p.fullName} (${p.code})`}
} - isInvalid={isPatientInvalid} + isInvalid={!!error.patient} />
{ label={t('labs.lab.type')} isRequired isEditable - isInvalid={isTypeInvalid} - feedback={typeFeedback} + isInvalid={!!error.type} + feedback={t(error.type || '')} value={newLabRequest.type} onChange={onLabTypeChange} /> diff --git a/src/store/index.ts b/src/store/index.ts index c381425dec..77ec4087cc 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -6,6 +6,7 @@ import appointment from '../scheduling/appointments/appointment-slice' 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 breadcrumbs from '../breadcrumbs/breadcrumbs-slice' import components from '../components/component-slice' @@ -18,6 +19,7 @@ const reducer = combineReducers({ appointments, breadcrumbs, components, + lab, }) const store = configureStore({