import { useEffect, useMemo, useState } from "react"; import { Routes, Route, useNavigate, useLocation, Navigate } from "react-router-dom"; type Slice = { usage: number; every: number; start: string; }; type Medication = { id: number; name: string; count: number; strips: number; stripSize: number; packCount?: number; stripsPerPack?: number; tabsPerStrip?: number; looseTablets?: number; slices: Slice[]; imageUrl?: string | null; updatedAt: string | number | null; }; type PlannerRow = { medicationId: number; medicationName: string; plannerUsage: number; stripSize: number; stripsNeeded: number; stripsAvailable: number; enough: boolean; }; type FormSlice = { usage: string; every: string; start: string }; type FormState = { name: string; packCount: string; stripsPerPack: string; tabsPerStrip: string; looseTablets: string; slices: FormSlice[]; }; const defaultSlice = (): FormSlice => ({ usage: "1", every: "1", start: toInputValue(new Date().toISOString()) }); const defaultForm = (): FormState => ({ name: "", packCount: "1", stripsPerPack: "1", tabsPerStrip: "1", looseTablets: "0", slices: [defaultSlice()] }); const todayIso = () => new Date().toISOString(); const plusDaysIso = (days: number) => { const d = new Date(); d.setDate(d.getDate() + days); return d.toISOString(); }; type Coverage = { name: string; medsLeft: number; daysLeft: number | null; depletionDate: string | null; depletionTime: number | null; nextDose: string | null; }; export default function App() { const [meds, setMeds] = useState([]); const [plannerRows, setPlannerRows] = useState([]); const [plannerLoading, setPlannerLoading] = useState(false); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(defaultForm()); const [range, setRange] = useState<{ start: string; end: string }>({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) }); const navigate = useNavigate(); const location = useLocation(); const currentPath = location.pathname; // Settings state const [settings, setSettings] = useState({ emailEnabled: false, notificationEmail: "", reminderDaysBefore: 7, repeatDailyReminders: false, lowStockDays: 30, normalStockDays: 90, highStockDays: 180, smtpHost: "", smtpPort: 587, smtpUser: "", smtpPass: "", smtpFrom: "", smtpSecure: false, hasSmtpPassword: false, lastAutoEmailSent: null as string | null, nextScheduledCheck: null as string | null, }); const [savedSettings, setSavedSettings] = useState(settings); const [settingsLoading, setSettingsLoading] = useState(false); const [settingsSaving, setSettingsSaving] = useState(false); const [settingsSaved, setSettingsSaved] = useState(false); const [testingEmail, setTestingEmail] = useState(false); const [testEmailResult, setTestEmailResult] = useState<{ success: boolean; message: string } | null>(null); const [sendingPlannerEmail, setSendingPlannerEmail] = useState(false); const [plannerEmailResult, setPlannerEmailResult] = useState<{ success: boolean; message: string } | null>(null); const [sendingReminderEmail, setSendingReminderEmail] = useState(false); const [reminderEmailResult, setReminderEmailResult] = useState<{ success: boolean; message: string } | null>(null); const [uploadingImage, setUploadingImage] = useState(false); const [selectedMed, setSelectedMed] = useState(null); const [showImageLightbox, setShowImageLightbox] = useState(false); // Close modal on Escape key useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { if (showImageLightbox) { setShowImageLightbox(false); } else if (selectedMed) { setSelectedMed(null); } } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [selectedMed, showImageLightbox]); // Check if settings have changed const settingsChanged = settings.emailEnabled !== savedSettings.emailEnabled || settings.notificationEmail !== savedSettings.notificationEmail || settings.reminderDaysBefore !== savedSettings.reminderDaysBefore || settings.repeatDailyReminders !== savedSettings.repeatDailyReminders || settings.lowStockDays !== savedSettings.lowStockDays || settings.normalStockDays !== savedSettings.normalStockDays || settings.highStockDays !== savedSettings.highStockDays; const schedule = useMemo(() => buildSchedulePreview(meds), [meds]); const totalTablets = useMemo(() => deriveTotal(form), [form]); const coverage = useMemo(() => calculateCoverage(meds, schedule.events), [meds, schedule.events]); const depletionByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c.depletionTime])), [coverage.all]); const groupedSchedule = useMemo(() => { const days = new Map }>(); schedule.events.slice(0, 30).forEach((event) => { const day = days.get(event.dateStr) ?? { dateStr: event.dateStr, meds: new Map() }; const medEntry = day.meds.get(event.medName) ?? { medName: event.medName, total: 0, times: [], lastWhen: event.when }; medEntry.total += event.usage; medEntry.times.push(event.timeStr); medEntry.lastWhen = Math.max(medEntry.lastWhen, event.when); day.meds.set(event.medName, medEntry); days.set(event.dateStr, day); }); return Array.from(days.values()).map((d) => ({ dateStr: d.dateStr, meds: Array.from(d.meds.values()) })); }, [schedule.events]); useEffect(() => { loadMeds(); loadSettings(); }, []); function loadMeds() { setLoading(true); fetch("/api/medications") .then((res) => res.json()) .then((data: Medication[]) => setMeds(data)) .catch(() => setMeds([])) .finally(() => setLoading(false)); } function loadSettings() { setSettingsLoading(true); fetch("/api/settings") .then((res) => res.json()) .then((data) => { const newSettings = { ...settings, ...data, smtpPass: "" }; setSettings(newSettings); setSavedSettings(newSettings); setSettingsSaved(false); }) .catch(() => {}) .finally(() => setSettingsLoading(false)); } async function saveSettings(e: React.FormEvent) { e.preventDefault(); setSettingsSaving(true); setTestEmailResult(null); const payload = { emailEnabled: settings.emailEnabled, notificationEmail: settings.notificationEmail, reminderDaysBefore: settings.reminderDaysBefore, repeatDailyReminders: settings.repeatDailyReminders, lowStockDays: settings.lowStockDays, normalStockDays: settings.normalStockDays, highStockDays: settings.highStockDays, smtpHost: settings.smtpHost, smtpPort: settings.smtpPort, smtpUser: settings.smtpUser, smtpPass: settings.smtpPass || undefined, smtpFrom: settings.smtpFrom, smtpSecure: settings.smtpSecure, }; await fetch("/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }).catch(() => null); setSettingsSaving(false); setSavedSettings(settings); setSettingsSaved(true); } async function testEmail() { if (!settings.notificationEmail) return; setTestingEmail(true); setTestEmailResult(null); try { const res = await fetch("/api/settings/test-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: settings.notificationEmail }), }); const data = await res.json(); if (res.ok) { setTestEmailResult({ success: true, message: data.message || "Email sent!" }); } else { setTestEmailResult({ success: false, message: data.error || "Failed to send" }); } } catch { setTestEmailResult({ success: false, message: "Network error" }); } setTestingEmail(false); } async function sendPlannerEmail() { if (!settings.notificationEmail || plannerRows.length === 0) return; setSendingPlannerEmail(true); setPlannerEmailResult(null); try { const res = await fetch("/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 || "Email sent!" }); } else { setPlannerEmailResult({ success: false, message: data.error || "Failed to send" }); } } catch { setPlannerEmailResult({ success: false, message: "Network error" }); } setSendingPlannerEmail(false); } async function sendReminderEmail() { if (!settings.notificationEmail || coverage.low.length === 0) return; setSendingReminderEmail(true); setReminderEmailResult(null); try { const res = await fetch("/api/reminder/send-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: settings.notificationEmail, lowStock: coverage.low, }), }); const data = await res.json(); if (res.ok) { setReminderEmailResult({ success: true, message: data.message || "Email sent!" }); // Reload settings to get updated lastAutoEmailSent loadSettings(); } else { setReminderEmailResult({ success: false, message: data.error || "Failed to send" }); } } catch { setReminderEmailResult({ success: false, message: "Network error" }); } setSendingReminderEmail(false); } async function deleteMed(id: number) { await fetch(`/api/medications/${id}`, { method: "DELETE" }).catch(() => null); if (editingId === id) resetForm(); loadMeds(); } async function uploadMedImage(medId: number, file: File) { setUploadingImage(true); const formData = new FormData(); formData.append("file", file); try { const res = await fetch(`/api/medications/${medId}/image`, { method: "POST", body: formData, }); if (res.ok) { loadMeds(); } } catch { // ignore } setUploadingImage(false); } async function deleteMedImage(medId: number) { await fetch(`/api/medications/${medId}/image`, { method: "DELETE" }).catch(() => null); loadMeds(); } function setSliceValue(idx: number, field: keyof FormSlice, value: string) { setForm((prev) => { const next = [...prev.slices]; next[idx] = { ...next[idx], [field]: value }; return { ...prev, slices: next }; }); } function addSlice() { setForm((prev) => ({ ...prev, slices: [...prev.slices, defaultSlice()] })); } function removeSlice(idx: number) { setForm((prev) => ({ ...prev, slices: prev.slices.filter((_, i) => i !== idx) })); } function startEdit(med: Medication) { setEditingId(med.id); setForm({ name: med.name, packCount: String(med.packCount ?? 1), stripsPerPack: String(med.stripsPerPack ?? med.strips ?? 1), tabsPerStrip: String(med.tabsPerStrip ?? med.stripSize ?? 1), looseTablets: String(med.looseTablets ?? 0), slices: med.slices.map((s) => ({ usage: String(s.usage), every: String(s.every), start: toInputValue(s.start) })), }); } function resetForm() { setEditingId(null); setForm(defaultForm()); } function handleValueChange(key: K, value: string) { setForm((prev) => ({ ...prev, [key]: value })); } async function saveMedication(e: React.FormEvent) { e.preventDefault(); if (!form.name.trim()) return; setSaving(true); const payload = { name: form.name.trim(), packCount: Number(form.packCount) || 0, stripsPerPack: Math.max(1, Number(form.stripsPerPack) || 1), tabsPerStrip: Math.max(1, Number(form.tabsPerStrip) || 1), looseTablets: Math.max(0, Number(form.looseTablets) || 0), slices: form.slices.map((s) => ({ usage: Number(s.usage) || 0, every: Math.max(1, Number(s.every) || 1), start: toIsoString(s.start) })), }; const method = editingId ? "PUT" : "POST"; const url = editingId ? `/api/medications/${editingId}` : "/api/medications"; await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).catch(() => null); setSaving(false); resetForm(); loadMeds(); } async function runPlanner(e: React.FormEvent) { e.preventDefault(); setPlannerLoading(true); const body = { startDate: toIsoString(range.start), endDate: toIsoString(range.end) }; const rows = await fetch("/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); } function resetRange() { setRange({ start: toInputValue(todayIso()), end: toInputValue(plusDaysIso(3)) }); setPlannerRows([]); } const [theme, setTheme] = useState<"light" | "dark">(() => { if (typeof window !== "undefined") { return (localStorage.getItem("theme") as "light" | "dark") || "dark"; } return "dark"; }); useEffect(() => { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("theme", theme); }, [theme]); function toggleTheme() { setTheme((prev) => (prev === "dark" ? "light" : "dark")); } return (

