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-4430: Optional Indication field in the drug order form #2228

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
Expand Up @@ -65,12 +65,10 @@ const MedicationSummary: React.FC<MedicationSummaryProps> = ({ medications }) =>
)}
</p>
<p className={styles.bodyLong01}>
{medication?.order?.orderReasonNonCoded ? (
<span>
<span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
{medication?.order?.orderReasonNonCoded}
</span>
) : null}
<span>
<span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
{medication?.order?.orderReasonNonCoded ?? t('none', 'None')}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{medication?.order?.orderReasonNonCoded ?? t('none', 'None')}
{medication?.order?.orderReasonNonCoded ?? t('noIndication', 'No indication provided')}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, be careful when placing text in front of users indicating the absence of data.

</span>
{medication?.order?.quantity ? (
<span>
<span className={styles.label01}> &mdash; {t('quantity', 'Quantity').toUpperCase()}</span>{' '}
Expand Down
1 change: 1 addition & 0 deletions packages/esm-patient-chart-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"noMatchingCodedCausesOfDeath": "No matching coded causes of death",
"nonCodedCauseOfDeath": "Non-coded cause of death",
"nonCodedCauseOfDeathRequired": "Please enter the non-coded cause of death",
"none": "None",
"noObservationsFound": "No observations found",
"notes": "Notes",
"notes__lower": "notes",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,115 +60,125 @@ export interface DrugOrderFormProps {
promptBeforeClosing: (testFcn: () => boolean) => void;
}

const createMedicationOrderFormSchema = (requireOutpatientQuantity: boolean, t: TFunction) => {
const comboSchema = {
default: z.boolean().optional(),
value: z.string(),
valueCoded: z.string(),
};
function useCreateMedicationOrderFormSchema() {
const { t } = useTranslation();
const { requireOutpatientQuantity } = useRequireOutpatientQuantity();
const { isIndicationFieldOptional } = useConfig<ConfigObject>();

const baseSchemaFields = {
freeTextDosage: z.string().refine((value) => !!value, {
message: t('freeDosageErrorMessage', 'Add free dosage note'),
}),
dosage: z.number({
invalid_type_error: t('dosageRequiredErrorMessage', 'Dosage is required'),
}),
unit: z.object(
{ ...comboSchema },
{
invalid_type_error: t('selectUnitErrorMessage', 'Dose unit is required'),
},
),
route: z.object(
{ ...comboSchema },
{
invalid_type_error: t('selectRouteErrorMessage', 'Route is required'),
},
),
patientInstructions: z.string().nullable(),
asNeeded: z.boolean(),
asNeededCondition: z.string().nullable(),
duration: z.number().nullable(),
durationUnit: z.object({ ...comboSchema }).nullable(),
indication: z.string().refine((value) => value !== '', {
message: t('indicationErrorMessage', 'Indication is required'),
}),
startDate: z.date(),
frequency: z.object(
{ ...comboSchema },
{
invalid_type_error: t('selectFrequencyErrorMessage', 'Frequency is required'),
},
),
};
const schema = useMemo(() => {
const comboSchema = {
default: z.boolean().optional(),
value: z.string(),
valueCoded: z.string(),
};

const outpatientDrugOrderFields = {
pillsDispensed: z
.number()
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && (typeof value !== 'number' || value < 1)) {
return false;
}
return true;
},
const baseSchemaFields = {
freeTextDosage: z.string().refine((value) => !!value, {
message: t('freeDosageErrorMessage', 'Add free dosage note'),
}),
dosage: z.number({
invalid_type_error: t('dosageRequiredErrorMessage', 'Dosage is required'),
}),
unit: z.object(
{ ...comboSchema },
{
message: t('pillDispensedErrorMessage', 'Quantity to dispense is required'),
invalid_type_error: t('selectUnitErrorMessage', 'Dose unit is required'),
},
),
quantityUnits: z
.object(comboSchema)
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && !value) {
return false;
}
return true;
},
route: z.object(
{ ...comboSchema },
{
message: t('selectQuantityUnitsErrorMessage', 'Quantity unit is required'),
invalid_type_error: t('selectRouteErrorMessage', 'Route is required'),
},
),
numRefills: z
.number()
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && (typeof value !== 'number' || value < 0)) {
return false;
}
return true;
},
patientInstructions: z.string().nullable(),
asNeeded: z.boolean(),
asNeededCondition: z.string().nullable(),
duration: z.number().nullable(),
durationUnit: z.object({ ...comboSchema }).nullable(),
indication: !isIndicationFieldOptional
? z.string().refine((value) => value !== '', {
message: t('indicationErrorMessage', 'Indication is required'),
})
: z.string().nullish(),
startDate: z.date(),
frequency: z.object(
{ ...comboSchema },
{
message: t('numRefillsErrorMessage', 'Number of refills is required'),
invalid_type_error: t('selectFrequencyErrorMessage', 'Frequency is required'),
},
),
};

const nonFreeTextDosageSchema = z.object({
...baseSchemaFields,
...outpatientDrugOrderFields,
isFreeTextDosage: z.literal(false),
freeTextDosage: z.string().optional(),
});
};

