import type React from "react"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useAuth } from "../components/Auth"; import { useCollapsedDays, useDoses, useMedications, useRefill, useSettings, useShare } from "../hooks"; import type { Coverage, FormState, Medication, ScheduleEvent, StockThresholds } from "../types"; import { getSystemLocale } from "../utils/formatters"; import { log } from "../utils/logger"; import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds, getStockStatus } from "../utils/schedule"; // ============================================================================= // Types // ============================================================================= export type DoseInfo = { id: string; timeStr: string; when: number; usage: number; intakeUnit?: "ml" | "tsp" | "tbsp" | null; takenBy: string[]; intakeRemindersEnabled: boolean; }; export type DayMedEntry = { medName: string; total: number; doses: DoseInfo[]; lastWhen: number; }; export type GroupedDay = { dateStr: string; date: Date; isPast: boolean; meds: DayMedEntry[]; }; export interface AppContextValue { // From useMedications meds: Medication[]; setMeds: React.Dispatch>; loading: boolean; saving: boolean; setSaving: React.Dispatch>; uploadingImage: boolean; loadMeds: () => void; deleteMed: (id: number, editingId: number | null, resetForm: () => void) => Promise; uploadMedImage: (medId: number, file: File) => Promise; deleteMedImage: (medId: number) => Promise; // From useSettings (selected fields) settings: ReturnType["settings"]; setSettings: ReturnType["setSettings"]; savedSettings: ReturnType["savedSettings"]; settingsLoading: boolean; settingsSaving: boolean; settingsSaved: boolean; testingEmail: boolean; testEmailResult: { success: boolean; message: string } | null; testingShoutrrr: boolean; testShoutrrrResult: { success: boolean; message: string } | null; loadSettings: () => void; saveSettings: (e?: React.FormEvent) => Promise; testEmail: () => Promise; testShoutrrr: () => Promise; // From useDoses takenDoses: Set; setTakenDoses: React.Dispatch>>; dismissedDoses: Set; clearingMissed: boolean; showClearMissedConfirm: boolean; setShowClearMissedConfirm: (show: boolean) => void; getDoseId: (baseDoseId: string, person: string | null) => string; isDoseTakenAutomatically: (doseId: string) => boolean; countTakenDoses: (doses: Array<{ id: string; takenBy: string[] }>) => { total: number; taken: number }; markDoseTaken: (doseId: string) => Promise; undoDoseTaken: (doseId: string) => Promise; dismissMissedDoses: (doseIds: string[]) => Promise; // From useCollapsedDays manuallyCollapsedDays: Set; manuallyExpandedDays: Set; toggleDayCollapse: (dateStr: string, isCurrentlyExpanded: boolean) => void; // From useShare showShareDialog: boolean; sharePeople: string[]; shareSelectedPerson: string; setShareSelectedPerson: React.Dispatch>; shareSelectedDays: number; setShareSelectedDays: React.Dispatch>; shareGenerating: boolean; shareLink: string | null; setShareLink: React.Dispatch>; shareCopied: boolean; setShareCopied: React.Dispatch>; openShareDialog: () => void; generateShareLink: () => Promise; copyShareLink: () => void; closeShareDialog: () => void; resetShareDialogState: () => void; // From useRefill showRefillModal: boolean; setShowRefillModal: React.Dispatch>; refillPacks: number; setRefillPacks: React.Dispatch>; refillLoose: number; setRefillLoose: React.Dispatch>; usePrescriptionRefill: boolean; setUsePrescriptionRefill: React.Dispatch>; refillSaving: boolean; refillHistory: ReturnType["refillHistory"]; refillHistoryExpanded: boolean; setRefillHistoryExpanded: React.Dispatch>; showEditStockModal: boolean; setShowEditStockModal: React.Dispatch>; editStockFullBlisters: number; setEditStockFullBlisters: React.Dispatch>; editStockPartialBlisterPills: number; setEditStockPartialBlisterPills: React.Dispatch>; editStockLoosePills: number; setEditStockLoosePills: React.Dispatch>; editStockSaving: boolean; editStockMedication: Medication | null; loadRefillHistory: (medId: number) => Promise; submitRefill: ( medId: number, editingId: number | null, setForm: React.Dispatch>, loadMeds: () => void, usePrescription?: boolean ) => Promise; submitStockCorrection: (medId: number, selectedMed: Medication, loadMeds: () => void) => Promise; openRefillModal: () => void; closeRefillModal: () => void; openEditStockModal: (selectedMed: Medication, coverage: { all: Coverage[] }) => void; closeEditStockModal: () => void; // Computed values schedule: { events: ScheduleEvent[] }; coverage: { all: Coverage[]; low: Coverage[] }; coverageByMed: Record; depletionByMed: Record; stockThresholds: StockThresholds; existingPeople: string[]; groupedSchedule: GroupedDay[]; pastDays: GroupedDay[]; todayDay: GroupedDay | null; futureDays: GroupedDay[]; missedPastDoseIds: string[]; getDayStockStatus: (dayMeds: { medName: string; lastWhen: number }[]) => "success" | "warning" | "danger"; // Schedule UI state scheduleDays: number; setScheduleDays: React.Dispatch>; showPastDays: boolean; setShowPastDays: React.Dispatch>; showFutureDays: boolean; setShowFutureDays: React.Dispatch>; // Modal state selectedMed: Medication | null; setSelectedMed: React.Dispatch>; showImageLightbox: boolean; setShowImageLightbox: React.Dispatch>; scheduleLightboxImage: string | null; setScheduleLightboxImage: React.Dispatch>; selectedUser: string | null; setSelectedUser: React.Dispatch>; // Export/Import state exporting: boolean; importing: boolean; showExportModal: boolean; setShowExportModal: React.Dispatch>; showImportConfirm: boolean; setShowImportConfirm: React.Dispatch>; pendingImportData: unknown; setPendingImportData: React.Dispatch>; importResult: { medications: number; doses: number; refills: number; shares: number; } | null; setImportResult: React.Dispatch< React.SetStateAction<{ medications: number; doses: number; refills: number; shares: number; } | null> >; handleExport: (includeImages?: boolean) => Promise; handleImportFileSelect: (e: React.ChangeEvent) => void; handleImportConfirm: () => Promise; settingsChanged: boolean; // Modal helpers openMedDetail: (med: Medication) => void; closeMedDetail: () => void; openImageLightbox: () => void; closeImageLightbox: () => void; openScheduleLightbox: (imageUrl: string) => void; closeScheduleLightbox: () => void; openUserFilter: (person: string) => void; closeUserFilter: () => void; } // ============================================================================= // Context // ============================================================================= const APP_CONTEXT_SINGLETON_KEY = "__MEDASSIST_APP_CONTEXT_SINGLETON__"; const AppContext = (() => { const globalRef = globalThis as typeof globalThis & { [APP_CONTEXT_SINGLETON_KEY]?: React.Context; }; if (!globalRef[APP_CONTEXT_SINGLETON_KEY]) { globalRef[APP_CONTEXT_SINGLETON_KEY] = createContext(null); } return globalRef[APP_CONTEXT_SINGLETON_KEY]; })(); // Helper for user-specific localStorage keys function userStorageKey(userId: number | undefined, key: string): string { return userId ? `user_${userId}_${key}` : key; } // ============================================================================= // Provider // ============================================================================= export function AppProvider({ children }: { children: React.ReactNode }) { const { i18n } = useTranslation(); const { user } = useAuth(); // Compose hooks const medications = useMedications(); const settingsHook = useSettings(); const doses = useDoses(); const collapsed = useCollapsedDays(user?.id); const share = useShare(); const refill = useRefill(); // Schedule UI state const [scheduleDays, setScheduleDays] = useState(30); const [showPastDays, setShowPastDays] = useState(false); const [showFutureDays, setShowFutureDays] = useState(false); // Modal state const [selectedMed, setSelectedMed] = useState(null); const selectedMedIdRef = useRef(null); const medDetailOpenedAtRef = useRef(0); const medDetailCloseInFlightRef = useRef(false); useEffect(() => { selectedMedIdRef.current = selectedMed?.id ?? null; if (!selectedMed) { medDetailCloseInFlightRef.current = false; } }, [selectedMed]); const [showImageLightbox, setShowImageLightbox] = useState(false); const imageLightboxOpenedAtRef = useRef(0); const imageLightboxCloseInFlightRef = useRef(false); const [scheduleLightboxImage, setScheduleLightboxImage] = useState(null); const scheduleLightboxOpenedAtRef = useRef(0); const scheduleLightboxCloseInFlightRef = useRef(false); const [selectedUser, setSelectedUser] = useState(null); useEffect(() => { if (!showImageLightbox) { imageLightboxCloseInFlightRef.current = false; } }, [showImageLightbox]); useEffect(() => { if (!scheduleLightboxImage) { scheduleLightboxCloseInFlightRef.current = false; } }, [scheduleLightboxImage]); // Export/Import state const [exporting, setExporting] = useState(false); const [importing, setImporting] = useState(false); const [showExportModal, setShowExportModal] = useState(false); const [showImportConfirm, setShowImportConfirm] = useState(false); const [pendingImportData, setPendingImportData] = useState(null); const [importResult, setImportResult] = useState<{ medications: number; doses: number; refills: number; shares: number; } | null>(null); // Load user-specific scheduleDays when user changes useEffect(() => { if (typeof window !== "undefined" && user?.id) { const storedDays = localStorage.getItem(userStorageKey(user.id, "scheduleDays")); setScheduleDays(storedDays ? Number(storedDays) : 30); } }, [user?.id]); // Load medications and settings when user changes useEffect(() => { medications.loadMeds(); settingsHook.loadSettings(); }, [medications.loadMeds, settingsHook.loadSettings]); // Update selectedMed when meds change (e.g., after refill) useEffect(() => { if (selectedMed) { const updated = medications.meds.find((m) => m.id === selectedMed.id); if ( updated && (updated.packCount !== selectedMed.packCount || updated.looseTablets !== selectedMed.looseTablets || updated.updatedAt !== selectedMed.updatedAt) ) { setSelectedMed(updated); } } }, [medications.meds, selectedMed]); // Computed values - combine app language with timezone region for locale const systemLocale = getSystemLocale(i18n.language); const activeMeds = useMemo(() => medications.meds.filter((m) => !m.isObsolete), [medications.meds]); const schedule = useMemo(() => buildSchedulePreview(activeMeds, systemLocale, true), [activeMeds, systemLocale]); const coverage = useMemo( () => calculateCoverage( activeMeds, schedule.events, systemLocale, settingsHook.settings.reminderDaysBefore, settingsHook.settings.stockCalculationMode, doses.takenDoses, doses.takenDoseTimestamps ), [ activeMeds, schedule.events, systemLocale, settingsHook.settings.reminderDaysBefore, settingsHook.settings.stockCalculationMode, doses.takenDoses, doses.takenDoseTimestamps, ] ); const depletionByMed = useMemo( () => Object.fromEntries(coverage.all.map((c) => [c.name, c.depletionTime])), [coverage.all] ); const coverageByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c])), [coverage.all]); // Centralized stock thresholds for consistent status display across all components const stockThresholds: StockThresholds = useMemo( () => ({ lowStockDays: settingsHook.settings.lowStockDays, normalStockDays: settingsHook.settings.normalStockDays, highStockDays: settingsHook.settings.highStockDays, criticalStockDays: settingsHook.settings.reminderDaysBefore, // Critical uses the reminder threshold expiryWarningDays: settingsHook.settings.expiryWarningDays, }), [ settingsHook.settings.lowStockDays, settingsHook.settings.normalStockDays, settingsHook.settings.highStockDays, settingsHook.settings.reminderDaysBefore, settingsHook.settings.expiryWarningDays, ] ); const existingPeople = useMemo(() => { const allPeople = medications.meds.flatMap((m) => m.takenBy || []); return [...new Set(allPeople)].filter(Boolean).sort(); }, [medications.meds]); // Get worst stock status for a day's medications const getDayStockStatus = useCallback( (dayMeds: { medName: string; lastWhen: number }[]): "success" | "warning" | "danger" => { const statuses = dayMeds.map((item) => { const cov = coverageByMed[item.medName]; const med = activeMeds.find((m) => m.name === item.medName || m.genericName === 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 (!cov) return "success"; const status = getStockStatus(cov.daysLeft, cov.medsLeft, stockThresholds, med?.packageType); if (status.className === "danger") return "danger"; if (status.className === "warning") return "warning"; return "success"; }); const fallbackStatus = statuses.includes("warning") ? "warning" : "success"; return statuses.includes("danger") ? "danger" : fallbackStatus; }, [coverageByMed, depletionByMed, activeMeds, stockThresholds] ); const groupedSchedule = useMemo(() => { const days = new Map }>(); // Limit past events to scheduleDays window to avoid overwhelming the UI. // Without this, medications with start dates far in the past generate thousands // of events that fill the display budget and push out today/future events. const pastCutoff = new Date(); pastCutoff.setDate(pastCutoff.getDate() - scheduleDays); pastCutoff.setHours(0, 0, 0, 0); const pastCutoffMs = pastCutoff.getTime(); schedule.events .filter((e) => !e.isPast || e.when >= pastCutoffMs) .forEach((event) => { const day = days.get(event.dateStr) ?? { dateStr: event.dateStr, date: new Date(event.when), isPast: event.isPast, meds: new Map(), }; const medEntry = day.meds.get(event.medName) ?? { medName: event.medName, total: 0, doses: [], lastWhen: event.when, }; medEntry.total += event.usage; medEntry.doses.push({ id: event.id, timeStr: event.timeStr, when: event.when, usage: event.usage, intakeUnit: event.intakeUnit ?? null, takenBy: event.takenBy ? [event.takenBy] : [], intakeRemindersEnabled: event.intakeRemindersEnabled, }); 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, date: d.date, isPast: d.isPast, meds: Array.from(d.meds.values()), })); }, [schedule.events, scheduleDays]); const pastDays = useMemo(() => groupedSchedule.filter((d) => d.isPast), [groupedSchedule]); // Separate today from future days const todayDay = useMemo(() => { const today = new Date(); today.setHours(0, 0, 0, 0); return ( groupedSchedule.find((d) => { const dayDate = new Date(d.date); dayDate.setHours(0, 0, 0, 0); return dayDate.getTime() === today.getTime(); }) || null ); }, [groupedSchedule]); const futureDays = useMemo(() => { const today = new Date(); today.setHours(0, 0, 0, 0); return groupedSchedule .filter((d) => { if (d.isPast) return false; const dayDate = new Date(d.date); dayDate.setHours(0, 0, 0, 0); return dayDate.getTime() > today.getTime(); }) .slice(0, scheduleDays); }, [groupedSchedule, scheduleDays]); const missedPastDoseIds = useMemo( () => computeMissedPastDoseIds(pastDays, activeMeds, doses.takenDoses, doses.dismissedDoses), [pastDays, activeMeds, doses.takenDoses, doses.dismissedDoses] ); // Modal helpers with browser history support const openMedDetail = useCallback( (med: Medication) => { if (selectedMedIdRef.current === med.id) return; selectedMedIdRef.current = med.id; medDetailOpenedAtRef.current = Date.now(); medDetailCloseInFlightRef.current = false; setSelectedMed(med); refill.setRefillHistoryExpanded(false); refill.loadRefillHistory(med.id); window.history.pushState({ modal: "medDetail", medId: med.id }, ""); }, [refill] ); const closeMedDetail = useCallback(() => { if (!selectedMed || medDetailCloseInFlightRef.current) return; // Ignore ultra-fast close requests caused by rapid double-click races if (Date.now() - medDetailOpenedAtRef.current < 320) return; const currentState = window.history.state as { modal?: string } | null; if (currentState?.modal !== "medDetail") { // State already popped by another event: close locally without another back step. selectedMedIdRef.current = null; setSelectedMed(null); return; } medDetailCloseInFlightRef.current = true; window.history.back(); }, [selectedMed]); const openImageLightbox = useCallback(() => { if (showImageLightbox) return; imageLightboxOpenedAtRef.current = Date.now(); imageLightboxCloseInFlightRef.current = false; setShowImageLightbox(true); window.history.pushState({ modal: "imageLightbox" }, ""); }, [showImageLightbox]); const closeImageLightbox = useCallback(() => { if (!showImageLightbox || imageLightboxCloseInFlightRef.current) return; if (Date.now() - imageLightboxOpenedAtRef.current < 320) return; const currentState = window.history.state as { modal?: string } | null; if (currentState?.modal !== "imageLightbox") { setShowImageLightbox(false); return; } imageLightboxCloseInFlightRef.current = true; window.history.back(); }, [showImageLightbox]); const openScheduleLightbox = useCallback( (imageUrl: string) => { if (scheduleLightboxImage) return; scheduleLightboxOpenedAtRef.current = Date.now(); scheduleLightboxCloseInFlightRef.current = false; setScheduleLightboxImage(imageUrl); window.history.pushState({ modal: "scheduleLightbox" }, ""); }, [scheduleLightboxImage] ); const closeScheduleLightbox = useCallback(() => { if (!scheduleLightboxImage || scheduleLightboxCloseInFlightRef.current) return; if (Date.now() - scheduleLightboxOpenedAtRef.current < 320) return; const currentState = window.history.state as { modal?: string } | null; if (currentState?.modal !== "scheduleLightbox") { setScheduleLightboxImage(null); return; } scheduleLightboxCloseInFlightRef.current = true; window.history.back(); }, [scheduleLightboxImage]); const openUserFilter = useCallback( (person: string) => { if (selectedUser === person) return; setSelectedUser(person); window.history.pushState({ modal: "userFilter", person }, ""); }, [selectedUser] ); const closeUserFilter = useCallback(() => { if (selectedUser) { window.history.back(); } }, [selectedUser]); // Wrapper to pass meds to openShareDialog const openShareDialog = useCallback(() => { share.openShareDialog(activeMeds); }, [share, activeMeds]); // Get t function for translations const { t } = useTranslation(); // Export data to JSON file const handleExport = useCallback( async (includeImages: boolean = true) => { setExporting(true); try { const res = await fetch(`/api/export?includeSensitive=true&includeImages=${includeImages}`, { credentials: "include", }); if (!res.ok) throw new Error("Export failed"); const data = await res.json(); // Create download const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const now = new Date(); const dateStr = now.toISOString().replace(/[-:]/g, "").replace(/T/, "-").slice(0, 13); const userPart = user?.username ? `-${user.username}` : ""; a.href = url; a.download = `${t("exportImport.downloadFilename")}${userPart}-${dateStr}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { log.error("Export error:", err); } setExporting(false); }, [t, user?.username] ); // Handle file selection for import const handleImportFileSelect = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const data = JSON.parse(event.target?.result as string); if (!data.version || !data.exportedAt) { alert(t("exportImport.invalidFile")); return; } setPendingImportData(data); setShowImportConfirm(true); } catch { alert(t("exportImport.invalidFile")); } }; reader.readAsText(file); // Reset file input e.target.value = ""; }, [t] ); // Confirm and execute import const handleImportConfirm = useCallback(async () => { if (!pendingImportData) return; setImporting(true); setShowImportConfirm(false); try { const res = await fetch("/api/import", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(pendingImportData), }); // Get the response text first to handle non-JSON responses const text = await res.text(); let data: { error?: string; message?: string; imported?: | { medications?: number; doseHistory?: number; refillHistory?: number; shareLinks?: number; } | number; } = {}; try { data = text ? JSON.parse(text) : {}; } catch { log.error("Import response parse error:", text); alert(`${t("exportImport.importError")}: Server returned invalid response`); return; } if (!res.ok) { alert(`${t("exportImport.importError")}: ${data.error || `HTTP ${res.status}`}`); return; } // Show success message in UI instead of browser alert const importedCounts = typeof data.imported === "object" && data.imported !== null ? data.imported : null; setImportResult({ medications: importedCounts?.medications || 0, doses: importedCounts?.doseHistory || 0, refills: importedCounts?.refillHistory || 0, shares: importedCounts?.shareLinks || 0, }); // Reload all data medications.loadMeds(); settingsHook.loadSettings(); doses.loadTakenDoses(); } catch (err) { log.error("Import error:", err); alert(t("exportImport.importError")); } setPendingImportData(null); setImporting(false); }, [pendingImportData, t, medications, settingsHook, doses]); // Compute settingsChanged const settingsChanged = useMemo(() => { const settings = settingsHook.settings; const savedSettings = settingsHook.savedSettings; return ( settings.emailEnabled !== savedSettings.emailEnabled || settings.notificationEmail !== savedSettings.notificationEmail || settings.emailStockReminders !== savedSettings.emailStockReminders || settings.emailIntakeReminders !== savedSettings.emailIntakeReminders || settings.emailPrescriptionReminders !== savedSettings.emailPrescriptionReminders || settings.reminderDaysBefore !== savedSettings.reminderDaysBefore || settings.repeatDailyReminders !== savedSettings.repeatDailyReminders || settings.lowStockDays !== savedSettings.lowStockDays || settings.normalStockDays !== savedSettings.normalStockDays || settings.highStockDays !== savedSettings.highStockDays || settings.shoutrrrEnabled !== savedSettings.shoutrrrEnabled || settings.shoutrrrUrl !== savedSettings.shoutrrrUrl || settings.shoutrrrStockReminders !== savedSettings.shoutrrrStockReminders || settings.shoutrrrIntakeReminders !== savedSettings.shoutrrrIntakeReminders || settings.shoutrrrPrescriptionReminders !== savedSettings.shoutrrrPrescriptionReminders || settings.skipRemindersForTakenDoses !== savedSettings.skipRemindersForTakenDoses || settings.repeatRemindersEnabled !== savedSettings.repeatRemindersEnabled || settings.reminderRepeatIntervalMinutes !== savedSettings.reminderRepeatIntervalMinutes || settings.maxNaggingReminders !== savedSettings.maxNaggingReminders || settings.stockCalculationMode !== savedSettings.stockCalculationMode || settings.shareStockStatus !== savedSettings.shareStockStatus || settings.upcomingTodayOnly !== savedSettings.upcomingTodayOnly || settings.shareScheduleTodayOnly !== savedSettings.shareScheduleTodayOnly || settings.expiryWarningDays !== savedSettings.expiryWarningDays ); }, [settingsHook.settings, settingsHook.savedSettings]); // New dismissMissedDoses that uses medication-level dismissedUntil dates // This is robust against timestamp changes from schedule updates or timezone fixes const [clearingMissedState, setClearingMissedState] = useState(false); const dismissMissedDoses = useCallback( async (doseIds: string[]) => { if (doseIds.length === 0) return; // Extract unique medication IDs from dose IDs (format: medId-blisterIdx-timestamp[-person]) const medIds = new Set(); for (const doseId of doseIds) { const parts = doseId.split("-"); if (parts.length >= 1) { const medId = parseInt(parts[0], 10); if (!Number.isNaN(medId)) { medIds.add(medId); } } } if (medIds.size === 0) return; // Get today's date in YYYY-MM-DD format const today = new Date(); const until = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; setClearingMissedState(true); try { const res = await fetch("/api/medications/dismiss-until", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ medicationIds: Array.from(medIds), until }), }); if (res.ok) { // Reload medications to get updated dismissedUntil values await medications.loadMeds(); doses.setShowClearMissedConfirm(false); } } catch { // Error - dialog stays open } finally { setClearingMissedState(false); } }, [medications, doses] ); // Build context value const value: AppContextValue = useMemo( () => ({ // From useMedications ...medications, // From useSettings settings: settingsHook.settings, setSettings: settingsHook.setSettings, savedSettings: settingsHook.savedSettings, settingsLoading: settingsHook.settingsLoading, settingsSaving: settingsHook.settingsSaving, settingsSaved: settingsHook.settingsSaved, testingEmail: settingsHook.testingEmail, testEmailResult: settingsHook.testEmailResult, testingShoutrrr: settingsHook.testingShoutrrr, testShoutrrrResult: settingsHook.testShoutrrrResult, loadSettings: settingsHook.loadSettings, saveSettings: settingsHook.saveSettings, testEmail: settingsHook.testEmail, testShoutrrr: settingsHook.testShoutrrr, // From useDoses takenDoses: doses.takenDoses, setTakenDoses: doses.setTakenDoses, dismissedDoses: doses.dismissedDoses, clearingMissed: clearingMissedState, showClearMissedConfirm: doses.showClearMissedConfirm, setShowClearMissedConfirm: doses.setShowClearMissedConfirm, getDoseId: doses.getDoseId, isDoseTakenAutomatically: doses.isDoseTakenAutomatically, countTakenDoses: doses.countTakenDoses, markDoseTaken: doses.markDoseTaken, undoDoseTaken: doses.undoDoseTaken, dismissMissedDoses, // From useCollapsedDays manuallyCollapsedDays: collapsed.manuallyCollapsedDays, manuallyExpandedDays: collapsed.manuallyExpandedDays, toggleDayCollapse: collapsed.toggleDayCollapse, // From useShare showShareDialog: share.showShareDialog, sharePeople: share.sharePeople, shareSelectedPerson: share.shareSelectedPerson, setShareSelectedPerson: share.setShareSelectedPerson, shareSelectedDays: share.shareSelectedDays, setShareSelectedDays: share.setShareSelectedDays, shareGenerating: share.shareGenerating, shareLink: share.shareLink, setShareLink: share.setShareLink, shareCopied: share.shareCopied, setShareCopied: share.setShareCopied, openShareDialog, generateShareLink: share.generateShareLink, copyShareLink: share.copyShareLink, closeShareDialog: share.closeShareDialog, resetShareDialogState: share.resetShareDialogState, // From useRefill showRefillModal: refill.showRefillModal, setShowRefillModal: refill.setShowRefillModal, refillPacks: refill.refillPacks, setRefillPacks: refill.setRefillPacks, refillLoose: refill.refillLoose, setRefillLoose: refill.setRefillLoose, usePrescriptionRefill: refill.usePrescriptionRefill, setUsePrescriptionRefill: refill.setUsePrescriptionRefill, refillSaving: refill.refillSaving, refillHistory: refill.refillHistory, refillHistoryExpanded: refill.refillHistoryExpanded, setRefillHistoryExpanded: refill.setRefillHistoryExpanded, showEditStockModal: refill.showEditStockModal, setShowEditStockModal: refill.setShowEditStockModal, editStockFullBlisters: refill.editStockFullBlisters, setEditStockFullBlisters: refill.setEditStockFullBlisters, editStockPartialBlisterPills: refill.editStockPartialBlisterPills, setEditStockPartialBlisterPills: refill.setEditStockPartialBlisterPills, editStockLoosePills: refill.editStockLoosePills, setEditStockLoosePills: refill.setEditStockLoosePills, editStockSaving: refill.editStockSaving, editStockMedication: refill.editStockMedication, loadRefillHistory: refill.loadRefillHistory, submitRefill: refill.submitRefill, submitStockCorrection: refill.submitStockCorrection, openRefillModal: refill.openRefillModal, closeRefillModal: refill.closeRefillModal, openEditStockModal: refill.openEditStockModal, closeEditStockModal: refill.closeEditStockModal, // Computed values schedule, coverage, coverageByMed, depletionByMed, stockThresholds, existingPeople, groupedSchedule, pastDays, todayDay, futureDays, missedPastDoseIds, getDayStockStatus, // Schedule UI state scheduleDays, setScheduleDays, showPastDays, setShowPastDays, showFutureDays, setShowFutureDays, // Modal state selectedMed, setSelectedMed, showImageLightbox, setShowImageLightbox, scheduleLightboxImage, setScheduleLightboxImage, selectedUser, setSelectedUser, // Modal helpers openMedDetail, closeMedDetail, openImageLightbox, closeImageLightbox, openScheduleLightbox, closeScheduleLightbox, openUserFilter, closeUserFilter, // Export/Import exporting, importing, showExportModal, setShowExportModal, showImportConfirm, setShowImportConfirm, pendingImportData, setPendingImportData, importResult, setImportResult, handleExport, handleImportFileSelect, handleImportConfirm, settingsChanged, }), [ medications, settingsHook, doses, collapsed, share, refill, schedule, coverage, coverageByMed, depletionByMed, stockThresholds, existingPeople, groupedSchedule, pastDays, todayDay, futureDays, missedPastDoseIds, getDayStockStatus, scheduleDays, showPastDays, showFutureDays, selectedMed, showImageLightbox, scheduleLightboxImage, selectedUser, openMedDetail, closeMedDetail, openImageLightbox, closeImageLightbox, openScheduleLightbox, closeScheduleLightbox, openUserFilter, closeUserFilter, openShareDialog, exporting, importing, showExportModal, showImportConfirm, pendingImportData, importResult, handleExport, handleImportFileSelect, handleImportConfirm, settingsChanged, clearingMissedState, dismissMissedDoses, ] ); return {children}; } // ============================================================================= // Hook // ============================================================================= export function useAppContext(): AppContextValue { const context = useContext(AppContext); if (!context) { throw new Error("useAppContext must be used within an AppProvider"); } return context; }