Medassist · Planner

Manage medication plans

} /> {settings.emailEnabled && settings.notificationEmail && (
📧 Automatic reminders active — {getReminderStatusText(settings.reminderDaysBefore, coverage.low, settings.lastAutoEmailSent)} → {settings.notificationEmail}
)}

Reorder Reminder

Stock watch
{meds.length === 0 ? (

No medications configured yet.

) : coverage.low.length === 0 ? (

All good, enough stock.

) : ( <>
Name Current pills Days left Status Runs out Auto-remind
{coverage.low.map((row) => { const status = getStockStatus(row.daysLeft, row.medsLeft, settings); const med = meds.find(m => m.name === row.name); return (
med && setSelectedMed(med)}> {row.name} {formatNumber(row.medsLeft)} {formatNumber(row.daysLeft)} {status.label} {row.depletionDate ?? "-"} {getNextReminderForMed(row, settings.reminderDaysBefore)}
); })}
{settings.emailEnabled && settings.notificationEmail && (
{reminderEmailResult && ( {reminderEmailResult.message} )}
)} )}

Medication Overview

Stock
Name Current pills Days left Runs out Status
{coverage.all.map((row) => { const status = getStockStatus(row.daysLeft, row.medsLeft, settings); const med = meds.find(m => m.name === row.name); return (
med && setSelectedMed(med)}> {row.name} {formatNumber(row.medsLeft)} {formatNumber(row.daysLeft)} {row.depletionDate ?? "-"} {status.label}
); })}

