From 5bfba0ba604dc9b35c461b604eddf8811894549a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 30 Jun 2026 23:41:06 +0200 Subject: [PATCH] feat(mental-health): open an instrument's trend from its card Clicking a PHQ-9 / GAD-7 card body now opens that instrument's history in a detail sheet, mirroring how a medication card opens its detail. The card body is the navigation target; the bottom-pinned Start button stays a separate action and keeps its canonical typography and spacing. The detail reuses the existing history chart and dated list, pinned to the chosen instrument: the PHQ-9 / GAD-7 toggle and the outer card chrome drop away since the sheet owns the title. The combined history card on the landing is unchanged. --- messages/de.json | 3 +- messages/en.json | 3 +- messages/es.json | 3 +- messages/fr.json | 3 +- messages/it.json | 3 +- messages/pl.json | 3 +- .../__tests__/mental-wellbeing.test.tsx | 73 +++++++ .../mental-health/assessment-history.tsx | 197 +++++++++++------- .../mental-health/instrument-card.tsx | 66 +++--- .../mental-health/mental-wellbeing.tsx | 34 +++ 10 files changed, 280 insertions(+), 108 deletions(-) diff --git a/messages/de.json b/messages/de.json index 4438719f7..6f5243478 100644 --- a/messages/de.json +++ b/messages/de.json @@ -7716,7 +7716,8 @@ "review": { "title": "Überblick", "recap": "Du hast {answered} von {total} Fragen beantwortet." - } + }, + "openDetail": "Verlauf anzeigen" }, "records": { "disclaimer": "Das sind deine eigenen Angaben, keine ärztliche Diagnose. HealthLog stellt keine Diagnose, behandelt nicht und deutet nichts.", diff --git a/messages/en.json b/messages/en.json index 11fdc9ee9..a1094539e 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7716,7 +7716,8 @@ "review": { "title": "Review", "recap": "You answered {answered} of {total} questions." - } + }, + "openDetail": "Show trend" }, "records": { "disclaimer": "These are your own records, not a medical diagnosis. HealthLog does not diagnose, treat, or interpret them.", diff --git a/messages/es.json b/messages/es.json index c498e22e8..5df7f651f 100644 --- a/messages/es.json +++ b/messages/es.json @@ -7716,7 +7716,8 @@ "review": { "title": "Resumen", "recap": "Has respondido {answered} de {total} preguntas." - } + }, + "openDetail": "Ver evolución" }, "records": { "disclaimer": "Son tus propios registros, no un diagnóstico médico. HealthLog no diagnostica, no trata ni los interpreta.", diff --git a/messages/fr.json b/messages/fr.json index 8cba119f6..f0dbd75a7 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -7716,7 +7716,8 @@ "review": { "title": "Récapitulatif", "recap": "Vous avez répondu à {answered} questions sur {total}." - } + }, + "openDetail": "Voir l’évolution" }, "records": { "disclaimer": "Ce sont vos propres informations, pas un diagnostic médical. HealthLog ne diagnostique pas, ne traite pas et ne les interprète pas.", diff --git a/messages/it.json b/messages/it.json index 88a6e34fb..4785ce709 100644 --- a/messages/it.json +++ b/messages/it.json @@ -7716,7 +7716,8 @@ "review": { "title": "Riepilogo", "recap": "Hai risposto a {answered} domande su {total}." - } + }, + "openDetail": "Mostra andamento" }, "records": { "disclaimer": "Sono i tuoi dati, non una diagnosi medica. HealthLog non diagnostica, non cura e non li interpreta.", diff --git a/messages/pl.json b/messages/pl.json index 3b16a27c3..4b6b91788 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -7716,7 +7716,8 @@ "review": { "title": "Podsumowanie", "recap": "Odpowiedziano na {answered} z {total} pytań." - } + }, + "openDetail": "Pokaż przebieg" }, "records": { "disclaimer": "To Twoje własne wpisy, a nie diagnoza lekarska. HealthLog nie diagnozuje, nie leczy i ich nie interpretuje.", diff --git a/src/components/mental-health/__tests__/mental-wellbeing.test.tsx b/src/components/mental-health/__tests__/mental-wellbeing.test.tsx index 125cc6a44..f983ed473 100644 --- a/src/components/mental-health/__tests__/mental-wellbeing.test.tsx +++ b/src/components/mental-health/__tests__/mental-wellbeing.test.tsx @@ -24,6 +24,7 @@ import { MentalWellbeing } from "../mental-wellbeing"; import { CheckInWizard } from "../check-in-wizard"; import { AssessmentResult } from "../assessment-result"; import { AssessmentHistory } from "../assessment-history"; +import { InstrumentCard } from "../instrument-card"; import type { AssessmentRow, CreateResponse } from "../types"; const mh = en.mentalHealth; @@ -194,3 +195,75 @@ describe("history rendering", () => { expect(empty).not.toContain('data-slot="history-list"'); }); }); + +describe("instrument card → per-instrument trend (v1.25.12)", () => { + it("exposes the card body as a detail target separate from Start", () => { + const html = withProviders( + {}} + onOpenDetail={() => {}} + />, + ); + // The title + last-result block is a button that opens the Verlauf… + expect(html).toContain('data-slot="instrument-card-open"'); + expect(html).toContain(mh.openDetail); + // …and the Start action is still its own button. + expect(html).toContain(mh.start); + }); + + it("pins the history to one instrument: no toggle, bare (no titled card)", () => { + const rows: AssessmentRow[] = [ + { + id: "p1", + instrument: "PHQ9", + locale: "en", + totalScore: 8, + severityBand: "mild", + item9Flagged: false, + crisisShownAt: null, + takenAt: "2026-06-20T00:00:00.000Z", + }, + { + id: "g1", + instrument: "GAD7", + locale: "en", + totalScore: 12, + severityBand: "moderate", + item9Flagged: false, + crisisShownAt: null, + takenAt: "2026-06-20T00:00:00.000Z", + }, + ]; + const html = withProviders( + , + ); + // Pinned: the PHQ-9 / GAD-7 toggle + the titled landing card are gone… + expect(html).toContain('data-pinned="PHQ9"'); + expect(html).not.toContain('role="tablist"'); + // …only the chosen instrument's row paints (its band), not the other's. + expect(html).toContain(mh.band.PHQ9.mild); + expect(html).not.toContain(mh.band.GAD7.moderate); + }); + + it("shows the per-instrument empty state when that instrument has no rows", () => { + const onlyGad: AssessmentRow[] = [ + { + id: "g2", + instrument: "GAD7", + locale: "en", + totalScore: 5, + severityBand: "mild", + item9Flagged: false, + crisisShownAt: null, + takenAt: "2026-06-20T00:00:00.000Z", + }, + ]; + const html = withProviders( + , + ); + expect(html).toContain(mh.history.empty); + expect(html).not.toContain('data-slot="history-list"'); + }); +}); diff --git a/src/components/mental-health/assessment-history.tsx b/src/components/mental-health/assessment-history.tsx index ca3a8458e..d570fe7e9 100644 --- a/src/components/mental-health/assessment-history.tsx +++ b/src/components/mental-health/assessment-history.tsx @@ -68,44 +68,60 @@ function crisisFromRow(row: AssessmentRow): CrisisSet { }; } -export function AssessmentHistory({ rows }: { rows: AssessmentRow[] }) { +export function AssessmentHistory({ + rows, + instrument, +}: { + rows: AssessmentRow[]; + /** + * Pin the history to a single instrument. When set, the PHQ-9 / GAD-7 + * toggle + the outer Card chrome are dropped — the surface is a per- + * instrument detail (opened from an instrument card; the chrome + title + * come from the host sheet). Unset, it renders the combined landing card + * with the toggle, exactly as before. + */ + instrument?: InstrumentId; +}) { const { t } = useTranslations(); const { date: formatDate } = useFormatters(); - const [tab, setTab] = useState("PHQ9"); + const [tab, setTab] = useState(instrument ?? "PHQ9"); // Which flagged row's crisis card is currently expanded (re-surfaced). const [openCrisisRowId, setOpenCrisisRowId] = useState(null); + const pinned = instrument != null; + // When pinned, the tab is forced to the host instrument so re-opening the + // detail for the other instrument never paints a stale series. + const activeTab = pinned ? instrument : tab; + const forTab = useMemo( () => rows - .filter((r) => r.instrument === tab) + .filter((r) => r.instrument === activeTab) .sort( (a, b) => new Date(b.takenAt).getTime() - new Date(a.takenAt).getTime(), ), - [rows, tab], + [rows, activeTab], ); - const isEmpty = rows.length === 0; + // Pinned: emptiness is per-instrument (the host opened THIS instrument's + // detail). Combined: emptiness spans both instruments, as before. + const isEmpty = pinned ? forTab.length === 0 : rows.length === 0; - return ( - - - - {t("mentalHealth.history.title")} - - - - {isEmpty ? ( - } - title={t("mentalHealth.history.title")} - description={t("mentalHealth.history.empty")} - /> - ) : ( - <> - {/* PHQ-9 / GAD-7 toggle — the two ranges + band sets can't share an - axis, so the chart + list paint one instrument at a time. */} + const body = ( + <> + {isEmpty ? ( + } + title={t("mentalHealth.history.title")} + description={t("mentalHealth.history.empty")} + /> + ) : ( + <> + {/* PHQ-9 / GAD-7 toggle — the two ranges + band sets can't share an + axis, so the chart + list paint one instrument at a time. Hidden + when pinned: the host already chose the instrument. */} + {!pinned && (
{ setTab(id); @@ -129,63 +145,88 @@ export function AssessmentHistory({ rows }: { rows: AssessmentRow[] }) { ))}
+ )} - + - {forTab.length === 0 ? ( -

- {t("mentalHealth.history.empty")} -

- ) : ( -
    - {forTab.map((row) => { - const flagged = row.item9Flagged; - const open = openCrisisRowId === row.id; - return ( -
  • -
    - - {formatDate(row.takenAt)} - -
    - {flagged && ( - + {forTab.length === 0 ? ( +

    + {t("mentalHealth.history.empty")} +

    + ) : ( +
      + {forTab.map((row) => { + const flagged = row.item9Flagged; + const open = openCrisisRowId === row.id; + return ( +
    • +
      + + {formatDate(row.takenAt)} + +
      + {flagged && ( + + )} + + {t( + `mentalHealth.band.${row.instrument}.${row.severityBand}`, )} - - {t( - `mentalHealth.band.${row.instrument}.${row.severityBand}`, - )} - - - {row.totalScore} - -
      + + + {row.totalScore} +
      - {flagged && open && ( - - )} -
    • - ); - })} -
    - )} - - )} - +
    + {flagged && open && ( + + )} +
  • + ); + })} +
+ )} + + )} + + ); + + // Pinned: render bare (the host sheet owns the chrome + title). Combined: + // the landing's titled history card, unchanged. + if (pinned) { + return ( +
+ {body} +
+ ); + } + + return ( + + + + {t("mentalHealth.history.title")} + + + {body} ); } diff --git a/src/components/mental-health/instrument-card.tsx b/src/components/mental-health/instrument-card.tsx index 2b24cf69b..57c9875b6 100644 --- a/src/components/mental-health/instrument-card.tsx +++ b/src/components/mental-health/instrument-card.tsx @@ -24,45 +24,63 @@ export function InstrumentCard({ instrument, last, onStart, + onOpenDetail, }: { instrument: InstrumentId; /** Most-recent assessment for this instrument, or undefined when none yet. */ last: AssessmentRow | undefined; onStart: () => void; + /** + * Open this instrument's trend detail. The card BODY (title + last-result + * line) is the navigation target — mirroring how a medication card opens its + * detail — while the bottom-pinned Start button stays a separate action. + */ + onOpenDetail: () => void; }) { const { t } = useTranslations(); const { date: formatDate } = useFormatters(); const key = lower(instrument); + const title = t(`mentalHealth.instrument.${key}`); return ( -
- - {t(`mentalHealth.instrument.${key}`)} - - - {t(`mentalHealth.instrumentSub.${key}`)} - -
+ {/* Body is the detail target: clicking the title / last-result block + opens this instrument's Verlauf, like a medication card. The Start + button below is a separate, deliberately non-overlapping action. */} + -
+
diff --git a/src/components/mental-health/mental-wellbeing.tsx b/src/components/mental-health/mental-wellbeing.tsx index 5b0483624..454f333dc 100644 --- a/src/components/mental-health/mental-wellbeing.tsx +++ b/src/components/mental-health/mental-wellbeing.tsx @@ -26,6 +26,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useTranslations } from "@/lib/i18n/context"; import { apiGet, apiPost } from "@/lib/api/api-fetch"; import { queryKeys } from "@/lib/query-keys"; +import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { AssessmentHistory } from "./assessment-history"; import { AssessmentResult } from "./assessment-result"; @@ -45,6 +46,11 @@ export function MentalWellbeing() { const [phase, setPhase] = useState("choose"); const [instrument, setInstrument] = useState("PHQ9"); const [result, setResult] = useState(null); + // Which instrument's trend detail is open (null = closed). Clicking a card + // body surfaces THAT instrument's Verlauf in a sheet; Start stays separate. + const [detailInstrument, setDetailInstrument] = useState( + null, + ); const { data: history } = useQuery({ queryKey: queryKeys.mentalHealthAssessments(), @@ -119,6 +125,7 @@ export function MentalWellbeing() { instrument={id} last={lastByInstrument.get(id)} onStart={() => begin(id)} + onOpenDetail={() => setDetailInstrument(id)} /> ))} @@ -126,6 +133,33 @@ export function MentalWellbeing() { + + {/* Per-instrument trend detail, opened from a card body. Reuses the + history's chart + dated list, pinned to the chosen instrument. */} + { + if (!open) setDetailInstrument(null); + }} + contentWidth="lg" + title={ + detailInstrument + ? t( + `mentalHealth.instrument.${ + detailInstrument === "PHQ9" ? "phq9" : "gad7" + }`, + ) + : t("mentalHealth.history.title") + } + description={t("mentalHealth.history.chartTitle")} + > + {detailInstrument && ( + + )} + )}