import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ConfirmModal, MedicationAvatar, MobileEditModal } from "../components"; import { useAppContext, useUnsavedChanges } from "../context"; import { useMedicationForm, useUnsavedChangesWarning } from "../hooks"; import type { DoseUnit, Medication } from "../types"; import { DOSE_UNITS, FIELD_LIMITS, getPackageSize } from "../types"; import { combineDateAndTime, formatDateTime, formatNumber } from "../utils/formatters"; import { log } from "../utils/logger"; export function MedicationsPage() { const { t } = useTranslation(); const { meds, saving, setSaving, loadMeds, deleteMed, uploadMedImage, deleteMedImage, uploadingImage, existingPeople, refillPacks, setRefillPacks, refillLoose, setRefillLoose, usePrescriptionRefill, setUsePrescriptionRefill, refillSaving, submitRefill, coverageByMed, } = useAppContext(); // Use the medication form hook const { form, setForm, setOriginalForm, editingId, formSaved, setFormSaved, formChanged, fieldErrors, hasValidationErrors, takenByInput, setTakenByInput, addTakenByPerson, removeTakenByPerson, handleTakenByKeyDown, handleValueChange, addBlister, removeBlister, setBlisterValue, addIntake, removeIntake, setIntakeValue, resetForm, startEdit, } = useMedicationForm(); // Warn user about unsaved changes when navigating away useUnsavedChangesWarning(formChanged); // View mode: grid (default) or form (edit/new) const [viewMode, setViewMode] = useState<"grid" | "form">("grid"); // Mobile modal state (declared early because it's used in useEffect below) const [showEditModal, setShowEditModal] = useState(false); // Sync formChanged state to the global context for navigation blocking const { setHasUnsavedChanges } = useUnsavedChanges(); useEffect(() => { setHasUnsavedChanges(formChanged); return () => setHasUnsavedChanges(false); // Clear on unmount }, [formChanged, setHasUnsavedChanges]); // Push history state when form changes to capture browser back button const hasUnsavedHistoryState = useRef(false); useEffect(() => { if (formChanged && !hasUnsavedHistoryState.current && !showEditModal) { // Push a history state so we can intercept browser back window.history.pushState({ unsavedChanges: true }, ""); hasUnsavedHistoryState.current = true; } else if (!formChanged && hasUnsavedHistoryState.current) { // Clean up history state when form is saved/reset hasUnsavedHistoryState.current = false; } }, [formChanged, showEditModal]); // Image state for new medications const [pendingImage, setPendingImage] = useState(null); const [pendingImagePreview, setPendingImagePreview] = useState(null); // Track if close was confirmed programmatically (to avoid double confirmation) const closeConfirmedRef = useRef(false); // Confirmation modal for unsaved changes const [showUnsavedConfirm, setShowUnsavedConfirm] = useState(false); const [unsavedConfirmSource, setUnsavedConfirmSource] = useState<"mobile-edit" | "desktop-form" | null>(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleteCandidate, setDeleteCandidate] = useState(null); // Calculate total tablets const totalTablets = useMemo(() => { if (form.packageType === "bottle") { // For bottle type, looseTablets is the current stock return Number(form.looseTablets) || 0; } // For blister type const packCount = Number(form.packCount) || 0; const blistersPerPack = Number(form.blistersPerPack) || 0; const pillsPerBlister = Number(form.pillsPerBlister) || 1; const looseTablets = Number(form.looseTablets) || 0; return packCount * blistersPerPack * pillsPerBlister + looseTablets; }, [form.packageType, form.packCount, form.blistersPerPack, form.pillsPerBlister, form.looseTablets]); // Open mobile edit modal function openEditModal() { setShowEditModal(true); window.history.pushState({ modal: "edit" }, ""); } // Close mobile edit modal function closeEditModal() { if (showEditModal) { // Check for unsaved changes before closing if (formChanged) { setUnsavedConfirmSource("mobile-edit"); setShowUnsavedConfirm(true); return; } // Mark as confirmed to avoid double confirmation in popstate handler closeConfirmedRef.current = true; window.history.back(); } } // Handle confirmed close (user clicked "Leave" in confirmation modal) function handleConfirmClose() { setShowUnsavedConfirm(false); setUnsavedConfirmSource(null); closeConfirmedRef.current = true; hasUnsavedHistoryState.current = false; if (showEditModal) { setShowEditModal(false); } resetForm(); window.history.back(); } // Handle cancelled close (user clicked "Stay" in confirmation modal) function handleCancelClose() { setShowUnsavedConfirm(false); if (unsavedConfirmSource === "mobile-edit") { setShowEditModal(true); } setUnsavedConfirmSource(null); } // Helper to reset form and clear history state function handleResetForm() { if (hasUnsavedHistoryState.current) { hasUnsavedHistoryState.current = false; // Go back to remove the unsaved changes history entry window.history.back(); } resetForm(); setViewMode("grid"); } function requestDeleteMed(med: Medication) { setDeleteCandidate(med); setShowDeleteConfirm(true); window.history.pushState({ modal: "delete-confirm" }, ""); } async function handleConfirmDelete() { if (!deleteCandidate) return; await deleteMed(deleteCandidate.id, editingId, resetForm); setShowDeleteConfirm(false); setDeleteCandidate(null); // Pop the delete-confirm history entry window.history.back(); } function handleCancelDelete() { setShowDeleteConfirm(false); setDeleteCandidate(null); // Pop the delete-confirm history entry window.history.back(); } // Handle submit refill async function handleSubmitRefill(medId: number) { await submitRefill(medId, editingId, setForm, loadMeds, usePrescriptionRefill); } // Save medication async function saveMedication(e: React.FormEvent) { e.preventDefault(); if (saving) return; setSaving(true); // Prepare intakes data with per-intake takenBy const intakes = form.intakes.map((intake) => ({ usage: Number(intake.usage) || 1, every: Number(intake.every) || 1, start: combineDateAndTime(intake.startDate, intake.startTime), takenBy: intake.takenBy.trim() || null, // Empty string becomes null intakeRemindersEnabled: intake.intakeRemindersEnabled, })); // Also prepare legacy blisters for backward compatibility const blisters = intakes.map((i) => ({ usage: i.usage, every: i.every, start: i.start, })); const authorizedRefills = Number(form.prescriptionAuthorizedRefills || 0); const remainingRefills = Math.min(Number(form.prescriptionRemainingRefills || 0), authorizedRefills); const lowRefillThreshold = Math.min(Number(form.prescriptionLowRefillThreshold || 1), authorizedRefills); const body = { name: form.name.trim(), genericName: form.genericName.trim() || null, takenBy: form.takenBy.length > 0 ? form.takenBy : [], packageType: form.packageType, packCount: Number(form.packCount) || 0, blistersPerPack: Number(form.blistersPerPack) || 1, pillsPerBlister: Number(form.pillsPerBlister) || 1, totalPills: Number(form.totalPills) || null, looseTablets: Number(form.looseTablets) || 0, pillWeightMg: Number(form.pillWeightMg) || null, doseUnit: form.doseUnit, expiryDate: form.expiryDate || null, notes: form.notes.trim() || null, intakeRemindersEnabled: form.intakeRemindersEnabled, prescriptionEnabled: form.prescriptionEnabled, prescriptionAuthorizedRefills: form.prescriptionEnabled ? authorizedRefills : null, prescriptionRemainingRefills: form.prescriptionEnabled ? remainingRefills : null, prescriptionLowRefillThreshold: form.prescriptionEnabled ? lowRefillThreshold : 1, prescriptionExpiryDate: form.prescriptionExpiryDate || null, blisters, // Legacy format for backward compatibility intakes, // New format with per-intake takenBy }; try { let url = "/api/medications"; let method = "POST"; if (editingId) { url = `/api/medications/${editingId}`; method = "PUT"; } const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), credentials: "include", }); if (!res.ok) { let errorMessage = t("common.saveFailed"); try { const errorBody = (await res.json()) as { error?: string; message?: string }; if (typeof errorBody?.error === "string" && errorBody.error.trim().length > 0) { errorMessage = errorBody.error; } else if (typeof errorBody?.message === "string" && errorBody.message.trim().length > 0) { errorMessage = errorBody.message; } } catch { // keep translated fallback } throw new Error(errorMessage); } const saved = await res.json(); // Upload image if pending (for new medications) if (!editingId && pendingImage && saved.id) { await uploadMedImage(saved.id, pendingImage); setPendingImage(null); setPendingImagePreview(null); } setFormSaved(true); loadMeds(); // Clean up history state if we had unsaved changes if (hasUnsavedHistoryState.current) { hasUnsavedHistoryState.current = false; // Don't go back here, just clear the flag - the state will be cleaned naturally } // Reset form after successful save if (!editingId) { resetForm(); setViewMode("grid"); } else { // Update originalForm so formChanged becomes false setOriginalForm(form); } } catch (err) { log.error("Save error:", err); alert(err instanceof Error && err.message ? err.message : t("common.saveFailed")); } setSaving(false); } // Handle browser back button for modals and unsaved changes useEffect(() => { const handlePopState = () => { // Delete confirmation is open — dismiss it and stay where we are if (showDeleteConfirm) { setShowDeleteConfirm(false); setDeleteCandidate(null); return; } // If close was already confirmed programmatically, allow navigation if (closeConfirmedRef.current) { closeConfirmedRef.current = false; if (showEditModal) { setShowEditModal(false); resetForm(); } return; } // Handle mobile edit modal if (showEditModal) { // Check for unsaved changes (user pressed browser back directly) if (formChanged) { // Re-push history state to stay in modal window.history.pushState({ modal: "edit" }, ""); // Show confirmation modal setUnsavedConfirmSource("mobile-edit"); setShowUnsavedConfirm(true); return; } setShowEditModal(false); resetForm(); return; } // Handle desktop form with unsaved changes if (formChanged && hasUnsavedHistoryState.current) { // Re-push history state to stay on page window.history.pushState({ unsavedChanges: true }, ""); // Show confirmation modal setUnsavedConfirmSource("desktop-form"); setShowUnsavedConfirm(true); } }; window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); }, [showDeleteConfirm, showEditModal, formChanged, resetForm]); // Close modal on Escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && showEditModal) { closeEditModal(); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [showEditModal, closeEditModal]); // Handle edit button click - open modal on mobile, switch to form on desktop function handleEditClick(med: Medication) { startEdit(med, openEditModal); setViewMode("form"); } const orderedMeds = useMemo(() => { if (!editingId) { return meds; } const selectedMedication = meds.find((med) => med.id === editingId); if (!selectedMedication) { return meds; } return [selectedMedication, ...meds.filter((med) => med.id !== editingId)]; }, [meds, editingId]); const medListRef = useRef(null); useEffect(() => { if (viewMode !== "form" || !editingId) { return; } if (medListRef.current) { medListRef.current.scrollTop = 0; } }, [viewMode, editingId]); return (
{viewMode === "grid" ? ( /* ── Grid View: compact medication cards ── */

{t("medications.list.title")}

{orderedMeds.map((med) => (
{med.name}
{t("medications.details.type")}:{" "} {med.packageType === "bottle" ? t("form.packageTypeBottle") : t("form.packageTypeBlister")} {med.packageType === "blister" ? ( <> {t("medications.details.packs")}: {med.packCount} {t("medications.details.blisters")}: {med.blistersPerPack} {t("medications.details.pillsPerBlister")}: {med.pillsPerBlister} {t("medications.details.loose")}: {med.looseTablets} ) : ( {t("medications.details.totalCapacity")}:{" "} {med.totalPills ?? med.looseTablets} )}
{t("medications.details.stock")}:{" "} {coverageByMed[med.name] ? Math.round(coverageByMed[med.name].medsLeft) : getPackageSize(med)} /{" "} {getPackageSize(med)} {getPackageSize(med) === 1 ? t("common.pill") : t("common.pills")} {(coverageByMed[med.name] ? Math.round(coverageByMed[med.name].medsLeft) : getPackageSize(med)) > getPackageSize(med) && ( {" "} ⚠️ )} {med.prescriptionEnabled && (
{t("prescription.remainingRefills")}: {med.prescriptionRemainingRefills ?? 0}
)}
{editingId !== med.id && ( )}
{med.blisters.map((s, idx) => (
{s.usage} {s.usage === 1 ? t("common.pill") : t("common.pills")} · {t("form.blisters.every")}{" "} {s.every} {s.every === 1 ? t("common.day") : t("common.days")} · {t("form.blisters.from")}{" "} {formatDateTime(s.start)}
))}
))}
) : ( /* ── Form View: list panel + form panel (existing layout) ── */ <>

{t("medications.list.title")}

{orderedMeds.map((med) => (
{med.name}
{t("medications.details.type")}:{" "} {med.packageType === "bottle" ? t("form.packageTypeBottle") : t("form.packageTypeBlister")} {med.packageType === "blister" ? ( <> {t("medications.details.packs")}: {med.packCount} {t("medications.details.blisters")}: {med.blistersPerPack} {t("medications.details.pillsPerBlister")}: {med.pillsPerBlister} {t("medications.details.loose")}: {med.looseTablets} ) : ( {t("medications.details.totalCapacity")}:{" "} {med.totalPills ?? med.looseTablets} )}
{t("medications.details.stock")}:{" "} {coverageByMed[med.name] ? Math.round(coverageByMed[med.name].medsLeft) : getPackageSize(med)} /{" "} {getPackageSize(med)} {getPackageSize(med) === 1 ? t("common.pill") : t("common.pills")} {(coverageByMed[med.name] ? Math.round(coverageByMed[med.name].medsLeft) : getPackageSize(med)) > getPackageSize(med) && ( {" "} ⚠️ )} {med.prescriptionEnabled && (
{t("prescription.remainingRefills")}:{" "} {med.prescriptionRemainingRefills ?? 0}
)}
{editingId !== med.id && ( )}
{med.blisters.map((s, idx) => (
{s.usage} {s.usage === 1 ? t("common.pill") : t("common.pills")} · {t("form.blisters.every")}{" "} {s.every} {s.every === 1 ? t("common.day") : t("common.days")} · {t("form.blisters.from")}{" "} {formatDateTime(s.start)}
))}
))}
{editingId ? ( <> m.id === editingId)?.name || ""} imageUrl={meds.find((m) => m.id === editingId)?.imageUrl} size="md" />

{t("form.editEntry")}: {meds.find((m) => m.id === editingId)?.name}

) : (

{t("form.newEntry")}

)}

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

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

{form.packageType === "blister" ? ( <> ) : ( <> )}