Upcoming Schedules

Next 10
{groupedSchedule.map((day) => (
{day.dateStr}
{day.meds.map((item) => { const depletionTime = depletionByMed[item.medName]; const outOfStock = typeof depletionTime === "number" && item.lastWhen > depletionTime; const med = meds.find(m => m.name === item.medName); return (
{item.medName}
{item.total} pills total {outOfStock ? "⚠ No pills left" : "✓ Stock OK"}
{item.times.join(" · ")}
); })}
))}
} />

Medication list

{loading ? "Loading..." : `${meds.length} entries`}
{meds.map((med) => (
{med.name}
Packs: {med.packCount ?? 1} Blisters per pack: {med.stripsPerPack ?? med.strips ?? 1} Pills per blister: {med.tabsPerStrip ?? med.stripSize} Loose: {med.looseTablets ?? 0}
Total: {med.count} pills
{med.slices.map((s, idx) => (
{s.usage} {s.usage === 1 ? "pill" : "pills"} · every {s.every} {s.every === 1 ? "day" : "days"} · from {formatDateTime(s.start)}
))}
))}

{editingId ? "Edit entry" : "New entry"}

Packs + loose pills

Intake schedule

{form.slices.map((s, idx) => (
{form.slices.length > 1 && ( )}
))}
{editingId && (
{(() => { const currentMed = meds.find(m => m.id === editingId); if (currentMed?.imageUrl) { return (
{currentMed.name}
); } return ( e.target.files?.[0] && uploadMedImage(editingId, e.target.files[0])} disabled={uploadingImage} /> ); })()}
)}
{editingId && ( )}
} />

