Skip to content

Conversation

jwnasambu
Copy link
Contributor

@jwnasambu jwnasambu commented Jul 23, 2025

Requirements

  • This PR has a title that briefly describes the work done, including the ticket number if there is a ticket.
  • My work conforms to the OpenMRS 3.0 Styleguide and design documentation.
  • My work includes tests or is validated by existing tests.

Summary

I implemented a functionality that triggers when all key medication details (dose, quantity, duration, dispense date) match an existing dispense record for the patient.

Screenshots

Dispense.mp4

Related Issue

https://openmrs.atlassian.net/browse/O3-4460

Other

@jwnasambu jwnasambu marked this pull request as draft July 23, 2025 14:06
@jwnasambu jwnasambu marked this pull request as ready for review August 4, 2025 16:27
@jwnasambu jwnasambu closed this Aug 4, 2025
@jwnasambu jwnasambu reopened this Aug 4, 2025
@jwnasambu jwnasambu closed this Aug 4, 2025
@jwnasambu jwnasambu deleted the fix/O3-4460 branch August 4, 2025 16:32
@jwnasambu jwnasambu restored the fix/O3-4460 branch August 4, 2025 16:33
@jwnasambu jwnasambu reopened this Aug 4, 2025
@jwnasambu jwnasambu marked this pull request as draft August 4, 2025 16:34
@mogoodrich
Copy link
Member

@jwnasambu happy to review this when it's ready, go ahead and flag me when it's out of the draft state!

@jwnasambu
Copy link
Contributor Author

@mogoodrich Sure! Thanks so much for reaching out. It really means a lot and is so encouraging.

Add missing properties to satisfy PharmacyConfig type
@jwnasambu jwnasambu marked this pull request as ready for review August 13, 2025 13:28
@jwnasambu
Copy link
Contributor Author

@mogoodrich kindly feel free to review my PR at your convinient time please!

