/* biome-ignore-all lint/style/noNestedTernary: schedule timeline branches are intentionally explicit */ import { Archive, Bell } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { ConfirmModal, MedicationAvatar } from "../components"; import { useAuth } from "../components/Auth"; import { ScheduleUsageTag } from "../features/schedule/components"; import { formatScheduleDoseUsageLabel, formatScheduleTotalUsageLabel } from "../features/schedule/formatters"; import { useScheduleController } from "../hooks"; import type { Coverage, IntakeUnit } from "../types"; import { getMedDisplayName, isLiquidContainerPackageType, isTubePackageType } from "../types"; import { buildClearMissedPayload, isDoseDismissed } from "../utils/schedule"; // Helper for user-specific localStorage keys function userStorageKey(userId: number | undefined, key: string): string { return userId ? `user_${userId}_${key}` : key; } // Helper function to get stock status based on thresholds function getStockStatus( daysLeft: number | null, medsLeft: number, settings: { lowStockDays: number; normalStockDays: number; highStockDays: number; reminderDaysBefore: number }, packageType?: string ) { if (isTubePackageType(packageType)) return { className: "success", label: "status.noSchedule" }; // Only a real zero-or-below stock count is out of stock. if (medsLeft <= 0) return { className: "danger", label: "status.outOfStock" }; // No schedule, but has stock = normal if (daysLeft === null) return { className: "success", label: "status.noSchedule" }; if (isLiquidContainerPackageType(packageType)) { const lowDays = Math.max(1, Math.floor(settings.reminderDaysBefore)); const criticalDays = Math.max(1, Math.ceil(lowDays / 2)); if (daysLeft <= criticalDays) return { className: "danger", label: "status.criticalStock" }; if (daysLeft <= lowDays) return { className: "warning", label: "status.lowStock" }; return { className: "success", label: "status.normal" }; } // Critical: at or below reminder threshold = danger (red) if (daysLeft <= settings.reminderDaysBefore) return { className: "danger", label: "status.criticalStock" }; // Low: below low stock threshold = warning (yellow) if (daysLeft < settings.lowStockDays) return { className: "warning", label: "status.lowStock" }; // High stock if (daysLeft >= settings.highStockDays) return { className: "high", label: "status.highStock" }; // Normal stock return { className: "success", label: "status.normal" }; } // Helper function to get worst stock status for a day function getDayStockStatus( dayMeds: Array<{ medName: string }>, coverageByMed: Record, settings: { lowStockDays: number; normalStockDays: number; highStockDays: number; reminderDaysBefore: number }, meds: Array<{ name: string; genericName?: string | null; packageType?: string }> ): string { let worstLevel = 3; // 3=success, 2=warning, 1=danger for (const item of dayMeds) { const cov = coverageByMed[item.medName]; if (!cov) continue; const med = meds.find((m) => getMedDisplayName(m) === item.medName); const status = getStockStatus(cov.daysLeft, cov.medsLeft, settings, med?.packageType); if (status.className === "danger") worstLevel = Math.min(worstLevel, 1); else if (status.className === "warning") worstLevel = Math.min(worstLevel, 2); } return worstLevel === 1 ? "danger" : worstLevel === 2 ? "warning" : "success"; } // Helper to get dose ID (with or without person) function getDoseId(baseId: string, person: string | null): string { return person ? `${baseId}-${person}` : baseId; } export function SchedulePage() { const { t } = useTranslation(); const { user } = useAuth(); const { meds, settings, scheduleDays, setScheduleDays, showPastDays, setShowPastDays, pastDays, futureDays, takenDoses, isDoseTakenAutomatically, dismissedDoses, markDoseTaken, skippedDoses, markDoseSkipped, undoDoseTaken, undoDoseSkipped, coverageByMed, depletionByMed, manuallyExpandedDays, toggleDayCollapse, openUserFilter, missedPastDoseIds, loadMeds, } = useScheduleController(); const [showClearMissedConfirm, setShowClearMissedConfirm] = useState(false); const [clearingMissed, setClearingMissed] = useState(false); const [showObsoleteConfirm, setShowObsoleteConfirm] = useState(false); const [obsoleteCandidate, setObsoleteCandidate] = useState<{ id: number; name: string } | null>(null); const isDoseTakenForDisplay = (doseId: string) => takenDoses.has(doseId); const shouldHideNoScheduleStatusForTube = ( med: (typeof meds)[number] | undefined, status: { className: string; label: string } | null ) => isTubePackageType(med?.packageType) && status?.label === "status.noSchedule"; const clearMissedDoses = async (missedCount: number) => { const payload = buildClearMissedPayload(pastDays, meds, takenDoses, dismissedDoses); if (payload.medicationIds.length === 0 || !payload.until) { setShowClearMissedConfirm(false); return; } setClearingMissed(true); try { const res = await fetch("/api/medications/dismiss-until", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } await loadMeds(); setShowClearMissedConfirm(false); alert(t("dashboard.schedules.clearMissedSuccess", { count: missedCount })); } catch { alert(t("common.saveFailed")); } finally { setClearingMissed(false); } }; const requestMarkObsolete = (med: { id: number; name: string }) => { setObsoleteCandidate(med); setShowObsoleteConfirm(true); }; const handleConfirmMarkObsolete = async () => { if (!obsoleteCandidate) return; try { const res = await fetch(`/api/medications/${obsoleteCandidate.id}/obsolete`, { method: "POST", credentials: "include", }); if (!res.ok) throw new Error(`HTTP ${res.status}`); await loadMeds(); setShowObsoleteConfirm(false); setObsoleteCandidate(null); } catch { alert(t("common.saveFailed")); } }; const handleCancelMarkObsolete = () => { setShowObsoleteConfirm(false); setObsoleteCandidate(null); }; const formatDoseUsageLabel = ( med: (typeof meds)[number] | undefined, usage: number, intakeUnit?: IntakeUnit | null ) => formatScheduleDoseUsageLabel(med, usage, t, intakeUnit); const formatTotalUsageLabel = ( med: (typeof meds)[number] | undefined, total: number, doses?: Array<{ usage: number; intakeUnit?: IntakeUnit | null }> ) => formatScheduleTotalUsageLabel(med, total, t, doses); const renderDoseActionButtons = (options: { doseId: string; isTaken: boolean; isSkipped: boolean; isAutomaticallyTaken: boolean; isEmpty: boolean; }) => { const takeButton = options.isTaken ? ( ) : ( ); const skipButton = options.isSkipped ? ( ) : ( ); return ( <> {takeButton} {skipButton} ); }; return (

