feat(mental-health): rebuild the test cards on the shared med-card anatomy

The PHQ-9 / GAD-7 cards adopt the medication / Vorsorge card grammar:
MedicationCardHeader (bold title + outline category badge), a label/value
block carrying "last test" as a relative day and "last result" as
score + band word, and a single bottom-pinned Start action. Titles read
"PHQ-9 · Stimmung" / "GAD-7 · Sorgen" (middle-dot separator, all six
locales) and a new last-result label lands in every locale bundle. The
card surface stays neutral regardless of the last band.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 14:49:46 +02:00
1 parent 44bc91c3ea
commit 43251ba59b
8 files changed
+95 -46

No files matched your search

+3 -2
View File
@@ -7636,8 +7636,8 @@
"error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
"considerProfessional": "Dein Wert liegt in einem Bereich, in dem ein Gespräch mit einer Fachperson helfen kann. Das ist ein behutsamer Hinweis, keine Diagnose.",
"instrument": {
"phq9": "PHQ-9 — Stimmung",
"gad7": "GAD-7 — Sorgen"
"phq9": "PHQ-9 · Stimmung",
"gad7": "GAD-7 · Sorgen"
},
"instrumentSub": {
"phq9": "Depressions-Screening, 9 Fragen",
@@ -7745,6 +7745,7 @@
"questionAria": "Frage {current} von {total}",
"next": "Weiter",
"lastResult": "Letzter Test",
"lastScore": "Letztes Ergebnis",
"noResultYet": "Noch kein Check-in",
"backToList": "Übersicht",
"jumpFirst": "Zur ersten Frage springen",
+3 -2
View File
@@ -7636,8 +7636,8 @@
"error": "Something went wrong. Please try again.",
"considerProfessional": "Your score is in a range where talking to a health professional may help. This is a gentle suggestion, not a diagnosis.",
"instrument": {
"phq9": "PHQ-9 — mood",
"gad7": "GAD-7 — worry"
"phq9": "PHQ-9 · mood",
"gad7": "GAD-7 · worry"
},
"instrumentSub": {
"phq9": "Depression screen, 9 questions",
@@ -7745,6 +7745,7 @@
"questionAria": "Question {current} of {total}",
"next": "Next",
"lastResult": "Last test",
"lastScore": "Last result",
"noResultYet": "No check-in yet",
"backToList": "Overview",
"jumpFirst": "Jump to first question",
+3 -2
View File
@@ -7636,8 +7636,8 @@
"error": "Algo salió mal. Inténtalo de nuevo.",
"considerProfessional": "Tu puntuación está en un rango en el que hablar con un profesional de salud puede ayudar. Es una sugerencia amable, no un diagnóstico.",
"instrument": {
"phq9": "PHQ-9 — ánimo",
"gad7": "GAD-7 — preocupación"
"phq9": "PHQ-9 · ánimo",
"gad7": "GAD-7 · preocupación"
},
"instrumentSub": {
"phq9": "Cribado de depresión, 9 preguntas",
@@ -7745,6 +7745,7 @@
"questionAria": "Pregunta {current} de {total}",
"next": "Siguiente",
"lastResult": "Última prueba",
"lastScore": "Último resultado",
"noResultYet": "Aún sin check-in",
"backToList": "Vista general",
"jumpFirst": "Ir a la primera pregunta",
+3 -2
View File
@@ -7636,8 +7636,8 @@
"error": "Une erreur est survenue. Réessaie.",
"considerProfessional": "Ton score se situe dans une plage où parler à un professionnel de santé peut aider. C'est une suggestion bienveillante, pas un diagnostic.",
"instrument": {
"phq9": "PHQ-9 — humeur",
"gad7": "GAD-7 — inquiétude"
"phq9": "PHQ-9 · humeur",
"gad7": "GAD-7 · inquiétude"
},
"instrumentSub": {
"phq9": "Dépistage de la dépression, 9 questions",
@@ -7745,6 +7745,7 @@
"questionAria": "Question {current} sur {total}",
"next": "Suivant",
"lastResult": "Dernier test",
"lastScore": "Dernier résultat",
"noResultYet": "Pas encore de check-in",
"backToList": "Vue d'ensemble",
"jumpFirst": "Aller à la première question",
+3 -2
View File
@@ -7636,8 +7636,8 @@
"error": "Qualcosa è andato storto. Riprova.",
"considerProfessional": "Il tuo punteggio è in un intervallo in cui parlare con un professionista sanitario può aiutare. È un suggerimento gentile, non una diagnosi.",
"instrument": {
"phq9": "PHQ-9 — umore",
"gad7": "GAD-7 — preoccupazione"
"phq9": "PHQ-9 · umore",
"gad7": "GAD-7 · preoccupazione"
},
"instrumentSub": {
"phq9": "Screening della depressione, 9 domande",
@@ -7745,6 +7745,7 @@
"questionAria": "Domanda {current} di {total}",
"next": "Avanti",
"lastResult": "Ultimo test",
"lastScore": "Ultimo risultato",
"noResultYet": "Ancora nessun check-in",
"backToList": "Panoramica",
"jumpFirst": "Vai alla prima domanda",
+3 -2
View File
@@ -7636,8 +7636,8 @@
"error": "Coś poszło nie tak. Spróbuj ponownie.",
"considerProfessional": "Twój wynik mieści się w zakresie, w którym rozmowa ze specjalistą może pomóc. To życzliwa sugestia, a nie diagnoza.",
"instrument": {
"phq9": "PHQ-9 — nastrój",
"gad7": "GAD-7 — lęk"
"phq9": "PHQ-9 · nastrój",
"gad7": "GAD-7 · lęk"
},
"instrumentSub": {
"phq9": "Przesiew depresji, 9 pytań",
@@ -7745,6 +7745,7 @@
"questionAria": "Pytanie {current} z {total}",
"next": "Dalej",
"lastResult": "Ostatni test",
"lastScore": "Ostatni wynik",
"noResultYet": "Brak check-inów",
"backToList": "Przegląd",
"jumpFirst": "Przejdź do pierwszego pytania",
@@ -200,14 +200,42 @@ describe("history rendering", () => {
});
});
describe("instrument card (v1.27.6 — no trend-detail target)", () => {
it("renders the card body as plain content with Start as the single action", () => {
describe("instrument card (v1.27.6 — med-/Vorsorge card anatomy)", () => {
const lastRow: AssessmentRow = {
id: "l1",
instrument: "PHQ9",
locale: "en",
totalScore: 8,
severityBand: "mild",
item9Flagged: false,
crisisShownAt: null,
takenAt: "2026-06-20T00:00:00.000Z",
};
it("renders the shared med-card header (title + category badge), no trend target", () => {
const html = withProviders(
<InstrumentCard instrument="PHQ9" last={undefined} onStart={() => {}} />,
);
// The med-card header primitive paints the bold name + category badge.
expect(html).toContain(mh.instrument.phq9);
expect(html).toContain(mh.instrumentSub.phq9);
// The former trend-detail button is gone…
expect(html).not.toContain('data-slot="instrument-card-open"');
// …and the Start action remains.
// …and the Start action remains the single bottom-pinned action.
expect(html).toContain(mh.start);
// No history yet → the calm no-check-in line, no fabricated dashes.
expect(html).toContain(mh.noResultYet);
});
it("shows last test (relative) and last result (score + band word)", () => {
const html = withProviders(
<InstrumentCard instrument="PHQ9" last={lastRow} onStart={() => {}} />,
);
expect(html).toContain(mh.lastResult);
expect(html).toContain(mh.lastScore);
// Score + band word ride one value slot ("8 · Mild").
expect(html).toContain('data-slot="instrument-card-last-score"');
expect(html).toContain(mh.band.PHQ9.mild);
expect(html).not.toContain(mh.noResultYet);
});
});
@@ -3,14 +3,17 @@
/**
* v1.25.3 — one neutral card per instrument on the landing.
*
* Mirrors the Vorsorge / medication card grammar: a header (title + sub),
* a discreet "last result" line when history exists (band badge + relative
* date), and a single bottom-pinned Start action. The surface stays NEUTRAL
* v1.27.6 — rebuilt onto the shared med-/Vorsorge card anatomy so the two
* test cards read exactly like their peers: `MedicationCardHeader` (bold
* name + outline category badge), a label/value block in the next-last
* grammar ("last test" → relative day, "last result" → score + band word),
* and a single bottom-pinned Start action. The surface stays NEUTRAL
* regardless of the last band — no severity tint (house rule: status reads
* through a discreet badge only).
* through discreet text only, never a card wash).
*/
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { MedicationCardHeader } from "@/components/medications/medication-card-header";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { relativeCalendarDate } from "@/lib/i18n/relative-time";
@@ -36,36 +39,48 @@ export function InstrumentCard({
const title = t(`mentalHealth.instrument.${key}`);
return (
<Card className="h-full gap-3" data-slot="instrument-card">
<CardContent className="flex h-full flex-col space-y-3.5 p-4">
{/* v1.27.6 — the card body is no longer a trend-detail target (the
trend chart left this surface); the card informs, the Start button
below is the single action. */}
<div className="flex flex-1 flex-col gap-3.5">
<div className="flex flex-col gap-0.5">
<span className="text-lg leading-none font-semibold">{title}</span>
<span className="text-muted-foreground text-xs">
{t(`mentalHealth.instrumentSub.${key}`)}
</span>
<Card className="h-full gap-3 md:gap-3" data-slot="instrument-card">
<MedicationCardHeader
name={title}
dose=""
categoryLabel={t(`mentalHealth.instrumentSub.${key}`)}
/>
<CardContent className="flex h-full flex-col space-y-3.5">
{/* Label/value block in the Vorsorge card's next-last grammar. With
no history yet, one calm line replaces the pair — no dashes
pretending a value exists. */}
{last ? (
<div className="min-h-[2.75rem] space-y-1.5 text-sm">
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
<span className="min-w-0 flex-shrink truncate font-medium">
{t("mentalHealth.lastResult")}
</span>
<span className="text-foreground text-right">
{relativeCalendarDate(last.takenAt, t, formatDate)}
</span>
</div>
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
<span className="min-w-0 flex-shrink truncate font-medium">
{t("mentalHealth.lastScore")}
</span>
<span
className="text-foreground text-right"
data-slot="instrument-card-last-score"
>
{last.totalScore}
{" · "}
{t(`mentalHealth.band.${last.instrument}.${last.severityBand}`)}
</span>
</div>
</div>
{/* Last-test line in the medication-card grammar: label left, the
relative day (today / yesterday / date) right-aligned. */}
<div className="text-muted-foreground flex min-h-5 items-center justify-between gap-2 text-xs">
{last ? (
<>
<span>{t("mentalHealth.lastResult")}</span>
<span className="shrink-0">
{relativeCalendarDate(last.takenAt, t, formatDate)}
</span>
</>
) : (
<span>{t("mentalHealth.noResultYet")}</span>
)}
) : (
<div className="text-muted-foreground flex min-h-[2.75rem] items-center text-sm">
{t("mentalHealth.noResultYet")}
</div>
</div>
)}
<div className="pt-0">
{/* Bottom-pinned single primary action — the med-/Vorsorge card slot. */}
<div className="mt-auto pt-0">
<Button type="button" className="min-h-11 w-full" onClick={onStart}>
{t("mentalHealth.start")}
</Button>