feat(medications): build the on-time band from the persisted dose window

Thread the persisted `doseWindows` through the shared band minter so the
on-time band for a dose time uses its explicit `[start, end]` range
(minted DST-correctly on the slot's local day) instead of the symmetric
±1h default; the late tail stays cadence-derived. A `normaliseDoseWindows`
helper coerces the stored JSON into a clean entry list, dropping anything
malformed so the read/write paths never defend against an arbitrary shape.

Every band consumer reads the same field: the dose-history endpoint, the
compliance %, the cadence timeline (card last/next dose), and the
write/edit slot attribution all resolve through one minter, so the
history view and the rate can never disagree. The write/edit and
health-score schedule selects now pull the column.
This commit is contained in:
Marc-André Bombeck committed 2026-06-08 18:29:17 +02:00
1 parent 9bb68ade8a
commit 6526780f3b
12 files changed
+438 -19

No files matched your search

@@ -23,6 +23,7 @@ import {
type CanonicalSchedule,
type RecurrenceContext,
} from "@/lib/medications/scheduling/recurrence";
import { normaliseDoseWindows } from "@/lib/medications/scheduling/worker-helpers";
import { assertMedicationOwnership } from "@/lib/medications/route-guards";
import { userDayKey } from "@/lib/tz/format";
@@ -40,6 +41,8 @@ type MedicationScheduleRow = {
scheduleType: "SCHEDULED" | "PRN" | "CYCLIC";
cyclicOnWeeks: number | null;
cyclicOffWeeks: number | null;
/** v1.15.18 — per-dose configurable on-time windows (persisted JSON). */
doseWindows: unknown;
};
/**
@@ -63,6 +66,7 @@ function toCanonicalForBands(
scheduleType: s.scheduleType,
cyclicOnWeeks: s.cyclicOnWeeks,
cyclicOffWeeks: s.cyclicOffWeeks,
doseWindows: normaliseDoseWindows(s.doseWindows),
};
if (
base.timesOfDay.length === 0 &&