{t("dashboard.schedules.title")}

{/* Past days (when expanded) — rendered above toggle */} {showPastDays && pastDays.map((day) => { // Get ALL dose IDs for this day (for total count and yellow styling) const allDoseIds = day.meds.flatMap((item) => item.doses.flatMap((d) => { const takenByArray = Array.isArray(d.takenBy) ? d.takenBy : []; return takenByArray.length > 0 ? takenByArray.map((p) => `${d.id}-${p}`) : [d.id]; }) ); // Really taken = all doses marked as taken by human (for green "All taken") const allReallyTaken = allDoseIds.length > 0 && allDoseIds.every((id) => isDoseTakenForDisplay(id)); const takenCount = allDoseIds.filter((id) => isDoseTakenForDisplay(id)).length; // Count missed doses that are NOT dismissed (for warning icon) const missedNotDismissedCount = day.meds.reduce((count, item) => { const med = meds.find((m) => getMedDisplayName(m) === item.medName); const dismissedUntilDate = med?.dismissedUntil ?? undefined; return ( count + item.doses.reduce((doseCount, d) => { if (isDoseDismissed(d.id, dismissedUntilDate)) return doseCount; const takenByArray = Array.isArray(d.takenBy) ? d.takenBy : []; const ids = takenByArray.length > 0 ? takenByArray.map((p) => `${d.id}-${p}`) : [d.id]; return doseCount + ids.filter((id) => !isDoseTakenForDisplay(id) && !dismissedDoses.has(id)).length; }, 0) ); }, 0); const hasRealMissed = missedNotDismissedCount > 0; const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isCollapsed = !isManuallyExpanded; const _worstStatus = getDayStockStatus(day.meds, coverageByMed, settings, meds); return (
0 ? "past-missed" : ""}`} >
toggleDayCollapse(day.dateStr, true)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, true); }} title={isCollapsed ? t("common.expand") : t("common.collapse")} > {isCollapsed ? "▶" : "▼"} {day.dateStr} {allReallyTaken ? ( ✓ {t("dashboard.schedules.allTaken")} ) : ( <> {hasRealMissed && ( ⚠️ )} {takenCount}/{allDoseIds.length} )}
{!isCollapsed && day.meds.map((item) => { const med = meds.find((m) => getMedDisplayName(m) === item.medName); const medCov = coverageByMed[item.medName]; const isEmpty = medCov ? medCov.medsLeft <= 0 : false; const rawStatus = medCov ? getStockStatus(medCov.daysLeft, medCov.medsLeft, settings, med?.packageType) : null; const visibleStatus = shouldHideNoScheduleStatusForTube(med, rawStatus) ? null : rawStatus; const isLowStock = !isEmpty && visibleStatus?.className === "warning"; const rowClasses = ["time-row"]; if (isEmpty) rowClasses.push("med-empty"); else if (isLowStock) rowClasses.push("med-low"); return (
{item.medName}
{formatTotalUsageLabel(med, item.total, item.doses)}
{item.doses.map((dose) => { // If no takenBy, show single checkbox; otherwise show one per person const people = (dose.takenBy || []).length > 0 ? dose.takenBy : [null]; const allTaken = people.every((person) => isDoseTakenForDisplay(getDoseId(dose.id, person)) ); const doseClasses = ["dose-item", "past"]; if (allTaken) doseClasses.push("all-taken"); if (isEmpty) doseClasses.push("med-empty"); else if (isLowStock) doseClasses.push("med-low"); return (
{dose.timeStr} {formatDoseUsageLabel(med, dose.usage, dose.intakeUnit)} {med?.pillWeightMg && ( {`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`} )} {" "} {dose.intakeRemindersEnabled && ( )}{" "}
{people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = isDoseTakenForDisplay(doseId); const isSkipped = skippedDoses.has(doseId); const isAutomaticallyTaken = isTaken && isDoseTakenAutomatically(doseId) && dose.when <= Date.now(); const personClasses = ["dose-person"]; if (isTaken) personClasses.push("taken"); if (isSkipped) personClasses.push("skipped"); return (
{person && ( openUserFilter(person)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") openUserFilter(person); }} > {person} )} {renderDoseActionButtons({ doseId, isTaken, isSkipped, isAutomaticallyTaken, isEmpty, })}
); })}
); })}
); })}
); })} {/* Past days toggle */} {pastDays.length > 0 && (() => { const missedCount = missedPastDoseIds.length; return (
0 ? "has-missed" : ""}`} onClick={() => { const wasCollapsed = !showPastDays; setShowPastDays(!showPastDays); if (wasCollapsed) { setTimeout(() => { document .querySelector(".day-block.today") ?.scrollIntoView({ behavior: "smooth", block: "center" }); }, 50); } }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { const wasCollapsed = !showPastDays; setShowPastDays(!showPastDays); if (wasCollapsed) { setTimeout(() => { document .querySelector(".day-block.today") ?.scrollIntoView({ behavior: "smooth", block: "center" }); }, 50); } } }} > {showPastDays ? "▼" : "▶"} {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) {missedCount > 0 && ( ⚠️ {missedCount} )}
{missedCount > 0 && ( )}
); })()} {showClearMissedConfirm && ( void clearMissedDoses(missedPastDoseIds.length)} onCancel={() => { if (!clearingMissed) setShowClearMissedConfirm(false); }} isLoading={clearingMissed} confirmVariant="warning" /> )} {showObsoleteConfirm && obsoleteCandidate && ( void handleConfirmMarkObsolete()} onCancel={handleCancelMarkObsolete} confirmVariant="warning" /> )} {/* Current and future days */} {futureDays.map((day) => { const today = new Date(); today.setHours(0, 0, 0, 0); const dayDate = new Date(day.date); dayDate.setHours(0, 0, 0, 0); const isToday = dayDate.getTime() === today.getTime(); return (
{day.dateStr}
{day.meds.map((item) => { const medCoverage = coverageByMed[item.medName]; const isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false; const med = meds.find((m) => getMedDisplayName(m) === item.medName); const depletionTime = depletionByMed[item.medName]; // Check if this dose is scheduled after medication runs out const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; const status = willBeOutOfStock ? { className: "danger", label: "status.outOfStock" } : medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings, med?.packageType) : null; const visibleStatus = shouldHideNoScheduleStatusForTube(med, status) ? null : status; const isLowStock = !isEmpty && visibleStatus?.className === "warning"; const rowClasses = ["time-row"]; if (isEmpty) rowClasses.push("med-empty"); else if (isLowStock) rowClasses.push("med-low"); return (
{item.medName}
{formatTotalUsageLabel(med, item.total, item.doses)} {visibleStatus && ( {t(visibleStatus.label)} )}
{isEmpty && med && !med.isObsolete && (
)}
{item.doses.map((dose) => { const people = (dose.takenBy || []).length > 0 ? dose.takenBy : [null]; const now = Date.now(); const dayStart = new Date(day.date).setHours(0, 0, 0, 0); const isPastDay = dayStart < new Date().setHours(0, 0, 0, 0); const allTaken = people.every((person) => isDoseTakenForDisplay(getDoseId(dose.id, person))); const doseClasses = ["dose-item"]; if (allTaken) doseClasses.push("all-taken"); if (isEmpty) doseClasses.push("med-empty"); else if (isLowStock) doseClasses.push("med-low"); return (
{dose.timeStr} {formatDoseUsageLabel(med, dose.usage, dose.intakeUnit)} {med?.pillWeightMg && ( {`${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"}`} )} {dose.intakeRemindersEnabled && ( )}
{people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = isDoseTakenForDisplay(doseId); const isSkipped = skippedDoses.has(doseId); const isAutomaticallyTaken = isTaken && isDoseTakenAutomatically(doseId) && dose.when <= now; const isOverdue = !isTaken && !isSkipped && !isEmpty && dose.when < now && !isPastDay; const personClasses = ["dose-person"]; if (isTaken) personClasses.push("taken"); if (isSkipped) personClasses.push("skipped"); if (isOverdue) personClasses.push("overdue"); return (
{person && ( openUserFilter(person)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") openUserFilter(person); }} > {person} )} {renderDoseActionButtons({ doseId, isTaken, isSkipped, isAutomaticallyTaken, isEmpty, })}
); })}
); })}
); })}
); })}
); }