import { useTranslation } from "react-i18next"; import { ConfirmModal, MedicationAvatar } from "../components"; import { useAuth } from "../components/Auth"; import { useAppContext } from "../context"; import type { Coverage } from "../types"; import { formatNumber, getExpiryClass, getSystemLocale } from "../utils/formatters"; import { getStockStatus } 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 calculate blister stock function getBlisterStock(totalPills: number, pillsPerBlister: number, _looseTablets: number, _originalTotal: number) { const fullBlisters = Math.floor(totalPills / pillsPerBlister); const openBlisterPills = totalPills % pillsPerBlister; return { fullBlisters, openBlisterPills, loosePills: openBlisterPills }; } // Helper to format full blisters function formatFullBlisters(count: number, t: (key: string) => string): string { return `${count} ${t("common.blisters")}`; } // Helper to format open blister and loose pills function formatOpenBlisterAndLoose( openBlisterPills: number, loosePills: number, pillsPerBlister: number, t: (key: string) => string ): string { if (openBlisterPills === 0 && loosePills === 0) return "-"; return `${openBlisterPills} ${t("common.of")} ${pillsPerBlister} ${t("common.pills")}`; } // Get total pills for a medication function getMedTotal(med: { packCount: number; blistersPerPack: number; pillsPerBlister: number; looseTablets: number; stockAdjustment?: number | null; }): number { return med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets + (med.stockAdjustment ?? 0); } // Notification bell SVG icon (no emoji) function NotificationBellIcon() { return ( ); } // Get structured reminder status data function getReminderStatusData( reminderDaysBefore: number, lowStockDays: number, lowCoverage: Coverage[], allCoverage: Coverage[], lastAutoEmailSent: string | null, lastNotificationType: string | null, _lastNotificationChannel: string | null, lastReminderMedName: string | null, lastReminderTakenBy: string | null, t: (key: string, options?: Record) => string, locale: string ): { status: { text: string; className: string }; lowStockMeds: { name: string; daysLeft: number; isCritical: boolean }[]; lastSent: { date: string; medName: string | null; takenBy: string | null } | null; } { const criticalCount = lowCoverage.length; const lowCount = allCoverage.filter((c) => { if (c.medsLeft <= 0) return false; if (c.daysLeft === null) return false; return c.daysLeft < lowStockDays && c.daysLeft > reminderDaysBefore; }).length; // Determine status let status: { text: string; className: string }; if (criticalCount > 0) { status = { text: t("dashboard.reminders.criticalMeds", { count: criticalCount }), className: "danger", }; } else if (lowCount > 0) { status = { text: t("dashboard.reminders.lowMeds", { count: lowCount }), className: "warning", }; } else { status = { text: t("dashboard.reminders.allOk"), className: "success", }; } // Collect all low stock medications (critical + low), deduplicated by name const lowStockMap = new Map(); // Add critical meds (from lowCoverage - these are ≤3 days) for (const c of lowCoverage) { if (c.daysLeft !== null) { const existing = lowStockMap.get(c.name); if (!existing || c.daysLeft < existing.daysLeft) { lowStockMap.set(c.name, { name: c.name, daysLeft: Math.round(c.daysLeft), isCritical: true }); } } } // Add low but not critical meds for (const c of allCoverage) { if (c.medsLeft <= 0) continue; if (c.daysLeft === null) continue; if (c.daysLeft < lowStockDays && c.daysLeft > reminderDaysBefore) { const existing = lowStockMap.get(c.name); if (!existing || c.daysLeft < existing.daysLeft) { lowStockMap.set(c.name, { name: c.name, daysLeft: Math.round(c.daysLeft), isCritical: false }); } } } // Convert to array and sort by days left (most urgent first) const lowStockMeds = Array.from(lowStockMap.values()).sort((a, b) => a.daysLeft - b.daysLeft); // Parse last sent info let lastSent: { date: string; medName: string | null; takenBy: string | null } | null = null; if (lastAutoEmailSent) { const lastSentDate = new Date(lastAutoEmailSent); const formattedDate = lastSentDate.toLocaleDateString(locale, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", }); lastSent = { date: formattedDate, medName: lastReminderMedName, takenBy: lastReminderTakenBy, }; } return { status, lowStockMeds, lastSent }; } export function DashboardPage() { const { t, i18n } = useTranslation(); const { user } = useAuth(); const { meds, settings, coverage, coverageByMed, depletionByMed, scheduleDays, setScheduleDays, showPastDays, setShowPastDays, showFutureDays, setShowFutureDays, pastDays, todayDay, futureDays, takenDoses, dismissedDoses, markDoseTaken, undoDoseTaken, manuallyCollapsedDays, manuallyExpandedDays, toggleDayCollapse, missedPastDoseIds, getDayStockStatus, getDoseId, showClearMissedConfirm, setShowClearMissedConfirm, clearingMissed, dismissMissedDoses, openMedDetail, openUserFilter, openShareDialog, openScheduleLightbox, stockThresholds, } = useAppContext(); // Get structured reminder data const reminderData = getReminderStatusData( settings.reminderDaysBefore, settings.lowStockDays, coverage.low, coverage.all, settings.lastAutoEmailSent, settings.lastNotificationType, settings.lastNotificationChannel, settings.lastReminderMedName, settings.lastReminderTakenBy, t, getSystemLocale(i18n.language) ); // Check which reminder types are actually enabled (channel must be enabled too) const stockRemindersEnabled = (settings.emailEnabled && settings.emailStockReminders) || (settings.shoutrrrEnabled && settings.shoutrrrStockReminders); const intakeRemindersEnabled = (settings.emailEnabled && settings.emailIntakeReminders) || (settings.shoutrrrEnabled && settings.shoutrrrIntakeReminders); const anyRemindersEnabled = stockRemindersEnabled || intakeRemindersEnabled; return ( <> {anyRemindersEnabled && (
{t("dashboard.reminders.active")} {reminderData.lowStockMeds.length === 0 && ( {reminderData.status.className === "success" && "✓ "} {reminderData.status.text} )}
{(reminderData.lowStockMeds.length > 0 || (intakeRemindersEnabled && reminderData.lastSent)) && (
{stockRemindersEnabled && reminderData.lowStockMeds.length > 0 && (
{t("dashboard.reminders.needsRefill")}: {reminderData.lowStockMeds.map((med, idx) => { const medication = meds.find((m) => m.name === med.name); const cov = coverage.all.find((c) => c.name === med.name); const status = cov ? getStockStatus(cov.daysLeft, cov.medsLeft, stockThresholds) : null; const textClass = status?.className === "danger" ? "danger-text" : status?.className === "warning" ? "warning-text" : ""; return ( {idx > 0 && ", "} medication && openMedDetail(medication)} > {med.name} {" "} {t("dashboard.reminders.daysLeft", { count: med.daysLeft, days: med.daysLeft })} ); })}
)} {intakeRemindersEnabled && reminderData.lastSent && (
{t("dashboard.reminders.lastSent")}: {reminderData.lastSent.medName && ( {reminderData.lastSent.medName} )} {reminderData.lastSent.takenBy && ( ({reminderData.lastSent.takenBy}) )} {reminderData.lastSent.date}
)}
)}
)} {/* Reorder Reminder card: Only show when reminders are NOT enabled (otherwise Reminder Bar shows the same info) */} {!anyRemindersEnabled && (

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

{(() => { if (meds.length === 0) { return

{t("dashboard.reorder.noMeds")}

; } // Count medications with low stock (based on lowStockDays setting), deduplicated by name const lowStockMap = new Map(); for (const c of coverage.all) { if (c.daysLeft === null && c.medsLeft > 0) continue; // no schedule, has stock if (c.medsLeft <= 0 || c.daysLeft === null || c.daysLeft < settings.lowStockDays) { const existing = lowStockMap.get(c.name); if (!existing || (c.daysLeft ?? 0) < (existing.daysLeft ?? 0)) { lowStockMap.set(c.name, c); } } } const lowStockMeds = Array.from(lowStockMap.values()); const lowStockCount = lowStockMeds.length; if (lowStockCount === 0) { // All good - everything is Normal or High return

{t("dashboard.reorder.allGood")}

; } // Some meds are low - show simple text with clickable names and days left return (

{t("dashboard.reorder.lowWarningPrefix")}{" "} {lowStockMeds.map((c, idx) => { const med = meds.find((m) => m.name === c.name); const status = getStockStatus(c.daysLeft, c.medsLeft, stockThresholds); const textClass = status.className === "danger" ? "danger-text" : status.className === "warning" ? "warning-text" : ""; return ( {idx > 0 && ", "} med && openMedDetail(med)}> {c.name} {" "} ({t("dashboard.reminders.daysLeft", { count: c.daysLeft ?? 0, days: c.daysLeft ?? 0 })}) ); })}{" "} {t("dashboard.reorder.lowWarningSuffix", { count: lowStockCount })}

); })()}
)}

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