</Button>
<Button kind="danger" onClick={() => void handleConfirm()} disabled={isProcessing}>
{isProcessing ? (
<InlineLoading description={`${t('dispensing', 'Dispensing')}...`} />
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
<InlineLoading description={`${t('dispensing', 'Dispensing')}...`} />
<InlineLoading description={t('dispensing', 'Dispensing')}} />

Comment on lines +131 to +132
checkDuplicateDispense: any;
enableDuplicateDispenseCheck: any;
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
checkDuplicateDispense: any;
enableDuplicateDispenseCheck: any;
enableDuplicateDispenseCheck: false

This should be properly gated behind a config property. So that means we should have a similarly named property in the config schema:

  enableDuplicateDispenseCheck: {
    _type: Type.Boolean,
    _description: 'Enable or disable duplicate dispense check before submitting the dispense form',
    _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.

Why do we have two things here ("checkDuplicateDispense" and "enableDuplicateDispenseCheck")?

enableStockDispense: false,
validateBatch: false,
leftNavMode: 'collapsed',
checkDuplicateDispense: 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
checkDuplicateDispense: false,


interface DuplicatePrescriptionModalProps {
onClose: () => void;
onConfirm: () => Promise<void> | void;
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
onConfirm: () => Promise<void> | void;
onConfirm: () => Promise<void>;

Comment on lines +30 to +43
showSnackbar({
isLowContrast: true,
kind: 'success',
title: t('dispenseSuccess', 'Dispensing completed successfully'),
});
onClose();
} catch (error) {
console.error('Error during dispensing:', error);
showSnackbar({
isLowContrast: false,
kind: 'error',
title: t('errorDispensing', 'Error during dispensing'),
subtitle: error instanceof Error ? error.message : String(error),
});
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
showSnackbar({
isLowContrast: true,
kind: 'success',
title: t('dispenseSuccess', 'Dispensing completed successfully'),
});
onClose();
} catch (error) {
console.error('Error during dispensing:', error);
showSnackbar({
isLowContrast: false,
kind: 'error',
title: t('errorDispensing', 'Error during dispensing'),
subtitle: error instanceof Error ? error.message : String(error),
});
onClose();
} catch (error) {
console.error('Error during dispensing:', error);

The submit flow in dispense-form.workspace.tsx should own the error and success notifications. That's so that we have a single source of truth and correct timing-success notifications should only be shown after all async steps complete (dispense save, optional stock update, status update, revalidate). The modal cannot reliably know that; showing toasts there risks “premature success.”

Copy link
Member

Choose a reason for hiding this comment

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

Agree, thanks @denniskigen !

Comment on lines +259 to +260
if (medicationDispensePayload && isDuplicateMedication(medicationDispensePayload)) {
handleDuplicateMedication();
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 (medicationDispensePayload && isDuplicateMedication(medicationDispensePayload)) {
handleDuplicateMedication();
if (
config.enableDuplicateDispenseCheck &&
medicationDispensePayload &&
isDuplicateMedication(medicationDispensePayload)
) {
const match = findDuplicateMedication(medicationDispensePayload);
handleDuplicateMedication({
previousDispenseDate: match?.whenHandedOver,
previousQuantity: match?.quantity?.value,
previousQuantityUnit: match?.quantity?.code ?? match?.quantity?.unit,
previousPerformer: match?.performer?.[0]?.actor?.display,
previousRoute: match?.dosageInstruction?.[0]?.route?.text,
previousSchedule: match?.dosageInstruction?.[0]?.timing?.code?.text,
});

This gates the duplicate dispense check so it only runs when config.enableDuplicateDispenseCheck is truthy. If a check is detected, it finds the matching prior dispense via findDuplicateMedication(medicationDispensePayload). It then launches the duplicate dispense confirmation modal with contextual details.

Copy link
Member

Choose a reason for hiding this comment

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

Nit, but would it be clearer to pull this all out into a separate duplicate check function instead of nesting it within the button onclick?

}
};

const handleDuplicateMedication = () => {
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
const handleDuplicateMedication = () => {
const handleDuplicateMedication = (opts?: {
previousDispenseDate?: string;
previousQuantity?: number;
previousQuantityUnit?: string;
previousPerformer?: string;
previousRoute?: string;
previousSchedule?: string;
}) => {

const handleDuplicateMedication = () => {
const dispose = showModal('duplicate-prescription-modal', {
onClose: () => dispose(),
medicationName: medicationDispensePayload?.medicationCodeableConcept?.text || '',
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
medicationName: medicationDispensePayload?.medicationCodeableConcept?.text || '',
medicationName: medicationDispensePayload?.medicationCodeableConcept?.text || '',
previousDispenseDate: opts?.previousDispenseDate,
previousQuantity: opts?.previousQuantity,
previousQuantityUnit: opts?.previousQuantityUnit,
previousPerformer: opts?.previousPerformer,
previousRoute: opts?.previousRoute,
previousSchedule: opts?.previousSchedule,

onClose: () => dispose(),
medicationName: medicationDispensePayload?.medicationCodeableConcept?.text || '',
onConfirm: () => {
handleSubmit();
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
handleSubmit();
handleSubmit();
return Promise.resolve();

Comment on lines +63 to +83
const isDuplicateMedication = (dispense: MedicationDispense): boolean => {
const dispenses = medicationRequestBundle?.dispenses ?? [];

return dispenses.some((existingDispense) => {
return (
existingDispense.medicationCodeableConcept?.coding?.[0]?.code ===
dispense.medicationCodeableConcept?.coding?.[0]?.code &&
existingDispense.performer?.[0]?.actor?.reference === dispense.performer?.[0]?.actor?.reference &&
existingDispense.quantity?.value === dispense.quantity?.value &&
existingDispense.quantity?.code === dispense.quantity?.code &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code &&
existingDispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code &&
existingDispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code
);
});
};
Copy link
Member

@denniskigen denniskigen Aug 13, 2025

Choose a reason for hiding this comment

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

Suggested change
const isDuplicateMedication = (dispense: MedicationDispense): boolean => {
const dispenses = medicationRequestBundle?.dispenses ?? [];
return dispenses.some((existingDispense) => {
return (
existingDispense.medicationCodeableConcept?.coding?.[0]?.code ===
dispense.medicationCodeableConcept?.coding?.[0]?.code &&
existingDispense.performer?.[0]?.actor?.reference === dispense.performer?.[0]?.actor?.reference &&
existingDispense.quantity?.value === dispense.quantity?.value &&
existingDispense.quantity?.code === dispense.quantity?.code &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code &&
existingDispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code &&
existingDispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code
);
});
};
const anyCodingOverlap = (
a?: Array<{ system?: string; code?: string }>,
b?: Array<{ system?: string; code?: string }>,
) => (a ?? []).some((c1) => (b ?? []).some((c2) => c1.system === c2.system && c1.code === c2.code));
const normalizeRefToUuid = (ref?: string) => (ref ? getUuidFromReference(ref) : undefined);
const sameQuantity = (
a?: { value?: number; code?: string; unit?: string },
b?: { value?: number; code?: string; unit?: string },
) => {
if (a?.value !== b?.value) {
return false;
}
const aUnit = (a?.code ?? a?.unit ?? '').toLowerCase();
const bUnit = (b?.code ?? b?.unit ?? '').toLowerCase();
return aUnit === bUnit;
};
const equalStrings = (a?: string, b?: string) => (a?.trim().toLowerCase() ?? '') === (b?.trim().toLowerCase() ?? '');
const isDuplicateMedication = (dispense: MedicationDispense): boolean => {
const dispenses = medicationRequestBundle?.dispenses ?? [];
const currentId = dispense?.id;
return dispenses.some((existingDispense) => {
if (mode === 'edit' && existingDispense.id === currentId) {
return false;
}
const sameRx =
normalizeRefToUuid(existingDispense.authorizingPrescription?.[0]?.reference) ===
normalizeRefToUuid(dispense.authorizingPrescription?.[0]?.reference);
const sameMedication =
anyCodingOverlap(
existingDispense.medicationCodeableConcept?.coding,
dispense.medicationCodeableConcept?.coding,
) ||
normalizeRefToUuid(existingDispense.medicationReference?.reference) ===
normalizeRefToUuid(dispense.medicationReference?.reference);
const samePerformer =
normalizeRefToUuid(existingDispense.performer?.[0]?.actor?.reference) ===
normalizeRefToUuid(dispense.performer?.[0]?.actor?.reference);
const sameQty = sameQuantity(existingDispense.quantity, dispense.quantity);
const sameDose = sameQuantity(
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity,
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity,
);
const sameRoute =
anyCodingOverlap(
existingDispense.dosageInstruction?.[0]?.route?.coding,
dispense.dosageInstruction?.[0]?.route?.coding,
) ||
equalStrings(
existingDispense.dosageInstruction?.[0]?.route?.text,
dispense.dosageInstruction?.[0]?.route?.text,
);
const sameTiming =
anyCodingOverlap(
existingDispense.dosageInstruction?.[0]?.timing?.code?.coding,
dispense.dosageInstruction?.[0]?.timing?.code?.coding,
) ||
equalStrings(
existingDispense.dosageInstruction?.[0]?.timing?.code?.text,
dispense.dosageInstruction?.[0]?.timing?.code?.text,
);
return sameRx && sameMedication && samePerformer && sameQty && sameDose && sameRoute && sameTiming;
});
};
const findDuplicateMedication = (dispense: MedicationDispense): MedicationDispense | undefined => {
const dispenses = medicationRequestBundle?.dispenses ?? [];
const currentId = dispense?.id;
return dispenses.find((existingDispense) => {
if (mode === 'edit' && existingDispense.id === currentId) {
return false;
}
const sameRx =
normalizeRefToUuid(existingDispense.authorizingPrescription?.[0]?.reference) ===
normalizeRefToUuid(dispense.authorizingPrescription?.[0]?.reference);
const sameMedication =
anyCodingOverlap(
existingDispense.medicationCodeableConcept?.coding,
dispense.medicationCodeableConcept?.coding,
) ||
normalizeRefToUuid(existingDispense.medicationReference?.reference) ===
normalizeRefToUuid(dispense.medicationReference?.reference);
const samePerformer =
normalizeRefToUuid(existingDispense.performer?.[0]?.actor?.reference) ===
normalizeRefToUuid(dispense.performer?.[0]?.actor?.reference);
const sameQty = sameQuantity(existingDispense.quantity, dispense.quantity);
const sameDose = sameQuantity(
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity,
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity,
);
const sameRoute =
anyCodingOverlap(
existingDispense.dosageInstruction?.[0]?.route?.coding,
dispense.dosageInstruction?.[0]?.route?.coding,
) ||
equalStrings(
existingDispense.dosageInstruction?.[0]?.route?.text,
dispense.dosageInstruction?.[0]?.route?.text,
);
const sameTiming =
anyCodingOverlap(
existingDispense.dosageInstruction?.[0]?.timing?.code?.coding,
dispense.dosageInstruction?.[0]?.timing?.code?.coding,
) ||
equalStrings(
existingDispense.dosageInstruction?.[0]?.timing?.code?.text,
dispense.dosageInstruction?.[0]?.timing?.code?.text,
);
return sameRx && sameMedication && samePerformer && sameQty && sameDose && sameRoute && sameTiming;
});
};

Maybe this might be a more robust duplicate check:

  • anyCodingOverlap: Returns true if two CodeableConcept coding arrays share at least one coding with the same system and code.
  • normalizeRefToUuid: Extracts the UUID from a FHIR reference string (e.g., “Practitioner/uuid” → “uuid”), or undefined if missing.
  • sameQuantity: Compares two quantities by value and by unit identifier (prefers code, falls back to unit), case-insensitive.
  • isDuplicateMedication: Detects whether the current dispense matches a prior one for the same prescription by checking:
    • sameRx: same authorizingPrescription (UUID-normalized)
    • sameMedication: any overlap in medication codings (system+code) OR same Medication reference UUID
    • samePerformer: same performer UUID
    • sameQty and sameDose: value plus code-or-unit
    • sameRoute and sameTiming: any overlap in codings (system+code) OR equal text when codings are absent
    • Ignores the current record in edit mode (prevents self-matching)
  • Submit button logic: If enableDuplicateDispenseCheck is true and a duplicate is detected, it finds the matching prior dispense and opens the confirmation modal with contextual details (date, quantity with human-readable unit, performer, route, schedule). Proceeding triggers the normal submit flow, which owns success/error toasts.

Copy link
Member

@denniskigen denniskigen Aug 13, 2025

Choose a reason for hiding this comment

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

@mogoodrich @dkayiwa @ibacher @mseaton it doesn't look like the backend has a duplicate-dispense enforcement. Should we add server-side validation to prevent exact duplicates for the same authorizingPrescription within a time window? What criteria should it consider? What should it return if a match is found? Should we file a ticket.

Copy link
Member

Choose a reason for hiding this comment

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

Whew, detailed @denniskigen ! I didn't fully review, I assume you have tested? Can we add some unit tests?

Also, do the findDuplicateMedication and isDuplicateMedication have a duplicate code block after the "return dispennses.find() or dispenses.some()" that could be refactored out, or is there a difference I'm missing?

But the biggest thing here: there is no checking of date of dispense as far as I can tell? In the refill use case it would be typical for multiple duplicate dispenses... there should only be a warning if the two duplicates are within some certain timeframe. Not sure what this time frame should be, but we should likely make it configurable.

Copy link
Member

Choose a reason for hiding this comment

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

And regarding the server-side validation, probably a question for @bmamlin (but probably worth discussing outside this PR so as not to clutter). I do think that it might be hard to some with a hard-and-fast validation rule, especially considering inpatient settings where (I assume) there may be multiple dispensing within a single day?

Comment on lines +62 to +68
{previousDispenseDate && previousQuantity !== undefined && (
<p>
{t('previousDispenseDetails', 'Previous dispense on')}{' '}
<strong>{new Date(previousDispenseDate).toLocaleDateString()}</strong> {t('withQuantity', 'with quantity')}{' '}
<strong>{previousQuantity}</strong>.
</p>
)}
Copy link
Member

@denniskigen denniskigen Aug 13, 2025

Choose a reason for hiding this comment

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

Suggested change
{previousDispenseDate && previousQuantity !== undefined && (
<p>
{t('previousDispenseDetails', 'Previous dispense on')}{' '}
<strong>{new Date(previousDispenseDate).toLocaleDateString()}</strong> {t('withQuantity', 'with quantity')}{' '}
<strong>{previousQuantity}</strong>.
</p>
)}
{previousDispense ? (
<Tile className={styles.previousDispenseTile}>
<div className={styles.headerRow}>
<Tag type="gray" size="sm">
{t('dispensed', 'Dispensed')}
</Tag>
<span className={styles.medicationName}>{medicationName}</span>
</div>
<div className={styles.metaRow}>
{previousPerformer && (
<span>
{t('performedBy', 'Performed by')}: <strong>{previousPerformer}</strong>
</span>
)}
{previousPerformer && previousDispenseDate && <span className={styles.metaDivider}></span>}
{previousDispenseDate && <span>{new Date(previousDispenseDate).toLocaleDateString()}</span>}
</div>
<div className={styles.tileContent}>
<MedicationEvent medicationEvent={previousDispense} />
</div>
</Tile>
) : (
<>
{medicationName && (
<p>
{t('medication', 'Medication')}: <strong>{medicationName}</strong>
</p>
)}
{previousDispenseDate && previousQuantity !== undefined && (
<p>
{t('previousDispenseDetails', 'Previous dispense on')}{' '}
<strong>{new Date(previousDispenseDate).toLocaleDateString()}</strong>{' '}
{t('withQuantity', 'with quantity')}{' '}
<strong>
{previousQuantity}
{previousQuantityUnit ? ` ${previousQuantityUnit}` : ''}
</strong>
.
</p>
)}
{previousPerformer && (
<p>
{t('performedBy', 'Performed by')}: <strong>{previousPerformer}</strong>
</p>
)}
{previousRoute && (
<p>
{t('route', 'Route')}: <strong>{previousRoute}</strong>
</p>
)}
{previousSchedule && (
<p>
{t('schedule', 'Schedule')}: <strong>{previousSchedule}</strong>
</p>
)}
</>
)}

We can pass previousQuantityUnit, previousPerformer, previousRoute, previousSchedule, previousDispense as optional props to the DuplicatePrescriptionModal component in the modal registration.

And then you'd have this in duplicate-dispense-modal.scss:

@use '@carbon/layout';
@use '@openmrs/esm-styleguide/src/vars' as *;

.previousDispenseTile {
  padding: layout.$spacing-04 layout.$spacing-05;
  margin: layout.$spacing-05 0;
}

.headerRow {
  display: flex;
  align-items: center;
  gap: layout.$spacing-02;
}

.medicationName {
  font-weight: 600;
}

.tileContent {
  margin-top: layout.$spacing-02;
}

.metaRow {
  margin-top: layout.$spacing-02;
  color: $text-02;
  display: flex;
  align-items: center;
  gap: layout.$spacing-03;
  font-size: layout.$spacing-04;
}

.metaDivider {
  color: $ui-04;
}

Copy link
Member

@denniskigen denniskigen Aug 13, 2025

Choose a reason for hiding this comment

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

This would give you something like this:

CleanShot 2025-08-13 at 23 00 51@2x

which gives the pharmacist a clear, compact summary of that prior dispense and quick ways to double‑check, so they can confidently decide to cancel or proceed without leaving the workflow.

Copy link
Member

Choose a reason for hiding this comment

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

We also need to decide whether we want to flag multiple potential matches. We already have all prior dispenses in the bundle, so we can compute and display multiple matches locally. Or would we want to offset this concern to the backend?

Copy link
Member

Choose a reason for hiding this comment

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

We likely should flag multiple matches here. But, again, we want to limit this to a (perhaps configurable) timeframe.

@denniskigen denniskigen changed the title fix/O3-4460: Missing Warning for Duplicate Medication Dispense  (feat) O3-4460: Add a duplicate dispense warning with a confirmation modal Aug 13, 2025
@denniskigen
Copy link
Member

How should the system deal with potential duplicates created by the user confirming the modal? Should we annotate the dispense record with some additional information?

@denniskigen denniskigen requested a review from bmamlin August 14, 2025 13:56
@jwnasambu
Copy link
Contributor Author

jwnasambu commented Aug 14, 2025

@denniskigen Kindly I haven't pushed the proposed changes as you had suggested during the coffee break session and 03- call as I wait for the review from other members. Kindly let me know when its ready to push the changes just incase other members consent or delay to respond.

@mogoodrich

@mogoodrich
Copy link
Member

Sorry I'm backlogged with things, will hopefully look at this tomorrow but if you don't hear by next week feel free to ping me again @jwnasambu

Copy link
Member

@mogoodrich mogoodrich left a comment

Choose a reason for hiding this comment

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

Thanks @jwnasambu and @denniskigen , see my comments... perhaps my biggest comment is that the warning should be limited to a (likely configurable) time window.

Comment on lines +131 to +132
checkDuplicateDispense: any;
enableDuplicateDispenseCheck: any;
Copy link
Member

Choose a reason for hiding this comment

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

Why do we have two things here ("checkDuplicateDispense" and "enableDuplicateDispenseCheck")?

Comment on lines +63 to +83
const isDuplicateMedication = (dispense: MedicationDispense): boolean => {
const dispenses = medicationRequestBundle?.dispenses ?? [];

return dispenses.some((existingDispense) => {
return (
existingDispense.medicationCodeableConcept?.coding?.[0]?.code ===
dispense.medicationCodeableConcept?.coding?.[0]?.code &&
existingDispense.performer?.[0]?.actor?.reference === dispense.performer?.[0]?.actor?.reference &&
existingDispense.quantity?.value === dispense.quantity?.value &&
existingDispense.quantity?.code === dispense.quantity?.code &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code &&
existingDispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code &&
existingDispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code
);
});
};
Copy link
Member

Choose a reason for hiding this comment

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

Whew, detailed @denniskigen ! I didn't fully review, I assume you have tested? Can we add some unit tests?

Also, do the findDuplicateMedication and isDuplicateMedication have a duplicate code block after the "return dispennses.find() or dispenses.some()" that could be refactored out, or is there a difference I'm missing?

But the biggest thing here: there is no checking of date of dispense as far as I can tell? In the refill use case it would be typical for multiple duplicate dispenses... there should only be a warning if the two duplicates are within some certain timeframe. Not sure what this time frame should be, but we should likely make it configurable.

}
};

// initialize the internal dispense payload with the dispenses passed in as props
Copy link
Member

Choose a reason for hiding this comment

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

Any particular reason we are removing these comments?

// initialize the internal dispense payload with the dispenses passed in as props
useEffect(() => setMedicationDispensePayload(medicationDispense), [medicationDispense]);

// check is valid on any changes
Copy link
Member

Choose a reason for hiding this comment

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

Same question here?

Comment on lines +259 to +260
if (medicationDispensePayload && isDuplicateMedication(medicationDispensePayload)) {
handleDuplicateMedication();
Copy link
Member

Choose a reason for hiding this comment

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

Nit, but would it be clearer to pull this all out into a separate duplicate check function instead of nesting it within the button onclick?

Comment on lines +30 to +43
showSnackbar({
isLowContrast: true,
kind: 'success',
title: t('dispenseSuccess', 'Dispensing completed successfully'),
});
onClose();
} catch (error) {
console.error('Error during dispensing:', error);
showSnackbar({
isLowContrast: false,
kind: 'error',
title: t('errorDispensing', 'Error during dispensing'),
subtitle: error instanceof Error ? error.message : String(error),
});
Copy link
Member

Choose a reason for hiding this comment

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

Agree, thanks @denniskigen !

Comment on lines +62 to +68
{previousDispenseDate && previousQuantity !== undefined && (
<p>
{t('previousDispenseDetails', 'Previous dispense on')}{' '}
<strong>{new Date(previousDispenseDate).toLocaleDateString()}</strong> {t('withQuantity', 'with quantity')}{' '}
<strong>{previousQuantity}</strong>.
</p>
)}
Copy link
Member

Choose a reason for hiding this comment

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

We likely should flag multiple matches here. But, again, we want to limit this to a (perhaps configurable) timeframe.

Comment on lines +63 to +83
const isDuplicateMedication = (dispense: MedicationDispense): boolean => {
const dispenses = medicationRequestBundle?.dispenses ?? [];

return dispenses.some((existingDispense) => {
return (
existingDispense.medicationCodeableConcept?.coding?.[0]?.code ===
dispense.medicationCodeableConcept?.coding?.[0]?.code &&
existingDispense.performer?.[0]?.actor?.reference === dispense.performer?.[0]?.actor?.reference &&
existingDispense.quantity?.value === dispense.quantity?.value &&
existingDispense.quantity?.code === dispense.quantity?.code &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.value &&
existingDispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code ===
dispense.dosageInstruction?.[0]?.doseAndRate?.[0]?.doseQuantity?.code &&
existingDispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.route?.coding?.[0]?.code &&
existingDispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code ===
dispense.dosageInstruction?.[0]?.timing?.code?.coding?.[0]?.code
);
});
};
Copy link
Member

Choose a reason for hiding this comment

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

And regarding the server-side validation, probably a question for @bmamlin (but probably worth discussing outside this PR so as not to clutter). I do think that it might be hard to some with a hard-and-fast validation rule, especially considering inpatient settings where (I assume) there may be multiple dispensing within a single day?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants