// ============================================================================= // SharedSchedule Component - Public view for shared schedules // ============================================================================= import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import type { ExpiredLinkData, SharedScheduleData } from "../types"; import { getMedTotal } from "../types"; import { getSystemLocale } from "../utils/formatters"; import { isDoseDismissed } from "../utils/schedule"; import { loadCollapsedDaysFromStorage } from "../utils/storage"; import { MedicationAvatar } from "./MedicationAvatar"; export function SharedSchedule() { const { token } = useParams<{ token: string }>(); const { t, i18n } = useTranslation(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expiredData, setExpiredData] = useState(null); const [takenDoses, setTakenDoses] = useState>(new Set()); const [dismissedDoses, setDismissedDoses] = useState>(new Set()); const [lightboxImage, setLightboxImage] = useState<{ url: string; name: string } | null>(null); const [showPastDays, setShowPastDays] = useState(false); const [showFutureDays, setShowFutureDays] = useState(false); const [theme, setTheme] = useState<"light" | "dark">(() => { if (typeof window !== "undefined") { return (localStorage.getItem("theme") as "light" | "dark") || "dark"; } return "dark"; }); // Apply theme to document useEffect(() => { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); }, [theme]); function toggleTheme() { setTheme((prev) => (prev === "dark" ? "light" : "dark")); } // Collapsed days state for SharedSchedule (token-specific localStorage) const [manuallyCollapsedDays, setManuallyCollapsedDays] = useState>(new Set()); const [manuallyExpandedDays, setManuallyExpandedDays] = useState>(new Set()); // Load collapsed/expanded state from localStorage useEffect(() => { if (token && typeof window !== "undefined") { const { collapsed, expanded } = loadCollapsedDaysFromStorage( `share_${token}_collapsedDays`, `share_${token}_expandedDays` ); setManuallyCollapsedDays(collapsed); setManuallyExpandedDays(expanded); } }, [token]); // Toggle day collapse/expand for SharedSchedule function toggleDayCollapse(dateStr: string, isAutoCollapsed: boolean) { if (isAutoCollapsed) { setManuallyExpandedDays((prev) => { const next = new Set(prev); if (next.has(dateStr)) { next.delete(dateStr); } else { next.add(dateStr); } if (token) localStorage.setItem(`share_${token}_expandedDays`, JSON.stringify([...next])); return next; }); } else { setManuallyCollapsedDays((prev) => { const next = new Set(prev); if (next.has(dateStr)) { next.delete(dateStr); } else { next.add(dateStr); } if (token) localStorage.setItem(`share_${token}_collapsedDays`, JSON.stringify([...next])); return next; }); } } // Helper functions for lightbox with history support (mobile back swipe) function openLightbox(url: string, name: string) { setLightboxImage({ url, name }); window.history.pushState({ modal: "lightbox" }, ""); } function closeLightbox() { if (lightboxImage) { window.history.back(); } } // Close lightbox on Escape key useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.key === "Escape" && lightboxImage) { closeLightbox(); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [lightboxImage, closeLightbox]); // Handle browser back button to close lightbox useEffect(() => { function handlePopState() { if (lightboxImage) { setLightboxImage(null); } } window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); }, [lightboxImage]); // Load taken doses from server with polling for real-time sync // Separates taken and dismissed doses (like main app's useDoses hook) useEffect(() => { if (token) { async function loadTakenDoses() { try { const res = await fetch(`/api/share/${token}/doses`); if (res.ok) { const data = await res.json(); const taken = new Set(); const dismissed = new Set(); for (const d of data.doses as Array<{ doseId: string; dismissed?: boolean }>) { if (d.dismissed) { dismissed.add(d.doseId); } else { taken.add(d.doseId); } } setTakenDoses(taken); setDismissedDoses(dismissed); } else { setTakenDoses(new Set()); setDismissedDoses(new Set()); } } catch { setTakenDoses(new Set()); setDismissedDoses(new Set()); } } loadTakenDoses(); // Poll for updates every 5 seconds (real-time sync with dashboard) const interval = setInterval(loadTakenDoses, 5000); return () => clearInterval(interval); } }, [token]); // Get dose ID - for per-intake takenBy, the ID already has the person suffix // This helper is kept for compatibility but since dose.id already includes the suffix, it just returns the id function getDoseId(doseId: string, _person: string | null): string { // The dose.id already includes the person suffix if there's a per-intake takenBy return doseId; } // Count taken doses for a day/item (simplified - per-intake takenBy means one person per dose) function _countTakenDoses(doses: Array<{ id: string; takenBy: string | null }>): { total: number; taken: number } { let total = 0; let taken = 0; for (const d of doses) { total++; if (takenDoses.has(d.id)) taken++; } return { total, taken }; } async function markDoseTaken(doseId: string) { // Optimistic update setTakenDoses((prev) => { const next = new Set(prev); next.add(doseId); return next; }); // Send to server try { await fetch(`/api/share/${token}/doses`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ doseId }), }); } catch { // Revert on error setTakenDoses((prev) => { const next = new Set(prev); next.delete(doseId); return next; }); } } async function undoDoseTaken(doseId: string) { // Optimistic update setTakenDoses((prev) => { const next = new Set(prev); next.delete(doseId); return next; }); // Send to server try { await fetch(`/api/share/${token}/doses/${encodeURIComponent(doseId)}`, { method: "DELETE", }); } catch { // Revert on error setTakenDoses((prev) => { const next = new Set(prev); next.add(doseId); return next; }); } } useEffect(() => { async function fetchData() { if (!token) { setError("Invalid link"); setLoading(false); return; } try { const res = await fetch(`/api/share/${token}`); if (res.ok) { const json = await res.json(); setData(json); } else if (res.status === 410) { // Link expired - get owner info const json = await res.json(); setExpiredData({ ownerUsername: json.ownerUsername, takenBy: json.takenBy, expiredAt: json.expiredAt, }); } else if (res.status === 404) { setError(t("share.notFound")); } else { setError(t("share.error")); } } catch { setError(t("share.error")); } finally { setLoading(false); } } fetchData(); }, [token, t]); // Build schedule from medications - matches buildSchedulePreview logic exactly const schedule = useMemo(() => { if (!data) return []; // Use same logic as buildSchedulePreview in main app const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // Midnight today // Use 180 days horizon like main app (scheduleDays only limits futureDays display) const end = new Date(); end.setDate(end.getDate() + 180); const doses: { id: string; when: number; medName: string; usage: number; timeStr: string; isPast: boolean; takenBy: string | null; // Per-intake takenBy (single person or null) dateStr: string; }[] = []; for (const med of data.medications) { // Use intakes (with per-intake takenBy) if available, fallback to blisters (legacy) const intakes = med.intakes || med.blisters.map((b) => ({ ...b, takenBy: null as string | null, intakeRemindersEnabled: false })); intakes.forEach((intake, intakeIdx) => { // Filter: only include intakes for this person (null = everyone, or matches share's takenBy) if (intake.takenBy !== null && intake.takenBy !== data.takenBy) return; const startDate = new Date(intake.start); if (Number.isNaN(startDate.getTime())) return; // Use the same iteration method as buildSchedulePreview (setDate instead of adding ms) // This ensures identical timestamps even across DST changes for (let d = new Date(startDate); d <= end; d.setDate(d.getDate() + intake.every)) { const t = d.getTime(); const isPast = d < todayStart; // Use date-only timestamp for stable ID (immune to time changes) // This ensures changing intake times doesn't invalidate past dose tracking // Must match buildSchedulePreview in schedule.ts exactly const dateOnlyMs = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // Dose ID includes person suffix if there's a per-intake takenBy const baseDoseId = `${med.id}-${intakeIdx}-${dateOnlyMs}`; const doseId = intake.takenBy ? `${baseDoseId}-${intake.takenBy}` : baseDoseId; doses.push({ id: doseId, when: t, medName: med.name, usage: intake.usage, isPast, takenBy: intake.takenBy, // Per-intake takenBy (string | null) timeStr: d.toLocaleTimeString(getSystemLocale(i18n.language), { hour: "2-digit", minute: "2-digit" }), dateStr: d.toLocaleDateString(getSystemLocale(i18n.language), { weekday: "short", day: "2-digit", month: "short", }), }); } }); } doses.sort((a, b) => a.when - b.when); // Group by date - matches groupedSchedule logic in main app type DoseInfo = (typeof doses)[number]; const days = new Map< string, { dateStr: string; date: Date; isPast: boolean; meds: Map; } >(); for (const dose of doses.slice(0, 2000)) { const day = days.get(dose.dateStr) ?? { dateStr: dose.dateStr, date: new Date(dose.when), isPast: dose.isPast, meds: new Map(), }; const medEntry = day.meds.get(dose.medName) ?? { medName: dose.medName, total: 0, doses: [], lastWhen: dose.when, }; medEntry.total += dose.usage; medEntry.doses.push(dose); medEntry.lastWhen = Math.max(medEntry.lastWhen, dose.when); day.meds.set(dose.medName, medEntry); days.set(dose.dateStr, day); } return Array.from(days.values()).map((d) => ({ dateStr: d.dateStr, date: d.date, isPast: d.isPast, meds: Array.from(d.meds.values()), })); }, [data, i18n.language]); // Split into past, today, and future - matches main app logic const pastDays = useMemo(() => schedule.filter((d) => d.isPast), [schedule]); // Separate today from future days const { todayDay, futureDays } = useMemo(() => { const today = new Date(); const todayStr = today.toLocaleDateString(getSystemLocale(i18n.language), { weekday: "short", day: "2-digit", month: "short", }); const nonPastDays = schedule.filter((d) => !d.isPast).slice(0, data?.scheduleDays ?? 30); const todayEntry = nonPastDays.find((d) => d.dateStr === todayStr); const future = nonPastDays.filter((d) => d.dateStr !== todayStr); return { todayDay: todayEntry || null, futureDays: future }; }, [schedule, data?.scheduleDays, i18n.language]); // Build a map of medication name -> dismissedUntil date string // This is robust against timestamp changes from schedule updates or timezone fixes const dismissedUntilByMed = useMemo(() => { if (!data) return new Map(); const map = new Map(); for (const med of data.medications) { if (med.dismissedUntil) { map.set(med.name, med.dismissedUntil); } } return map; }, [data]); // Helper to check if a dose date is on or before the dismissedUntil date function isDoseDismissedByName(doseTimestamp: number, medName: string): boolean { const dismissedUntilDate = dismissedUntilByMed.get(medName); if (!dismissedUntilDate) return false; // Compare date strings (YYYY-MM-DD format sorts correctly) const doseDate = new Date(doseTimestamp); const doseDateStr = `${doseDate.getFullYear()}-${String(doseDate.getMonth() + 1).padStart(2, "0")}-${String(doseDate.getDate()).padStart(2, "0")}`; return doseDateStr <= dismissedUntilDate; } // Calculate coverage for stock status colors (matches main app logic) // This needs to account for taken doses and calculate depletion time const { coverageByMed, depletionByMed } = useMemo(() => { if (!data) return { coverageByMed: {}, depletionByMed: {} }; const coverage: Record = {}; const depletion: Record = {}; // Calculate total pills taken per medication from takenDoses // With per-intake takenBy, each dose.id is unique and already has person suffix if needed const takenByMed: Record = {}; for (const dose of schedule.flatMap((d) => d.meds.flatMap((m) => m.doses))) { if (takenDoses.has(dose.id)) { takenByMed[dose.medName] = (takenByMed[dose.medName] || 0) + dose.usage; } } for (const med of data.medications) { const totalCount = getMedTotal(med); const taken = takenByMed[med.name] || 0; const currentCount = Math.max(0, totalCount - taken); // Calculate daily usage from intakes (or blisters for legacy) const intakes = med.intakes || med.blisters; const dailyUsage = intakes.reduce((sum, b) => sum + b.usage / b.every, 0); const daysLeft = dailyUsage > 0 ? currentCount / dailyUsage : null; coverage[med.name] = { daysLeft, medsLeft: currentCount, dailyUsage }; // Calculate depletion time (when medication will run out) if (dailyUsage > 0 && currentCount > 0) { const daysUntilEmpty = currentCount / dailyUsage; depletion[med.name] = Date.now() + daysUntilEmpty * 24 * 60 * 60 * 1000; } else if (currentCount <= 0) { depletion[med.name] = Date.now(); // Already empty } else { depletion[med.name] = null; // No usage schedule } } return { coverageByMed: coverage, depletionByMed: depletion }; }, [data, schedule, takenDoses]); // Stock thresholds from user settings (provided by API) or defaults const lowStockDays = data?.stockThresholds?.lowStockDays ?? 30; // Get worst stock status for a day's medications (matches main app logic with depletion) const getDayStockStatus = (meds: { medName: string; lastWhen: number }[]) => { const statuses = meds.map((item) => { const coverage = coverageByMed[item.medName]; const depletionTime = depletionByMed[item.medName]; // Will be out of stock by this day? if (typeof depletionTime === "number" && item.lastWhen > depletionTime) { return "danger"; } if (!coverage) return "success"; const { daysLeft, medsLeft } = coverage; // Currently out of stock if (medsLeft <= 0 || daysLeft === 0) return "danger"; // No schedule (can't calculate) if (daysLeft === null) return "success"; // Low stock: < lowStockDays (warning) if (daysLeft < lowStockDays) return "warning"; // Normal/High stock return "success"; }); return statuses.includes("danger") ? "danger" : statuses.includes("warning") ? "warning" : "success"; }; if (loading) { return (

