fix(medications): drop the duplicated overdue line below the streak row

The injection/GLP-1 card showed an overdue dose twice: once on the
next-intake value line (the destructive escalation with the due time)
and again as the open-cycle status line below the compliance bars. The
cycle line's missed tier is retired; the due-today tier stays. The
reserved min-h slot in the shared card body is untouched, so card
heights stay symmetric across medication types.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 14:49:22 +02:00
1 parent 53f84abfbb
commit d323d01c93
8 files changed
+22 -39

No files matched your search

-1
View File
@@ -870,7 +870,6 @@
"dayStreak": "Tage Serie",
"dayStreakOne": "Tag Serie",
"cycleDueToday": "Heute fällig",
"cycleOverdue": "Überfällig",
"cycleNoClosedCycles": "Noch keine abgeschlossenen Dosiszyklen",
"taken": "Genommen",
"skipped": "Übersprungen",
-1
View File
@@ -870,7 +870,6 @@
"dayStreak": "day streak",
"dayStreakOne": "day streak",
"cycleDueToday": "Due today",
"cycleOverdue": "Overdue",
"cycleNoClosedCycles": "No closed dose cycles yet",
"taken": "Taken",
"skipped": "Skipped",
-1
View File
@@ -870,7 +870,6 @@
"dayStreak": "días de racha",
"dayStreakOne": "día de racha",
"cycleDueToday": "Pendiente hoy",
"cycleOverdue": "Atrasada",
"cycleNoClosedCycles": "Aún no hay ciclos de dosis completados",
"taken": "Tomada",
"skipped": "Omitida",
-1
View File
@@ -870,7 +870,6 @@
"dayStreak": "jours de série",
"dayStreakOne": "jour de série",
"cycleDueToday": "À prendre aujourd'hui",
"cycleOverdue": "En retard",
"cycleNoClosedCycles": "Aucun cycle de dose terminé pour l'instant",
"taken": "Pris",
"skipped": "Sauté",
-1
View File
@@ -870,7 +870,6 @@
"dayStreak": "giorni di serie",
"dayStreakOne": "giorno di serie",
"cycleDueToday": "In scadenza oggi",
"cycleOverdue": "In ritardo",
"cycleNoClosedCycles": "Ancora nessun ciclo di dose completato",
"taken": "Assunta",
"skipped": "Saltata",
-1
View File
@@ -870,7 +870,6 @@
"dayStreak": "dni serii",
"dayStreakOne": "dzień serii",
"cycleDueToday": "Termin dzisiaj",
"cycleOverdue": "Zaległa",
"cycleNoClosedCycles": "Brak zakończonych cykli dawkowania",
"taken": "Przyjęte",
"skipped": "Pominięte",
@@ -288,7 +288,10 @@ describe("medication cycle status — open-cycle line", () => {
expect(html).toContain("lucide-calendar-clock");
});
it("missed reads the destructive overdue line", () => {
it("missed renders nothing — the overdue escalation already reads on the next-intake line", () => {
// v1.27.5 — the card's next-intake value line carries the destructive
// overdue escalation; a second overdue line below the streak row doubled
// the signal and is retired.
const html = render(
<MedicationCycleStatus
cycle={{
@@ -299,9 +302,7 @@ describe("medication cycle status — open-cycle line", () => {
}}
/>,
);
expect(html).toContain("Overdue");
expect(html).toContain("text-destructive");
expect(html).toContain("lucide-triangle-alert");
expect(html).toBe("");
});
});
@@ -1,4 +1,4 @@
import { AlertTriangle, CalendarClock, CircleDashed } from "lucide-react";
import { CalendarClock, CircleDashed } from "lucide-react";
import type { CurrentCycle } from "@/lib/analytics/compliance";
import { useTranslations } from "@/lib/i18n/context";
@@ -20,13 +20,15 @@ interface MedicationCycleStatusProps {
*
* 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.
* next-intake slot at the top, so repeating it would be a duplicate. The same
* reasoning retired the `missed` tier in v1.27.5 — an overdue dose already
* escalates on the next-intake value line (the destructive "Überfällig" /
* "Stark überfällig" row), so a second overdue line below the streak row
* doubled the signal. Only the `due` tier (and the neutral no-closed-cycles
* note) earns this line.
*
* Driven by `currentCycle.state` (`on_track` / `due` / `missed` / `none`) plus
* `hasClosedCycles`. Colour follows the same semantic ramp the status pill
* uses (warning / destructive) and pairs every tier with a Lucide glyph so
* `hasClosedCycles`. The due tier uses the warning tone + 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.
*/
@@ -49,31 +51,17 @@ 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;
if (cycle.state === "missed") {
tone = "text-destructive";
icon = <AlertTriangle className="size-3.5 shrink-0" aria-hidden="true" />;
label = t("medications.cycleOverdue");
} else {
// due
tone = "text-warning";
icon = <CalendarClock className="size-3.5 shrink-0" aria-hidden="true" />;
label = t("medications.cycleDueToday");
}
// 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. missed —
// the overdue escalation already reads on that same next-intake value line
// (destructive tone + glyph), so a second overdue line here doubled it.
// Both render nothing; only the due tier earns this line.
if (cycle.state !== "due") return null;
return (
<p className={`flex items-center gap-1.5 text-xs font-medium ${tone}`}>
{icon}
{label}
<p className="text-warning flex items-center gap-1.5 text-xs font-medium">
<CalendarClock className="size-3.5 shrink-0" aria-hidden="true" />
{t("medications.cycleDueToday")}
</p>
);
}