From 0e184e7f1bc1169aeca395fbf0758820b1acf972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 6 Jun 2026 23:25:47 +0200 Subject: [PATCH] fix(medications): drop the duplicate next-dose line and the injection-site recommendation from the card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The open-cycle status line repeated the upcoming-dose timing already shown in the card's next-intake slot; the calm on-track "next dose in N days" phrasing is now suppressed, leaving only the actionable due / overdue tiers. Remove the "recommended next site" nudge from the GLP-1 card — it is noise on the at-a-glance surface. Injection-site tracking stays: the last site still reads on the last-injection line and the post-dose picker still captures the next site. --- .../medications/__tests__/card-parts.test.tsx | 55 ++++------------ .../__tests__/medication-card-glp1.test.tsx | 22 +++---- .../card-parts/medication-cycle-status.tsx | 65 ++++++------------- .../medications/glp1-medication-card.tsx | 27 +------- 4 files changed, 41 insertions(+), 128 deletions(-) diff --git a/src/components/medications/__tests__/card-parts.test.tsx b/src/components/medications/__tests__/card-parts.test.tsx index 708362399..4fbea4ab1 100644 --- a/src/components/medications/__tests__/card-parts.test.tsx +++ b/src/components/medications/__tests__/card-parts.test.tsx @@ -207,49 +207,18 @@ describe("medication cycle status — open-cycle line", () => { expect(html).not.toContain("Next dose in"); }); - it("on_track several days out reads the relative-day phrasing in success tone", () => { - const html = render( - , - ); - expect(html).toContain("Next dose in 4 days"); - expect(html).toContain("text-success"); - expect(html).toContain("lucide-circle-check"); - }); - - it("on_track reads the same day count when nextDueAt arrives as an ISO string", () => { - // Over the wire `nextDueAt` is JSON, so it reaches the card as a string - // even though the type says `Date`. The component must re-hydrate it for - // the Berlin day-bucketing — a raw string would silently drift the count. - const iso = inDays(4).toISOString() as unknown as Date; - const html = render( - , - ); - expect(html).toContain("Next dose in 4 days"); - }); - - it("on_track one day out reads tomorrow", () => { - const html = render( - , - ); - expect(html).toContain("Next dose tomorrow"); - expect(html).not.toContain("in 1 days"); - }); - - it("on_track later today reads today", () => { - // Six hours past the pinned noon — still the same Berlin calendar day. - const later = new Date(Date.now() + 6 * 60 * 60 * 1000); - const html = render( - , - ); - expect(html).toContain("Next dose today"); + it("on_track renders nothing — the next-dose timing already reads on the card's top slot", () => { + // The calm on_track "next dose in N days" phrasing duplicates the + // next-intake slot at the top of the card, so the open-cycle line stays + // silent for an on-track med; only due / overdue earn a line here. + for (const nextDueAt of [inDays(4), inDays(1), new Date(Date.now() + 6 * 60 * 60 * 1000)]) { + const html = render( + , + ); + expect(html).toBe(""); + } }); it("due reads the amber due-today line", () => { diff --git a/src/components/medications/__tests__/medication-card-glp1.test.tsx b/src/components/medications/__tests__/medication-card-glp1.test.tsx index a7130b2f0..c77f983ea 100644 --- a/src/components/medications/__tests__/medication-card-glp1.test.tsx +++ b/src/components/medications/__tests__/medication-card-glp1.test.tsx @@ -370,15 +370,11 @@ describe(" — GLP-1 variant rendering", () => { expect(html).not.toContain("Dosis-Historie"); }); - it("renders the injection-site rotation nudge folded into the upcoming line", () => { - // The rotation nudge surfaces only when `lastSite` and the recommender - // disagree. We feed a left-cluster history → the recommender lands on a - // right-side or thigh/arm site. The nudge is no longer a separate - // bordered block (which made the GLP-1 card taller than the generic card - // in a grid row); it is folded into the upcoming-injection line as a - // "· Recommended next: …" suffix, so the card keeps the same row - // inventory as the generic card. The last site stays visible on the - // last-injection line. + it("does not render an injection-site recommendation on the card", () => { + // The card no longer carries a "recommended next site" nudge — the + // recommendation is noise on the at-a-glance surface. Site TRACKING stays + // (the last site reads on the last-injection line; the post-dose picker + // still captures the next site), only the recommendation copy is gone. const client = makeClient(); seedCompliance(client, med7p5.id); seedGlp1Details(client, med7p5.id, { @@ -401,11 +397,9 @@ describe(" — GLP-1 variant rendering", () => { client, ); - // The recommended-next nudge renders in the upcoming line. - expect(html).toContain("Recommended next:"); - // The standalone bordered rotation block is gone (height-neutral fold). - expect(html).not.toContain("bg-muted/40 rounded-md border"); - // Last site stays visible on the last-injection line. + // No recommendation copy anywhere on the card. + expect(html).not.toContain("Recommended next:"); + // Last site stays visible on the last-injection line (tracking preserved). expect(html).toContain("Abdomen, lower left"); }); diff --git a/src/components/medications/card-parts/medication-cycle-status.tsx b/src/components/medications/card-parts/medication-cycle-status.tsx index 8ff46e633..c364b379f 100644 --- a/src/components/medications/card-parts/medication-cycle-status.tsx +++ b/src/components/medications/card-parts/medication-cycle-status.tsx @@ -1,30 +1,13 @@ -import { AlertTriangle, CalendarClock, CircleCheck, CircleDashed } from "lucide-react"; +import { AlertTriangle, CalendarClock, CircleDashed } from "lucide-react"; import type { CurrentCycle } from "@/lib/analytics/compliance"; import { useTranslations } from "@/lib/i18n/context"; -import { toBerlinDate } from "@/lib/medications/window-status"; interface MedicationCycleStatusProps { /** The open-cycle descriptor from `complianceDisplay.currentCycle`. */ cycle: CurrentCycle; } -const DAY_MS = 24 * 60 * 60 * 1000; - -/** - * Whole-day delta between two instants, measured on the user's calendar - * (Europe/Berlin) so "in 2 days" lines up with the day a sparse dose lands on - * rather than a raw 48-hour count. Positive = future, 0 = today, negative = - * past. Mirrors the day-bucketing the card already does for its next/last slot. - */ -function calendarDayDelta(target: Date, now: Date): number { - const a = toBerlinDate(target); - const b = toBerlinDate(now); - const targetMidnight = new Date(a.getFullYear(), a.getMonth(), a.getDate()); - const nowMidnight = new Date(b.getFullYear(), b.getMonth(), b.getDate()); - return Math.round((targetMidnight.getTime() - nowMidnight.getTime()) / DAY_MS); -} - /** * v1.14.0 — the open-cycle status line for the medication cards. * @@ -32,15 +15,20 @@ function calendarDayDelta(target: Date, now: Date): number { * rolling med (a GLP-1 injection) that is simply between doses has no scored * cycle in flight, so the percentage alone reads as a scary 0% / 100% that * says nothing about whether the user is actually on schedule. This line is - * decoupled from the rate: it reports the open cycle's calm status — when the - * next dose lands, whether it's due or overdue, or that there's not enough - * history to score yet. + * decoupled from the rate: it reports the open cycle's calm status — whether + * the dose is due or overdue, or that there's not enough history to score yet. + * + * The calm `on_track` "next dose in N days" phrasing is intentionally NOT + * rendered here: the upcoming-dose timing already reads on the card's + * next-intake slot at the top, so repeating it would be a duplicate. Only the + * actionable due / overdue tiers (and the neutral no-closed-cycles note) earn + * this line. * * Driven by `currentCycle.state` (`on_track` / `due` / `missed` / `none`) plus * `hasClosedCycles`. Colour follows the same semantic ramp the status pill - * uses (success / warning / destructive) and pairs every tier with a Lucide - * glyph so colour-blind users can disambiguate — WCAG 1.4.1 (Use of Color). - * It complements the compliance number; it does not replace it. + * uses (warning / destructive) and pairs every tier with a Lucide glyph so + * colour-blind users can disambiguate — WCAG 1.4.1 (Use of Color). It + * complements the compliance number; it does not replace it. */ export function MedicationCycleStatus({ cycle }: MedicationCycleStatusProps) { const { t } = useTranslations(); @@ -61,6 +49,12 @@ export function MedicationCycleStatus({ cycle }: MedicationCycleStatusProps) { ); } + // on_track — the next dose simply hasn't come round yet. The upcoming-dose + // timing already reads on the card's next-intake slot at the top, so a second + // "next dose in N days" line here is a duplicate. Render nothing for the + // on-track case; only the actionable due / overdue tiers earn this line. + if (cycle.state !== "missed" && cycle.state !== "due") return null; + let tone: string; let icon: React.ReactNode; let label: string; @@ -69,30 +63,11 @@ export function MedicationCycleStatus({ cycle }: MedicationCycleStatusProps) { tone = "text-destructive"; icon =