๐Ÿ’Š MedAssist-ng

{t("common.loading")}

); } if (expiredData) { return (

๐Ÿ’Š MedAssist-ng

โฐ

{t("share.expired.title")}

{t("share.expired.message", { takenBy: expiredData.takenBy })}

{t("share.expired.contact", { username: expiredData.ownerUsername })}

{t("share.expired.expiredOn", { date: new Date(expiredData.expiredAt).toLocaleDateString(getSystemLocale(i18n.language)), })}

); } if (error || !data) { return (

๐Ÿ’Š MedAssist-ng

{error || "Unknown error"}

); } return (

๐Ÿ’Š {t("share.scheduleFor")} {data.takenBy}

{t("share.period")}:{" "} {data.scheduleDays === 30 ? t("dashboard.schedules.1month") : data.scheduleDays === 90 ? t("dashboard.schedules.3months") : t("dashboard.schedules.6months")}

{schedule.length === 0 ? (

{t("share.noSchedule")}

) : ( <> {/* Past days toggle */} {pastDays.length > 0 && (() => { // Count all past doses (for display) // With per-intake takenBy, each dose.id is unique const totalPastDoses = pastDays.flatMap((d) => d.meds.flatMap((m) => m.doses.map((dose) => dose.id))); // Count missed doses (not taken AND not dismissed AND not from previous schedule) // Check: per-dose dismissed flag, medication-level dismissedUntil, and updatedAt const missedPastDoses = totalPastDoses.filter((id) => { if (takenDoses.has(id)) return false; // Check if this dose is dismissed via per-dose flag from API if (dismissedDoses.has(id)) return false; // Check if dismissed via medication-level dismissedUntil date const parts = id.split("-"); if (parts.length >= 3) { const medId = parts[0]; const med = data?.medications.find((m) => String(m.id) === medId); if (med) { if (isDoseDismissed(id, med.dismissedUntil ?? undefined)) { return false; // dismissed = not missed } } } return true; // not taken, not dismissed = missed }).length; return (
0 ? "has-missed" : ""}`} onClick={() => setShowPastDays(!showPastDays)} > {showPastDays ? "โ–ผ" : "โ–ถ"} {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) {missedPastDoses > 0 ? ( โš ๏ธ {missedPastDoses} ) : totalPastDoses.length > 0 ? ( โœ“ ) : null}
); })()} {/* Past days (when expanded) */} {showPastDays && pastDays.map((day) => { // Helper to check if a dose ID is "done" (taken or dismissed) // Checks: per-dose dismissed flag and medication-level dismissedUntil const isDoseIdDone = (doseId: string) => { if (takenDoses.has(doseId)) return true; // Check if this dose is dismissed via per-dose flag from API if (dismissedDoses.has(doseId)) return true; // Check if dismissed via medication-level dismissedUntil date const parts = doseId.split("-"); if (parts.length >= 3) { const medId = parts[0]; const med = data?.medications.find((m) => String(m.id) === medId); if (med) { if (isDoseDismissed(doseId, med.dismissedUntil ?? undefined)) { return true; } } } return false; }; const allDoseIds = day.meds.flatMap((item) => item.doses.map((d) => d.id)); const allDayDone = allDoseIds.length > 0 && allDoseIds.every(isDoseIdDone); const doneCount = allDoseIds.filter(isDoseIdDone).length; const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isCollapsed = !isManuallyExpanded; // Calculate stock status for this day const worstStatus = getDayStockStatus(day.meds); return (
toggleDayCollapse(day.dateStr, true)} title={isCollapsed ? t("common.expand") : t("common.collapse")} > {isCollapsed ? "โ–ถ" : "โ–ผ"} {day.dateStr} {allDayDone ? ( โœ“ {t("dashboard.schedules.allTaken")} ) : ( <> โš ๏ธ {doneCount}/{allDoseIds.length} )}
{!isCollapsed && day.meds.map((item) => { const med = data.medications.find((m) => m.name === item.medName); const medCoverage = coverageByMed[item.medName]; const isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false; const depletionTime = depletionByMed[item.medName]; const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; // Calculate status for this medication on this day let status: { className: string; label: string } | null = null; if (willBeOutOfStock) { status = { className: "danger", label: "status.outOfStock" }; } else if (medCoverage) { const { daysLeft, medsLeft } = medCoverage; if (medsLeft <= 0 || daysLeft === 0) { status = { className: "danger", label: "status.outOfStock" }; } else if (daysLeft !== null && daysLeft < lowStockDays) { status = { className: "warning", label: "status.lowStock" }; } else { status = { className: "success", label: "status.normal" }; } } const itemDoseIds = item.doses.map((d) => d.id); // A dose is "done" if taken OR dismissed const allDone = itemDoseIds.every(isDoseIdDone); return (
med?.imageUrl && openLightbox(med.imageUrl, med.name)} > {item.medName} {med?.genericName && ({med.genericName})}
{item.total} {t("common.pills")} {t("common.total")} {status && {t(status.label)}}
{item.doses.map((dose) => { // Check: medication-level dismissedUntil and per-dose dismissed flag const isMedLevelDismissed = isDoseDismissedByName(dose.when, dose.medName); const isTaken = takenDoses.has(dose.id); const isPerDoseDismissed = dismissedDoses.has(dose.id); const isDone = isTaken || isPerDoseDismissed || isMedLevelDismissed; return (
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")} {med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}
{dose.takenBy && {dose.takenBy}} {isDone ? ( isTaken ? ( ) : ( // Dismissed - show checkmark but no undo โœ“ ) ) : ( )}
); })}
); })}
); })} {/* Today (always visible) */} {todayDay && (() => { const day = todayDay; const allDoseIds = day.meds.flatMap((item) => item.doses.map((d) => d.id)); const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; const worstStatus = getDayStockStatus(day.meds); // Today: only collapse if manually collapsed or all taken const isAutoCollapsed = allDayTaken; const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isManuallyCollapsed = manuallyCollapsedDays.has(day.dateStr); const isCollapsed = isAutoCollapsed ? !isManuallyExpanded : isManuallyCollapsed; return (
toggleDayCollapse(day.dateStr, isAutoCollapsed)} title={isCollapsed ? t("common.expand") : t("common.collapse")} > {isCollapsed ? "โ–ถ" : "โ–ผ"} {day.dateStr} {allDayTaken ? ( โœ“ {t("dashboard.schedules.allTaken")} ) : ( {takenCount}/{allDoseIds.length} )}
{!isCollapsed && day.meds.map((item) => { const med = data.medications.find((m) => m.name === item.medName); const medCoverage = coverageByMed[item.medName]; const isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false; const depletionTime = depletionByMed[item.medName]; const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; let status: { className: string; label: string } | null = null; if (willBeOutOfStock) { status = { className: "danger", label: "status.outOfStock" }; } else if (medCoverage) { const { daysLeft, medsLeft } = medCoverage; if (medsLeft <= 0 || daysLeft === 0) { status = { className: "danger", label: "status.outOfStock" }; } else if (daysLeft !== null && daysLeft < lowStockDays) { status = { className: "warning", label: "status.lowStock" }; } else { status = { className: "success", label: "status.normal" }; } } const itemDoseIds = item.doses.map((d) => d.id); const allTaken = itemDoseIds.every((id) => takenDoses.has(id)); return (
med?.imageUrl && openLightbox(med.imageUrl, med.name)} > {item.medName} {med?.genericName && ({med.genericName})}
{item.total} {t("common.pills")} {t("common.total")} {status && {t(status.label)}}
{item.doses.map((dose) => { const isTaken = takenDoses.has(dose.id); const isOverdue = dose.when < Date.now() && !isTaken; return (
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")} {med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}
{dose.takenBy && {dose.takenBy}} {isTaken ? ( ) : ( )}
); })}
); })}
); })()} {/* Future days toggle */} {futureDays.length > 0 && (
setShowFutureDays(!showFutureDays)} > {showFutureDays ? "โ–ผ" : "โ–ถ"} {showFutureDays ? t("dashboard.schedules.hideFutureDays") : t("dashboard.schedules.showFutureDays")} ({t("dashboard.schedules.futureDaysCount", { count: futureDays.length })})
)} {/* Future days (when expanded) */} {showFutureDays && futureDays.map((day) => { // Check if all doses in this day are taken (auto-collapse) const allDoseIds = day.meds.flatMap((item) => item.doses.map((d) => d.id)); const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; // Calculate stock status for this day const worstStatus = getDayStockStatus(day.meds); // Determine if day should be collapsed (auto-collapsed by default, manual override) const isAutoCollapsed = allDayTaken; const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isManuallyCollapsed = manuallyCollapsedDays.has(day.dateStr); const isCollapsed = isAutoCollapsed ? !isManuallyExpanded : isManuallyCollapsed; return (
toggleDayCollapse(day.dateStr, isAutoCollapsed)} title={isCollapsed ? t("common.expand") : t("common.collapse")} > {isCollapsed ? "โ–ถ" : "โ–ผ"} {day.dateStr} {allDayTaken ? ( โœ“ {t("dashboard.schedules.allTaken")} ) : ( {takenCount}/{allDoseIds.length} )}
{!isCollapsed && day.meds.map((item) => { const med = data.medications.find((m) => m.name === item.medName); const medCoverage = coverageByMed[item.medName]; const isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false; const depletionTime = depletionByMed[item.medName]; const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; // Calculate status for this medication on this day let status: { className: string; label: string } | null = null; if (willBeOutOfStock) { status = { className: "danger", label: "status.outOfStock" }; } else if (medCoverage) { const { daysLeft, medsLeft } = medCoverage; if (medsLeft <= 0 || daysLeft === 0) { status = { className: "danger", label: "status.outOfStock" }; } else if (daysLeft !== null && daysLeft < lowStockDays) { status = { className: "warning", label: "status.lowStock" }; } else { status = { className: "success", label: "status.normal" }; } } const itemDoseIds = item.doses.map((d) => d.id); const allTaken = itemDoseIds.every((id) => takenDoses.has(id)); return (
med?.imageUrl && openLightbox(med.imageUrl, med.name)} > {item.medName} {med?.genericName && ({med.genericName})}
{item.total} {t("common.pills")} {t("common.total")} {status && {t(status.label)}}
{item.doses.map((dose) => { const isTaken = takenDoses.has(dose.id); // Only disable doses on future DAYS, not later today const doseDate = new Date(dose.when); doseDate.setHours(0, 0, 0, 0); const todayMidnight = new Date(); todayMidnight.setHours(0, 0, 0, 0); const isFutureDose = doseDate.getTime() > todayMidnight.getTime(); const isOverdue = dose.when < Date.now() && !isTaken && !isFutureDose; return (
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")} {med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}
{dose.takenBy && {dose.takenBy}} {isTaken ? ( ) : ( )}
); })}
); })}
); })} )}

{t("share.generatedBy")}{" "} {data?.sharedBy && ( <> {data.sharedBy} ยท{" "} )} MedAssist

{/* Image Lightbox */} {lightboxImage && (
{lightboxImage.name} e.stopPropagation()} />
)}
); }