Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export const configSchema = {
};

export interface PharmacyConfig {
checkDuplicateDispense: any;
enableDuplicateDispenseCheck: any;
appName: string;
actionButtons: {
pauseButton: {
Expand Down
65 changes: 47 additions & 18 deletions src/forms/dispense-form.workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type DefaultWorkspaceProps,
ExtensionSlot,
getCoreTranslation,
showModal,
showSnackbar,
useConfig,
usePatient,
Expand Down Expand Up @@ -54,23 +55,38 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
const { patient, isLoading } = usePatient(patientUuid);
const config = useConfig<PharmacyConfig>();

// Keep track of inventory item
const [inventoryItem, setInventoryItem] = useState<InventoryItem>();

// Keep track of medication dispense payload
const [medicationDispensePayload, setMedicationDispensePayload] = useState<MedicationDispense>();

// whether or not the form is valid and ready to submit
const [isValid, setIsValid] = useState(false);

// to prevent duplicate submits
const [isSubmitting, setIsSubmitting] = useState(false);

// Submit medication dispense form
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 handleSubmit = () => {
if (!isSubmitting) {
setIsSubmitting(true);
const abortController = new AbortController();

saveMedicationDispense(medicationDispensePayload, MedicationDispenseStatus.completed, abortController)
.then((response) => {
if (response.ok) {
Expand All @@ -81,9 +97,7 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
);
if (getFulfillerStatus(medicationRequestBundle.request) !== newFulfillerStatus) {
return updateMedicationRequestFulfillerStatus(
getUuidFromReference(
medicationDispensePayload.authorizingPrescription[0].reference, // assumes authorizing prescription exist
),
getUuidFromReference(medicationDispensePayload.authorizingPrescription[0].reference),
newFulfillerStatus,
);
}
Expand Down Expand Up @@ -148,6 +162,16 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
}
};

const handleDuplicateMedication = () => {
const dispose = showModal('duplicate-prescription-modal', {
onClose: () => dispose(),
medicationName: medicationDispensePayload?.medicationCodeableConcept?.text || '',
onConfirm: () => {
handleSubmit();
},
});
};

const checkIsValid = () => {
if (
medicationDispensePayload &&
Expand All @@ -160,20 +184,17 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
medicationDispensePayload.dosageInstruction[0]?.doseAndRate[0]?.doseQuantity?.code &&
medicationDispensePayload.dosageInstruction[0]?.route?.coding[0].code &&
medicationDispensePayload.dosageInstruction[0]?.timing?.code.coding[0].code &&
(!medicationDispensePayload.substitution.wasSubstituted ||
(medicationDispensePayload.substitution.reason[0]?.coding[0].code &&
medicationDispensePayload.substitution.type?.coding[0].code))
(!medicationDispensePayload.substitution?.wasSubstituted ||
(medicationDispensePayload.substitution.reason?.[0]?.coding?.[0]?.code &&
medicationDispensePayload.substitution.type?.coding?.[0]?.code))
) {
setIsValid(true);
} else {
setIsValid(false);
}
};

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

// check is valid on any changes
useEffect(checkIsValid, [medicationDispensePayload, quantityRemaining, inventoryItem]);

const isButtonDisabled = (config.enableStockDispense ? !inventoryItem : false) || !isValid || isSubmitting;
Expand Down Expand Up @@ -232,7 +253,15 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
<Button disabled={isSubmitting} onClick={() => closeWorkspace()} kind="secondary">
{getCoreTranslation('cancel', 'Cancel')}
</Button>
<Button disabled={isButtonDisabled} onClick={handleSubmit}>
<Button
disabled={isButtonDisabled}
onClick={() => {
if (medicationDispensePayload && isDuplicateMedication(medicationDispensePayload)) {
handleDuplicateMedication();
} else {
handleSubmit();
}
}}>
{t(
mode === 'enter' ? 'dispensePrescription' : 'saveChanges',
mode === 'enter' ? 'Dispense prescription' : 'Save changes',
Expand Down
87 changes: 87 additions & 0 deletions src/forms/duplicate-prescription.modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { showSnackbar } from '@openmrs/esm-framework';
import { Button, ModalHeader, ModalBody, ModalFooter, InlineLoading } from '@carbon/react';

interface DuplicatePrescriptionModalProps {
onClose: () => void;
onConfirm: () => Promise<void> | void;
title?: string;
message?: string;
previousDispenseDate?: string;
previousQuantity?: number;
}

const DuplicatePrescriptionModal: React.FC<DuplicatePrescriptionModalProps> = ({
onClose,
onConfirm,
title,
message,
previousDispenseDate,
previousQuantity,
}) => {
const { t } = useTranslation();
const [isProcessing, setIsProcessing] = useState(false);

const handleConfirm = useCallback(async () => {
setIsProcessing(true);
try {
await onConfirm();
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),
});
} finally {
setIsProcessing(false);
}
}, [onConfirm, t, onClose]);

const modalTitle = title ?? t('duplicatePrescriptionTitle', 'Duplicate Dispensing Detected');
const modalMessage =
message ??
t(
'duplicatePrescriptionMessage',
'An identical medication (same dose, quantity, and duration) appears to have already been dispensed.',
);

return (
<>
<ModalHeader closeModal={onClose} title={modalTitle} />
<ModalBody>
<p>{modalMessage}</p>
{previousDispenseDate && previousQuantity !== undefined && (
<p>
{t('previousDispenseDetails', 'Previous dispense on')}{' '}
<strong>{new Date(previousDispenseDate).toLocaleDateString()}</strong> {t('withQuantity', 'with quantity')}{' '}
<strong>{previousQuantity}</strong>.
</p>
)}
<p>{t('confirmationPrompt', 'Do you want to proceed with dispensing?')}</p>
</ModalBody>
<ModalFooter>
<Button kind="secondary" onClick={onClose} disabled={isProcessing}>
{t('cancel', 'Cancel')}
</Button>
<Button kind="danger" onClick={() => void handleConfirm()} disabled={isProcessing}>
{isProcessing ? (
<InlineLoading description={`${t('dispensing', 'Dispensing')}...`} />
) : (
<span>{t('dispense', 'Dispense')}</span>
)}
</Button>
</ModalFooter>
</>
);
};

export default DuplicatePrescriptionModal;
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export const pauseDispenseWorkspace = getAsyncLifecycle(() => import('./forms/pa

export const printPrescriptionPreviewModal = getSyncLifecycle(PrescriptionPrintPreviewModal, options);
export const deleteConfirmModal = getAsyncLifecycle(() => import('./history/delete-confrim.modal'), options);
export const duplicatePrescriptionModal = getAsyncLifecycle(
() => import('./forms/duplicate-prescription.modal'),
options,
);

export const patientDiagnoses = getAsyncLifecycle(() => import('./diagnoses/diagnoses.component'), options);
export const patientConditions = getAsyncLifecycle(() => import('./conditions/conditions.component'), options);
2 changes: 2 additions & 0 deletions src/location/location.resource.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const pharmacyConfig: PharmacyConfig = {
enableStockDispense: false,
validateBatch: false,
leftNavMode: 'collapsed',
checkDuplicateDispense: false,
enableDuplicateDispenseCheck: false,
};

describe('Location Resource tests', () => {
Expand Down
5 changes: 4 additions & 1 deletion src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@
{
"name": "delete-confirm-modal",
"component": "deleteConfirmModal"

},
{
"name": "duplicate-prescription-modal",
"component": "duplicatePrescriptionModal"
}
]
}