import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useSWRConfig } from 'swr'; import { useTranslation } from 'react-i18next'; import { Button, Checkbox, Form, FormLabel, InlineLoading } from '@carbon/react'; import { ExtensionSlot, getCoreTranslation, showModal, showSnackbar, useConfig, usePatient, Workspace2, type Workspace2DefinitionProps, } from '@openmrs/esm-framework'; import { type MedicationDispense, MedicationDispenseStatus, type MedicationRequestBundle, type InventoryItem, MedicationRequestFulfillerStatus, } from '../types'; import { calculateIsFreeTextDosage, computeNewFulfillerStatusAfterDispenseEvent, getDosageInstruction, getFulfillerStatus, getUuidFromReference, markEncounterAsStale, revalidate, } from '../utils'; import { type PharmacyConfig } from '../config-schema'; import { createStockDispenseRequestPayload, sendStockDispenseRequest } from './stock-dispense/stock.resource'; import { saveMedicationDispense } from '../medication-dispense/medication-dispense.resource'; import { updateMedicationRequestFulfillerStatus } from '../medication-request/medication-request.resource'; import MedicationDispenseReview from './medication-dispense-review.component'; import StockDispense from './stock-dispense/stock-dispense.component'; import styles from './forms.scss'; type DispenseFormProps = { medicationDispense: MedicationDispense; medicationRequestBundle: MedicationRequestBundle; mode: 'enter' | 'edit'; patientUuid?: string; encounterUuid: string; quantityRemaining: number; quantityDispensed: number; customWorkspaceTitle?: string; onWorkspaceClosed?(): void; }; const DispenseForm: React.FC> = ({ workspaceProps: { medicationDispense, medicationRequestBundle, mode, patientUuid, encounterUuid, quantityRemaining, quantityDispensed, customWorkspaceTitle, onWorkspaceClosed, }, closeWorkspace, }) => { const { t } = useTranslation(); const { mutate } = useSWRConfig(); const { patient, isLoading } = usePatient(patientUuid); const config = useConfig(); // Keep track of inventory item const [inventoryItem, setInventoryItem] = useState(); // Keep track of medication dispense payload const [medicationDispensePayload, setMedicationDispensePayload] = useState(medicationDispense); // to prevent duplicate submits const [isSubmitting, setIsSubmitting] = useState(false); const [shouldCompleteOrder, setShouldCompleteOrder] = useState(false); const [isFreeTextDosage, setIsFreeTextDosage] = useState(() => { const dosageInstruction = getDosageInstruction(medicationDispense?.dosageInstruction); return dosageInstruction ? calculateIsFreeTextDosage(dosageInstruction) : false; }); const getDuplicateDispense = (dispense: MedicationDispense): MedicationDispense => { const dispenses = medicationRequestBundle?.dispenses ?? []; const duplicateCheckWindowDays = config.duplicateCheckWindowDays; const getDispenseDate = (d: MedicationDispense) => d.whenHandedOver ?? d.whenPrepared; const getTime = (date?: string) => { if (!date) { return null; } const parsedTime = new Date(date).getTime(); return Number.isNaN(parsedTime) ? null : parsedTime; }; const windowMs = duplicateCheckWindowDays * 24 * 60 * 60 * 1000; const currentDispenseTime = getTime(getDispenseDate(dispense)) ?? Date.now(); return dispenses .filter((d) => d.status === MedicationDispenseStatus.completed) .filter((d) => { const dispenseTime = getTime(getDispenseDate(d)); if (dispenseTime === null) { return false; } // Duplicate checks are relative to the dispense date being submitted, not "now". return dispenseTime <= currentDispenseTime && currentDispenseTime - dispenseTime <= windowMs; }) .sort((a, b) => { return getTime(getDispenseDate(b)) - getTime(getDispenseDate(a)); }) .find((existingDispense) => { if (mode === 'edit' && existingDispense.id && dispense.id && existingDispense.id === dispense.id) { return false; } const sameMedication = existingDispense.medicationCodeableConcept?.coding?.[0]?.code === dispense.medicationCodeableConcept?.coding?.[0]?.code; const sameQuantity = existingDispense.quantity?.value === dispense.quantity?.value && existingDispense.quantity?.code === dispense.quantity?.code; const sameDose = 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; return sameMedication && sameQuantity && sameDose; }); }; const handleDuplicateMedication = (previousDispense: MedicationDispense) => { const dispose = showModal('duplicate-dispense-modal', { onClose: () => dispose(), medicationName: medicationDispensePayload?.medicationCodeableConcept?.text || '', previousDispense: previousDispense, previousDispenseDate: previousDispense?.whenHandedOver ?? previousDispense?.whenPrepared ?? undefined, previousSchedule: previousDispense?.dosageInstruction?.[0]?.timing?.code?.text ?? medicationDispensePayload?.dosageInstruction?.[0]?.timing?.code?.text, previousQuantity: previousDispense?.quantity?.value ?? medicationDispensePayload?.quantity?.value, previousQuantityUnit: previousDispense?.quantity?.unit ?? previousDispense?.quantity?.code ?? medicationDispensePayload?.quantity?.code, previousPerformer: previousDispense?.performer?.[0]?.actor?.display ?? medicationDispensePayload?.performer?.[0]?.actor?.display, onConfirm: () => handleSubmit(), }); }; // Submit medication dispense form const handleSubmit = () => { if (isSubmitting) { return Promise.resolve(); } setIsSubmitting(true); const abortController = new AbortController(); markEncounterAsStale(encounterUuid); return saveMedicationDispense(medicationDispensePayload, MedicationDispenseStatus.completed, abortController) .then((response) => { if (response.ok) { if (config.completeOrderWithThisDispense && shouldCompleteOrder) { return updateMedicationRequestFulfillerStatus( getUuidFromReference( medicationDispensePayload.authorizingPrescription[0].reference, // assumes authorizing prescription exist ), MedicationRequestFulfillerStatus.completed, ).then(() => response); } const newFulfillerStatus = computeNewFulfillerStatusAfterDispenseEvent( medicationDispensePayload, medicationRequestBundle, config.dispenseBehavior.restrictTotalQuantityDispensed, ); if (getFulfillerStatus(medicationRequestBundle.request) !== newFulfillerStatus) { return updateMedicationRequestFulfillerStatus( getUuidFromReference( medicationDispensePayload.authorizingPrescription[0].reference, // assumes authorizing prescription exist ), newFulfillerStatus, ).then(() => response); } } return response; }) .then((response) => { const { status } = response; if (config.enableStockDispense && (status === 201 || status === 200)) { const stockDispenseRequestPayload = createStockDispenseRequestPayload( inventoryItem, patientUuid, encounterUuid, medicationDispensePayload, ); sendStockDispenseRequest(stockDispenseRequestPayload, abortController).then( () => { showSnackbar({ title: t('stockDispensed', 'Stock dispensed'), kind: 'success', subtitle: t('stockDispensedSuccessfully', 'Stock dispensed successfully and batch level updated.'), }); }, (error) => { showSnackbar({ title: 'Stock dispense error', kind: 'error', subtitle: error?.message, }); }, ); } return response; }) .then( (response) => { const { status } = response; if (config.completeOrderWithThisDispense && shouldCompleteOrder && response?.data?.status === 'completed') { showSnackbar({ title: t('prescriptionCompleted', 'Prescription completed'), kind: 'success', subtitle: t( 'prescriptionCompletedSuccessfully', 'Medication dispensed and prescription marked as completed', ), }); } if (status === 201 || status === 200) { revalidate(mutate, encounterUuid); showSnackbar({ kind: 'success', subtitle: t('medicationListUpdated', 'Medication dispense list has been updated.'), title: t( mode === 'enter' ? 'medicationDispensed' : 'medicationDispenseUpdated', mode === 'enter' ? 'Medication successfully dispensed.' : 'Medication dispense record successfully updated.', ), }); closeWorkspace({ discardUnsavedChanges: true }); setIsSubmitting(false); onWorkspaceClosed?.(); } }, (error) => { showSnackbar({ kind: 'error', title: t( mode === 'enter' ? 'medicationDispenseError' : 'medicationDispenseUpdatedError', mode === 'enter' ? 'Error dispensing medication.' : 'Error updating dispense record', ), subtitle: error?.message, }); setIsSubmitting(false); }, ); }; const updateMedicationDispense = useCallback((medicationDispenseUpdate: Partial) => { setMedicationDispensePayload((prevState) => ({ ...prevState, ...medicationDispenseUpdate, })); }, []); // whether or not the form is valid and ready to submit const isValid = useMemo(() => { if (!medicationDispensePayload) { return false; } const anyCodedDosage = medicationDispensePayload.dosageInstruction[0]?.doseAndRate[0]?.doseQuantity?.value || medicationDispensePayload.dosageInstruction[0]?.doseAndRate[0]?.doseQuantity?.code || medicationDispensePayload.dosageInstruction[0]?.route?.coding[0]?.code || medicationDispensePayload.dosageInstruction[0]?.timing?.code?.coding[0].code; const allCodedDosage = medicationDispensePayload.dosageInstruction[0]?.doseAndRate[0]?.doseQuantity?.value && medicationDispensePayload.dosageInstruction[0]?.doseAndRate[0]?.doseQuantity?.code && medicationDispensePayload.dosageInstruction[0]?.route?.coding[0]?.code && medicationDispensePayload.dosageInstruction[0]?.timing?.code?.coding[0].code; return ( medicationDispensePayload.performer && medicationDispensePayload.performer[0]?.actor.reference && medicationDispensePayload.quantity?.value && (!quantityRemaining || medicationDispensePayload?.quantity?.value <= quantityRemaining) && medicationDispensePayload.quantity?.code && ((allCodedDosage && !isFreeTextDosage) || (!anyCodedDosage && isFreeTextDosage && medicationDispensePayload.dosageInstruction[0]?.text)) && (!medicationDispensePayload.substitution.wasSubstituted || (medicationDispensePayload.substitution.reason[0]?.coding[0].code && medicationDispensePayload.substitution.type?.coding[0].code)) ); }, [isFreeTextDosage, medicationDispensePayload, quantityRemaining]); // Auto-default "Complete Order With This Dispense" checkbox for orders with no refills useEffect(() => { // Only auto-default in 'enter' mode when creating a new dispense if (mode === 'enter' && medicationRequestBundle?.request?.dispenseRequest) { const numberOfRepeatsAllowed = medicationRequestBundle.request.dispenseRequest.numberOfRepeatsAllowed; // Default to true if order doesn't support refills if (numberOfRepeatsAllowed === 0 || numberOfRepeatsAllowed === null || numberOfRepeatsAllowed === undefined) { setShouldCompleteOrder(true); } } }, [medicationRequestBundle, mode]); const isButtonDisabled = (config.enableStockDispense ? !inventoryItem : false) || !isValid || isSubmitting; const handleSubmitOrDuplicateCheck = () => { const duplicateDispense = medicationDispensePayload ? getDuplicateDispense(medicationDispensePayload) : null; if (config.enableDuplicateDispenseCheck && duplicateDispense) { handleDuplicateMedication(duplicateDispense); } else { handleSubmit(); } }; const bannerState = useMemo(() => { if (patient) { return { patient, patientUuid, hideActionsOverflow: true, }; } }, [patient, patientUuid]); return (
{isLoading && ( )} {patient && }
{config.dispenseBehavior.allowModifyingPrescription ? t('drugHelpText', 'You may edit the formulation and quantity dispensed here') : t('drugHelpTextNoEdit', 'You may edit quantity dispensed here')} {medicationDispensePayload ? (
{config.completeOrderWithThisDispense && mode === 'enter' && !medicationDispense?.id && ( setShouldCompleteOrder(checked)} /> )} {config.enableStockDispense && ( )}
) : null}
); }; export default DispenseForm;