/** * MedDetailModal - Medication detail view with nested modals * Displays medication information, stock, schedules, and provides refill/edit functionality * * Can work in two modes: * 1. Context mode: Uses useAppContext() for all state (when no props provided) * 2. Props mode: Accepts all required data as props (for gradual adoption) */ /* biome-ignore-all lint/a11y/noLabelWithoutControl: modal uses label-styled wrappers with custom interactive rows */ /* biome-ignore-all lint/style/noNestedTernary: stock/preview rendering keeps explicit branch mapping */ import { Bell, Calendar, ClipboardList, FilePenLine, Minus, NotebookPen, Pencil, Plus, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Lightbox, MedicationAvatar } from "../components"; import { useEscapeKey } from "../hooks"; import type { Coverage, Medication, RefillEntry, StockThresholds } from "../types"; import { getMedTotal, getPackageSize } from "../types"; import { formatNumber, generateICS, getExpiryClass, getSystemLocale } from "../utils"; import { getStockStatus } from "../utils/schedule"; import { splitCurrentBlisterStock } from "../utils/stock"; // ============================================================================= // Local Helper Functions // ============================================================================= /** * Format full blisters column */ function formatFullBlisters(fullBlisters: number, t: (key: string) => string): string { if (fullBlisters === 0) return "—"; return `${fullBlisters} ${fullBlisters === 1 ? t("common.blister") : t("common.blisters")}`; } /** * Format open blister column */ function formatOpenBlisterAndLoose( openBlisterPills: number, loosePills: number, pillsPerBlister: number, t: (key: string) => string ): string { if (openBlisterPills > 0 && loosePills > 0) { return `${openBlisterPills} ${t("common.of")} ${pillsPerBlister} ${t("common.pills")} + ${loosePills} ${t("modal.loosePills")}`; } if (openBlisterPills > 0) { return `${openBlisterPills} ${t("common.of")} ${pillsPerBlister} ${t("common.pills")}`; } if (loosePills > 0) { return `${loosePills} ${t("modal.loosePills")}`; } return "—"; } // ============================================================================= // Props Interface // ============================================================================= export interface MedDetailModalProps { // Required selectedMed: Medication | null; coverage: { all: Coverage[] }; settings: StockThresholds; // Modal state showImageLightbox: boolean; showRefillModal: boolean; showEditStockModal: boolean; editStockOnly?: boolean; // Modal actions onClose: () => void; onOpenImageLightbox: () => void; onCloseImageLightbox: () => void; onOpenRefillModal: () => void; onCloseRefillModal: () => void; onOpenMedicationEdit?: () => void; onOpenEditStockModal?: () => void; onCloseEditStockModal: () => void; // Refill state refillPacks: number; onRefillPacksChange: (value: number) => void; refillLoose: number; onRefillLooseChange: (value: number) => void; usePrescriptionRefill: boolean; onUsePrescriptionRefillChange: (value: boolean) => void; refillSaving: boolean; refillHistory: RefillEntry[]; refillHistoryExpanded: boolean; onRefillHistoryExpandedChange: (value: boolean) => void; onSubmitRefill: (medId: number, usePrescription?: boolean) => Promise; // Edit stock state editStockFullBlisters: number; onEditStockFullBlistersChange: (value: number) => void; editStockPartialBlisterPills: number; onEditStockPartialBlisterPillsChange: (value: number) => void; editStockLoosePills: number; onEditStockLoosePillsChange: (value: number) => void; editStockSaving: boolean; onSubmitStockCorrection: (medId: number) => Promise; } export function MedDetailModal({ selectedMed, coverage, settings, showImageLightbox, showRefillModal, showEditStockModal, editStockOnly = false, onClose, onOpenImageLightbox, onCloseImageLightbox, onOpenRefillModal, onCloseRefillModal, onOpenMedicationEdit, onOpenEditStockModal, onCloseEditStockModal, refillPacks, onRefillPacksChange, refillLoose, onRefillLooseChange, usePrescriptionRefill, onUsePrescriptionRefillChange, refillSaving, refillHistory, refillHistoryExpanded, onRefillHistoryExpandedChange, onSubmitRefill, editStockFullBlisters, onEditStockFullBlistersChange, editStockPartialBlisterPills, onEditStockPartialBlisterPillsChange, editStockLoosePills, onEditStockLoosePillsChange, editStockSaving, onSubmitStockCorrection, }: MedDetailModalProps) { const { t, i18n } = useTranslation(); const [editStockFullInput, setEditStockFullInput] = useState("0"); const [editStockPartialInput, setEditStockPartialInput] = useState("0"); const [editStockLooseInput, setEditStockLooseInput] = useState("0"); const [showStockCapNotice, setShowStockCapNotice] = useState(false); const detailModalRef = useRef(null); const parseStockInput = (value: string): number => { const parsed = Number.parseInt(value, 10); return Number.isNaN(parsed) ? 0 : parsed; }; useEffect(() => { if (showEditStockModal) { setEditStockFullInput(String(editStockFullBlisters)); setEditStockPartialInput(String(editStockPartialBlisterPills)); setEditStockLooseInput(String(editStockLoosePills)); setShowStockCapNotice(false); } }, [showEditStockModal, editStockFullBlisters, editStockPartialBlisterPills, editStockLoosePills]); // Escape key: only one handler is active at a time (sub-modal states are mutually exclusive). // Lightbox has its own useEscapeKey internally. useEscapeKey(!showEditStockModal && !showImageLightbox && !showRefillModal, onClose); useEscapeKey(showEditStockModal, onCloseEditStockModal); useEscapeKey(showRefillModal, onCloseRefillModal); useEffect(() => { if (showEditStockModal) return; window.requestAnimationFrame(() => { detailModalRef.current?.focus(); }); }, [showEditStockModal]); const remainingPrescriptionRefills = Math.max(0, Number(selectedMed?.prescriptionRemainingRefills) || 0); const prescriptionPackCapEnabled = selectedMed?.packageType === "blister" && usePrescriptionRefill; const cappedRefillPacks = prescriptionPackCapEnabled ? Math.min(refillPacks, remainingPrescriptionRefills) : refillPacks; const exceedsPrescriptionPackLimit = prescriptionPackCapEnabled && refillPacks > remainingPrescriptionRefills; useEffect(() => { if (!selectedMed) return; if (!showRefillModal) return; if (selectedMed.packageType !== "blister" || !usePrescriptionRefill) return; if (refillPacks <= remainingPrescriptionRefills) return; onRefillPacksChange(remainingPrescriptionRefills); }, [ selectedMed, showRefillModal, usePrescriptionRefill, refillPacks, remainingPrescriptionRefills, onRefillPacksChange, ]); if (!selectedMed) return null; const medCoverage = coverage.all.find((c) => c.name === selectedMed.name); const packageSize = getPackageSize(selectedMed); // Structural max = sealed package capacity only (excludes pre-existing looseTablets). const structuralMax = selectedMed.packageType === "bottle" ? (selectedMed.totalPills ?? packageSize) : selectedMed.packCount * selectedMed.blistersPerPack * selectedMed.pillsPerBlister; const currentStock = medCoverage ? Math.round(medCoverage.medsLeft) : getMedTotal(selectedMed); const status = medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null; const fallbackTextClass = status?.className === "warning" ? "warning-text" : "success-text"; const textClass = status?.className === "danger" ? "danger-text" : fallbackTextClass; const stock = splitCurrentBlisterStock(currentStock, selectedMed.pillsPerBlister, selectedMed.looseTablets); const currentFullBlisters = Math.max(0, stock.fullBlisters); const currentPartialPills = Math.max(0, stock.openBlisterPills); const currentLoosePills = Math.max(0, stock.loosePills); const stockDisplayTotal = selectedMed.packageType === "bottle" ? (selectedMed.totalPills ?? packageSize) : Math.max(0, structuralMax); const maxPartialPills = Math.min( Math.max(0, selectedMed.pillsPerBlister), Math.max(0, structuralMax - Math.max(0, editStockFullBlisters) * selectedMed.pillsPerBlister) ); const partialForDisplay = Math.min(Math.max(0, editStockPartialBlisterPills), maxPartialPills); const maxFullBlisters = Math.floor(structuralMax / selectedMed.pillsPerBlister); const closeLabel = t("common.close"); const decrementLabel = t("editStock.decreaseValue"); const incrementLabel = t("editStock.increaseValue"); const normalizeBlisterStock = (nextFull: number, nextPartial: number, nextLoose: number) => { let normalizedFull = Math.max(0, nextFull); let normalizedPartial = Math.max(0, nextPartial); const normalizedLoose = Math.max(0, nextLoose); if (selectedMed.pillsPerBlister > 0) { normalizedFull += Math.floor(normalizedPartial / selectedMed.pillsPerBlister); normalizedPartial %= selectedMed.pillsPerBlister; } // Enforce cap only for sealed package pills (full + partial). const sealedTotal = normalizedFull * selectedMed.pillsPerBlister + normalizedPartial; if (sealedTotal > structuralMax) { const excess = sealedTotal - structuralMax; const partialReduction = Math.min(normalizedPartial, excess); normalizedPartial -= partialReduction; const remainingExcess = excess - partialReduction; if (remainingExcess > 0) { normalizedFull = Math.max(0, normalizedFull - Math.ceil(remainingExcess / selectedMed.pillsPerBlister)); } } return { full: normalizedFull, partial: normalizedPartial, loose: normalizedLoose }; }; const renderStepperInput = ({ value, min, max, onChange, onBlur, onStep, }: { value: string; min: number; max: number; onChange: (raw: string) => void; onBlur: () => void; onStep: (delta: number) => void; }) => { const parsed = Number.parseInt(value, 10); const current = Number.isNaN(parsed) ? min : parsed; const canDecrement = current > min; const canIncrement = current < max; return (
onChange(e.target.value)} onBlur={onBlur} />
); }; const renderRefillStepperInput = ({ value, min, max, onChange, }: { value: number; min: number; max: number; onChange: (next: number) => void; }) => { const clamped = Math.min(max, Math.max(min, Number.isFinite(value) ? value : min)); const canDecrement = clamped > min; const canIncrement = clamped < max; return (
{ const parsed = Number.parseInt(e.target.value, 10); onChange(Number.isNaN(parsed) ? min : Math.min(max, Math.max(min, parsed))); }} />
); }; const renderEditStockModal = () => { if (!showEditStockModal) return null; const fullInputMax = Math.min( maxFullBlisters, Math.floor(Math.max(0, structuralMax - Math.max(0, editStockPartialBlisterPills)) / selectedMed.pillsPerBlister) ); return (
{ e.stopPropagation(); onCloseEditStockModal(); }} onKeyDown={(e) => { if (e.key !== "Escape") e.stopPropagation(); }} >
e.stopPropagation()} onKeyDown={(e) => { if (e.key !== "Escape") e.stopPropagation(); }} >

