diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 5565877a09..e4bc36dd63 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -100,7 +100,7 @@ const HospitalRun = () => { path="/appointments/:id" component={ViewAppointment} /> - + diff --git a/src/clients/db/LabRepository.ts b/src/clients/db/LabRepository.ts new file mode 100644 index 0000000000..c1b292be93 --- /dev/null +++ b/src/clients/db/LabRepository.ts @@ -0,0 +1,11 @@ +import Lab from 'model/Lab' +import Repository from './Repository' +import { labs } from '../../config/pouchdb' + +export class LabRepository extends Repository { + constructor() { + super(labs) + } +} + +export default new LabRepository() diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index d822410920..c935d88273 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -70,6 +70,27 @@ const Navbar = () => { }, ], }, + { + type: 'link-list', + label: t('labs.label'), + className: 'labs-link-list', + children: [ + { + type: 'link', + label: t('labs.label'), + onClick: () => { + history.push('/labs') + }, + }, + { + type: 'link', + label: t('labs.requests.new'), + onClick: () => { + history.push('/labs/new') + }, + }, + ], + }, { type: 'search', placeholderText: t('actions.search'), diff --git a/src/config/pouchdb.ts b/src/config/pouchdb.ts index dcf9a4311f..a4b0a13bae 100644 --- a/src/config/pouchdb.ts +++ b/src/config/pouchdb.ts @@ -28,3 +28,4 @@ function createDb(name: string) { export const patients = createDb('patients') export const appointments = createDb('appointments') +export const labs = createDb('labs') diff --git a/src/labs/Labs.tsx b/src/labs/Labs.tsx index 2179c527b4..a610af4052 100644 --- a/src/labs/Labs.tsx +++ b/src/labs/Labs.tsx @@ -1,12 +1,46 @@ import React from 'react' import PrivateRoute from 'components/PrivateRoute' import { Switch } from 'react-router' -import LabRequests from './requests/LabRequests' +import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' +import { useSelector } from 'react-redux' +import Permissions from 'model/Permissions' +import LabRequests from './ViewLabs' +import NewLabRequest from './requests/NewLabRequest' +import ViewLab from './ViewLab' +import { RootState } from '../store' -const Labs = () => ( - - - -) +const Labs = () => { + const { permissions } = useSelector((state: RootState) => state.user) + const breadcrumbs = [ + { + i18nKey: 'labs.label', + location: `/labs`, + }, + ] + useAddBreadcrumbs(breadcrumbs, true) + + return ( + + + + + + ) +} export default Labs diff --git a/src/labs/ViewLab.tsx b/src/labs/ViewLab.tsx new file mode 100644 index 0000000000..c135e9dbae --- /dev/null +++ b/src/labs/ViewLab.tsx @@ -0,0 +1,241 @@ +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 { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' +import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' +import { useSelector } from 'react-redux' +import Permissions from 'model/Permissions' +import { RootState } from '../store' + +const getTitle = (patient: Patient | undefined, lab: Lab | undefined) => + patient && lab ? `${lab.type} for ${patient.fullName}` : '' + +const ViewLab = () => { + const { id } = useParams() + const { t } = useTranslation() + const history = useHistory() + const setButtons = useButtonToolbarSetter() + + const { permissions } = useSelector((state: RootState) => state.user) + const [patient, setPatient] = useState() + const [lab, setLab] = useState() + const [isEditable, setIsEditable] = useState(true) + const [isResultInvalid, setIsResultInvalid] = useState(false) + const [resultFeedback, setResultFeedback] = useState() + const [errorMessage, setErrorMessage] = useState() + + useTitle(getTitle(patient, lab)) + + const breadcrumbs = [ + { + i18nKey: 'labs.requests.view', + location: `/labs/${lab?.id}`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + useEffect(() => { + const fetchLab = async () => { + if (id) { + const fetchedLab = await LabRepository.find(id) + setLab(fetchedLab) + setIsEditable(fetchedLab.status === 'requested') + } + } + fetchLab() + }, [id]) + + useEffect(() => { + const fetchPatient = async () => { + if (lab) { + const fetchedPatient = await PatientRepository.find(lab.patientId) + setPatient(fetchedPatient) + } + } + + fetchPatient() + }, [lab]) + + const onResultChange = (event: React.ChangeEvent) => { + const result = event.currentTarget.value + const newLab = lab as Lab + setLab({ ...newLab, result }) + } + + const onNotesChange = (event: React.ChangeEvent) => { + const notes = event.currentTarget.value + const newLab = lab as Lab + setLab({ ...newLab, notes }) + } + + const onUpdate = async () => { + await LabRepository.saveOrUpdate(lab as Lab) + history.push('/labs') + } + + 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 + } + + await LabRepository.saveOrUpdate({ + ...newLab, + completedOn: new Date().toISOString(), + status: 'completed', + }) + history.push('/labs') + } + + const onCancel = async () => { + const newLab = lab as Lab + await LabRepository.saveOrUpdate({ + ...newLab, + canceledOn: new Date().toISOString(), + status: 'canceled', + }) + history.push('/labs') + } + + const getButtons = () => { + const buttons: React.ReactNode[] = [] + if (lab?.status === 'completed' || lab?.status === 'canceled') { + return buttons + } + + if (permissions.includes(Permissions.CompleteLab)) { + buttons.push( + , + ) + } + + if (permissions.includes(Permissions.CancelLab)) { + buttons.push( + , + ) + } + + return buttons + } + + setButtons(getButtons()) + + if (lab && patient) { + const getBadgeColor = () => { + if (lab.status === 'completed') { + return 'primary' + } + if (lab.status === 'canceled') { + return 'danger' + } + return 'warning' + } + + const getCanceledOnOrCompletedOnDate = () => { + if (lab.status === 'completed') { + return ( + +
+

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

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

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

+
{format(new Date(lab.completedOn), 'yyyy-MM-dd hh:mm a')}
+
+
+ ) + } + return <> + } + + return ( + <> + {isResultInvalid && ( + + )} + + +
+

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

+ +
{lab.status}
+
+
+
+ +
+

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

+
{patient.fullName}
+
+
+ +
+

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

+
{lab.type}
+
+
+ +
+

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

+
{format(new Date(lab.requestedOn), 'yyyy/mm/dd hh:mm a')}
+
+
+ {getCanceledOnOrCompletedOnDate()} +
+
+
+ + + {isEditable && ( +
+
+ +
+
+ )} + + + ) + } + return

