diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f81deb5d..d7ef0ce74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +### Fixed + +- Marking an older missed dose as taken from the intake history now records + the dose at the slot's own date and time. Marking Tuesday 09:00 from a + Thursday evening used to land on the right slot but stamp the intake with + the current moment; the recorded time now matches the claim being made. The + optimistic paint had promised this all along, and the skipped-to-taken flip + in the same view already behaved this way. + ## [1.37.1] — 2026-08-08 ### Added diff --git a/public/sw.js b/public/sw.js index d2632934b..d62c81e76 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.37.0"; + /* @sw-version-fallback */ "v1.37.1"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/components/medications/__tests__/dose-history-ledger-compute.test.ts b/src/components/medications/__tests__/dose-history-ledger-compute.test.ts index 420d73a1b..496be10f6 100644 --- a/src/components/medications/__tests__/dose-history-ledger-compute.test.ts +++ b/src/components/medications/__tests__/dose-history-ledger-compute.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { applyOptimisticSlotMark, + slotMarkRequestBody, complianceFromLedger, formatSlotDelta, groupLedgerByDay, @@ -195,3 +196,23 @@ describe("applyOptimisticSlotMark", () => { expect(next.rows[0].status).toBe("taken_late"); }); }); + +describe("slotMarkRequestBody", () => { + const row = { at: "2026-08-04T07:00:00.000Z" }; + + it("anchors takenAt on the slot instant, not on now", () => { + const body = slotMarkRequestBody(row, "taken"); + expect(body).toEqual({ + skipped: false, + scheduledFor: "2026-08-04T07:00:00.000Z", + takenAt: "2026-08-04T07:00:00.000Z", + }); + }); + + it("keeps the skipped arm free of takenAt", () => { + expect(slotMarkRequestBody(row, "skipped")).toEqual({ + skipped: true, + scheduledFor: "2026-08-04T07:00:00.000Z", + }); + }); +}); diff --git a/src/components/medications/dose-history-ledger-compute.ts b/src/components/medications/dose-history-ledger-compute.ts index 06f51e084..9bb009e35 100644 --- a/src/components/medications/dose-history-ledger-compute.ts +++ b/src/components/medications/dose-history-ledger-compute.ts @@ -204,3 +204,18 @@ export function applyOptimisticSlotMark( }); return { ...payload, rows }; } + +/** + * Request body for marking a ledger slot from the Verlauf. The taken arm + * anchors `takenAt` on the slot instant: marking Tuesday 09:00 from a + * Thursday evening records a dose taken Tuesday 09:00, which is the claim + * the person is making. The optimistic paint above already promised this. + */ +export function slotMarkRequestBody( + row: Pick, + action: "taken" | "skipped", +): Record { + return action === "skipped" + ? { skipped: true, scheduledFor: row.at } + : { skipped: false, scheduledFor: row.at, takenAt: row.at }; +} diff --git a/src/components/medications/dose-history-ledger.tsx b/src/components/medications/dose-history-ledger.tsx index 9204ed83d..82b6ae50e 100644 --- a/src/components/medications/dose-history-ledger.tsx +++ b/src/components/medications/dose-history-ledger.tsx @@ -106,6 +106,7 @@ import { formatSlotDelta, groupLedgerByDay, isSlotActionable, + slotMarkRequestBody, type LedgerPayload, type LedgerRow, } from "@/components/medications/dose-history-ledger-compute"; @@ -273,18 +274,13 @@ export function DoseHistoryLedger({ try { // Pin the displayed slot so the server marks THAT dose (the canonical - // slot upsert), not a now-snap to the nearest one. The POST returns - // the created event; its id drives the Undo affordance (the same - // soft-delete route the cards use). + // slot upsert), not a now-snap to the nearest one — and anchor + // `takenAt` on the same instant, matching the optimistic paint. The + // POST returns the created event; its id drives the Undo affordance + // (the same soft-delete route the cards use). const created = await apiPost<{ id?: string } | undefined>( `/api/medications/${medicationId}/intake`, - action === "skipped" - ? { skipped: true, scheduledFor: row.at } - : { - skipped: false, - scheduledFor: row.at, - takenAt: new Date().toISOString(), - }, + slotMarkRequestBody(row, action), ); const eventId: string | undefined = created?.id; toast.success(