{t("editStock.title")}

{selectedMed.name}

{t("editStock.hint")}

{selectedMed.packageType === "blister" && (

{t("editStock.currentComposition", { fullBlisters: currentFullBlisters, partialPills: currentPartialPills, loosePills: currentLoosePills, total: Math.max(0, currentStock), })}

)} {selectedMed.packageType === "bottle" && (

{t("editStock.packageSize", { count: structuralMax })}

)} {showStockCapNotice && (

{t("editStock.maxExceeded", { count: structuralMax })}

)} {(() => { const dbTotal = getMedTotal(selectedMed); const currentTotal = medCoverage ? Math.round(medCoverage.medsLeft) : dbTotal; const isBottle = selectedMed.packageType === "bottle"; const enteredTotal = isBottle ? editStockPartialBlisterPills : editStockFullBlisters * selectedMed.pillsPerBlister + editStockPartialBlisterPills + editStockLoosePills; const newTotal = Math.max(0, enteredTotal); const difference = newTotal - currentTotal; const differenceClass = difference > 0 ? "positive" : difference < 0 ? "negative" : ""; return ( <>
{isBottle ? ( ) : ( <> )}
{t("editStock.currentTotal")}: {currentTotal} {currentTotal === 1 ? t("common.pill") : t("common.pills")}
{t("editStock.newTotal")}: {newTotal} {newTotal === 1 ? t("common.pill") : t("common.pills")}
{t("editStock.difference")}: {difference > 0 ? "+" : ""} {difference} {Math.abs(difference) === 1 ? t("common.pill") : t("common.pills")}
); })()}
); }; if (editStockOnly) { return renderEditStockModal(); } return (
{ if (e.key !== "Escape") e.stopPropagation(); }} >
e.stopPropagation()} onKeyDown={(e) => { if (e.key !== "Escape") e.stopPropagation(); }} >
{/* Header */}
selectedMed.imageUrl && onOpenImageLightbox()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { if (selectedMed.imageUrl) onOpenImageLightbox(); } }} > {selectedMed.imageUrl && 🔍}

