fix(medications): category label uses the category-map lookup uniformly

The generic medication card mapped `medication.category` to one of the
localised category labels via an inline lookup table, while the GLP-1
card hard-coded `medications.treatmentClassGlp1` into the same slot.
The asymmetry made Mounjaro and Ramipril cards visually diverge on the
medications list even though both should read the same row shape.

Lift the lookup into `src/lib/medications/category-label.ts` and use it
from both card variants. The GLP-1 nature of a medication is already
implied by the rotation hint + injection-metadata rows below the
header, so the category badge now carries the real classification
(BLOOD_PRESSURE, HORMONE, OTHER, ...) on both cards.

Tests pinned the old contract; they now assert the new shared label
shape for both English and German locales.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 14:25:49 +02:00
1 parent d0ba800d9c
commit b643baed82
4 files changed
+63 -22

No files matched your search

@@ -164,10 +164,13 @@ beforeEach(() => {
describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
it("renders the GLP-1 variant when treatmentClass === 'GLP1' is active", () => {
// Headline differentiator: the GLP-1 card stamps the
// `medications.treatmentClassGlp1` badge ("GLP-1 injection")
// which the generic card never renders. Presence of the badge is
// the contract pin for "GLP-1 variant is showing".
// v1.4.37 W4b — the category-label slot now mirrors the generic
// card (real `medication.category` lookup) so Ramipril and
// Mounjaro share the same row shape. The variant differentiator
// moved into the GLP-1-specific rows below the header (last/next
// injection, rotation hint). We verify the GLP-1 card no longer
// hard-codes the treatment-class label into the category slot,
// and that the actual category lookup wins.
const client = makeClient();
seedCompliance(client, med7p5.id);
seedGlp1Details(client, med7p5.id, {});
@@ -177,7 +180,9 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
client,
);
expect(html).toContain("GLP-1 injection");
// Category is OTHER → "Other" badge wins.
expect(html).toContain("Other");
expect(html).not.toContain("GLP-1 injection");
// v1.4.28 FB-G1 — the Syringe glyph + middle-dot separator on the
// list row are gone. The list row reads as the canonical two-line
// shape: `{name} {dose}` on line 1, class label on line 2.
@@ -468,7 +473,12 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
"de",
);
expect(html).toContain("GLP-1-Injektion");
// v1.4.37 W4b — the German category badge now comes from the
// shared category-label lookup (Sonstiges for OTHER) instead of
// the hard-coded "GLP-1-Injektion" label, so the GLP-1 card stays
// visually symmetric with the generic card on the medications list.
expect(html).toContain("Sonstiges");
expect(html).not.toContain("GLP-1-Injektion");
expect(html).toContain("Letzter Termin:");
expect(html).toContain("Bauch, unten links");
// v1.4.28 retired the "Dosis-Historie" disclosure on the GLP-1 card.
@@ -21,6 +21,7 @@ import { Progress } from "@/components/ui/progress";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { invalidateKeys, medicationDependentKeys } from "@/lib/query-keys";
import { formatDateTime, formatTime } from "@/lib/format";
import { getMedicationCategoryLabel } from "@/lib/medications/category-label";
import {
describeInjectionSite,
nextInjectionSite,
@@ -283,12 +284,14 @@ export function Glp1MedicationCard({
</>
);
const categoryLabel = getMedicationCategoryLabel(medication.category, t);
return (
<Card className={medication.active ? "" : "opacity-60"}>
<MedicationCardHeader
name={medication.name}
dose={medication.dose}
categoryLabel={t("medications.treatmentClassGlp1")}
categoryLabel={categoryLabel}
stateBadges={stateBadges}
actions={headerActions}
/>
+2 -15
View File
@@ -10,6 +10,7 @@ import { MedicationCardHeader } from "@/components/medications/MedicationCardHea
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
import { formatTimeWindowRange } from "@/lib/time-window-format";
import { formatDateTime, formatTime } from "@/lib/format";
import { getMedicationCategoryLabel } from "@/lib/medications/category-label";
import {
parseTimeToMinutes,
reduceCurrentWindowStatus,
@@ -172,21 +173,7 @@ export function MedicationCard({ medication, onEdit }: MedicationCardProps) {
const rate7 = compliance?.compliance7?.rate ?? 0;
const rate30 = compliance?.compliance30?.rate ?? 0;
const streak = compliance?.compliance7?.streak ?? 0;
const categoryLabels: Record<string, string> = {
BLOOD_PRESSURE: t("medications.categoryBloodPressure"),
VITAMIN: t("medications.categoryVitamin"),
SUPPLEMENT: t("medications.categorySupplement"),
PAIN_RELIEF: t("medications.categoryPainRelief"),
ALLERGY: t("medications.categoryAllergy"),
DIGESTIVE: t("medications.categoryDigestive"),
THYROID: t("medications.categoryThyroid"),
HORMONE: t("medications.categoryHormone"),
SKIN: t("medications.categorySkin"),
SLEEP_AID: t("medications.categorySleepAid"),
OTHER: t("medications.categoryOther"),
};
const categoryLabel =
categoryLabels[medication.category] ?? t("medications.categoryOther");
const categoryLabel = getMedicationCategoryLabel(medication.category, t);
const sortedSchedules = [...medication.schedules].sort(
(a, b) =>
a.windowStart.localeCompare(b.windowStart) ||
+41
View File
@@ -0,0 +1,41 @@
/**
* v1.4.37 W4b — shared medication category-label lookup.
*
* Both the generic `<MedicationCard>` and the GLP-1 variant render
* `categoryLabel` into the same `<MedicationCardHeader>` outline badge.
* Historically the generic card kept its own inline lookup table while
* the GLP-1 card hard-coded `medications.treatmentClassGlp1` into the
* slot — Marc reported the asymmetry during the v1.4.37 UX audit
* (audit item 11). The GLP-1 nature is already implied by the
* rotation hint + injection metadata rows, so both surfaces should
* paint the actual medication category instead.
*
* The lookup is a small string map; we keep it as a function that takes
* the bound `t()` translator so the keys stay one source of truth and
* we don't need to drill a separate `categoryLabels` object through
* the card props.
*/
type Translator = (key: string, params?: Record<string, string | number>) => string;
const MEDICATION_CATEGORY_KEYS: Record<string, string> = {
BLOOD_PRESSURE: "medications.categoryBloodPressure",
VITAMIN: "medications.categoryVitamin",
SUPPLEMENT: "medications.categorySupplement",
PAIN_RELIEF: "medications.categoryPainRelief",
ALLERGY: "medications.categoryAllergy",
DIGESTIVE: "medications.categoryDigestive",
THYROID: "medications.categoryThyroid",
HORMONE: "medications.categoryHormone",
SKIN: "medications.categorySkin",
SLEEP_AID: "medications.categorySleepAid",
OTHER: "medications.categoryOther",
};
export function getMedicationCategoryLabel(
category: string,
t: Translator,
): string {
const key = MEDICATION_CATEGORY_KEYS[category] ?? "medications.categoryOther";
return t(key);
}