Skip to content

Commit

Permalink
(feat) Lab results form improvements (#1985)
Browse files Browse the repository at this point in the history
This PR makes the following improvements to the lab results form following the recent change to make it the single source of truth for lab orders:

- Refactors the error notification shown when the user tries to submit the form without filling any fields to use the Carbon InlineNotification component
- Refactors the logic used to determine whether the form is empty to leverage the getValues method of react-hook-form
- Increases the Stack gap to 1rem to make the form more spacious
- Removes error handling concerns from the form field component
- Refactors the form field component to use the Carbon TextInput or NumberInput components based on the concept datatype
- Renames the result form field component to lab results form field component
  • Loading branch information
denniskigen authored Sep 2, 2024
1 parent 1eaec53 commit 686aece
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import React from 'react';
import { TextInput, Select, SelectItem } from '@carbon/react';
import { NumberInput, Select, SelectItem, TextInput } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { Controller } from 'react-hook-form';
import { type Control, Controller } from 'react-hook-form';
import { type LabOrderConcept } from './lab-results.resource';
import styles from './lab-results-form.scss';

interface ResultFormFieldProps {
defaultValue: any;
concept: LabOrderConcept;
control: any;
register: any;
errors?: any;
control: Control<any, any>;
defaultValue: any;
}

const ResultFormField: React.FC<ResultFormFieldProps> = ({ defaultValue, register, concept, control, errors }) => {
const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, defaultValue }) => {
const { t } = useTranslation();
const isTextOrNumeric = (concept) => concept.datatype?.display === 'Text' || concept.datatype?.display === 'Numeric';
const isCoded = (concept) => concept.datatype?.display === 'Coded';
const isPanel = (concept) => concept.setMembers?.length > 0;

const isCoded = (concept: LabOrderConcept) => concept.datatype?.display === 'Coded';
const isNumeric = (concept: LabOrderConcept) => concept.datatype?.display === 'Numeric';
const isPanel = (concept: LabOrderConcept) => concept.setMembers?.length > 0;
const isText = (concept: LabOrderConcept) => concept.datatype?.display === 'Text';

const printValueRange = (concept: LabOrderConcept) => {
if (concept?.datatype?.display === 'Numeric') {
const maxVal = Math.max(concept?.hiAbsolute, concept?.hiCritical, concept?.hiNormal);
const minVal = Math.min(concept?.lowAbsolute, concept?.lowCritical, concept?.lowNormal);
return `(${minVal ?? 0} - ${maxVal > 0 ? maxVal : '--'} ${concept?.units ?? ''})`;
return ` (${minVal ?? 0} - ${maxVal > 0 ? maxVal : '--'} ${concept?.units ?? ''})`;
}
return '';
};
Expand All @@ -36,25 +36,38 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ defaultValue, registe

return (
<>
{Object.keys(errors).length > 0 && <div className={styles.errorDiv}>All fields are required</div>}
{isTextOrNumeric(concept) && (
{isText(concept) && (
<Controller
name={concept.uuid}
control={control}
rules={{
required: true,
}}
name={concept.uuid}
render={({ field }) => (
<TextInput
key={concept.uuid}
className={styles.textInput}
id={concept.uuid}
key={concept.uuid}
labelText={concept?.display ?? ''}
type="text"
{...field}
type={concept.datatype.display === 'Numeric' ? 'number' : 'text'}
labelText={
concept?.display + (concept.datatype.display === 'Numeric' ? ` ${printValueRange(concept)}` ?? '' : '')
}
defaultValue={defaultValue?.value}
autoFocus
/>
)}
/>
)}

{isNumeric(concept) && (
<Controller
control={control}
name={concept.uuid}
render={({ field }) => (
<NumberInput
allowEmpty
className={styles.numberInput}
disableWheel
hideSteppers
id={concept.uuid}
key={concept.uuid}
label={concept?.display + printValueRange(concept)}
onChange={(event) => field.onChange(event.target.value)}
value={field.value || ''}
/>
)}
/>
Expand All @@ -64,18 +77,14 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ defaultValue, registe
<Controller
name={concept.uuid}
control={control}
rules={{
required: true,
}}
render={({ field }) => (
<Select
key={concept.uuid}
className={styles.textInput}
{...field}
invalidText={t('required', 'Required')}
labelText={concept?.display}
rules={{ required: true }}
className={styles.textInput}
defaultValue={defaultValue?.value?.uuid}
id={`select-${concept.uuid}`}
key={concept.uuid}
labelText={concept?.display}
>
<SelectItem text={t('chooseAnOption', 'Choose an option')} value="" />
{concept?.answers?.length &&
Expand All @@ -90,49 +99,56 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ defaultValue, registe
)}

{isPanel(concept) &&
concept.setMembers.map((member, index) => {
if (isTextOrNumeric(member)) {
return (
concept.setMembers.map((member) => (
<React.Fragment key={member.uuid}>
{isText(member) && (
<Controller
control={control}
name={member.uuid}
rules={{
required: true,
}}
render={({ field }) => (
<TextInput
key={member.uuid}
className={styles.textInput}
{...field}
type={member.datatype.display === 'Numeric' ? 'number' : 'text'}
labelText={
member?.display + (member.datatype.display === 'Numeric' ? printValueRange(member) ?? '' : '')
}
autoFocus={index === 0}
defaultValue={getSavedMemberValue(member.uuid, member.datatype.display)}
id={`text-${member.uuid}`}
className={styles.textInput}
key={member.uuid}
labelText={member?.display ?? ''}
type="text"
/>
)}
/>
);
}

if (isCoded(member)) {
return (
)}
{isNumeric(member) && (
<Controller
control={control}
name={member.uuid}
render={({ field }) => (
<NumberInput
allowEmpty
className={styles.numberInput}
disableWheel
hideSteppers
id={`number-${member.uuid}`}
key={member.uuid}
label={member?.display + printValueRange(member)}
onChange={(event) => field.onChange(event.target.value)}
value={field.value || ''}
/>
)}
/>
)}
{isCoded(member) && (
<Controller
name={member.uuid}
control={control}
rules={{
required: true,
}}
render={({ field }) => (
<Select
key={member.uuid}
className={styles.textInput}
{...field}
type="text"
labelText={member?.display}
autoFocus={index === 0}
className={styles.textInput}
defaultValue={getSavedMemberValue(member.uuid, member.datatype.display)}
id={`select-${member.uuid}`}
key={member.uuid}
labelText={member?.display}
type="text"
>
<SelectItem text={t('chooseAnOption', 'Choose an option')} value="" />

Expand All @@ -144,9 +160,9 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ defaultValue, registe
</Select>
)}
/>
);
}
})}
)}
</React.Fragment>
))}
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { restBaseUrl, showSnackbar, useAbortController, useLayoutType } from '@openmrs/esm-framework';
import { Button, ButtonSet, Form, InlineLoading, Stack } from '@carbon/react';
import { mutate } from 'swr';
import { Button, ButtonSet, Form, InlineLoading, InlineNotification, Stack } from '@carbon/react';
import { type DefaultPatientWorkspaceProps, type Order } from '@openmrs/esm-patient-common-lib';
import { restBaseUrl, showSnackbar, useAbortController, useLayoutType } from '@openmrs/esm-framework';
import { useOrderConceptByUuid, updateOrderResult, useLabEncounter, useObservation } from './lab-results.resource';
import ResultFormField from './result-form-field.component';
import ResultFormField from './lab-results-form-field.component';
import styles from './lab-results-form.scss';
import { mutate } from 'swr';

export interface LabResultsFormProps extends DefaultPatientWorkspaceProps {
order: Order;
}

const LabResultsForm: React.FC<LabResultsFormProps> = ({
order,
closeWorkspace,
closeWorkspaceWithSavedChanges,
order,
promptBeforeClosing,
}) => {
const { t } = useTranslation();
const abortController = useAbortController();
const isTablet = useLayoutType() === 'tablet';
const [obsUuid, setObsUuid] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [initialValues, setInitialValues] = useState(null);
const [isLoadingInitialValues, setIsLoadingInitialValues] = useState(false);
const { concept, isLoading: isLoadingConcepts } = useOrderConceptByUuid(order.concept.uuid);
const { encounter, isLoading: isLoadingEncounter, mutate: mutateLabOrders } = useLabEncounter(order.encounter.uuid);
const { data, isLoading: isLoadingObs, error: isErrorObs } = useObservation(obsUuid);
const [showEmptyFormErrorNotification, setShowEmptyFormErrorNotification] = useState(false);

const {
control,
register,
formState: { errors, isDirty },
formState: { errors, isDirty, isSubmitting },
getValues,
handleSubmit,
} = useForm<{ testResult: any }>({
Expand Down Expand Up @@ -82,11 +82,18 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
);
}

const saveLabResults = (data, e) => {
setIsSubmitting(true);
e.preventDefault();
// assign result to test order
const documentedValues = getValues();
const saveLabResults = () => {
const formValues = getValues();

const isEmptyForm = Object.values(formValues).every(
(value) => value === '' || value === null || value === undefined,
);

if (isEmptyForm) {
setShowEmptyFormErrorNotification(true);
return;
}

let obsValue = [];

if (concept.set && concept.setMembers.length > 0) {
Expand Down Expand Up @@ -160,7 +167,6 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
abortController,
).then(
() => {
setIsSubmitting(false);
closeWorkspaceWithSavedChanges();
mutateLabOrders();
mutate(
Expand All @@ -177,37 +183,39 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
});
},
(err) => {
setIsSubmitting(false);
showSnackbar({
title: t('errorSavingLabResults', 'Error saving lab results'),
kind: 'error',
subtitle: err?.message,
});
},
);

setShowEmptyFormErrorNotification(false);
};

return (
<Form className={styles.form}>
<div className={styles.grid}>
{concept.setMembers.length > 0 && <p className={styles.heading}>{concept.display}</p>}
{concept && (
<Stack gap={2}>
<Stack gap={5}>
{!isLoadingInitialValues ? (
<ResultFormField
defaultValue={initialValues}
register={register}
concept={concept}
control={control}
errors={errors}
/>
<ResultFormField defaultValue={initialValues} concept={concept} control={control} />
) : (
<InlineLoading description={t('loadingInitialValues', 'Loading initial values') + '...'} />
)}
</Stack>
)}
{showEmptyFormErrorNotification && (
<InlineNotification
className={styles.emptyFormError}
lowContrast
title={t('error', 'Error')}
subtitle={t('pleaseFillField', 'Please fill at least one field') + '.'}
/>
)}
</div>

<ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
<Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
{t('discard', 'Discard')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,7 @@
.spacer {
margin-top: layout.$spacing-05;
}

.emptyFormError {
margin: 0.625rem 0;
}
2 changes: 1 addition & 1 deletion packages/esm-patient-orders-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"orders": "Orders",
"Orders": "Orders",
"orderType": "Order type",
"pleaseFillField": "Please fill at least one field",
"pleaseFillRequiredFields": "Please fill all the required fields",
"print": "Print",
"printedBy": "Printed by",
Expand All @@ -55,7 +56,6 @@
"reasonForCancellation": "Reason for cancellation",
"reasonForCancellationRequired": "Reason for cancellation is required",
"refills": "Refills",
"required": "Required",
"result": "Result",
"saveAndClose": "Save and close",
"saveDrugOrderFailed": "Error ordering {{orderName}}",
Expand Down

0 comments on commit 686aece

Please sign in to comment.