fix: restore schedule interaction correctness

* fix: restore schedule interaction correctness

* fix: use scheduled stock timing for historical doses
This commit is contained in:
Daniel Volz
2026-03-14 20:49:13 +01:00
committed by GitHub
parent 816888a697
commit 0160ef3ddf
15 changed files with 888 additions and 286 deletions
+29 -62
View File
@@ -3,7 +3,14 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
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 {
type Coverage,
type FormState,
getMedDisplayName,
type Medication,
type ScheduleEvent,
type StockThresholds,
} from "../types";
import { getSystemLocale } from "../utils/formatters";
import { log } from "../utils/logger";
import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds, getStockStatus } from "../utils/schedule";
@@ -70,15 +77,11 @@ export interface AppContextValue {
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;
isDoseTakenAutomatically: (doseId: string) => boolean;
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>;
@@ -393,6 +396,25 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
const coverageByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c])), [coverage.all]);
const outOfStockMedicationIds = useMemo(
() =>
new Set(
activeMeds.filter((med) => (coverageByMed[getMedDisplayName(med)]?.medsLeft ?? 1) <= 0).map((med) => med.id)
),
[activeMeds, coverageByMed]
);
const effectiveTakenDoses = useMemo(
() =>
new Set(
Array.from(doses.takenDoses).filter((doseId) => {
const medId = Number.parseInt(doseId.split("-")[0] ?? "", 10);
return Number.isNaN(medId) || !outOfStockMedicationIds.has(medId);
})
),
[doses.takenDoses, outOfStockMedicationIds]
);
// Centralized stock thresholds for consistent status display across all components
const stockThresholds: StockThresholds = useMemo(
() => ({
@@ -516,8 +538,8 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
}, [groupedSchedule, scheduleDays]);
const missedPastDoseIds = useMemo(
() => computeMissedPastDoseIds(pastDays, activeMeds, doses.takenDoses, doses.dismissedDoses),
[pastDays, activeMeds, doses.takenDoses, doses.dismissedDoses]
() => computeMissedPastDoseIds(pastDays, activeMeds, effectiveTakenDoses, doses.dismissedDoses),
[pastDays, activeMeds, effectiveTakenDoses, doses.dismissedDoses]
);
// Modal helpers with browser history support
@@ -777,55 +799,6 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
);
}, [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<number>();
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(
() => ({
@@ -853,15 +826,11 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
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,
@@ -1020,8 +989,6 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
handleImportFileSelect,
handleImportConfirm,
settingsChanged,
clearingMissedState,
dismissMissedDoses,
]
);