From e725700d10beb22856342149df0c18b7016aacd8 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sun, 25 Jan 2026 20:45:11 +0100 Subject: [PATCH] fix: only count missed doses scheduled after medication update (#79) When medication intake times change, dose IDs change (they include timestamps). Previously, this caused all past doses to appear as 'missed' because the old 'taken' markers no longer matched. Now doses are only counted as 'missed' if they were scheduled AFTER the medication's last update (updatedAt). This means: - Legitimately missed doses still show as missed (e.g., yesterday's dose not taken) - Doses from before a schedule change are NOT counted as missed (they were from a previous schedule configuration) Changes: - AppContext: Add isDoseFromPreviousSchedule helper - SchedulePage: Use context's missedPastDoseIds instead of local calc - Update tests to include missedPastDoseIds in mocks --- frontend/src/components/SharedSchedule.tsx | 1 + frontend/src/context/AppContext.tsx | 33 +++++++++++++++++-- frontend/src/pages/SchedulePage.tsx | 19 ++++------- frontend/src/test/pages/SchedulePage.test.tsx | 3 ++ 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/SharedSchedule.tsx b/frontend/src/components/SharedSchedule.tsx index 6ec468f..76c1f16 100644 --- a/frontend/src/components/SharedSchedule.tsx +++ b/frontend/src/components/SharedSchedule.tsx @@ -535,6 +535,7 @@ export function SharedSchedule() { ) ); // Count missed doses (not taken AND not dismissed) + // Note: SharedSchedule doesn't have updatedAt info, so we only check dismissed status const missedPastDoses = totalPastDoses.filter((id) => { if (takenDoses.has(id)) return false; // Check if this dose is dismissed diff --git a/frontend/src/context/AppContext.tsx b/frontend/src/context/AppContext.tsx index 813cf7c..4a51fc3 100644 --- a/frontend/src/context/AppContext.tsx +++ b/frontend/src/context/AppContext.tsx @@ -419,12 +419,35 @@ export function AppProvider({ children }: { children: React.ReactNode }) { return doseDateStr <= dismissedUntilDate; }, []); + // Helper to check if a dose was scheduled BEFORE the medication was last updated + // If so, it's from a previous schedule configuration and shouldn't count as "missed" + const isDoseFromPreviousSchedule = useCallback( + (doseId: string, medUpdatedAt: string | number | null | undefined): boolean => { + if (!medUpdatedAt) return false; // No updatedAt means it was never changed, all doses are valid + + // Extract timestamp from dose ID (format: medId-blisterIdx-timestamp or medId-blisterIdx-timestamp-person) + const parts = doseId.split("-"); + if (parts.length < 3) return false; + const doseTimestamp = parseInt(parts[2], 10); + if (Number.isNaN(doseTimestamp)) return false; + + // Convert updatedAt to timestamp + const updatedAtTimestamp = typeof medUpdatedAt === "number" ? medUpdatedAt : new Date(medUpdatedAt).getTime(); + if (Number.isNaN(updatedAtTimestamp)) return false; + + // If the dose was scheduled before the medication was updated, it's from a previous schedule + return doseTimestamp < updatedAtTimestamp; + }, + [] + ); + const missedPastDoseIds = useMemo(() => { const totalPastDoses = pastDays.flatMap((d) => d.meds.flatMap((m) => { - // Find the medication to get its dismissedUntil + // Find the medication to get its dismissedUntil and updatedAt const med = medications.meds.find((med) => med.name === m.medName); const dismissedUntilDate = med?.dismissedUntil ?? undefined; + const medUpdatedAt = med?.updatedAt; return m.doses.flatMap((dose) => { // Check if this dose is on or before the dismissed date for this medication @@ -432,13 +455,19 @@ export function AppProvider({ children }: { children: React.ReactNode }) { return []; } + // Check if this dose is from a previous schedule configuration + // (scheduled before the medication was last updated) + if (isDoseFromPreviousSchedule(dose.id, medUpdatedAt)) { + return []; + } + return (dose.takenBy || []).length > 0 ? dose.takenBy.map((p: string) => `${dose.id}-${p}`) : [dose.id]; }); }) ); // Also filter out doses that are marked as taken or individually dismissed (legacy) return totalPastDoses.filter((id) => !doses.takenDoses.has(id) && !doses.dismissedDoses.has(id)); - }, [pastDays, medications.meds, doses.takenDoses, doses.dismissedDoses, isDoseDismissed]); + }, [pastDays, medications.meds, doses.takenDoses, doses.dismissedDoses, isDoseDismissed, isDoseFromPreviousSchedule]); // Modal helpers with browser history support const openMedDetail = useCallback( diff --git a/frontend/src/pages/SchedulePage.tsx b/frontend/src/pages/SchedulePage.tsx index 6f2012c..6b6c0bb 100644 --- a/frontend/src/pages/SchedulePage.tsx +++ b/frontend/src/pages/SchedulePage.tsx @@ -63,6 +63,7 @@ export function SchedulePage() { manuallyExpandedDays, toggleDayCollapse, openUserFilter, + missedPastDoseIds, } = useAppContext(); return ( @@ -88,17 +89,11 @@ export function SchedulePage() { {/* Past days toggle */} {pastDays.length > 0 && (() => { - const totalPastDoses = pastDays.flatMap((d) => - d.meds.flatMap((m) => - m.doses.flatMap((dose) => - (dose.takenBy || []).length > 0 ? dose.takenBy.map((p) => `${dose.id}-${p}`) : [dose.id] - ) - ) - ); - const missedPastDoses = totalPastDoses.filter((id) => !takenDoses.has(id)).length; + // Use context's missedPastDoseIds which handles dismissed doses and previous schedule detection + const missedCount = missedPastDoseIds.length; return (
0 ? "has-missed" : ""}`} + className={`past-days-toggle ${showPastDays ? "expanded" : ""} ${missedCount > 0 ? "has-missed" : ""}`} onClick={() => setShowPastDays(!showPastDays)} > {showPastDays ? "▼" : "▶"} @@ -108,12 +103,12 @@ export function SchedulePage() { ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) - {missedPastDoses > 0 && ( + {missedCount > 0 && ( - ⚠️ {missedPastDoses} + ⚠️ {missedCount} )}
diff --git a/frontend/src/test/pages/SchedulePage.test.tsx b/frontend/src/test/pages/SchedulePage.test.tsx index eeebcd0..dcdecd5 100644 --- a/frontend/src/test/pages/SchedulePage.test.tsx +++ b/frontend/src/test/pages/SchedulePage.test.tsx @@ -101,6 +101,7 @@ const createMockContext = (overrides = {}) => ({ manuallyExpandedDays: new Set(), toggleDayCollapse: vi.fn(), openUserFilter: vi.fn(), + missedPastDoseIds: [], ...overrides, }); @@ -436,6 +437,7 @@ describe("SchedulePage with past days", () => { futureDays: mockFutureDays, coverageByMed: mockCoverageByMed, showPastDays: false, + missedPastDoseIds: [`1-0-${Date.now() - 86400000}-John`], // One missed dose }); }); @@ -467,6 +469,7 @@ describe("SchedulePage with past days", () => { pastDays: mockPastDays, showPastDays: false, setShowPastDays, + missedPastDoseIds: [], }); render(