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) Lab results form improvements #1985

Merged
merged 1 commit into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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,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