Loading...

+} + +export default ViewLab diff --git a/src/labs/ViewLabs.tsx b/src/labs/ViewLabs.tsx new file mode 100644 index 0000000000..a74ebedcd8 --- /dev/null +++ b/src/labs/ViewLabs.tsx @@ -0,0 +1,76 @@ +import React, { useState, useEffect } from 'react' +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 { useHistory } from 'react-router' +import LabRepository from 'clients/db/LabRepository' +import Lab from 'model/Lab' +import { useSelector } from 'react-redux' +import Permissions from 'model/Permissions' +import { RootState } from '../store' + +const ViewLabs = () => { + const { t } = useTranslation() + const history = useHistory() + const setButtons = useButtonToolbarSetter() + useTitle(t('labs.label')) + + const { permissions } = useSelector((state: RootState) => state.user) + const [labs, setLabs] = useState([]) + + const getButtons = () => { + const buttons: React.ReactNode[] = [] + + if (permissions.includes(Permissions.RequestLab)) { + buttons.push( + , + ) + } + + return buttons + } + + setButtons([getButtons()]) + + useEffect(() => { + const fetch = async () => { + const fetchedLabs = await LabRepository.findAll() + setLabs(fetchedLabs) + } + + fetch() + }, []) + + const onTableRowClick = (lab: Lab) => { + history.push(`/labs/${lab.id}`) + } + + return ( + <> + + + + + + + + + + {labs.map((lab) => ( + onTableRowClick(lab)}> + + + + + ))} + +
Lab TypeRequested OnStatus
{lab.type}{format(new Date(lab.requestedOn), 'yyyy-MM-dd hh:mm a')}{lab.status}
+ + ) +} + +export default ViewLabs diff --git a/src/labs/requests/LabRequests.tsx b/src/labs/requests/LabRequests.tsx deleted file mode 100644 index fc770928bd..0000000000 --- a/src/labs/requests/LabRequests.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import useTitle from 'page-header/useTitle' -import { useTranslation } from 'react-i18next' - -const LabRequests = () => { - const { t } = useTranslation() - useTitle(t('labs.requests.label')) - - return

Lab Requests

-} - -export default LabRequests diff --git a/src/labs/requests/NewLabRequest.tsx b/src/labs/requests/NewLabRequest.tsx new file mode 100644 index 0000000000..27e96f2db4 --- /dev/null +++ b/src/labs/requests/NewLabRequest.tsx @@ -0,0 +1,137 @@ +import React, { useState } from 'react' +import useTitle from 'page-header/useTitle' +import { useTranslation } from 'react-i18next' +import { Typeahead, Label, Button, Alert } from '@hospitalrun/components' +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' + +const NewLabRequest = () => { + const { t } = useTranslation() + const history = useHistory() + useTitle(t('labs.requests.new')) + + const [isPatientInvalid, setIsPatientInvalid] = useState(false) + const [isTypeInvalid, setIsTypeInvalid] = useState(false) + const [typeFeedback, setTypeFeedback] = useState() + + const [newLabRequest, setNewLabRequest] = useState({ + patientId: '', + type: '', + notes: '', + status: 'requested', + }) + + const breadcrumbs = [ + { + i18nKey: 'labs.requests.new', + location: `/labs/new`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + const onPatientChange = (patient: Patient) => { + setNewLabRequest((previousNewLabRequest) => ({ + ...previousNewLabRequest, + patientId: patient.id, + })) + } + + const onLabTypeChange = (event: React.ChangeEvent) => { + const type = event.currentTarget.value + setNewLabRequest((previousNewLabRequest) => ({ + ...previousNewLabRequest, + type, + })) + } + + const onNoteChange = (event: React.ChangeEvent) => { + const notes = event.currentTarget.value + setNewLabRequest((previousNewLabRequest) => ({ + ...previousNewLabRequest, + notes, + })) + } + + const onSave = async () => { + const newLab = newLabRequest as Lab + + if (!newLab.patientId) { + setIsPatientInvalid(true) + } + + if (!newLab.type) { + setIsTypeInvalid(true) + setTypeFeedback(t('labs.requests.error.typeRequired')) + return + } + + newLab.requestedOn = new Date().toISOString() + const createdLab = await LabRepository.save(newLab) + history.push(`/labs/${createdLab.id}`) + } + const onCancel = () => { + history.push('/labs') + } + + return ( + <> + {(isTypeInvalid || isPatientInvalid) && ( + + )} +
+
+
+ +
+ +
+
+
+ + +
+
+ + + ) +} + +export default NewLabRequest diff --git a/src/locales/enUs/translations/actions/index.ts b/src/locales/enUs/translations/actions/index.ts index be640fd1aa..3e4f94a0b6 100644 --- a/src/locales/enUs/translations/actions/index.ts +++ b/src/locales/enUs/translations/actions/index.ts @@ -2,11 +2,13 @@ export default { actions: { edit: 'Edit', save: 'Save', + update: 'Update', + complete: 'Complete', + delete: 'Delete', cancel: 'Cancel', new: 'New', list: 'List', search: 'Search', - delete: 'Delete', confirmDelete: 'Delete Confirmation', }, } diff --git a/src/locales/enUs/translations/labs/index.ts b/src/locales/enUs/translations/labs/index.ts index 90dc4342e4..2f9402905b 100644 --- a/src/locales/enUs/translations/labs/index.ts +++ b/src/locales/enUs/translations/labs/index.ts @@ -3,6 +3,26 @@ export default { label: 'Labs', requests: { label: 'Lab Requests', + new: 'New Lab Request', + view: 'View Lab', + cancel: 'Cancel Lab', + complete: 'Complete Lab', + error: { + unableToRequest: 'Unable to create new lab request.', + unableToComplete: 'Unable to complete lab request.', + typeRequired: 'Type is required.', + resultRequiredToComplete: 'Result is required to complete.', + }, + }, + lab: { + status: 'Status', + for: 'For', + type: 'Type', + result: 'Result', + notes: 'Notes', + requestedOn: 'Requested On', + completedOn: 'Completed On', + canceledOn: 'Canceled On', }, }, } diff --git a/src/model/Lab.ts b/src/model/Lab.ts new file mode 100644 index 0000000000..88d9ca43ad --- /dev/null +++ b/src/model/Lab.ts @@ -0,0 +1,12 @@ +import AbstractDBModel from './AbstractDBModel' + +export default interface Lab extends AbstractDBModel { + patientId: string + type: string + notes: string + result: string + status: 'requested' | 'completed' | 'canceled' + requestedOn: string + completedOn: string + canceledOn: string +} diff --git a/src/model/Permissions.ts b/src/model/Permissions.ts index 90884d6037..170fb173ba 100644 --- a/src/model/Permissions.ts +++ b/src/model/Permissions.ts @@ -6,6 +6,11 @@ enum Permissions { DeleteAppointment = 'delete:appointment', AddAllergy = 'write:allergy', AddDiagnosis = 'write:diagnosis', + RequestLab = 'write:labs', + CancelLab = 'cancel:lab', + CompleteLab = 'complete:lab', + ViewLab = 'read:lab', + ViewLabs = 'read:labs', } export default Permissions diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index 81bae4cde1..98c163429c 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -14,6 +14,11 @@ const initialState: UserState = { Permissions.DeleteAppointment, Permissions.AddAllergy, Permissions.AddDiagnosis, + Permissions.ViewLabs, + Permissions.ViewLab, + Permissions.RequestLab, + Permissions.CompleteLab, + Permissions.CancelLab, ], }