mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
expose the open dose's per-dose status on the compliance read
Derive a single DoseStatus for the open cycle in buildComplianceDisplay and surface it as complianceDisplay.currentDose, so the medication card renders its take-window / overdue presentation from one server authority instead of re-spelling the window math client-side. Add the field to the OpenAPI ComplianceDisplay schema and regenerate the spec.
This commit is contained in:
1 parent
7bbb473070
commit
939a732aa6
4 files changed
+161
No files matched your search
@@ -8550,6 +8550,35 @@ components:
|
||||
description: v1.13.x — the current (open) dose cycle, surfaced so a between-doses sparse med renders a neutral 'next
|
||||
dose in N days' line instead of a scary red 0%. The percentage rows above already exclude the open forward
|
||||
cycle from their denominator.
|
||||
currentDose:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- upcoming
|
||||
- on_time_window
|
||||
- overdue
|
||||
- missed
|
||||
- taken_on_time
|
||||
- taken_late
|
||||
- skipped
|
||||
description: Per-dose state of the open cycle, server-derived from the window model so the card renders the take-window
|
||||
(green) / overdue / heavily-overdue escalation from one authority. `upcoming` when no dose is open (PRN
|
||||
/ paused / ended).
|
||||
targetAt:
|
||||
anyOf:
|
||||
- type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
- type: "null"
|
||||
description: Target instant of the open dose. Null when no dose is open.
|
||||
required:
|
||||
- status
|
||||
- targetAt
|
||||
additionalProperties: false
|
||||
description: v1.15.9 — the open dose's per-dose status + target, so the card highlights the actionable take-window green
|
||||
and escalates an overdue dose without re-deriving the window math client-side.
|
||||
required:
|
||||
- shortDays
|
||||
- longDays
|
||||
@@ -8559,6 +8588,7 @@ components:
|
||||
- short
|
||||
- long
|
||||
- currentCycle
|
||||
- currentDose
|
||||
additionalProperties: false
|
||||
description: The two-row card block whose windows scale with dosing cadence (dense meds keep 7 / 30 days, sparse meds
|
||||
step both windows up). NOT the 30-day denominator — read `compliance30.totalExpected` for that.
|
||||
|
||||
@@ -1604,6 +1604,69 @@ describe("buildComplianceDisplay — two rows, cadence-scaled windows", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("currentDose — rolling weekly, last shot 3 days ago → upcoming (card stays calm)", () => {
|
||||
// v1.15.9 — the open dose's per-dose status drives the card's green /
|
||||
// overdue presentation. A weekly shot due in 4 days is well before the
|
||||
// ±1-day on-time window opens → `upcoming`, with the target echoed.
|
||||
const schedules: ComplianceSchedule[] = [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
rollingIntervalDays: 7,
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
const lastShot = new Date(NOW.getTime() - 3 * DAY_MS);
|
||||
const events = [
|
||||
{ scheduledFor: lastShot, takenAt: lastShot, skipped: false },
|
||||
];
|
||||
const display = buildComplianceDisplay(
|
||||
events,
|
||||
schedules,
|
||||
ctx({ startsOn: lastShot, lastIntakeAt: lastShot }),
|
||||
{ now: NOW },
|
||||
);
|
||||
expect(display.currentDose.status).toBe("upcoming");
|
||||
expect(display.currentDose.targetAt).not.toBeNull();
|
||||
expect(display.currentDose.targetAt!.getTime()).toBe(
|
||||
display.currentCycle.nextDueAt!.getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
it("currentDose — weekly shot due now is on_time_window (green); long overdue is missed", () => {
|
||||
const schedules: ComplianceSchedule[] = [
|
||||
{
|
||||
windowStart: "08:00",
|
||||
windowEnd: "09:00",
|
||||
daysOfWeek: null,
|
||||
rollingIntervalDays: 7,
|
||||
timesOfDay: ["08:00"],
|
||||
},
|
||||
];
|
||||
// Last shot exactly 7 days ago → next due ~now → inside the ±1-day
|
||||
// on-time window → green.
|
||||
const dueNow = new Date(NOW.getTime() - 7 * DAY_MS);
|
||||
const dueNowDisplay = buildComplianceDisplay(
|
||||
[{ scheduledFor: dueNow, takenAt: dueNow, skipped: false }],
|
||||
schedules,
|
||||
ctx({ startsOn: dueNow, lastIntakeAt: dueNow }),
|
||||
{ now: NOW },
|
||||
);
|
||||
expect(dueNowDisplay.currentDose.status).toBe("on_time_window");
|
||||
|
||||
// Last shot 14 days ago → due ~7 days ago → past the 4-day overdue tail
|
||||
// → missed (the card escalates to "Stark überfällig").
|
||||
const longAgo = new Date(NOW.getTime() - 14 * DAY_MS);
|
||||
const missedDisplay = buildComplianceDisplay(
|
||||
[{ scheduledFor: longAgo, takenAt: longAgo, skipped: false }],
|
||||
schedules,
|
||||
ctx({ startsOn: longAgo, lastIntakeAt: longAgo }),
|
||||
{ now: NOW },
|
||||
);
|
||||
expect(missedDisplay.currentDose.status).toBe("missed");
|
||||
});
|
||||
|
||||
it("currentCycle — brand-new sparse med with zero closed cycles → hasClosedCycles false", () => {
|
||||
// v1.13.x Fix 4 — a med created today with no logged intake has no
|
||||
// closed dose cycle; the percentage rows are vacuous and the card should
|
||||
|
||||
@@ -820,6 +820,15 @@ export interface ComplianceDisplay {
|
||||
* rows so a between-doses sparse med never renders a scary red number.
|
||||
*/
|
||||
currentCycle: CurrentCycle;
|
||||
/**
|
||||
* v1.15.9 — the open cycle's per-dose {@link DoseStatus}, server-derived
|
||||
* so the card renders its state (green take-window, overdue / heavily-
|
||||
* overdue escalation) from one authority instead of re-deriving the window
|
||||
* math client-side. `status` is `upcoming` when no dose is open yet and
|
||||
* there is no projected next dose (PRN / paused / ended). `targetAt` is the
|
||||
* open dose's target instant (echo of `currentCycle.nextDueAt`).
|
||||
*/
|
||||
currentDose: { status: DoseStatus; targetAt: Date | null };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -954,6 +963,24 @@ export function buildComplianceDisplay(
|
||||
events,
|
||||
);
|
||||
|
||||
// v1.15.9 — derive the open dose's per-dose status from one server
|
||||
// authority so the card renders the take-window (green) / overdue /
|
||||
// heavily-overdue escalation without re-spelling the window math. The
|
||||
// cadence family comes from the SOONEST non-PRN schedule (the one the
|
||||
// open cycle anchors on). `none` cycles (PRN / paused / ended) carry an
|
||||
// `upcoming` status with a null target so the card stays calm.
|
||||
const currentDose: { status: DoseStatus; targetAt: Date | null } =
|
||||
currentCycle.nextDueAt
|
||||
? {
|
||||
status: deriveDoseStatus(
|
||||
currentCycle.nextDueAt,
|
||||
soonestCadenceFamily(schedules),
|
||||
now,
|
||||
),
|
||||
targetAt: currentCycle.nextDueAt,
|
||||
}
|
||||
: { status: "upcoming", targetAt: null };
|
||||
|
||||
return {
|
||||
shortDays,
|
||||
longDays,
|
||||
@@ -977,9 +1004,27 @@ export function buildComplianceDisplay(
|
||||
missed: long.missed,
|
||||
},
|
||||
currentCycle,
|
||||
currentDose,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.15.9 — the {@link DoseCadenceFamily} of the soonest non-PRN schedule,
|
||||
* used to pick the window model for the open dose's status. A multi-schedule
|
||||
* med whose cycle anchors on its earliest window keeps that window's cadence;
|
||||
* when every schedule is PRN (no projected dose) the family is irrelevant
|
||||
* because the caller only reads `currentDose` when a cycle is open.
|
||||
*/
|
||||
function soonestCadenceFamily(
|
||||
schedules: ComplianceSchedule[],
|
||||
): DoseCadenceFamily {
|
||||
for (const s of schedules) {
|
||||
if (s.scheduleType === "PRN") continue;
|
||||
return doseCadenceFamily(s);
|
||||
}
|
||||
return "daily";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compliance for a medication over a given period.
|
||||
*
|
||||
|
||||
@@ -1332,6 +1332,29 @@ const complianceDisplay = z
|
||||
.describe(
|
||||
"v1.13.x — the current (open) dose cycle, surfaced so a between-doses sparse med renders a neutral 'next dose in N days' line instead of a scary red 0%. The percentage rows above already exclude the open forward cycle from their denominator.",
|
||||
),
|
||||
currentDose: z
|
||||
.object({
|
||||
status: z
|
||||
.enum([
|
||||
"upcoming",
|
||||
"on_time_window",
|
||||
"overdue",
|
||||
"missed",
|
||||
"taken_on_time",
|
||||
"taken_late",
|
||||
"skipped",
|
||||
])
|
||||
.describe(
|
||||
"Per-dose state of the open cycle, server-derived from the window model so the card renders the take-window (green) / overdue / heavily-overdue escalation from one authority. `upcoming` when no dose is open (PRN / paused / ended).",
|
||||
),
|
||||
targetAt: z.iso
|
||||
.datetime({ offset: true })
|
||||
.nullable()
|
||||
.describe("Target instant of the open dose. Null when no dose is open."),
|
||||
})
|
||||
.describe(
|
||||
"v1.15.9 — the open dose's per-dose status + target, so the card highlights the actionable take-window green and escalates an overdue dose without re-deriving the window math client-side.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "ComplianceDisplay",
|
||||
|
||||
Reference in new issue
Block a user