Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) O3-4084: Implement numeric validation for lab results entry form #2061

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import React from 'react';
import { NumberInput, Select, SelectItem, TextInput } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { type Control, Controller } from 'react-hook-form';
import { type Control, Controller, type FieldErrors } from 'react-hook-form';
import { type LabOrderConcept } from './lab-results.resource';
import styles from './lab-results-form.scss';

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

const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, defaultValue }) => {
const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, defaultValue, errors }) => {
const { t } = useTranslation();

const isCoded = (concept: LabOrderConcept) => concept.datatype?.display === 'Coded';
Expand Down Expand Up @@ -42,12 +43,14 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def
name={concept.uuid}
render={({ field }) => (
<TextInput
{...field}
className={styles.textInput}
id={concept.uuid}
key={concept.uuid}
labelText={concept?.display ?? ''}
type="text"
{...field}
invalidText={errors[concept.uuid]?.message}
invalid={!!errors[concept.uuid]}
/>
)}
/>
Expand All @@ -66,8 +69,10 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def
id={concept.uuid}
key={concept.uuid}
label={concept?.display + printValueRange(concept)}
onChange={(event) => field.onChange(event.target.value)}
onChange={(event) => field.onChange(parseFloat(event.target.value))}
value={field.value || ''}
invalidText={errors[concept.uuid]?.message}
invalid={!!errors[concept.uuid]}
/>
)}
/>
Expand All @@ -85,6 +90,8 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def
id={`select-${concept.uuid}`}
key={concept.uuid}
labelText={concept?.display}
invalidText={errors[concept.uuid]?.message}
invalid={!!errors[concept.uuid]}
>
<SelectItem text={t('chooseAnOption', 'Choose an option')} value="" />
{concept?.answers?.length &&
Expand Down Expand Up @@ -113,6 +120,8 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def
key={member.uuid}
labelText={member?.display ?? ''}
type="text"
invalidText={errors[member.uuid]?.message}
invalid={!!errors[member.uuid]}
/>
)}
/>
Expand All @@ -130,8 +139,10 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def
id={`number-${member.uuid}`}
key={member.uuid}
label={member?.display + printValueRange(member)}
onChange={(event) => field.onChange(event.target.value)}
onChange={(event) => field.onChange(parseFloat(event.target.value))}
value={field.value || ''}
invalidText={errors[member.uuid]?.message}
invalid={!!errors[member.uuid]}
/>
)}
/>
Expand All @@ -149,6 +160,8 @@ const ResultFormField: React.FC<ResultFormFieldProps> = ({ concept, control, def
key={member.uuid}
labelText={member?.display}
type="text"
invalidText={errors[member.uuid]?.message}
invalid={!!errors[member.uuid]}
>
<SelectItem text={t('chooseAnOption', 'Choose an option')} value="" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { restBaseUrl, showSnackbar, useAbortController, useLayoutType } from '@o
import { useOrderConceptByUuid, updateOrderResult, useLabEncounter, useObservation } from './lab-results.resource';
import ResultFormField from './lab-results-form-field.component';
import styles from './lab-results-form.scss';
import { useLabResultsFormSchema } from './useLabResultsFormSchema';
import { zodResolver } from '@hookform/resolvers/zod';

export interface LabResultsFormProps extends DefaultPatientWorkspaceProps {
order: Order;
Expand All @@ -30,19 +32,22 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
const { encounter, isLoading: isLoadingEncounter, mutate: mutateLabOrders } = useLabEncounter(order.encounter.uuid);
const { data, isLoading: isLoadingObs, error: isErrorObs } = useObservation(obsUuid);
const [showEmptyFormErrorNotification, setShowEmptyFormErrorNotification] = useState(false);
const schema = useLabResultsFormSchema(order.concept.uuid);

const {
control,
register,
formState: { errors, isDirty, isSubmitting },
getValues,
handleSubmit,
setError,
} = useForm<{ testResult: any }>({
defaultValues: {},
resolver: zodResolver(schema),
mode: 'all',
});

useEffect(() => {
if (!isLoadingEncounter && encounter?.obs.length > 0 && !isEditing) {
if (!isLoadingEncounter && encounter?.obs?.length > 0 && !isEditing) {
const obs = encounter.obs.find((obs) => obs.concept?.uuid === order?.concept.uuid);
if (obs) {
setObsUuid(obs.uuid);
Expand Down Expand Up @@ -82,7 +87,7 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
);
}

const saveLabResults = () => {
const saveLabResults = async (formData) => {
const formValues = getValues();

const isEmptyForm = Object.values(formValues).every(
Expand Down Expand Up @@ -158,50 +163,53 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
fulfillerComment: 'Test Results Entered',
};

updateOrderResult(
order.uuid,
order.encounter.uuid,
obsPayload,
resultsStatusPayload,
orderDiscontinuationPayload,
abortController,
).then(
() => {
closeWorkspaceWithSavedChanges();
mutateLabOrders();
mutate(
(key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?patient=${order.patient.uuid}`),
undefined,
{ revalidate: true },
);
showSnackbar({
title: t('saveLabResults', 'Save lab results'),
kind: 'success',
subtitle: t('successfullySavedLabResults', 'Lab results for {{orderNumber}} have been successfully updated', {
orderNumber: order?.orderNumber,
}),
});
},
(err) => {
showSnackbar({
title: t('errorSavingLabResults', 'Error saving lab results'),
kind: 'error',
subtitle: err?.message,
});
},
);

setShowEmptyFormErrorNotification(false);
try {
await updateOrderResult(
order.uuid,
order.encounter.uuid,
obsPayload,
resultsStatusPayload,
orderDiscontinuationPayload,
abortController,
);

closeWorkspaceWithSavedChanges();
mutateLabOrders();
mutate(
(key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?patient=${order.patient.uuid}`),
undefined,
{ revalidate: true },
);
donaldkibet marked this conversation as resolved.
Show resolved Hide resolved
showSnackbar({
title: t('saveLabResults', 'Save lab results'),
kind: 'success',
subtitle: t('successfullySavedLabResults', 'Lab results for {{orderNumber}} have been successfully updated', {
orderNumber: order?.orderNumber,
}),
});
} catch (err) {
showSnackbar({
title: t('errorSavingLabResults', 'Error saving lab results'),
kind: 'error',
subtitle: err?.message,
});
setError('root', {
type: 'manual',
message: err?.message || t('unknownError', 'An unknown error occurred'),
});
} finally {
setShowEmptyFormErrorNotification(false);
}
};

return (
<Form className={styles.form}>
<Form className={styles.form} onSubmit={handleSubmit(saveLabResults)}>
<div className={styles.grid}>
{concept.setMembers.length > 0 && <p className={styles.heading}>{concept.display}</p>}
{concept && (
<Stack gap={5}>
{!isLoadingInitialValues ? (
<ResultFormField defaultValue={initialValues} concept={concept} control={control} />
<ResultFormField defaultValue={initialValues} concept={concept} control={control} errors={errors} />
) : (
<InlineLoading description={t('loadingInitialValues', 'Loading initial values') + '...'} />
)}
Expand All @@ -220,13 +228,7 @@ const LabResultsForm: React.FC<LabResultsFormProps> = ({
<Button className={styles.button} kind="secondary" disabled={isSubmitting} onClick={closeWorkspace}>
{t('discard', 'Discard')}
</Button>
<Button
className={styles.button}
kind="primary"
onClick={handleSubmit(saveLabResults)}
disabled={isSubmitting}
type="submit"
>
<Button className={styles.button} kind="primary" disabled={isSubmitting} type="submit">
{isSubmitting ? (
<InlineLoading description={t('saving', 'Saving') + '...'} />
) : (
Expand Down
Loading
Loading