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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={`past-days-toggle ${showPastDays ? "expanded" : ""} ${missedPastDoses > 0 ? "has-missed" : ""}`}
|
||||
className={`past-days-toggle ${showPastDays ? "expanded" : ""} ${missedCount > 0 ? "has-missed" : ""}`}
|
||||
onClick={() => setShowPastDays(!showPastDays)}
|
||||
>
|
||||
<span className="past-days-icon">{showPastDays ? "▼" : "▶"}</span>
|
||||
@@ -108,12 +103,12 @@ export function SchedulePage() {
|
||||
<span className="past-days-count">
|
||||
({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })})
|
||||
</span>
|
||||
{missedPastDoses > 0 && (
|
||||
{missedCount > 0 && (
|
||||
<span
|
||||
className="past-days-warning"
|
||||
title={t("dashboard.schedules.missedDoses", { count: missedPastDoses })}
|
||||
title={t("dashboard.schedules.missedDoses", { count: missedCount })}
|
||||
>
|
||||
⚠️ {missedPastDoses}
|
||||
⚠️ {missedCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user