/* biome-ignore-all lint/a11y/noLabelWithoutControl: planner uses custom DateTimeInput control wrappers */ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { DateTimeInput, MedicationAvatar } from "../components"; import { useAuth } from "../components/Auth"; import { useAppContext } from "../context"; import type { PlannerRow } from "../types"; import { getMedDisplayName, isAmountBasedPackageType, isLiquidContainerPackageType, isTubePackageType } from "../types"; import { toInputValue } from "../utils/formatters"; // Date helpers function todayIso(): string { return new Date().toISOString().slice(0, 10); } function plusDaysIso(days: number): string { const d = new Date(); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); } // Convert datetime-local value to ISO string function toIsoString(value: string): string { if (!value) return new Date().toISOString(); const date = new Date(value); return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString(); } // Helper for user-specific localStorage keys function userStorageKey(userId: number | undefined, key: string): string { return userId ? `user_${userId}_${key}` : key; } export function PlannerPage() { const { t } = useTranslation(); const { user, authFetch } = useAuth(); const { meds, settings, openMedDetail } = useAppContext(); // Local state for planner const [plannerRows, setPlannerRows] = useState([]); const [plannerLoading, setPlannerLoading] = useState(false); const [range, setRange] = useState<{ start: string; end: string }>({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)), }); const [includeUntilStart, setIncludeUntilStart] = useState(false); const [sendingPlannerEmail, setSendingPlannerEmail] = useState(false); const [plannerEmailResult, setPlannerEmailResult] = useState<{ success: boolean; message: string } | null>(null); // Load user-specific planner data when user changes useEffect(() => { if (typeof window !== "undefined" && user?.id) { const savedRows = localStorage.getItem(userStorageKey(user.id, "plannerRows")); const savedRange = localStorage.getItem(userStorageKey(user.id, "plannerRange")); const savedIncludeUntilStart = localStorage.getItem(userStorageKey(user.id, "plannerIncludeUntilStart")); if (savedRows) { try { setPlannerRows(JSON.parse(savedRows)); } catch { setPlannerRows([]); } } else { setPlannerRows([]); } if (savedRange) { try { setRange(JSON.parse(savedRange)); } catch { /* keep default */ } } else { setRange({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) }); } if (savedIncludeUntilStart) { setIncludeUntilStart(savedIncludeUntilStart === "true"); } else { setIncludeUntilStart(false); } } else { setPlannerRows([]); setRange({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) }); setIncludeUntilStart(false); } }, [user?.id]); async function runPlanner(e: React.FormEvent) { e.preventDefault(); setPlannerLoading(true); const body = { startDate: toIsoString(range.start), endDate: toIsoString(range.end), includeUntilStart }; const rows = (await authFetch("/api/medications/usage", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }) .then((res) => res.json()) .catch(() => [])) as PlannerRow[]; setPlannerRows(rows); setPlannerLoading(false); // Save to user-specific localStorage if (user?.id) { localStorage.setItem(userStorageKey(user.id, "plannerRange"), JSON.stringify(range)); localStorage.setItem(userStorageKey(user.id, "plannerRows"), JSON.stringify(rows)); localStorage.setItem(userStorageKey(user.id, "plannerIncludeUntilStart"), String(includeUntilStart)); } } function resetRange() { setRange({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) }); setIncludeUntilStart(false); setPlannerRows([]); if (user?.id) { localStorage.removeItem(userStorageKey(user.id, "plannerRange")); localStorage.removeItem(userStorageKey(user.id, "plannerRows")); localStorage.removeItem(userStorageKey(user.id, "plannerIncludeUntilStart")); } } const canSendNotification = (settings.emailEnabled && settings.notificationEmail) || (settings.shoutrrrEnabled && settings.shoutrrrUrl); const getDiscreteUnitLabel = (packageType: string | undefined, count: number): string => { if (packageType === "inhaler") return count === 1 ? t("common.puff") : t("common.puffs"); if (packageType === "injection") return count === 1 ? t("common.injection") : t("common.injections"); return count === 1 ? t("common.pill") : t("common.pills"); }; const getUsageUnitLabel = (medicationId: number, count: number): string => { const med = meds.find((m) => m.id === medicationId); if (isLiquidContainerPackageType(med?.packageType)) { return t("form.ml"); } if (isTubePackageType(med?.packageType)) { return med?.medicationForm === "liquid" ? t("form.ml") : t("blisters.applications"); } return getDiscreteUnitLabel(med?.packageType, count); }; const getAvailableLabel = (medicationId: number, loosePills: number): string => { const med = meds.find((m) => m.id === medicationId); const roundedLoose = Math.round(loosePills * 10) / 10; if (isLiquidContainerPackageType(med?.packageType)) { return `${roundedLoose} ${t("form.ml")}`; } if (isTubePackageType(med?.packageType)) { const unit = med?.medicationForm === "liquid" ? t("form.ml") : t("blisters.applications"); return `${roundedLoose} ${unit}`; } return `${roundedLoose} ${getDiscreteUnitLabel(med?.packageType, roundedLoose)}`; }; async function sendPlannerNotification() { if (!canSendNotification || plannerRows.length === 0) return; setSendingPlannerEmail(true); setPlannerEmailResult(null); try { const res = await authFetch("/api/planner/send-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: settings.notificationEmail, from: range.start, until: range.end, rows: plannerRows, }), }); const data = await res.json(); if (res.ok) { setPlannerEmailResult({ success: true, message: data.message || t("common.sent") }); } else { setPlannerEmailResult({ success: false, message: data.error || t("common.sendFailed") }); } } catch { setPlannerEmailResult({ success: false, message: t("common.networkError") }); } setSendingPlannerEmail(false); } return (

{t("planner.title")}

{plannerRows.length > 0 && ( <>
{t("planner.table.medication")} {t("planner.table.usage")} {t("planner.table.blistersNeeded")} {t("planner.table.prescriptionRefills")} {t("planner.table.available")} {t("table.status")}
{plannerRows.map((row) => { const med = meds.find((m) => m.id === row.medicationId) || meds.find((m) => getMedDisplayName(m) === row.medicationName); const remainingRefills = med?.prescriptionEnabled ? (med.prescriptionRemainingRefills ?? 0) : null; return (
med && openMedDetail(med)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { if (med) openMedDetail(med); } }} > {row.medicationName} {row.plannerUsage}  {getUsageUnitLabel(row.medicationId, row.plannerUsage)} {isAmountBasedPackageType(row.packageType) ? "–" : `${row.blistersNeeded} × ${row.blisterSize}`} {remainingRefills ?? "–"} {isAmountBasedPackageType(row.packageType) ? ( getAvailableLabel(row.medicationId, row.loosePills) ) : ( <> {row.fullBlisters} {t("common.blisters")} {row.loosePills > 0 && ` + ${Math.round(row.loosePills * 10) / 10} ${Math.round(row.loosePills * 10) / 10 === 1 ? t("common.pill") : t("common.pills")}`} )} {row.enough ? t("status.enough") : t("status.outOfStock")}
); })}
{canSendNotification && (
{plannerEmailResult && ( {plannerEmailResult.message} )}
)} )}
); }