mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
fix(medications): restore the two-row compliance display, scaled to cadence
v1.8.5 swapped the long-standing 7-/30-day compliance rows for a per-dose adherence timeline on sparse-cadence medications. The timeline breaks the card layout and reads as restless, so every medication is back to two percentage rows. The two windows are no longer hardcoded — they scale with the dosing cadence. selectComplianceWindows walks a 7/30 -> 30/90 -> 90/365 ladder and returns the densest rung whose both windows clear MIN_STABLE_DOSES realised expected doses, so a daily med stays on 7/30, a weekly med steps up to 30/90, and a rolling 35-day injection lands on the widest 90/365 rung. When even the top rung can't clear the floor (a brand-new or very rare med) the widest rung is returned so the card still shows two honest rows. The row labels follow the chosen windows via a parametrised complianceWindow string. The compliance percentage math is unchanged: each row is the existing cadence-aware calculateCompliance over the selected window. compliance7 / compliance30 stay on the wire verbatim for iOS and the Health Score. - compliance.ts: replace the timeline display with selectComplianceWindows + the window ladder; buildComplianceDisplay now returns the two scaled rows - remove the dose-adherence-timeline component and its now-dead strings - medication-compliance-bars: parametrise the row labels on the day-counts - switch the standard and GLP-1 cards back to the always-two-row bars
This commit is contained in:
1 parent
6c44d4a2c1
commit
e5b873833a
15 files changed
+376
-627
No files matched your search
@@ -247,13 +247,12 @@ describe("GET /api/medications/[id]/compliance — per-slot timing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// v1.8.5 — the additive `complianceDisplay` block. The server decides the
|
||||
// render mode from expected-dose density: a daily med keeps the percentage
|
||||
// bars (`"percent"`), a sparse rolling/long-interval med swaps to the
|
||||
// per-dose timeline (`"timeline"`). The existing `compliance7` /
|
||||
// `compliance30` fields stay untouched.
|
||||
// v1.8.6 — the `complianceDisplay` block is always two percentage rows; the
|
||||
// server scales the two windows to the dosing cadence. A daily med keeps
|
||||
// 7 / 30 days; a sparse rolling med steps both windows up to the widest rung.
|
||||
// The existing `compliance7` / `compliance30` fields stay untouched.
|
||||
describe("GET /api/medications/[id]/compliance — complianceDisplay", () => {
|
||||
it("daily med → mode: percent", async () => {
|
||||
it("daily med → 7 / 30-day windows", async () => {
|
||||
vi.mocked(prisma.medication.findUnique).mockResolvedValue(
|
||||
medication([
|
||||
{
|
||||
@@ -277,24 +276,32 @@ describe("GET /api/medications/[id]/compliance — complianceDisplay", () => {
|
||||
|
||||
const data = await callRouteData();
|
||||
const display = data.complianceDisplay as {
|
||||
mode: string;
|
||||
expected30: number;
|
||||
shortDays: number;
|
||||
longDays: number;
|
||||
expectedLong: number;
|
||||
minStableDoses: number;
|
||||
short: { rate: number; streak: number };
|
||||
long: { rate: number };
|
||||
};
|
||||
expect(display.mode).toBe("percent");
|
||||
expect(display.expected30).toBeGreaterThanOrEqual(display.minStableDoses);
|
||||
expect(display.shortDays).toBe(7);
|
||||
expect(display.longDays).toBe(30);
|
||||
expect(display.expectedLong).toBeGreaterThanOrEqual(display.minStableDoses);
|
||||
expect(display.short).toBeDefined();
|
||||
expect(display.long).toBeDefined();
|
||||
// No timeline field — the display is two rows only.
|
||||
expect(display).not.toHaveProperty("mode");
|
||||
// The legacy fields stay on the wire.
|
||||
expect(data.compliance7).toBeDefined();
|
||||
expect(data.compliance30).toBeDefined();
|
||||
});
|
||||
|
||||
it("35-day-interval med → mode: timeline", async () => {
|
||||
it("35-day-interval med → steps the windows up past 7 / 30 days", async () => {
|
||||
vi.mocked(prisma.medication.findUnique).mockResolvedValue(
|
||||
{
|
||||
id: "med-1",
|
||||
userId: "user-1",
|
||||
createdAt: new Date(Date.now() - 200 * 24 * 60 * 60 * 1000),
|
||||
startsOn: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000),
|
||||
createdAt: new Date(Date.now() - 400 * 24 * 60 * 60 * 1000),
|
||||
startsOn: new Date(Date.now() - 380 * 24 * 60 * 60 * 1000),
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
schedules: [
|
||||
@@ -314,8 +321,6 @@ describe("GET /api/medications/[id]/compliance — complianceDisplay", () => {
|
||||
],
|
||||
} as never,
|
||||
);
|
||||
// One past intake re-anchors the rolling cadence ~25 days ago, so the
|
||||
// next slot lands beyond the 30-day window → <4 expected → timeline.
|
||||
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue([
|
||||
{
|
||||
takenAt: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000),
|
||||
@@ -326,11 +331,12 @@ describe("GET /api/medications/[id]/compliance — complianceDisplay", () => {
|
||||
|
||||
const data = await callRouteData();
|
||||
const display = data.complianceDisplay as {
|
||||
mode: string;
|
||||
expected30: number;
|
||||
minStableDoses: number;
|
||||
shortDays: number;
|
||||
longDays: number;
|
||||
};
|
||||
expect(display.mode).toBe("timeline");
|
||||
expect(display.expected30).toBeLessThan(display.minStableDoses);
|
||||
// A 35-day cadence can't clear four expected doses in a 30-day window, so
|
||||
// the ladder steps the short window beyond 7 days and the long beyond 30.
|
||||
expect(display.shortDays).toBeGreaterThan(7);
|
||||
expect(display.longDays).toBeGreaterThan(30);
|
||||
});
|
||||
});
|
||||
@@ -142,12 +142,12 @@ export const GET = apiHandler(
|
||||
{ medicationContext },
|
||||
);
|
||||
|
||||
// v1.8.5 — the additive display block. `mode` is server-decided from
|
||||
// expected-dose density: dense cadences keep the 7-/30-day percentage
|
||||
// bars (`"percent"`), sparse cadences (weekly, bi-/tri-weekly, rolling
|
||||
// 35-day injections) swap to the per-dose uptime strip (`"timeline"`).
|
||||
// `compliance7` / `compliance30` above are untouched — iOS + the Health
|
||||
// Score read them verbatim.
|
||||
// v1.8.6 — the two-row compliance display. The card always shows two
|
||||
// percentage rows; the server scales the two windows to the dosing
|
||||
// cadence (dense meds keep 7 / 30 days, sparse meds step both windows
|
||||
// up) and computes each row's rate over the chosen span. `compliance7`
|
||||
// / `compliance30` above are untouched — iOS + the Health Score read
|
||||
// them verbatim.
|
||||
const complianceDisplay = buildComplianceDisplay(
|
||||
mapped,
|
||||
medication.schedules,
|
||||
@@ -289,7 +289,8 @@ export const GET = apiHandler(
|
||||
meta: {
|
||||
compliance7: compliance7.rate,
|
||||
compliance30: compliance30.rate,
|
||||
displayMode: complianceDisplay.mode,
|
||||
complianceShortDays: complianceDisplay.shortDays,
|
||||
complianceLongDays: complianceDisplay.longDays,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user