Demand Calculator

Plan your supply
{plannerRows.length > 0 && ( <>
Medication Usage Blisters needed Available Status
{plannerRows.map((row) => { const med = meds.find(m => m.name === row.medicationName); return (
med && setSelectedMed(med)}> {row.medicationName} {row.plannerUsage} pills {row.stripsNeeded} × {row.stripSize} {row.stripsAvailable} blisters {row.enough ? "✓ Enough" : "⚠ Out of Stock"}
); })}
{settings.emailEnabled && settings.notificationEmail && (
{plannerEmailResult && ( {plannerEmailResult.message} )}
)} )}
} />

Automatic Email Reminders

Daily check
{settingsLoading ? (

Loading settings...

) : (

Automatically send email when medications are running low

{settings.emailEnabled && ( <>

🤖 How it works: The server checks daily at 6:00 AM. When a medication drops below the threshold, you get an email.

Send daily emails while stock is low (otherwise only once per medication)

Next automatic check: {settings.nextScheduledCheck ? new Date(settings.nextScheduledCheck).toLocaleString() : "—"}

{settings.lastAutoEmailSent && (

✓ Last automatic email: {new Date(settings.lastAutoEmailSent).toLocaleString()}

)}
)}

Stock Thresholds

Define stock levels based on how many days of medication you have left.

{settings.emailEnabled && ( <>

SMTP Configuration

These settings are configured in the .env file.

Host {settings.smtpHost || "—"}
Port {settings.smtpPort}
User {settings.smtpUser || "—"}
Password {settings.hasSmtpPassword ? "••••••••" : "—"}
From {settings.smtpFrom || "—"}
SSL/TLS {settings.smtpSecure ? "Yes" : "No"}
{testEmailResult && ( {testEmailResult.message} )}
)}
)}
} />
{/* Medication Detail Modal */} {selectedMed && (
setSelectedMed(null)}>
e.stopPropagation()}>
selectedMed.imageUrl && setShowImageLightbox(true)} > {selectedMed.imageUrl && 🔍}

{selectedMed.name}

Stock Information