{t("table.name")} {t("table.stock")} {t("table.stockDetails")} {t("table.daysLeft")} {t("table.runsOut")} {t("table.expiry")} {t("table.status")}
{coverage.all.map((row) => { const status = getStockStatus(row.daysLeft, row.medsLeft, stockThresholds); const med = meds.find((m) => m.name === row.name); const expiryClass = getExpiryClass(med?.expiryDate, settings.expiryWarningDays); const textClass = status.className === "danger" ? "danger-text" : status.className === "warning" ? "warning-text" : "success-text"; const stock = getBlisterStock( Math.round(row.medsLeft), med?.pillsPerBlister ?? 1, med?.looseTablets ?? 0, med ? getMedTotal(med) : Math.round(row.medsLeft) ); return (
med && openMedDetail(med)}> {row.name} {med?.takenBy && med.takenBy.length > 0 && med.takenBy.map((person) => ( { e.stopPropagation(); openUserFilter(person); }} > {person} ))} {(med?.intakeRemindersEnabled || med?.notes) && ( {med?.intakeRemindersEnabled && ( 🔔 )} {med?.notes && ( 📝 )} )} {med?.packageType === "bottle" ? t("table.pillsCount", { count: Math.round(row.medsLeft) }) : formatFullBlisters(stock.fullBlisters, t)} {med?.packageType === "bottle" ? "-" : formatOpenBlisterAndLoose( stock.openBlisterPills, stock.loosePills, med?.pillsPerBlister ?? 1, t )} {formatNumber(row.daysLeft)} {row.depletionDate ?? "-"} {med?.expiryDate ? new Date(med.expiryDate).toLocaleDateString(getSystemLocale(i18n.language), { day: "2-digit", month: "short", year: "2-digit", }) : "-"} {t(status.label)}
); })}

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

