mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
drop dose counts and injection site from the medication card
Remove the per-row dose count (the '· 12 / 12' caption) from the compliance bars — the percentage, bar and window-days label remain. The auto-miss engine now makes the two windows' percentages genuinely diverge, so the counts no longer earn the row noise; the engine still carries the counts on the wire for other consumers. Remove the injection-site display from the overview card, including the last-injection-site shown on the last-intake line. Site tracking and logging are unchanged elsewhere (the post-dose picker, the detail history); only the card surface drops it.
This commit is contained in:
1 parent
5b47aae8b1
commit
a30b5f0d36
3 files changed
+125
-120
No files matched your search
@@ -13,7 +13,8 @@ import { MedicationStatusPill } from "@/components/medications/card-parts/medica
|
||||
import { MedicationIntakeActions } from "@/components/medications/card-parts/medication-intake-actions";
|
||||
import { MedicationStateBadges } from "@/components/medications/card-parts/medication-state-badges";
|
||||
import { MedicationCycleStatus } from "@/components/medications/card-parts/medication-cycle-status";
|
||||
import type { CurrentCycle } from "@/lib/analytics/compliance";
|
||||
import { MedicationCardBody } from "@/components/medications/card-parts/medication-card-body";
|
||||
import type { CurrentCycle, DoseStatus } from "@/lib/analytics/compliance";
|
||||
|
||||
/**
|
||||
* v1.7.2 — the medication-card status pill, compliance bars, intake-action
|
||||
@@ -84,49 +85,22 @@ describe("medication card-parts — shared presentational components", () => {
|
||||
expect(html).not.toContain("lucide-flame");
|
||||
});
|
||||
|
||||
it("compliance bars show taken / expected counts next to each rate", () => {
|
||||
// v1.15.8 — two identical percentages with no context read as a stuck
|
||||
// display; the count after each rate makes them distinguishable. A
|
||||
// rolling weekly med reading 100% on both windows now shows
|
||||
// `100% · 4 / 4` and `100% · 52 / 52`.
|
||||
it("compliance bars render the percentage only — no dose count beside it", () => {
|
||||
// v1.15.9 — the operator does not want a count next to the percentage.
|
||||
// The auto-miss engine now makes the two windows' percentages genuinely
|
||||
// diverge, so the per-row count (`· 12 / 12`) is gone: percentage + bar
|
||||
// + window-days label only.
|
||||
const html = render(
|
||||
<MedicationComplianceBars
|
||||
rate7={100}
|
||||
rate30={100}
|
||||
streak={3}
|
||||
takenShort={4}
|
||||
expectedShort={4}
|
||||
takenLong={52}
|
||||
expectedLong={52}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("4 / 4");
|
||||
expect(html).toContain("52 / 52");
|
||||
// The middle-dot separator joins the percentage and the count.
|
||||
expect(html).toContain("·");
|
||||
});
|
||||
|
||||
it("compliance bars fall back to the bare expected count when no taken count is on the wire", () => {
|
||||
const html = render(
|
||||
<MedicationComplianceBars
|
||||
rate7={80}
|
||||
rate30={75}
|
||||
streak={0}
|
||||
expectedShort={12}
|
||||
expectedLong={52}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("12 doses");
|
||||
expect(html).toContain("52 doses");
|
||||
});
|
||||
|
||||
it("compliance bars render no count line when neither count is available", () => {
|
||||
// Older mocks / the pre-display fallback path pass only the rates.
|
||||
const html = render(
|
||||
<MedicationComplianceBars rate7={90} rate30={88} streak={0} />,
|
||||
<MedicationComplianceBars rate7={100} rate30={92} streak={3} />,
|
||||
);
|
||||
expect(html).toContain("100%");
|
||||
expect(html).toContain("92%");
|
||||
// No middle-dot separator and no "doses" count caption anywhere.
|
||||
expect(html).not.toContain("·");
|
||||
expect(html).not.toContain("doses");
|
||||
// The bar + the window-days label survive.
|
||||
expect(html).toContain("7-day compliance");
|
||||
expect(html).toContain("30-day compliance");
|
||||
});
|
||||
|
||||
it("status pill stamps the success token + take-now glyph in window", () => {
|
||||
@@ -397,3 +371,89 @@ describe("streak-token parity — generic vs GLP-1 card", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.15.9 — the ONE shared `<MedicationCardBody>` both cards render. These
|
||||
* tests pin the state-driven presentation (green take-window highlight,
|
||||
* overdue / heavily-overdue top line) and the structural identity that makes
|
||||
* the two variants impossible to diverge — they pass content into THIS body.
|
||||
*/
|
||||
describe("medication card body — shared shell + dose-state presentation", () => {
|
||||
function renderBody(doseStatus: DoseStatus, active = true) {
|
||||
return render(
|
||||
<MedicationCardBody
|
||||
name="Ramipril"
|
||||
dose="5 mg"
|
||||
categoryLabel="Blood Pressure"
|
||||
active={active}
|
||||
href="/medications/m1"
|
||||
linkLabel="Open"
|
||||
stateBadges={null}
|
||||
headerActions={null}
|
||||
windowStatus={
|
||||
doseStatus === "on_time_window"
|
||||
? { status: "in_window", windowStart: "08:00", windowEnd: "20:00" }
|
||||
: null
|
||||
}
|
||||
doseStatus={doseStatus}
|
||||
nextLine="Tomorrow, 08:00"
|
||||
lastLine="Today, 07:30"
|
||||
compliance={{ rate7: 90, rate30: 88, streak: 0, shortDays: 7, longDays: 30 }}
|
||||
currentCycle={null}
|
||||
intakeLoading={null}
|
||||
onRecordIntake={() => {}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
it("highlights the card green when the dose is in its take-window", () => {
|
||||
const html = renderBody("on_time_window");
|
||||
// Green ring + tinted background on the Card shell.
|
||||
expect(html).toContain("ring-success/40");
|
||||
expect(html).toContain("bg-success/5");
|
||||
});
|
||||
|
||||
it("stays calm (no green highlight) for upcoming / taken states", () => {
|
||||
for (const status of ["upcoming", "taken_on_time", "taken_late", "skipped"] as const) {
|
||||
const html = renderBody(status);
|
||||
expect(html).not.toContain("ring-success/40");
|
||||
}
|
||||
});
|
||||
|
||||
it("shows a calm 'Overdue' top line for an overdue dose", () => {
|
||||
const html = renderBody("overdue");
|
||||
expect(html).toContain("Overdue");
|
||||
expect(html).toContain("text-destructive");
|
||||
expect(html).not.toContain("Very overdue");
|
||||
});
|
||||
|
||||
it("escalates to 'Very overdue' at / past the miss cutoff", () => {
|
||||
const html = renderBody("missed");
|
||||
expect(html).toContain("Very overdue");
|
||||
expect(html).toContain("text-destructive");
|
||||
});
|
||||
|
||||
it("never highlights green on an inactive medication", () => {
|
||||
const html = renderBody("on_time_window", false);
|
||||
expect(html).not.toContain("ring-success/40");
|
||||
expect(html).toContain("opacity-60");
|
||||
});
|
||||
|
||||
it("renders the decisive next + last lines exactly once each", () => {
|
||||
const html = renderBody("upcoming");
|
||||
expect((html.match(/Next intake:/g) ?? []).length).toBe(1);
|
||||
expect((html.match(/Last intake:/g) ?? []).length).toBe(1);
|
||||
expect(html).toContain("Tomorrow, 08:00");
|
||||
expect(html).toContain("Today, 07:30");
|
||||
});
|
||||
|
||||
it("keeps the body shell + spacing tokens that guarantee cross-variant identity", () => {
|
||||
const html = renderBody("upcoming");
|
||||
// Single shared CardContent body with the canonical spacing rhythm.
|
||||
expect(html).toContain("flex h-full flex-col space-y-3.5");
|
||||
// Bottom-pinned action wrapper.
|
||||
expect(html).toContain("mt-auto");
|
||||
// The shared reserved-height next/last slot.
|
||||
expect(html).toContain("min-h-[2.75rem] space-y-3.5 text-sm");
|
||||
});
|
||||
});
|
||||
@@ -321,11 +321,12 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
);
|
||||
|
||||
// v1.15.8 — the GLP-1 card unified onto the generic "Next intake" /
|
||||
// "Last intake" labels (the appointment-phrased override is gone). The
|
||||
// last-line renders the "Last intake:" label in its own left column and
|
||||
// the value (label · site) flush right; both halves show in the SSR.
|
||||
// "Last intake" labels (the appointment-phrased override is gone).
|
||||
// v1.15.9 — the injection SITE is no longer shown on the card line; the
|
||||
// last-line reads the relative-day label only, exactly like the generic
|
||||
// card. (Tracking / logging is unchanged everywhere else.)
|
||||
expect(html).toContain("Last intake:");
|
||||
expect(html).toContain("Abdomen, lower left");
|
||||
expect(html).not.toContain("Abdomen, lower left");
|
||||
// The next-line carries the "Next intake:" label in its left column,
|
||||
// independent of which of the three value variants (today / tomorrow /
|
||||
// "in N days") the helper produced.
|
||||
@@ -372,11 +373,13 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
expect(html).not.toContain("Dosis-Historie");
|
||||
});
|
||||
|
||||
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.
|
||||
it("does not render any injection-site display on the card", () => {
|
||||
// v1.15.9 — the card surface drops the injection SITE entirely: neither
|
||||
// the old "recommended next site" nudge NOR the last-site label on the
|
||||
// last-injection line. The operator: "where I injected doesn't interest
|
||||
// me." Site TRACKING is unchanged everywhere else (the post-dose picker
|
||||
// captures the next site; the detail history shows it) — only the card
|
||||
// surface is clean.
|
||||
const client = makeClient();
|
||||
seedCompliance(client, med7p5.id);
|
||||
seedGlp1Details(client, med7p5.id, {
|
||||
@@ -399,10 +402,9 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
client,
|
||||
);
|
||||
|
||||
// No recommendation copy anywhere on the card.
|
||||
// No recommendation copy and no last-site label 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");
|
||||
expect(html).not.toContain("Abdomen, lower left");
|
||||
});
|
||||
|
||||
it("no longer renders the pen-inventory line (retired v1.4.28)", () => {
|
||||
@@ -609,9 +611,10 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
expect(html).toContain("Sonstiges");
|
||||
expect(html).not.toContain("GLP-1-Injektion");
|
||||
// v1.15.8 — unified onto the generic "Letzte Einnahme:" label (the
|
||||
// appointment-phrased "Letzter Termin:" override is gone).
|
||||
// appointment-phrased "Letzter Termin:" override is gone). v1.15.9 — the
|
||||
// injection site no longer renders on the card.
|
||||
expect(html).toContain("Letzte Einnahme:");
|
||||
expect(html).toContain("Bauch, unten links");
|
||||
expect(html).not.toContain("Bauch, unten links");
|
||||
// v1.4.28 retired the "Dosis-Historie" disclosure on the GLP-1 card.
|
||||
expect(html).not.toContain("Dosis-Historie");
|
||||
});
|
||||
|
||||
@@ -3,30 +3,6 @@ import { Flame } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
|
||||
type Translate = ReturnType<typeof useTranslations>["t"];
|
||||
type Formatters = ReturnType<typeof useFormatters>;
|
||||
|
||||
/**
|
||||
* v1.15.8 — build the dose-count caption shown after the percentage.
|
||||
*
|
||||
* Returns `taken / expected` (`4 / 12`) when both counts are known, the
|
||||
* pluralised `<expected> doses` string when only the denominator is on the
|
||||
* wire, and `null` when neither is available (older mocks / pre-display
|
||||
* fallback) so the caller renders nothing rather than an empty separator.
|
||||
*/
|
||||
function doseCount(
|
||||
t: Translate,
|
||||
fmt: Formatters,
|
||||
taken: number | undefined,
|
||||
expected: number | undefined,
|
||||
): string | null {
|
||||
if (typeof expected !== "number") return null;
|
||||
if (typeof taken === "number") {
|
||||
return `${fmt.number(taken)} / ${fmt.number(expected)}`;
|
||||
}
|
||||
return t("medications.complianceDoses", { count: expected });
|
||||
}
|
||||
|
||||
interface MedicationComplianceBarsProps {
|
||||
rate7: number;
|
||||
rate30: number;
|
||||
@@ -41,20 +17,6 @@ interface MedicationComplianceBarsProps {
|
||||
shortDays?: number;
|
||||
/** v1.8.6 — the span of the long row in days. */
|
||||
longDays?: number;
|
||||
/**
|
||||
* v1.15.8 — taken-dose count over the short window (numerator). Rendered
|
||||
* after the percentage as a `taken / expected` count so two identical
|
||||
* percentages stay distinguishable: a rolling weekly med reading 100% on
|
||||
* every window now shows `100% · 4 / 4` vs `100% · 52 / 52` instead of two
|
||||
* bare identical numbers that read as a stuck display.
|
||||
*/
|
||||
takenShort?: number;
|
||||
/** v1.15.8 — expected-dose count over the short window (denominator). */
|
||||
expectedShort?: number;
|
||||
/** v1.15.8 — taken-dose count over the long window (numerator). */
|
||||
takenLong?: number;
|
||||
/** v1.15.8 — expected-dose count over the long window (denominator). */
|
||||
expectedLong?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,6 +29,12 @@ interface MedicationComplianceBarsProps {
|
||||
* 365-day long window. The labels are parametrised on the chosen day-counts
|
||||
* so each row names the window it actually covers.
|
||||
*
|
||||
* v1.15.9 — the per-row dose count (`· 12 / 12`) is gone: the operator wants
|
||||
* the percentage only. The auto-miss engine now makes the two windows'
|
||||
* percentages genuinely diverge, so the counts no longer earn the row noise.
|
||||
* The engine still carries `expected` / `taken` / `missed` on the wire for
|
||||
* iOS + the Health Score; the card simply does not render them.
|
||||
*
|
||||
* The streak flame uses the semantic `text-warning` token (an alias over
|
||||
* Dracula orange in dark mode, AA-safe on the light card). The generic card
|
||||
* historically drifted onto Tailwind stock `text-orange-400`, and the flame
|
||||
@@ -79,10 +47,6 @@ export function MedicationComplianceBars({
|
||||
streak,
|
||||
shortDays = 7,
|
||||
longDays = 30,
|
||||
takenShort,
|
||||
expectedShort,
|
||||
takenLong,
|
||||
expectedLong,
|
||||
}: MedicationComplianceBarsProps) {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
@@ -96,27 +60,12 @@ export function MedicationComplianceBars({
|
||||
const shortPct = fmt.number(Math.round(rate7));
|
||||
const longPct = fmt.number(Math.round(rate30));
|
||||
|
||||
// v1.15.8 — the dose-count line rendered after each percentage. Shows the
|
||||
// taken-of-expected count when both are known (`4 / 12`), else the bare
|
||||
// expected count when only the denominator is on the wire (`12 Dosen`).
|
||||
// Two windows reading the same percentage stay distinguishable by their
|
||||
// counts, which is what an operator needs to trust the number.
|
||||
const shortCount = doseCount(t, fmt, takenShort, expectedShort);
|
||||
const longCount = doseCount(t, fmt, takenLong, expectedLong);
|
||||
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{shortLabel}</span>
|
||||
<span className="font-medium">
|
||||
{shortPct}%
|
||||
{shortCount && (
|
||||
<span className="text-muted-foreground ml-1.5 font-normal">
|
||||
· {shortCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-medium">{shortPct}%</span>
|
||||
</div>
|
||||
{/* aria-label so the bar has an accessible name. */}
|
||||
<Progress value={rate7} className="h-2" aria-label={shortLabel} />
|
||||
@@ -125,14 +74,7 @@ export function MedicationComplianceBars({
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{longLabel}</span>
|
||||
<span className="font-medium">
|
||||
{longPct}%
|
||||
{longCount && (
|
||||
<span className="text-muted-foreground ml-1.5 font-normal">
|
||||
· {longCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-medium">{longPct}%</span>
|
||||
</div>
|
||||
<Progress value={rate30} className="h-2" aria-label={longLabel} />
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user