-
Notifications
You must be signed in to change notification settings - Fork 50
(feat) O3-4460: Add a duplicate dispense warning with a confirmation modal #350
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
base: main
Are you sure you want to change the base?
Conversation
@jwnasambu happy to review this when it's ready, go ahead and flag me when it's out of the draft state! |
@mogoodrich Sure! Thanks so much for reaching out. It really means a lot and is so encouraging. |
Add missing properties to satisfy PharmacyConfig type
@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')}...`} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<InlineLoading description={`${t('dispensing', 'Dispensing')}...`} /> | |
<InlineLoading description={t('dispensing', 'Dispensing')}} /> |
checkDuplicateDispense: any; | ||
enableDuplicateDispenseCheck: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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,
},
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checkDuplicateDispense: false, |
|
||
interface DuplicatePrescriptionModalProps { | ||
onClose: () => void; | ||
onConfirm: () => Promise<void> | void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onConfirm: () => Promise<void> | void; | |
onConfirm: () => Promise<void>; |
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), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.”
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, thanks @denniskigen !
if (medicationDispensePayload && isDuplicateMedication(medicationDispensePayload)) { | ||
handleDuplicateMedication(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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 = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 || '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleSubmit(); | |
handleSubmit(); | |
return Promise.resolve(); |
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 | ||
); | ||
}); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 twoCodeableConcept
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 UUIDsamePerformer
: same performer UUIDsameQty
and sameDose: value plus code-or-unitsameRoute
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
{previousDispenseDate && previousQuantity !== undefined && ( | ||
<p> | ||
{t('previousDispenseDetails', 'Previous dispense on')}{' '} | ||
<strong>{new Date(previousDispenseDate).toLocaleDateString()}</strong> {t('withQuantity', 'with quantity')}{' '} | ||
<strong>{previousQuantity}</strong>. | ||
</p> | ||
)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{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;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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 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. |
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 |
There was a problem hiding this 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.
checkDuplicateDispense: any; | ||
enableDuplicateDispenseCheck: any; |
There was a problem hiding this comment.
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")?
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 | ||
); | ||
}); | ||
}; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here?
if (medicationDispensePayload && isDuplicateMedication(medicationDispensePayload)) { | ||
handleDuplicateMedication(); |
There was a problem hiding this comment.
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?
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), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, thanks @denniskigen !
{previousDispenseDate && previousQuantity !== undefined && ( | ||
<p> | ||
{t('previousDispenseDetails', 'Previous dispense on')}{' '} | ||
<strong>{new Date(previousDispenseDate).toLocaleDateString()}</strong> {t('withQuantity', 'with quantity')}{' '} | ||
<strong>{previousQuantity}</strong>. | ||
</p> | ||
)} |
There was a problem hiding this comment.
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.
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 | ||
); | ||
}); | ||
}; |
There was a problem hiding this comment.
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?
Requirements
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