feat(mental-health): drop the trend chart from the check-in page

A rising score curve on the check-in page itself reads as a verdict at
the moment someone is taking stock of how they feel. The page keeps the
calm dated list of past check-ins (severity badge, score, the discreet
crisis-resource re-surface); the score trend stays available through the
PHQ9_SCORE / GAD7_SCORE measurement rows on the Insights / Measurements
surfaces. The per-card trend sheet and its chart component are removed,
along with the now-unused chart i18n keys in all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 14:47:58 +02:00
1 parent 53f84abfbb
commit 44bc91c3ea
11 files changed
+125 -474

No files matched your search

+2 -6
View File
@@ -7680,14 +7680,11 @@
"history": {
"title": "Dein Verlauf",
"empty": "Noch keine Check-ins. Dein Verlauf erscheint hier, sobald du einen abgeschlossen hast.",
"chartTitle": "Punkteverlauf",
"flaggedBadge": "Hilfe angezeigt",
"totalLabel": "Punkte",
"tab": {
"phq9": "PHQ-9",
"gad7": "GAD-7"
},
"bandLabel": "Einstufung"
}
},
"crisis": {
"title": "Du musst das nicht allein durchstehen",
@@ -7755,8 +7752,7 @@
"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.",
+2 -6
View File
@@ -7680,14 +7680,11 @@
"history": {
"title": "Your history",
"empty": "No check-ins yet. Your trend will appear here once you complete one.",
"chartTitle": "Score trend",
"flaggedBadge": "Support shown",
"totalLabel": "Score",
"tab": {
"phq9": "PHQ-9",
"gad7": "GAD-7"
},
"bandLabel": "Category"
}
},
"crisis": {
"title": "You don't have to face this alone",
@@ -7755,8 +7752,7 @@
"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.",
+2 -6
View File
@@ -7680,14 +7680,11 @@
"history": {
"title": "Tu historial",
"empty": "Aún no hay revisiones. Tu tendencia aparecerá aquí cuando completes una.",
"chartTitle": "Evolución de la puntuación",
"flaggedBadge": "Apoyo mostrado",
"totalLabel": "Puntuación",
"tab": {
"phq9": "PHQ-9",
"gad7": "GAD-7"
},
"bandLabel": "Categoría"
}
},
"crisis": {
"title": "No tienes que afrontar esto a solas",
@@ -7755,8 +7752,7 @@
"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.",
+2 -6
View File
@@ -7680,14 +7680,11 @@
"history": {
"title": "Ton historique",
"empty": "Aucun bilan pour le moment. Ta tendance apparaîtra ici dès que tu en auras terminé un.",
"chartTitle": "Évolution du score",
"flaggedBadge": "Aide affichée",
"totalLabel": "Score",
"tab": {
"phq9": "PHQ-9",
"gad7": "GAD-7"
},
"bandLabel": "Catégorie"
}
},
"crisis": {
"title": "Tu n'as pas à affronter cela seul(e)",
@@ -7755,8 +7752,7 @@
"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.",
+2 -6
View File
@@ -7680,14 +7680,11 @@
"history": {
"title": "La tua cronologia",
"empty": "Ancora nessun check-in. Il tuo andamento apparirà qui dopo che ne avrai completato uno.",
"chartTitle": "Andamento del punteggio",
"flaggedBadge": "Supporto mostrato",
"totalLabel": "Punteggio",
"tab": {
"phq9": "PHQ-9",
"gad7": "GAD-7"
},
"bandLabel": "Categoria"
}
},
"crisis": {
"title": "Non devi affrontare tutto questo da solo/a",
@@ -7755,8 +7752,7 @@
"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.",
+2 -6
View File
@@ -7680,14 +7680,11 @@
"history": {
"title": "Twoja historia",
"empty": "Brak sprawdzeń. Twój trend pojawi się tutaj po ukończeniu pierwszego.",
"chartTitle": "Przebieg wyniku",
"flaggedBadge": "Pokazano wsparcie",
"totalLabel": "Wynik",
"tab": {
"phq9": "PHQ-9",
"gad7": "GAD-7"
},
"bandLabel": "Kategoria"
}
},
"crisis": {
"title": "Nie musisz mierzyć się z tym sam(a)",
@@ -7755,8 +7752,7 @@
"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.",
@@ -11,8 +11,9 @@ import en from "../../../../messages/en.json";
* means clicks can't be driven here — the wizard's advance/back grammar is
* pinned in `check-in-nav.test.ts`. These tests pin the SSR-observable
* contracts: the disclaimer renders ONLY on the landing (never while testing),
* a positive item-9 surfaces the crisis card, and the history paints its chart
* shell + dated list with severity + "support shown" badges.
* a positive item-9 surfaces the crisis card, and the history paints its
* dated list with severity + "support shown" badges — and NO trend chart
* (v1.27.6: the score curve lives in Insights / Measurements, not here).
*/
vi.mock("@/lib/api/api-fetch", () => ({
@@ -174,9 +175,12 @@ describe("history rendering", () => {
const html = withProviders(<AssessmentHistory rows={rows} />);
it("paints the lazy chart shell + the dated list", () => {
expect(html).toContain('data-slot="skeleton"');
it("paints the dated list WITHOUT a trend chart shell", () => {
expect(html).toContain('data-slot="history-list"');
// v1.27.6 — no chart on this surface: no lazy-chart skeleton, no
// recharts container.
expect(html).not.toContain('data-slot="skeleton"');
expect(html).not.toContain("recharts");
});
it("shows the PHQ-9 / GAD-7 toggle and a severity badge", () => {
@@ -196,74 +200,14 @@ describe("history rendering", () => {
});
});
describe("instrument card → per-instrument trend (v1.25.12)", () => {
it("exposes the card body as a detail target separate from Start", () => {
describe("instrument card (v1.27.6 — no trend-detail target)", () => {
it("renders the card body as plain content with Start as the single action", () => {
const html = withProviders(
<InstrumentCard
instrument="PHQ9"
last={undefined}
onStart={() => {}}
onOpenDetail={() => {}}
/>,
<InstrumentCard instrument="PHQ9" last={undefined} onStart={() => {}} />,
);
// 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.
// The former trend-detail button is gone…
expect(html).not.toContain('data-slot="instrument-card-open"');
// …and the Start action remains.
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(
<AssessmentHistory rows={rows} instrument="PHQ9" />,
);
// 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(
<AssessmentHistory rows={onlyGad} instrument="PHQ9" />,
);
expect(html).toContain(mh.history.empty);
expect(html).not.toContain('data-slot="history-list"');
});
});
@@ -1,161 +0,0 @@
"use client";
/**
* v1.25.3 — per-instrument screener trend chart.
*
* v1.25.5 — aligned to the dashboard's chart language exactly: a single
* `type="monotone"` line over the primary colour with a soft gradient area
* fill beneath (same `fillOpacity` + margins the dashboard line charts use),
* the shared grid / axis tokens, and `RichChartTooltip`. The earlier
* severity-band `ReferenceArea`s — unique to this surface — are gone so it
* reads like every other trend; the band still surfaces in the tooltip.
*
* One instrument at a time: PHQ-9 (0–27) and GAD-7 (0–21) have different ranges
* and bands, so the parent toggles which series this paints.
*/
import { useMemo } from "react";
import {
Area,
CartesianGrid,
ComposedChart,
Line,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { RichChartTooltip, type RichTooltipRow } from "../charts/chart-tooltip";
import { CHART_HEIGHT_PX } from "@/lib/charts/constants";
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
import { formatDateShort } from "@/lib/format";
import { useTranslations } from "@/lib/i18n/context";
import { INSTRUMENTS } from "@/lib/mental-health/instruments";
import type { AssessmentRow, InstrumentId } from "./types";
interface ChartPoint {
timestamp: number;
value: number;
band: string;
}
export function AssessmentHistoryChart({
instrument,
rows,
}: {
instrument: InstrumentId;
/** Rows for THIS instrument, any order (sorted internally, ascending time). */
rows: AssessmentRow[];
}) {
const { t } = useTranslations();
const def = INSTRUMENTS[instrument];
const points = useMemo<ChartPoint[]>(() => {
return [...rows]
.sort(
(a, b) => new Date(a.takenAt).getTime() - new Date(b.takenAt).getTime(),
)
.map((r) => ({
timestamp: new Date(r.takenAt).getTime(),
value: r.totalScore,
band: r.severityBand,
}));
}, [rows]);
const animate = !prefersReducedMotion();
const primary = "var(--primary)";
const gradientId = `mh-trend-${instrument}`;
if (points.length === 0) {
return (
<p className="text-muted-foreground py-10 text-center text-sm">
{t("mentalHealth.history.empty")}
</p>
);
}
return (
<ResponsiveContainer width="100%" height={CHART_HEIGHT_PX}>
<ComposedChart
data={points}
margin={{ top: 10, right: 8, bottom: 8, left: 8 }}
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={primary} stopOpacity={0.18} />
<stop offset="95%" stopColor={primary} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--border)"
vertical={false}
/>
<XAxis
dataKey="timestamp"
type="number"
scale="time"
domain={["dataMin", "dataMax"]}
tickFormatter={(ts: number) => formatDateShort(new Date(ts))}
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
stroke="var(--border)"
minTickGap={32}
/>
<YAxis
domain={[0, def.maxScore]}
allowDecimals={false}
tick={{ fontSize: 11, fill: "var(--muted-foreground)" }}
stroke="var(--border)"
width={32}
/>
<Tooltip
content={(props) => {
const active = props.active ?? false;
const payload = props.payload as
ReadonlyArray<{ payload?: ChartPoint }> | undefined;
const point = payload?.[0]?.payload;
if (!active || !point) {
return <RichChartTooltip active={false} rows={[]} />;
}
const rowsOut: RichTooltipRow[] = [
{
name: t("mentalHealth.history.totalLabel"),
value: String(point.value),
color: primary,
},
{
name: t("mentalHealth.history.bandLabel"),
value: t(`mentalHealth.band.${instrument}.${point.band}`),
color: "var(--muted-foreground)",
},
];
return (
<RichChartTooltip
active
label={formatDateShort(new Date(point.timestamp))}
rows={rowsOut}
/>
);
}}
/>
<Area
type="monotone"
dataKey="value"
stroke="none"
fill={`url(#${gradientId})`}
isAnimationActive={animate}
/>
<Line
type="monotone"
dataKey="value"
stroke={primary}
strokeWidth={2}
dot={{ r: 3, fill: primary }}
activeDot={{ r: 5 }}
isAnimationActive={animate}
/>
</ComposedChart>
</ResponsiveContainer>
);
}
@@ -1,13 +1,17 @@
"use client";
/**
* v1.25.3 — the history card: a per-instrument trend chart over a band-shaded
* plot, then the dated list with severity badges. Mirrors the labs detail spine
* (chart → list) and lazy-loads Recharts through `next/dynamic` so it stays off
* the surface's first-load JS, with a `ChartSkeleton` shell.
* v1.25.3 — the history card: a dated list with severity badges.
*
* PHQ-9 / GAD-7 toggle: the two instruments have different score ranges + bands,
* so the chart paints one at a time.
* v1.27.6 — the trend CHART is gone from this surface. A rising score curve
* on the check-in page itself reads as a verdict at the exact moment someone
* is taking stock of how they feel; the page stays a calm list of past
* check-ins. The score trend still exists — the totals ride PHQ9_SCORE /
* GAD7_SCORE measurement rows, so Insights / Measurements carry the curve
* for whoever seeks it out.
*
* PHQ-9 / GAD-7 toggle: the two instruments have different score ranges +
* bands, so the list shows one at a time.
*
* CRISIS-SAFETY: a flagged PHQ-9 row carries a discreet "support shown" marker;
* tapping it re-surfaces the STATIC crisis-resource card derived from the row's
@@ -16,16 +20,11 @@
* leaves the safety boundary.
*/
import { useMemo, useState } from "react";
import dynamic from "next/dynamic";
import type { ComponentProps } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { ChartErrorBoundary } from "@/components/charts/chart-error-state";
import { ChartSkeleton } from "@/components/charts/chart-skeleton";
import { importWithRetry } from "@/lib/retry-import";
import { useFormatters, useTranslations } from "@/lib/i18n/context";
import { crisisResourcesForLocale } from "@/lib/mental-health/crisis-resources";
import { Activity } from "lucide-react";
@@ -33,25 +32,6 @@ import { Activity } from "lucide-react";
import { CrisisCard } from "./crisis-card";
import type { AssessmentRow, CrisisSet, InstrumentId } from "./types";
// Defer the recharts trend chart so recharts stays off the surface's first-load
// JS (the labs detail precedent). The `<ChartSkeleton>` shell holds the layout.
const AssessmentHistoryChartLazy = dynamic(
() =>
importWithRetry(() => import("./assessment-history-chart")).then((mod) => ({
default: mod.AssessmentHistoryChart,
})),
{ ssr: false, loading: () => <ChartSkeleton /> },
);
function AssessmentHistoryChart(
props: ComponentProps<typeof AssessmentHistoryChartLazy>,
) {
return (
<ChartErrorBoundary>
<AssessmentHistoryChartLazy {...props} />
</ChartErrorBoundary>
);
}
function lower(id: InstrumentId): "phq9" | "gad7" {
return id === "PHQ9" ? "phq9" : "gad7";
}
@@ -68,73 +48,57 @@ function crisisFromRow(row: AssessmentRow): CrisisSet {
};
}
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;
}) {
export function AssessmentHistory({ rows }: { rows: AssessmentRow[] }) {
const { t } = useTranslations();
const { date: formatDate } = useFormatters();
const [tab, setTab] = useState<InstrumentId>(instrument ?? "PHQ9");
const [tab, setTab] = useState<InstrumentId>("PHQ9");
// Which flagged row's crisis card is currently expanded (re-surfaced).
const [openCrisisRowId, setOpenCrisisRowId] = useState<string | null>(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 === activeTab)
.filter((r) => r.instrument === tab)
.sort(
(a, b) =>
new Date(b.takenAt).getTime() - new Date(a.takenAt).getTime(),
),
[rows, activeTab],
[rows, tab],
);
// 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;
const isEmpty = rows.length === 0;
const body = (
<>
{isEmpty ? (
<EmptyState
icon={<Activity className="size-6" />}
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 && (
return (
<Card data-slot="mental-health-history">
<CardHeader>
<CardTitle className="text-base">
{t("mentalHealth.history.title")}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{isEmpty ? (
<EmptyState
icon={<Activity className="size-6" />}
title={t("mentalHealth.history.title")}
description={t("mentalHealth.history.empty")}
/>
) : (
<>
{/* PHQ-9 / GAD-7 toggle — the two ranges + band sets differ, so
the list shows one instrument at a time. */}
<div
className="flex gap-1"
role="tablist"
aria-label={t("mentalHealth.history.chartTitle")}
aria-label={t("mentalHealth.history.title")}
>
{(["PHQ9", "GAD7"] as InstrumentId[]).map((id) => (
<Button
key={id}
type="button"
role="tab"
aria-selected={activeTab === id}
aria-selected={tab === id}
size="sm"
variant={activeTab === id ? "secondary" : "ghost"}
variant={tab === id ? "secondary" : "ghost"}
className="h-8 px-3 text-xs"
onClick={() => {
setTab(id);
@@ -145,88 +109,61 @@ export function AssessmentHistory({
</Button>
))}
</div>
)}
<AssessmentHistoryChart instrument={activeTab} rows={forTab} />
{forTab.length === 0 ? (
<p className="text-muted-foreground py-4 text-center text-sm">
{t("mentalHealth.history.empty")}
</p>
) : (
<ul className="flex flex-col gap-1.5" data-slot="history-list">
{forTab.map((row) => {
const flagged = row.item9Flagged;
const open = openCrisisRowId === row.id;
return (
<li key={row.id} className="flex flex-col gap-1.5">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="text-muted-foreground">
{formatDate(row.takenAt)}
</span>
<div className="flex items-center gap-2">
{flagged && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-auto p-0"
aria-expanded={open}
onClick={() =>
setOpenCrisisRowId(open ? null : row.id)
}
data-slot="history-flagged-marker"
>
<Badge variant="outline">
{t("mentalHealth.history.flaggedBadge")}
</Badge>
</Button>
)}
<Badge variant="secondary">
{t(
`mentalHealth.band.${row.instrument}.${row.severityBand}`,
)}
</Badge>
<span className="font-medium tabular-nums">
{row.totalScore}
{forTab.length === 0 ? (
<p className="text-muted-foreground py-4 text-center text-sm">
{t("mentalHealth.history.empty")}
</p>
) : (
<ul className="flex flex-col gap-1.5" data-slot="history-list">
{forTab.map((row) => {
const flagged = row.item9Flagged;
const open = openCrisisRowId === row.id;
return (
<li key={row.id} className="flex flex-col gap-1.5">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="text-muted-foreground">
{formatDate(row.takenAt)}
</span>
<div className="flex items-center gap-2">
{flagged && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-auto p-0"
aria-expanded={open}
onClick={() =>
setOpenCrisisRowId(open ? null : row.id)
}
data-slot="history-flagged-marker"
>
<Badge variant="outline">
{t("mentalHealth.history.flaggedBadge")}
</Badge>
</Button>
)}
<Badge variant="secondary">
{t(
`mentalHealth.band.${row.instrument}.${row.severityBand}`,
)}
</Badge>
<span className="font-medium tabular-nums">
{row.totalScore}
</span>
</div>
</div>
</div>
{flagged && open && (
<CrisisCard crisis={crisisFromRow(row)} />
)}
</li>
);
})}
</ul>
)}
</>
)}
</>
);
// Pinned: render bare (the host sheet owns the chrome + title). Combined:
// the landing's titled history card, unchanged.
if (pinned) {
return (
<div
data-slot="mental-health-history"
data-pinned={instrument}
className="flex flex-col gap-4"
>
{body}
</div>
);
}
return (
<Card data-slot="mental-health-history">
<CardHeader>
<CardTitle className="text-base">
{t("mentalHealth.history.title")}
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">{body}</CardContent>
{flagged && open && (
<CrisisCard crisis={crisisFromRow(row)} />
)}
</li>
);
})}
</ul>
)}
</>
)}
</CardContent>
</Card>
);
}
@@ -24,18 +24,11 @@ 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();
@@ -45,16 +38,10 @@ export function InstrumentCard({
return (
<Card className="h-full gap-3" data-slot="instrument-card">
<CardContent className="flex h-full flex-col space-y-3.5 p-4">
{/* 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. */}
<button
type="button"
onClick={onOpenDetail}
data-slot="instrument-card-open"
aria-label={`${title} — ${t("mentalHealth.openDetail")}`}
className="focus-visible:ring-ring/60 hover:bg-accent/40 -m-1 flex flex-1 cursor-pointer flex-col gap-3.5 rounded-md p-1 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
>
{/* 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">
@@ -63,9 +50,7 @@ export function InstrumentCard({
</div>
{/* Last-test line in the medication-card grammar: label left, the
relative day (today / yesterday / date) right-aligned. The severity
band is intentionally NOT shown here — the trend behind the card
carries it. */}
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 ? (
<>
@@ -78,7 +63,7 @@ export function InstrumentCard({
<span>{t("mentalHealth.noResultYet")}</span>
)}
</div>
</button>
</div>
<div className="pt-0">
<Button type="button" className="min-h-11 w-full" onClick={onStart}>
@@ -15,10 +15,14 @@
*
* v1.25.3 — this is now the ORCHESTRATOR + landing only; the 387-line monolith
* split into focused components (`instrument-card`, `check-in-wizard`,
* `assessment-result`, `assessment-history` + its lazy chart, `crisis-card`).
* `assessment-result`, `assessment-history`, `crisis-card`).
* The landing follows the card grammar of Vorsorge / Medications: instrument
* cards (last-result line + Start) + a history card. The disclaimer renders
* ONLY here (a muted caption) and behind the InfoHint — never while testing.
*
* v1.27.6 — the trend chart (and the per-card trend sheet) left this surface;
* the score curve lives in Insights / Measurements via the PHQ9_SCORE /
* GAD7_SCORE measurement rows. The landing keeps the calm dated list only.
*/
import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -27,7 +31,6 @@ import { useTranslations } from "@/lib/i18n/context";
import { apiGet, apiPost } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import { PageHeader } from "@/components/ui/page-header";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import { AssessmentHistory } from "./assessment-history";
import { AssessmentResult } from "./assessment-result";
@@ -47,11 +50,6 @@ export function MentalWellbeing() {
const [phase, setPhase] = useState<Phase>("choose");
const [instrument, setInstrument] = useState<InstrumentId>("PHQ9");
const [result, setResult] = useState<CreateResponse | null>(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<InstrumentId | null>(
null,
);
const { data: history } = useQuery({
queryKey: queryKeys.mentalHealthAssessments(),
@@ -124,7 +122,6 @@ export function MentalWellbeing() {
instrument={id}
last={lastByInstrument.get(id)}
onStart={() => begin(id)}
onOpenDetail={() => setDetailInstrument(id)}
/>
</li>
))}
@@ -132,33 +129,6 @@ export function MentalWellbeing() {
</section>
<AssessmentHistory rows={history?.assessments ?? []} />
{/* Per-instrument trend detail, opened from a card body. Reuses the
history's chart + dated list, pinned to the chosen instrument. */}
<ResponsiveSheet
open={detailInstrument !== null}
onOpenChange={(open) => {
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 && (
<AssessmentHistory
rows={history?.assessments ?? []}
instrument={detailInstrument}
/>
)}
</ResponsiveSheet>
</>
)}