{meds.some((m) => m.takenBy && m.takenBy.length > 0) && ( )}
{/* Past days toggle */} {pastDays.length > 0 && (() => { const missedCount = missedPastDoseIds.length; const totalPastDoses = pastDays.flatMap((d) => d.meds.flatMap((m) => m.doses.flatMap((dose) => (dose.takenBy ? [`${dose.id}-${dose.takenBy}`] : [dose.id])) ) ); return (
0 ? "has-missed" : ""}`} onClick={() => setShowPastDays(!showPastDays)} > {showPastDays ? "▼" : "▶"} {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) {missedCount > 0 ? ( ⚠️ {missedCount} ) : totalPastDoses.length > 0 ? ( ) : null}
{missedCount > 0 && ( )}
); })()} {/* Past days (when expanded) */} {showPastDays && pastDays.map((day) => { const allDoseIds = day.meds.flatMap((item) => item.doses.flatMap((d) => (d.takenBy ? [`${d.id}-${d.takenBy}`] : [d.id])) ); const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id) || dismissedDoses.has(id)); const takenCount = allDoseIds.filter((id) => takenDoses.has(id) || dismissedDoses.has(id)).length; const isAutoCollapsed = true; // Past days are always auto-collapsed const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isCollapsed = !isManuallyExpanded; const worstStatus = getDayStockStatus(day.meds); 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 = meds.find((m) => m.name === item.medName); const medCov = coverageByMed[item.medName]; const isEmpty = medCov ? medCov.medsLeft <= 0 : false; const itemDoseIds = item.doses.flatMap((d) => (d.takenBy ? [`${d.id}-${d.takenBy}`] : [d.id])); const allTaken = itemDoseIds.every((id) => takenDoses.has(id)); return (
med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} >
{item.medName} {med?.intakeRemindersEnabled && ( 🔔 )}
{item.total} {t("common.pills")} {t("common.total")}
{item.doses.map((dose) => { // If no takenBy, show single checkbox; otherwise show one per person const people = dose.takenBy.length > 0 ? dose.takenBy : [null]; return (
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")} {med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}
{people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); return (
{person && ( openUserFilter(person)} > {person} )} {isTaken ? ( ) : ( )}
); })}
); })}
); })}
); })} {/* Today - always visible */} {todayDay && (() => { const day = todayDay; const allDoseIds = day.meds.flatMap((item) => item.doses.flatMap((d) => (d.takenBy ? [`${d.id}-${d.takenBy}`] : [d.id])) ); const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; const dayStockStatuses = day.meds.map((item) => { const medCoverage = coverageByMed[item.medName]; const depletionTime = depletionByMed[item.medName]; const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; if (willBeOutOfStock) return "danger"; if (!medCoverage) return "success"; const status = getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds); return status.className; }); const worstStatus = dayStockStatuses.includes("danger") ? "danger" : dayStockStatuses.includes("warning") ? "warning" : "success"; // Today: expanded by default, can be manually collapsed 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 medCoverage = coverageByMed[item.medName]; const med = meds.find((m) => m.name === item.medName); const depletionTime = depletionByMed[item.medName]; const isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false; const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; const status = willBeOutOfStock ? { className: "danger", label: "status.outOfStock" } : medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds) : null; const itemDoseIds = item.doses.flatMap((d) => (d.takenBy ? [`${d.id}-${d.takenBy}`] : [d.id])); const allTaken = itemDoseIds.every((id) => takenDoses.has(id)); return (
med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} >
{item.medName} {med?.intakeRemindersEnabled && ( 🔔 )}
{item.total} {t("common.pills")} {t("common.total")} {status && {t(status.label)}}
{item.doses.map((dose) => { const isOverdue = dose.when < Date.now(); const people = dose.takenBy ? [dose.takenBy] : [null]; const allTaken = people.every((person) => takenDoses.has(getDoseId(dose.id, person))); return (
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")} {med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}
{people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); return (
{person && ( openUserFilter(person)} > {person} )} {isTaken ? ( ) : ( )}
); })}
); })}
); })}
); })()} {/* Future days toggle */} {futureDays.length > 0 && (() => { const totalFutureDoses = futureDays.flatMap((d) => d.meds.flatMap((m) => m.doses.flatMap((dose) => dose.takenBy.length > 0 ? dose.takenBy.map((p) => `${dose.id}-${p}`) : [dose.id] ) ) ); const takenFutureDoses = totalFutureDoses.filter((id) => takenDoses.has(id)).length; return (
setShowFutureDays(!showFutureDays)} > {showFutureDays ? "▼" : "▶"} {showFutureDays ? t("dashboard.schedules.hideFutureDays") : t("dashboard.schedules.showFutureDays")} ({t("dashboard.schedules.futureDaysCount", { count: futureDays.length })}) {takenFutureDoses > 0 && totalFutureDoses.length > 0 && ( {takenFutureDoses}/{totalFutureDoses.length} )}
); })()} {/* Future days */} {showFutureDays && futureDays.map((day) => { const allDoseIds = day.meds.flatMap((item) => item.doses.flatMap((d) => (d.takenBy ? [`${d.id}-${d.takenBy}`] : [d.id])) ); const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; const dayStockStatuses = day.meds.map((item) => { const medCoverage = coverageByMed[item.medName]; const depletionTime = depletionByMed[item.medName]; const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; if (willBeOutOfStock) return "danger"; if (!medCoverage) return "success"; const status = getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds); return status.className; }); const worstStatus = dayStockStatuses.includes("danger") ? "danger" : dayStockStatuses.includes("warning") ? "warning" : "success"; // Future days: collapsed by default const isAutoCollapsed = true; const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isCollapsed = !isManuallyExpanded; 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 medCoverage = coverageByMed[item.medName]; const med = meds.find((m) => m.name === item.medName); const depletionTime = depletionByMed[item.medName]; const _isEmpty = medCoverage ? medCoverage.medsLeft <= 0 : false; const willBeOutOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; const status = willBeOutOfStock ? { className: "danger", label: "status.outOfStock" } : medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, stockThresholds) : null; const itemDoseIds = item.doses.flatMap((d) => (d.takenBy ? [`${d.id}-${d.takenBy}`] : [d.id])); const allTaken = itemDoseIds.every((id) => takenDoses.has(id)); return (
med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} >
{item.medName} {med?.intakeRemindersEnabled && ( 🔔 )}
{item.total} {t("common.pills")} {t("common.total")} {status && {t(status.label)}}
{item.doses.map((dose) => { const people = dose.takenBy.length > 0 ? dose.takenBy : [null]; const allTaken = people.every((person) => takenDoses.has(getDoseId(dose.id, person))); return (
{dose.timeStr} {dose.usage} {dose.usage !== 1 ? t("common.pills") : t("common.pill")} {med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}
{people.map((person) => { const doseId = getDoseId(dose.id, person); const isTaken = takenDoses.has(doseId); return (
{person && ( openUserFilter(person)} > {person} )} {isTaken ? ( ) : ( )}
); })}
); })}
); })}
); })}
{/* Clear Missed Doses Confirmation Modal */} {showClearMissedConfirm && ( dismissMissedDoses(missedPastDoseIds)} onCancel={() => setShowClearMissedConfirm(false)} isLoading={clearingMissed} /> )} ); }