import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { MedicationAvatar } from "../components"; import type { SharedMedicationOverviewItem, SharedMedicationOverviewResponse } from "../types"; import { getSystemLocale } from "../utils/formatters"; type ThemePreference = "light" | "dark" | "system"; function getSystemTheme(): "light" | "dark" { if (typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: light)").matches) { return "light"; } return "dark"; } function formatPackageInfo(medication: SharedMedicationOverviewItem): string { if (medication.packageType === "blister") { return `${medication.packCount} x ${medication.blistersPerPack} x ${medication.pillsPerBlister}`; } if (medication.totalPills !== null) { return `${medication.packCount} x ${medication.totalPills}`; } return `${medication.packCount}`; } function formatDate(dateValue: string | null, locale: string): string { if (!dateValue) return "-"; const parsed = new Date(`${dateValue}T00:00:00`); if (Number.isNaN(parsed.getTime())) return dateValue; return parsed.toLocaleDateString(locale); } export function SharedOverviewPage() { 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 [expiredAt, setExpiredAt] = useState(null); const [themePreference, setThemePreference] = useState(() => { if (typeof window !== "undefined") { const stored = localStorage.getItem("theme") as ThemePreference | null; if (stored === "light" || stored === "dark" || stored === "system") return stored; } return "dark"; }); const [themeMenuOpen, setThemeMenuOpen] = useState(false); const themeMenuRef = useRef(null); const resolvedTheme = themePreference === "system" ? getSystemTheme() : themePreference; useEffect(() => { document.documentElement.setAttribute("data-theme", resolvedTheme); localStorage.setItem("theme", themePreference); }, [themePreference, resolvedTheme]); useEffect(() => { if (themePreference !== "system") return; const mq = window.matchMedia?.("(prefers-color-scheme: light)"); if (!mq) return; const handler = () => { const resolved = mq.matches ? "light" : "dark"; document.documentElement.setAttribute("data-theme", resolved); }; mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, [themePreference]); useEffect(() => { if (!themeMenuOpen) return; const handleClickOutside = (event: MouseEvent) => { if (themeMenuRef.current && !themeMenuRef.current.contains(event.target as Node)) { setThemeMenuOpen(false); } }; document.addEventListener("click", handleClickOutside); return () => document.removeEventListener("click", handleClickOutside); }, [themeMenuOpen]); useEffect(() => { let isCancelled = false; async function loadOverview() { if (!token) { if (!isCancelled) { setLoading(false); setError(t("sharedOverview.error.notFound")); } return; } setLoading(true); setError(null); setExpiredAt(null); try { const response = await fetch(`/api/share/${token}/overview`); const responseData = await response.json().catch(() => ({})); if (!response.ok) { if (response.status === 404) { throw new Error("not_found"); } if (response.status === 410) { setExpiredAt(responseData.expiredAt ?? null); throw new Error("expired"); } if (response.status === 429) { throw new Error("rate_limited"); } throw new Error("load_failed"); } if (!isCancelled) { setData(responseData as SharedMedicationOverviewResponse); } } catch (loadError) { if (isCancelled) return; const message = loadError instanceof Error ? loadError.message : "load_failed"; if (message === "not_found") { setError(t("sharedOverview.error.notFound")); return; } if (message === "expired") { setError(t("sharedOverview.error.expired")); return; } if (message === "rate_limited") { setError(t("sharedOverview.error.rateLimit")); return; } setError(t("sharedOverview.error.generic")); } finally { if (!isCancelled) { setLoading(false); } } } void loadOverview(); return () => { isCancelled = true; }; }, [token, t]); if (loading) { return (

💊 MedAssist-ng

{t("common.loading")}
); } if (error || !data) { return (

💊 MedAssist-ng

{expiredAt ?
: null}

{t("sharedOverview.title")}

{error ?? t("sharedOverview.error.generic")}

{expiredAt ? (

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

) : null}
); } const locale = getSystemLocale(i18n.language); return (

{t("sharedOverview.title", { person: data.takenBy })}

{t("sharedOverview.sharedBy", { user: data.sharedBy ?? "-" })}

{data.medications.length === 0 ? (

{t("sharedOverview.noMedications")}

) : ( <>
{data.medications.map((medication) => { const priorityKey = medication.priority === "high" ? "sharedOverview.priority.high" : "sharedOverview.priority.normal"; return ( ); })}
{t("sharedOverview.columns.name")} {t("sharedOverview.columns.package")} {t("sharedOverview.columns.stock")} {t("sharedOverview.columns.daysLeft")}
{t("sharedOverview.columns.nextIntake")} {t("sharedOverview.columns.depletion")}
{t("sharedOverview.columns.priority")}
{medication.name} {medication.genericName ? {medication.genericName} : null}
{formatPackageInfo(medication)} {medication.currentStock === null || medication.capacity === null ? "-" : t("sharedOverview.stock.of", { current: medication.currentStock, capacity: medication.capacity, })} {medication.daysLeft === null ? "-" : medication.daysLeft}
{t("sharedOverview.columns.nextIntake")} {formatDate(medication.nextIntakeDate, locale)}
{t("sharedOverview.columns.depletion")} {formatDate(medication.depletionDate, locale)}
{medication.priority === null ? ( "-" ) : ( {t(priorityKey)} )}
{data.medications.map((medication) => { const priorityKey = medication.priority === "high" ? "sharedOverview.priority.high" : "sharedOverview.priority.normal"; return (
{medication.name} {medication.genericName ?

{medication.genericName}

: null}
{t("sharedOverview.columns.package")} {formatPackageInfo(medication)} {t("sharedOverview.columns.stock")} {medication.currentStock === null || medication.capacity === null ? "-" : t("sharedOverview.stock.of", { current: medication.currentStock, capacity: medication.capacity, })} {t("sharedOverview.columns.daysLeft")} {medication.daysLeft === null ? "-" : medication.daysLeft} {t("sharedOverview.columns.nextIntake")} {formatDate(medication.nextIntakeDate, locale)} {t("sharedOverview.columns.depletion")} {formatDate(medication.depletionDate, locale)}
{medication.priority ? ( {t(priorityKey)} ) : null}
); })}
)}
); }