const freeTextDosageSchema = z.object({
...baseSchemaFields,
...outpatientDrugOrderFields,
isFreeTextDosage: z.literal(true),
dosage: z.number().nullable(),
unit: z.object(comboSchema).nullable(),
route: z.object(comboSchema).nullable(),
frequency: z.object(comboSchema).nullable(),
});
const outpatientDrugOrderFields = {
pillsDispensed: z
.number()
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && (typeof value !== 'number' || value < 1)) {
return false;
}
return true;
},
{
message: t('pillDispensedErrorMessage', 'Quantity to dispense is required'),
},
),
quantityUnits: z
.object(comboSchema)
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && !value) {
return false;
}
return true;
},
{
message: t('selectQuantityUnitsErrorMessage', 'Quantity unit is required'),
},
),
numRefills: z
.number()
.nullable()
.refine(
(value) => {
if (requireOutpatientQuantity && (typeof value !== 'number' || value < 0)) {
return false;
}
return true;
},
{
message: t('numRefillsErrorMessage', 'Number of refills is required'),
},
),
};

return z.discriminatedUnion('isFreeTextDosage', [nonFreeTextDosageSchema, freeTextDosageSchema]);
};
const nonFreeTextDosageSchema = z.object({
...baseSchemaFields,
...outpatientDrugOrderFields,
isFreeTextDosage: z.literal(false),
freeTextDosage: z.string().optional(),
});

const freeTextDosageSchema = z.object({
...baseSchemaFields,
...outpatientDrugOrderFields,
isFreeTextDosage: z.literal(true),
dosage: z.number().nullable(),
unit: z.object(comboSchema).nullable(),
route: z.object(comboSchema).nullable(),
frequency: z.object(comboSchema).nullable(),
});

return z.discriminatedUnion('isFreeTextDosage', [nonFreeTextDosageSchema, freeTextDosageSchema]);
}, [isIndicationFieldOptional, requireOutpatientQuantity, t]);

return schema;
}

type MedicationOrderFormData = z.infer<ReturnType<typeof createMedicationOrderFormSchema>>;
type MedicationOrderFormData = z.infer<ReturnType<typeof useCreateMedicationOrderFormSchema>>;