Total Pills {formatNumber(selectedMed.count)}
Packs {selectedMed.packCount ?? 0}
Blisters/Pack {selectedMed.stripsPerPack ?? 0}
Pills/Blister {selectedMed.tabsPerStrip ?? 1}
Loose Pills {selectedMed.looseTablets ?? 0}
{selectedMed.slices.length > 0 && (

Intake Schedule

{selectedMed.slices.map((slice, idx) => (
{slice.usage} pill{slice.usage !== 1 ? "s" : ""} every {slice.every} day{slice.every !== 1 ? "s" : ""} at {new Date(slice.start).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
))}
)} {(() => { const medCoverage = coverage.all.find(c => c.name === selectedMed.name); if (!medCoverage) return null; const status = getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings); return (

Coverage Status

Days Left {medCoverage.daysLeft !== null ? formatNumber(medCoverage.daysLeft) : "—"}
Runs Out {medCoverage.depletionDate ?? "—"}
Status {status.label}
); })()}
{/* Image Lightbox */} {showImageLightbox && selectedMed.imageUrl && (
setShowImageLightbox(false)}> {selectedMed.name} e.stopPropagation()} />
)}
)}
); } function deriveTotal(form: FormState) { const packCount = Number(form.packCount) || 0; const stripsPerPack = Number(form.stripsPerPack) || 0; const tabsPerStrip = Number(form.tabsPerStrip) || 1; const looseTablets = Number(form.looseTablets) || 0; return packCount * stripsPerPack * tabsPerStrip + looseTablets; } function toIsoString(value: string) { if (!value) return new Date().toISOString(); const date = new Date(value); return Number.isNaN(date.getTime()) ? new Date().toISOString() : date.toISOString(); } function toInputValue(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return new Date().toISOString().slice(0, 16); const iso = date.toISOString(); return iso.slice(0, 16); } function formatDateTime(value: string) { const d = new Date(value); if (Number.isNaN(d.getTime())) return value; return d.toLocaleString([], { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }); } function buildSchedulePreview(meds: Medication[]) { const events: Array<{ id: string; medName: string; timeStr: string; dateStr: string; usage: number; when: number }> = []; const now = new Date(); const end = new Date(); end.setDate(end.getDate() + 3); meds.forEach((med) => { med.slices.forEach((slice, idx) => { const start = new Date(slice.start); if (Number.isNaN(start.getTime())) return; for (let d = new Date(start); d <= end; d.setDate(d.getDate() + slice.every)) { if (d < now) continue; const whenMs = d.getTime(); events.push({ id: `${med.id}-${idx}-${whenMs}`, medName: med.name, usage: slice.usage, when: whenMs, timeStr: d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), dateStr: d.toLocaleDateString([], { weekday: "short", day: "2-digit", month: "short" }), }); } }); }); events.sort((a, b) => a.when - b.when); const todayCount = events.filter((e) => { const t = new Date(e.when); const n = new Date(); return t.getFullYear() === n.getFullYear() && t.getMonth() === n.getMonth() && t.getDate() === n.getDate(); }).length; return { events, today: todayCount, nextThree: events.length, totalSlices: meds.reduce((acc, m) => acc + m.slices.length, 0) }; } function formatNumber(value: number | null) { if (value === null || Number.isNaN(value)) return "-"; if (Math.abs(value % 1) < 0.05) return Math.round(value).toLocaleString(); return value.toFixed(1); } function calculateCoverage(meds: Medication[], events: Array<{ medName: string; when: number }>) { const MS_PER_DAY = 86_400_000; const now = Date.now(); const coverage: Coverage[] = meds.map((m) => { const dailyRate = m.slices.reduce((sum, s) => sum + (s.every > 0 ? s.usage / s.every : 0), 0); let consumed = 0; m.slices.forEach((s) => { const start = new Date(s.start).getTime(); if (Number.isNaN(start) || start > now) return; const period = Math.max(1, s.every) * MS_PER_DAY; const occurrences = Math.floor((now - start) / period) + 1; // include today if started consumed += occurrences * s.usage; }); const medsLeft = Math.max(0, m.count - consumed); const rawDaysLeft = dailyRate > 0 ? medsLeft / dailyRate : null; const daysLeft = rawDaysLeft !== null ? Math.max(0, Math.floor(rawDaysLeft)) : null; // conservative: round down const depletionMs = daysLeft !== null ? now + daysLeft * MS_PER_DAY : null; const depletionDate = depletionMs !== null ? new Date(depletionMs).toLocaleDateString([], { weekday: "short", day: "2-digit", month: "short" }) : null; const nextEvent = events.find((e) => e.medName === m.name); return { name: m.name, medsLeft: Number(medsLeft.toFixed(1)), daysLeft, depletionDate, depletionTime: depletionMs, nextDose: nextEvent ? new Date(nextEvent.when).toLocaleString([], { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : null, }; }); const low = coverage.filter((c) => c.medsLeft <= 0 || (c.daysLeft !== null && c.daysLeft <= 3)); return { low, all: coverage }; } function getReminderStatusText(reminderDaysBefore: number, lowStock: Coverage[], lastSent: string | null): React.ReactNode { // Find the earliest medication that needs a reminder (based on reminderDaysBefore) const medsNeedingReminder = lowStock .filter((c) => c.depletionTime !== null && c.daysLeft !== null && c.daysLeft <= reminderDaysBefore) .sort((a, b) => (a.daysLeft ?? 0) - (b.daysLeft ?? 0)); const formatLastSent = (iso: string) => { const date = new Date(iso); return date.toLocaleDateString([], { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }); }; if (medsNeedingReminder.length > 0) { // There are medications that need reminders if (lastSent) { return ( <> ⚠ {medsNeedingReminder.length} med{medsNeedingReminder.length > 1 ? "s" : ""} need reorder {" · "}Last email: {formatLastSent(lastSent)} ); } return ⚠ {medsNeedingReminder.length} med{medsNeedingReminder.length > 1 ? "s" : ""} need reorder — waiting for first check; } // Calculate when next reminder would be triggered const allWithDepletion = lowStock .filter((c) => c.depletionTime !== null && c.daysLeft !== null) .sort((a, b) => (a.daysLeft ?? Infinity) - (b.daysLeft ?? Infinity)); if (allWithDepletion.length > 0) { const nextMed = allWithDepletion[0]; const daysUntilReminder = (nextMed.daysLeft ?? 0) - reminderDaysBefore; if (daysUntilReminder > 0) { return ( <> ✓ All OK {" · "}Next: {nextMed.name} in {daysUntilReminder} days ); } } // No low stock medications at all if (lastSent) { return ( <> ✓ All stock OK {" · "}Last email: {formatLastSent(lastSent)} ); } return ✓ All stock OK — no reminders needed; } function getNextReminderForMed(med: Coverage, reminderDaysBefore: number): string { if (!med.depletionTime) return "—"; const reminderTime = med.depletionTime - reminderDaysBefore * 86_400_000; const now = Date.now(); if (reminderTime <= now) { return "Due now"; } return new Date(reminderTime).toLocaleDateString([], { day: "2-digit", month: "short", }); } type StockStatus = { level: "out-of-stock" | "low" | "normal" | "high"; className: string; label: string; }; type StockThresholds = { lowStockDays: number; normalStockDays: number; highStockDays: number; }; function getStockStatus(daysLeft: number | null, medsLeft: number, thresholds: StockThresholds): StockStatus { // Out of stock: 0 pills if (medsLeft <= 0 || daysLeft === 0) { return { level: "out-of-stock", className: "danger", label: "Out of Stock" }; } // No schedule set (no daysLeft calculation possible) if (daysLeft === null) { return { level: "normal", className: "success", label: "No Schedule" }; } // High stock: > highStockDays (e.g. > 180 days) if (daysLeft > thresholds.highStockDays) { return { level: "high", className: "high", label: "★ High Stock" }; } // Normal stock: between lowStockDays and highStockDays if (daysLeft >= thresholds.lowStockDays) { return { level: "normal", className: "success", label: "Normal" }; } // Low stock: < lowStockDays (e.g. < 30 days) return { level: "low", className: "warning", label: "Low Stock" }; } function MedicationAvatar({ name, imageUrl, size = "sm" }: { name: string; imageUrl?: string | null; size?: "sm" | "md" | "lg" }) { const initials = name.split(" ").map(w => w[0]).join("").toUpperCase().slice(0, 2) || "?"; const sizeClass = `med-avatar med-avatar-${size}`; if (imageUrl) { return {name}; } return
{initials}
; }