8718311876
- Extract App.tsx from 764 lines to ~404 lines - Create reusable components: MedDetailModal, MobileEditModal, ShareDialog, etc. - Add AppContext for global state management - Split pages: DashboardPage, MedicationsPage, SchedulePage, SettingsPage, PlannerPage - Create custom hooks: useAuth, useMedications, useSettings, useDoses, useSchedule - Add utility functions in separate modules - Fix stock status logic (>30 days = green/normal) - Fix reminder threshold calculation (use reminderDaysBefore not lowStockDays) - Fix takenBy validation (send [] instead of null) - Fix datetime format for blister start times (add Z suffix) - Style 'All OK' status as green/bold BREAKING: None - all existing functionality preserved
727 lines
24 KiB
TypeScript
727 lines
24 KiB
TypeScript
import React, { createContext, useContext, useMemo, useState, useEffect, useCallback } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useAuth } from "../components/Auth";
|
|
import {
|
|
useDoses,
|
|
useCollapsedDays,
|
|
useSettings,
|
|
useShare,
|
|
useMedications,
|
|
useRefill,
|
|
} from "../hooks";
|
|
import type {
|
|
Medication,
|
|
Coverage,
|
|
ScheduleEvent,
|
|
} from "../types";
|
|
import { buildSchedulePreview, calculateCoverage } from "../utils/schedule";
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
export type DoseInfo = {
|
|
id: string;
|
|
timeStr: string;
|
|
when: number;
|
|
usage: number;
|
|
takenBy: string[];
|
|
};
|
|
|
|
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<React.SetStateAction<Medication[]>>;
|
|
loading: boolean;
|
|
saving: boolean;
|
|
setSaving: React.Dispatch<React.SetStateAction<boolean>>;
|
|
uploadingImage: boolean;
|
|
loadMeds: () => void;
|
|
deleteMed: (id: number, editingId: number | null, resetForm: () => void) => Promise<void>;
|
|
uploadMedImage: (medId: number, file: File) => Promise<void>;
|
|
deleteMedImage: (medId: number) => Promise<void>;
|
|
|
|
// From useSettings (selected fields)
|
|
settings: ReturnType<typeof useSettings>["settings"];
|
|
setSettings: ReturnType<typeof useSettings>["setSettings"];
|
|
savedSettings: ReturnType<typeof useSettings>["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<void>;
|
|
testEmail: () => Promise<void>;
|
|
testShoutrrr: () => Promise<void>;
|
|
|
|
// From useDoses
|
|
takenDoses: Set<string>;
|
|
setTakenDoses: React.Dispatch<React.SetStateAction<Set<string>>>;
|
|
dismissedDoses: Set<string>;
|
|
clearingMissed: boolean;
|
|
showClearMissedConfirm: boolean;
|
|
setShowClearMissedConfirm: (show: boolean) => void;
|
|
getDoseId: (baseDoseId: string, person: string | null) => string;
|
|
countTakenDoses: (doses: Array<{ id: string; takenBy: string[] }>) => { total: number; taken: number };
|
|
markDoseTaken: (doseId: string) => Promise<void>;
|
|
undoDoseTaken: (doseId: string) => Promise<void>;
|
|
dismissMissedDoses: (doseIds: string[]) => Promise<void>;
|
|
|
|
// From useCollapsedDays
|
|
manuallyCollapsedDays: Set<string>;
|
|
manuallyExpandedDays: Set<string>;
|
|
toggleDayCollapse: (dateStr: string, isCurrentlyExpanded: boolean) => void;
|
|
|
|
// From useShare
|
|
showShareDialog: boolean;
|
|
sharePeople: string[];
|
|
shareSelectedPerson: string;
|
|
setShareSelectedPerson: React.Dispatch<React.SetStateAction<string>>;
|
|
shareSelectedDays: number;
|
|
setShareSelectedDays: React.Dispatch<React.SetStateAction<number>>;
|
|
shareGenerating: boolean;
|
|
shareLink: string | null;
|
|
setShareLink: React.Dispatch<React.SetStateAction<string | null>>;
|
|
shareCopied: boolean;
|
|
setShareCopied: React.Dispatch<React.SetStateAction<boolean>>;
|
|
openShareDialog: () => void;
|
|
generateShareLink: () => Promise<void>;
|
|
copyShareLink: () => void;
|
|
closeShareDialog: () => void;
|
|
resetShareDialogState: () => void;
|
|
|
|
// From useRefill
|
|
showRefillModal: boolean;
|
|
setShowRefillModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
refillPacks: number;
|
|
setRefillPacks: React.Dispatch<React.SetStateAction<number>>;
|
|
refillLoose: number;
|
|
setRefillLoose: React.Dispatch<React.SetStateAction<number>>;
|
|
refillSaving: boolean;
|
|
refillHistory: ReturnType<typeof useRefill>["refillHistory"];
|
|
refillHistoryExpanded: boolean;
|
|
setRefillHistoryExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
|
showEditStockModal: boolean;
|
|
setShowEditStockModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
editStockFullBlisters: number;
|
|
setEditStockFullBlisters: React.Dispatch<React.SetStateAction<number>>;
|
|
editStockPartialBlisterPills: number;
|
|
setEditStockPartialBlisterPills: React.Dispatch<React.SetStateAction<number>>;
|
|
editStockSaving: boolean;
|
|
loadRefillHistory: (medId: number) => Promise<void>;
|
|
submitRefill: (medId: number, editingId: number | null, setForm: React.Dispatch<React.SetStateAction<any>>, loadMeds: () => void) => Promise<void>;
|
|
submitStockCorrection: (medId: number, selectedMed: Medication, loadMeds: () => void) => Promise<void>;
|
|
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<string, Coverage>;
|
|
depletionByMed: Record<string, number | null>;
|
|
existingPeople: string[];
|
|
groupedSchedule: GroupedDay[];
|
|
pastDays: GroupedDay[];
|
|
futureDays: GroupedDay[];
|
|
missedPastDoseIds: string[];
|
|
getDayStockStatus: (dayMeds: { medName: string; lastWhen: number }[]) => "success" | "warning" | "danger";
|
|
|
|
// Schedule UI state
|
|
scheduleDays: number;
|
|
setScheduleDays: React.Dispatch<React.SetStateAction<number>>;
|
|
showPastDays: boolean;
|
|
setShowPastDays: React.Dispatch<React.SetStateAction<boolean>>;
|
|
|
|
// Modal state
|
|
selectedMed: Medication | null;
|
|
setSelectedMed: React.Dispatch<React.SetStateAction<Medication | null>>;
|
|
showImageLightbox: boolean;
|
|
setShowImageLightbox: React.Dispatch<React.SetStateAction<boolean>>;
|
|
scheduleLightboxImage: string | null;
|
|
setScheduleLightboxImage: React.Dispatch<React.SetStateAction<string | null>>;
|
|
selectedUser: string | null;
|
|
setSelectedUser: React.Dispatch<React.SetStateAction<string | null>>;
|
|
|
|
// Export/Import state
|
|
exporting: boolean;
|
|
importing: boolean;
|
|
showExportModal: boolean;
|
|
setShowExportModal: React.Dispatch<React.SetStateAction<boolean>>;
|
|
showImportConfirm: boolean;
|
|
setShowImportConfirm: React.Dispatch<React.SetStateAction<boolean>>;
|
|
pendingImportData: unknown;
|
|
setPendingImportData: React.Dispatch<React.SetStateAction<unknown>>;
|
|
importResult: { medications: number; doses: number; shares: number } | null;
|
|
setImportResult: React.Dispatch<React.SetStateAction<{ medications: number; doses: number; shares: number } | null>>;
|
|
handleExport: (includeImages?: boolean) => Promise<void>;
|
|
handleImportFileSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
handleImportConfirm: () => Promise<void>;
|
|
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 AppContext = createContext<AppContextValue | null>(null);
|
|
|
|
// 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<number>(30);
|
|
const [showPastDays, setShowPastDays] = useState(false);
|
|
|
|
// Modal state
|
|
const [selectedMed, setSelectedMed] = useState<Medication | null>(null);
|
|
const [showImageLightbox, setShowImageLightbox] = useState(false);
|
|
const [scheduleLightboxImage, setScheduleLightboxImage] = useState<string | null>(null);
|
|
const [selectedUser, setSelectedUser] = useState<string | null>(null);
|
|
|
|
// 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<unknown>(null);
|
|
const [importResult, setImportResult] = useState<{ medications: number; doses: 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();
|
|
}, [user?.id]);
|
|
|
|
// 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
|
|
const schedule = useMemo(
|
|
() => buildSchedulePreview(medications.meds, i18n.language, true),
|
|
[medications.meds, i18n.language]
|
|
);
|
|
|
|
const coverage = useMemo(
|
|
() => calculateCoverage(
|
|
medications.meds,
|
|
schedule.events,
|
|
i18n.language,
|
|
settingsHook.settings.reminderDaysBefore,
|
|
settingsHook.settings.stockCalculationMode,
|
|
doses.takenDoses
|
|
),
|
|
[medications.meds, schedule.events, i18n.language, settingsHook.settings.reminderDaysBefore, settingsHook.settings.stockCalculationMode, doses.takenDoses]
|
|
);
|
|
|
|
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]
|
|
);
|
|
|
|
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 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 { daysLeft, medsLeft } = cov;
|
|
|
|
// Currently out of stock
|
|
if (medsLeft <= 0 || daysLeft === 0) return "danger";
|
|
// No schedule (can't calculate)
|
|
if (daysLeft === null) return "success";
|
|
// Low stock: < lowStockDays (warning)
|
|
if (daysLeft < settingsHook.settings.lowStockDays) return "warning";
|
|
// Normal/High stock
|
|
return "success";
|
|
});
|
|
return statuses.includes("danger") ? "danger" : statuses.includes("warning") ? "warning" : "success";
|
|
}, [coverageByMed, depletionByMed, settingsHook.settings.lowStockDays]);
|
|
|
|
const groupedSchedule = useMemo(() => {
|
|
const days = new Map<string, { dateStr: string; date: Date; isPast: boolean; meds: Map<string, DayMedEntry> }>();
|
|
schedule.events.slice(0, 2000).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, takenBy: event.takenBy || [] });
|
|
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]);
|
|
|
|
const pastDays = useMemo(() => groupedSchedule.filter(d => d.isPast), [groupedSchedule]);
|
|
const futureDays = useMemo(() => groupedSchedule.filter(d => !d.isPast).slice(0, scheduleDays), [groupedSchedule, scheduleDays]);
|
|
|
|
const missedPastDoseIds = useMemo(() => {
|
|
const totalPastDoses = pastDays.flatMap(d =>
|
|
d.meds.flatMap(m =>
|
|
m.doses.flatMap(dose =>
|
|
(dose.takenBy || []).length > 0
|
|
? dose.takenBy.map((p: string) => `${dose.id}-${p}`)
|
|
: [dose.id]
|
|
)
|
|
)
|
|
);
|
|
return totalPastDoses.filter(id => !doses.takenDoses.has(id) && !doses.dismissedDoses.has(id));
|
|
}, [pastDays, doses.takenDoses, doses.dismissedDoses]);
|
|
|
|
// Modal helpers with browser history support
|
|
const openMedDetail = useCallback((med: Medication) => {
|
|
setSelectedMed(med);
|
|
refill.setRefillHistoryExpanded(false);
|
|
refill.loadRefillHistory(med.id);
|
|
window.history.pushState({ modal: 'medDetail', medId: med.id }, '');
|
|
}, [refill]);
|
|
|
|
const closeMedDetail = useCallback(() => {
|
|
if (selectedMed) {
|
|
window.history.back();
|
|
}
|
|
}, [selectedMed]);
|
|
|
|
const openImageLightbox = useCallback(() => {
|
|
setShowImageLightbox(true);
|
|
window.history.pushState({ modal: 'imageLightbox' }, '');
|
|
}, []);
|
|
|
|
const closeImageLightbox = useCallback(() => {
|
|
if (showImageLightbox) {
|
|
window.history.back();
|
|
}
|
|
}, [showImageLightbox]);
|
|
|
|
const openScheduleLightbox = useCallback((imageUrl: string) => {
|
|
setScheduleLightboxImage(imageUrl);
|
|
window.history.pushState({ modal: 'scheduleLightbox' }, '');
|
|
}, []);
|
|
|
|
const closeScheduleLightbox = useCallback(() => {
|
|
if (scheduleLightboxImage) {
|
|
window.history.back();
|
|
}
|
|
}, [scheduleLightboxImage]);
|
|
|
|
const openUserFilter = useCallback((person: string) => {
|
|
setSelectedUser(person);
|
|
window.history.pushState({ modal: 'userFilter', person }, '');
|
|
}, []);
|
|
|
|
const closeUserFilter = useCallback(() => {
|
|
if (selectedUser) {
|
|
window.history.back();
|
|
}
|
|
}, [selectedUser]);
|
|
|
|
// Wrapper to pass meds to openShareDialog
|
|
const openShareDialog = useCallback(() => {
|
|
share.openShareDialog(medications.meds);
|
|
}, [share, medications.meds]);
|
|
|
|
// 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 dateStr = new Date().toISOString().split("T")[0];
|
|
a.href = url;
|
|
a.download = `${t('exportImport.downloadFilename')}-${dateStr}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
} catch (err) {
|
|
console.error("Export error:", err);
|
|
}
|
|
setExporting(false);
|
|
}, [t]);
|
|
|
|
// Handle file selection for import
|
|
const handleImportFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
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;
|
|
try {
|
|
data = text ? JSON.parse(text) : {};
|
|
} catch {
|
|
console.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
|
|
setImportResult({
|
|
medications: data.imported?.medications || 0,
|
|
doses: data.imported?.doseHistory || 0,
|
|
shares: data.imported?.shareLinks || 0,
|
|
});
|
|
|
|
// Reload all data
|
|
medications.loadMeds();
|
|
settingsHook.loadSettings();
|
|
doses.loadTakenDoses();
|
|
} catch (err) {
|
|
console.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.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.skipRemindersForTakenDoses !== savedSettings.skipRemindersForTakenDoses ||
|
|
settings.repeatRemindersEnabled !== savedSettings.repeatRemindersEnabled ||
|
|
settings.reminderRepeatIntervalMinutes !== savedSettings.reminderRepeatIntervalMinutes ||
|
|
settings.maxNaggingReminders !== savedSettings.maxNaggingReminders ||
|
|
settings.stockCalculationMode !== savedSettings.stockCalculationMode;
|
|
}, [settingsHook.settings, settingsHook.savedSettings]);
|
|
|
|
// 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: doses.clearingMissed,
|
|
showClearMissedConfirm: doses.showClearMissedConfirm,
|
|
setShowClearMissedConfirm: doses.setShowClearMissedConfirm,
|
|
getDoseId: doses.getDoseId,
|
|
countTakenDoses: doses.countTakenDoses,
|
|
markDoseTaken: doses.markDoseTaken,
|
|
undoDoseTaken: doses.undoDoseTaken,
|
|
dismissMissedDoses: doses.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,
|
|
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,
|
|
editStockSaving: refill.editStockSaving,
|
|
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,
|
|
existingPeople,
|
|
groupedSchedule,
|
|
pastDays,
|
|
futureDays,
|
|
missedPastDoseIds,
|
|
getDayStockStatus,
|
|
|
|
// Schedule UI state
|
|
scheduleDays,
|
|
setScheduleDays,
|
|
showPastDays,
|
|
setShowPastDays,
|
|
|
|
// 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,
|
|
existingPeople,
|
|
groupedSchedule,
|
|
pastDays,
|
|
futureDays,
|
|
missedPastDoseIds,
|
|
getDayStockStatus,
|
|
scheduleDays,
|
|
showPastDays,
|
|
selectedMed,
|
|
showImageLightbox,
|
|
scheduleLightboxImage,
|
|
selectedUser,
|
|
openMedDetail,
|
|
closeMedDetail,
|
|
openImageLightbox,
|
|
closeImageLightbox,
|
|
openScheduleLightbox,
|
|
closeScheduleLightbox,
|
|
openUserFilter,
|
|
closeUserFilter,
|
|
openShareDialog,
|
|
exporting,
|
|
importing,
|
|
showExportModal,
|
|
showImportConfirm,
|
|
pendingImportData,
|
|
importResult,
|
|
handleExport,
|
|
handleImportFileSelect,
|
|
handleImportConfirm,
|
|
settingsChanged,
|
|
]);
|
|
|
|
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Hook
|
|
// =============================================================================
|
|
|
|
export function useAppContext(): AppContextValue {
|
|
const context = useContext(AppContext);
|
|
if (!context) {
|
|
throw new Error("useAppContext must be used within an AppProvider");
|
|
}
|
|
return context;
|
|
}
|