{selectedMed.name}

{selectedMed.genericName && {selectedMed.genericName}} {selectedMed.takenBy && (selectedMed.takenBy || []).length > 0 && ( {t("modal.for")}{" "} {selectedMed.takenBy.map((person, index) => ( {index > 0 && (index === selectedMed.takenBy.length - 1 ? ` ${t("common.and")} ` : ", ")} {person} {selectedMed.intakes?.some( (intake) => intake.takenBy === person && intake.intakeRemindersEnabled ) && 🔔} ))} )}
{/* Stock Info Section */}

{t("modal.stockInfo")}

{selectedMed.packageType === "blister" && ( <>
{t("table.fullBlisters")} {formatFullBlisters(stock.fullBlisters, t)}
{t("table.openBlister")} {formatOpenBlisterAndLoose( stock.openBlisterPills, stock.loosePills, selectedMed.pillsPerBlister ?? 1, t )}
)}
{t("modal.currentStock")} {currentStock} / {stockDisplayTotal} {currentStock > stockDisplayTotal && ( {" "} ⚠️ )}
{/* Package Details Section */}

{t("modal.packageDetails")} ( {selectedMed.packageType === "bottle" ? t("form.packageTypeBottle") : t("form.packageTypeBlister")})

{selectedMed.packageType === "blister" ? ( <>
{t("modal.packs")} {selectedMed.packCount}
{t("modal.blistersPerPack")} {selectedMed.blistersPerPack}
{t("modal.pillsPerBlister")} {selectedMed.pillsPerBlister}
) : (
{t("form.totalCapacity")} {(selectedMed.totalPills ?? packageSize) || "—"}
)} {selectedMed.pillWeightMg && (
{t("modal.pillWeight")} {selectedMed.pillWeightMg} {selectedMed.doseUnit ?? "mg"}
)} {selectedMed.expiryDate && (
{t("modal.expiryDate")} {new Date(selectedMed.expiryDate).toLocaleDateString(getSystemLocale(i18n.language), { day: "2-digit", month: "short", year: "numeric", })}
)}
{/* Intake Schedule Section */} {selectedMed.blisters.length > 0 && (

{t("modal.intakeSchedule")}{" "} {selectedMed.intakeRemindersEnabled && ( )}

{(selectedMed.intakes && selectedMed.intakes.length > 0 ? selectedMed.intakes : selectedMed.blisters.map((blister) => ({ usage: blister.usage, every: blister.every, start: blister.start, takenBy: null, intakeRemindersEnabled: selectedMed.intakeRemindersEnabled ?? false, })) ).map((intake, idx) => { const hasPerIntakeTakenBy = !!intake.takenBy; const personCount = Math.max(1, selectedMed.takenBy?.length ?? 0); const totalUsage = hasPerIntakeTakenBy ? intake.usage : intake.usage * personCount; const showIntakeBell = intake.intakeRemindersEnabled ?? selectedMed.intakeRemindersEnabled ?? false; return (
{totalUsage} {totalUsage !== 1 ? t("common.pills") : t("common.pill")} {selectedMed.pillWeightMg && ` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`} {intake.every === 1 ? t("common.daily") : t("common.everyNDays", { count: intake.every })} {hasPerIntakeTakenBy && ( {intake.takenBy} {showIntakeBell && ( )} )} {!hasPerIntakeTakenBy && showIntakeBell && ( )} {t("modal.at")}{" "} {new Date(intake.start).toLocaleTimeString(getSystemLocale(i18n.language), { hour: "2-digit", minute: "2-digit", })}
); })}
)} {/* Prescription Details Section */} {selectedMed.prescriptionEnabled && (

{t("form.sections.prescription")}

{t("prescription.authorizedRefills")} {selectedMed.prescriptionAuthorizedRefills ?? "—"}
{t("prescription.remainingRefills")} {selectedMed.prescriptionRemainingRefills ?? "—"}
{t("prescription.lowThreshold")} {selectedMed.prescriptionLowRefillThreshold ?? "—"}
{t("prescription.expiryDate")} {selectedMed.prescriptionExpiryDate ? new Date(selectedMed.prescriptionExpiryDate).toLocaleDateString( getSystemLocale(i18n.language), { day: "2-digit", month: "short", year: "numeric", } ) : "—"}
)} {/* Coverage Status Section */} {medCoverage && status && (

{t("modal.coverageStatus")} {t(status.label)}

{t("modal.daysLeft")} {medCoverage.daysLeft !== null ? formatNumber(medCoverage.daysLeft) : "—"}
{t("modal.runsOut")} {medCoverage.depletionDate ?? "—"}
)} {/* Notes Section */} {selectedMed.notes && (

{t("modal.notes")}{" "}

{selectedMed.notes}
)} {/* Refill History Section */} {refillHistory.length > 0 && (

onRefillHistoryExpandedChange(!refillHistoryExpanded)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") onRefillHistoryExpandedChange(!refillHistoryExpanded); }} > {t("refill.history")} ({refillHistory.length}) {refillHistoryExpanded ? "▼" : "▶"}

{refillHistoryExpanded && (
{refillHistory.map((entry) => (
{new Date(entry.refillDate).toLocaleDateString(getSystemLocale(i18n.language), { day: "2-digit", month: "short", year: "numeric", })} ,{" "} {new Date(entry.refillDate).toLocaleTimeString(getSystemLocale(i18n.language), { hour: "2-digit", minute: "2-digit", })} {(() => { const total = selectedMed.packageType === "bottle" ? entry.loosePillsAdded : entry.packsAdded * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + entry.loosePillsAdded; return `+${total} ${total === 1 ? t("common.pill") : t("common.pills")}`; })()} {entry.usedPrescription && ( {" "} )}
))}
)}
)}
{/* Footer */}
{onOpenMedicationEdit && ( )} {onOpenEditStockModal && ( )} {selectedMed.blisters.length > 0 && ( )}
{/* Image Lightbox */} {showImageLightbox && selectedMed.imageUrl && ( )} {/* Refill Modal */} {showRefillModal && (
{ e.stopPropagation(); onCloseRefillModal(); }} onKeyDown={(e) => { if (e.key !== "Escape") e.stopPropagation(); }} >
e.stopPropagation()} onKeyDown={(e) => { if (e.key !== "Escape") e.stopPropagation(); }} >

{t("refill.title")}

{selectedMed.name}

{selectedMed.packageType === "blister" ? ( <> ) : ( )} {selectedMed.prescriptionEnabled && (
{t("prescription.remainingRefills")}: {Number(selectedMed.prescriptionRemainingRefills) || 0}
)}
{(() => { const totalRefill = selectedMed.packageType === "blister" ? cappedRefillPacks * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + refillLoose : refillLoose; return totalRefill > 0 ? ( +{totalRefill} {totalRefill === 1 ? t("common.pill") : t("common.pills")} ) : null; })()}
)} {/* Edit Stock Modal */} {renderEditStockModal()}
); }