Record a backfilled dose at its slot's own time

Marking an older missed dose as taken from the Verlauf pinned the
right slot but stamped the intake with the current moment. The body
builder is a pure helper now, anchored on the slot instant, matching
both the optimistic paint and the skipped-to-taken flip that already
behaved this way.
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 09:38:30 +02:00
1 parent 2f68b3dc79
commit f3c84018f9
5 files changed
+52 -11

No files matched your search

@@ -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",
});
});
});
@@ -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<LedgerRow, "at">,
action: "taken" | "skipped",
): Record<string, unknown> {
return action === "skipped"
? { skipped: true, scheduledFor: row.at }
: { skipped: false, scheduledFor: row.at, takenAt: row.at };
}
@@ -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(