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"; export function MedicationsPage() { const { t } = useTranslation(); const { meds, saving, setSaving, loadMeds, deleteMed, uploadMedImage, deleteMedImage, uploadingImage, existingPeople, refillPacks, setRefillPacks, refillLoose, setRefillLoose, 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); // 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); // 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) { 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); 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); } // 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(); } // Handle delete medication async function handleDeleteMed(id: number) { if (!confirm(t("medications.deleteConfirm"))) return; await deleteMed(id, editingId, resetForm); } // Handle submit refill async function handleSubmitRefill(medId: number) { await submitRefill(medId, editingId, setForm, loadMeds); } // 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 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, 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) { throw new Error("Failed to save"); } 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(); } else { // Update originalForm so formChanged becomes false setOriginalForm(form); } } catch (err) { console.error("Save error:", err); alert(t("common.saveFailed")); } setSaving(false); } // Handle browser back button for modals and unsaved changes useEffect(() => { const handlePopState = () => { // 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 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 setShowUnsavedConfirm(true); } }; window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); }, [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 function handleEditClick(med: Medication) { startEdit(med, openEditModal); } return (

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

{meds.map((med) => (
{med.name}
{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.stock")}:{" "} {coverageByMed[med.name] ? Math.round(coverageByMed[med.name].medsLeft) : getPackageSize(med)} /{" "} {getPackageSize(med)} {t("common.pills")}
{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")}

)}
{form.packageType === "blister" ? ( <> ) : ( <> )} {/* Refill section - only shown when editing */} {editingId && (

{t("refill.title")}

{(refillPacks > 0 || refillLoose > 0) && ( +{refillPacks * Number(form.blistersPerPack || 0) * Number(form.pillsPerBlister || 1) + refillLoose}{" "} {t("common.pills")} )}
)}