| "use client"; |
|
|
| import { useState, useEffect, useCallback, useMemo } from "react"; |
| import { |
| todayISO, |
| loadMedications, |
| loadMedicationLogs, |
| loadAppointments, |
| type Medication, |
| type Appointment, |
| } from "../health-store"; |
|
|
| export interface Notification { |
| id: string; |
| type: "medication" | "appointment" | "reminder" | "info"; |
| title: string; |
| message: string; |
| time: string; |
| read: boolean; |
| urgent: boolean; |
| } |
|
|
| const DISMISSED_KEY = "medos_dismissed_notifications"; |
|
|
| function loadDismissed(): Set<string> { |
| try { |
| const raw = localStorage.getItem(DISMISSED_KEY); |
| return raw ? new Set(JSON.parse(raw)) : new Set(); |
| } catch { |
| return new Set(); |
| } |
| } |
|
|
| function saveDismissed(ids: Set<string>): void { |
| try { |
| localStorage.setItem(DISMISSED_KEY, JSON.stringify([...ids])); |
| } catch {} |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function useNotifications() { |
| const [dismissed, setDismissed] = useState<Set<string>>(new Set()); |
| const [tick, setTick] = useState(0); |
|
|
| |
| useEffect(() => { |
| setDismissed(loadDismissed()); |
| const id = setInterval(() => setTick((t) => t + 1), 60000); |
| return () => clearInterval(id); |
| }, []); |
|
|
| const today = todayISO(); |
| const now = new Date(); |
| const nowMinutes = now.getHours() * 60 + now.getMinutes(); |
|
|
| const notifications: Notification[] = useMemo(() => { |
| const items: Notification[] = []; |
| const meds = loadMedications().filter((m) => m.active); |
| const logs = loadMedicationLogs(); |
| const appts = loadAppointments().filter( |
| (a) => a.date === today && a.status === "upcoming", |
| ); |
|
|
| |
| for (const med of meds) { |
| for (const time of med.times) { |
| const [h, m] = time.split(":").map(Number); |
| const medMinutes = (h || 0) * 60 + (m || 0); |
| const taken = logs.some( |
| (l) => |
| l.medicationId === med.id && |
| l.date === today && |
| l.time === time && |
| l.taken, |
| ); |
|
|
| if (taken) continue; |
|
|
| const overdue = nowMinutes > medMinutes + 30; |
| const dueSoon = !overdue && nowMinutes >= medMinutes - 15; |
|
|
| if (overdue || dueSoon) { |
| items.push({ |
| id: `med-${med.id}-${time}-${today}`, |
| type: "medication", |
| title: overdue ? `Overdue: ${med.name}` : `Due now: ${med.name}`, |
| message: `${med.dose} scheduled at ${time}`, |
| time, |
| read: false, |
| urgent: overdue, |
| }); |
| } |
| } |
| } |
|
|
| |
| for (const appt of appts) { |
| const [h, m] = appt.time.split(":").map(Number); |
| const apptMinutes = (h || 0) * 60 + (m || 0); |
| const in30 = apptMinutes - nowMinutes <= 30 && apptMinutes >= nowMinutes; |
| const overdue = nowMinutes > apptMinutes; |
|
|
| if (in30 || overdue) { |
| items.push({ |
| id: `appt-${appt.id}-${today}`, |
| type: "appointment", |
| title: overdue ? `Missed: ${appt.title}` : `Coming up: ${appt.title}`, |
| message: `${appt.time}${appt.doctor ? ` · ${appt.doctor}` : ""}${appt.location ? ` · ${appt.location}` : ""}`, |
| time: appt.time, |
| read: false, |
| urgent: overdue, |
| }); |
| } |
| } |
|
|
| return items.sort((a, b) => (a.urgent === b.urgent ? 0 : a.urgent ? -1 : 1)); |
| |
| }, [today, tick]); |
|
|
| const active = notifications.filter((n) => !dismissed.has(n.id)); |
| const count = active.length; |
|
|
| const dismiss = useCallback( |
| (id: string) => { |
| const next = new Set(dismissed); |
| next.add(id); |
| setDismissed(next); |
| saveDismissed(next); |
| }, |
| [dismissed], |
| ); |
|
|
| const dismissAll = useCallback(() => { |
| const next = new Set(dismissed); |
| for (const n of notifications) next.add(n.id); |
| setDismissed(next); |
| saveDismissed(next); |
| }, [dismissed, notifications]); |
|
|
| return { notifications: active, count, dismiss, dismissAll }; |
| } |
|
|