function MedicationInfoHeader({
orderBasketItem,
Expand Down Expand Up @@ -221,18 +231,14 @@ export function DrugOrderForm({ initialOrderBasketItem, onSave, onCancel, prompt
const config = useConfig<ConfigObject>();
const isTablet = useLayoutType() === 'tablet';
const { orderConfigObject, error: errorFetchingOrderConfig } = useOrderConfig();
const { requireOutpatientQuantity } = useRequireOutpatientQuantity();

const defaultStartDate = useMemo(() => {
if (typeof initialOrderBasketItem?.startDate === 'string') parseDate(initialOrderBasketItem?.startDate);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (typeof initialOrderBasketItem?.startDate === 'string') parseDate(initialOrderBasketItem?.startDate);
if (typeof initialOrderBasketItem?.startDate === 'string') {
return parseDate(initialOrderBasketItem?.startDate);
}

Tangential to your changes, but this logic has a bug - the result of the parseDate call isn't getting stored or returned. Digging into this some more reveals that when modifying a medication, we're incorrectly setting the startDate to new Date() instead of the existing order's dateActivated property.

So IMO, that line should change to:

      startDate: medication.dateActivated,

Unless there's a specific reason for doing otherwise, @ibacher.

We can address this in a separate PR.


return initialOrderBasketItem?.startDate as Date;
}, [initialOrderBasketItem?.startDate]);

const medicationOrderFormSchema = useMemo(
() => createMedicationOrderFormSchema(requireOutpatientQuantity, t),
[requireOutpatientQuantity, t],
);
const medicationOrderFormSchema = useCreateMedicationOrderFormSchema();

const {
control,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,10 @@ const MedicationsDetailsTable: React.FC<MedicationsDetailsTableProps> = ({
</p>
</div>
<p className={styles.bodyLong01}>
{medication.orderReasonNonCoded && (
<span>
<span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
{medication.orderReasonNonCoded}
</span>
)}
<span>
<span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
{medication.orderReasonNonCoded ?? t('none', 'None')}
</span>
{medication.quantity && (
<span>
<span className={styles.label01}> &mdash; {t('quantity', 'Quantity').toUpperCase()}</span>{' '}
Expand Down
6 changes: 6 additions & 0 deletions packages/esm-patient-medications-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const configSchema = {
'Number of milliseconds to delay the search operation in the drug search input by after the user starts typing. The useDebounce hook delays the search by 300ms by default',
_default: 300,
},
isIndicationFieldOptional: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
isIndicationFieldOptional: {
requireIndication: {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for requireIndication: { _default: true }.

Positive phrasing is easier to grok (implies "we require this by default") as opposed to the double negative isIndicationFieldOptional: { _default: false } (implies "it's optional by default").

The validation logic reads better as well:

// Current validation logic
if (!isIndicationFieldOptional) { /* require indication */ }

// With requireIndication
if (requireIndication) { /* require indication */ }

_type: Type.Boolean,
_description: 'Whether to make the indication field optional in the drug order form',
vasharma05 marked this conversation as resolved.
Show resolved Hide resolved
_default: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_default: false,
_default: true,

},
};

export interface ConfigObject {
Expand All @@ -47,4 +52,5 @@ export interface ConfigObject {
showPrintButton: boolean;
maxDispenseDurationInDays: number;
debounceDelayInMs: number;
isIndicationFieldOptional: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,10 @@ const MedicationRecord: React.FC<MedicationRecordProps> = ({ medication }) => {
{medication.dosingInstructions && <span> &mdash; {medication.dosingInstructions.toLocaleLowerCase()}</span>}
</p>
<p className={styles.bodyLong01}>
{medication.orderReasonNonCoded ? (
<span>
<span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
{medication.orderReasonNonCoded}
</span>
) : null}
<span>
<span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
{medication.orderReasonNonCoded ?? t('none', 'None')}
</span>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think the previous implementation here is better. Can we reverse this please?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the quantity text right after the Indication text has a &mdash, hence when we don't have indication, the row looks like: - QUANTITY 5 tablets, hence I needed to put a fallback value for the missing indication.

{medication.quantity ? (
<span>
<span className={styles.label01}> &mdash; {t('quantity', 'Quantity').toUpperCase()}</span>{' '}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,9 @@ const OrderDetailsTable: React.FC<OrderDetailsProps> = ({ patientUuid, showAddBu
dosage:
order.type === 'drugorder' ? (
<div className={styles.singleLineText}>{`${t('indication', 'Indication').toUpperCase()}
${order.orderReasonNonCoded} ${'-'} ${t('quantity', 'Quantity').toUpperCase()} ${order.quantity} ${order
?.quantityUnits?.display} `}</div>
${order.orderReasonNonCoded ?? t('none', 'None')} ${'-'} ${t('quantity', 'Quantity').toUpperCase()} ${
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"No indication provided"

order.quantity
} ${order?.quantityUnits?.display} `}</div>
) : (
'--'
),
Expand Down
1 change: 1 addition & 0 deletions packages/esm-patient-orders-app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"medications": "Medications",
"modifyOrder": "Modify order",
"noMatchingOrdersToDisplay": "No matching orders to display",
"none": "None",
"noResultsForTestTypeSearch": "No results to display for \"{{searchTerm}}\"",
"normalRange": "Normal range",
"onDate": "on",
Expand Down
Loading