feat(mental-health): ask the PHQ-9 functional follow-up as the last question

The PHQ-9's validated functional-impairment item left the web wizard
with the review step; the API kept the field but no web client captured
it. It returns as a regular question 10 of 10 in the same style as the
nine scored items: optional per the instrument (submit stays armed
without an answer, a second tap clears the selection, a short hint says
skipping is fine) and never scored into the total. The answer rides the
existing functionalDifficulty field; GAD-7 keeps its seven steps. The
validated wording returns to all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 18:40:59 +02:00
1 parent 620b26a1e9
commit 133b84fa72
8 files changed
+110 -28

No files matched your search

+8
View File
@@ -7664,6 +7664,14 @@
"2": "An mehr als der Hälfte der Tage",
"3": "Beinahe jeden Tag"
},
"functionalTitle": "Wenn du eine oder mehrere Beschwerden angegeben hast: Wie sehr haben diese deine Arbeit, deinen Haushalt oder den Umgang mit anderen Menschen erschwert?",
"functional": {
"0": "Überhaupt nicht erschwert",
"1": "Etwas erschwert",
"2": "Sehr erschwert",
"3": "Extrem erschwert"
},
"functionalOptionalHint": "Optional — du kannst auch ohne Antwort auf diese Frage absenden.",
"result": {
"title": "Dein Ergebnis",
"totalLabel": "Gesamtwert",
+8
View File
@@ -7664,6 +7664,14 @@
"2": "More than half the days",
"3": "Nearly every day"
},
"functionalTitle": "If you checked off any problems, how difficult have these made it to do your work, take care of things at home, or get along with other people?",
"functional": {
"0": "Not difficult at all",
"1": "Somewhat difficult",
"2": "Very difficult",
"3": "Extremely difficult"
},
"functionalOptionalHint": "Optional — you can submit without answering this question.",
"result": {
"title": "Your result",
"totalLabel": "Total score",
+8
View File
@@ -7664,6 +7664,14 @@
"2": "Más de la mitad de los días",
"3": "Casi todos los días"
},
"functionalTitle": "Si marcaste algún problema, ¿cuánta dificultad te han causado para trabajar, ocuparte de la casa o relacionarte con otras personas?",
"functional": {
"0": "Ninguna dificultad",
"1": "Algo de dificultad",
"2": "Mucha dificultad",
"3": "Dificultad extrema"
},
"functionalOptionalHint": "Opcional: puedes enviar sin responder esta pregunta.",
"result": {
"title": "Tu resultado",
"totalLabel": "Puntuación total",
+8
View File
@@ -7664,6 +7664,14 @@
"2": "Plus de la moitié des jours",
"3": "Presque tous les jours"
},
"functionalTitle": "Si tu as coché des problèmes, à quel point t'ont-ils gêné(e) dans ton travail, à la maison ou dans tes relations avec les autres ?",
"functional": {
"0": "Pas du tout gênant",
"1": "Un peu gênant",
"2": "Très gênant",
"3": "Extrêmement gênant"
},
"functionalOptionalHint": "Facultatif — tu peux envoyer sans répondre à cette question.",
"result": {
"title": "Ton résultat",
"totalLabel": "Score total",
+8
View File
@@ -7664,6 +7664,14 @@
"2": "Più della metà dei giorni",
"3": "Quasi ogni giorno"
},
"functionalTitle": "Se hai indicato dei problemi, quanto ti hanno reso difficile lavorare, occuparti della casa o andare d'accordo con gli altri?",
"functional": {
"0": "Per niente difficile",
"1": "Un po' difficile",
"2": "Molto difficile",
"3": "Estremamente difficile"
},
"functionalOptionalHint": "Facoltativo — puoi inviare anche senza rispondere a questa domanda.",
"result": {
"title": "Il tuo risultato",
"totalLabel": "Punteggio totale",
+8
View File
@@ -7664,6 +7664,14 @@
"2": "Więcej niż połowę dni",
"3": "Niemal codziennie"
},
"functionalTitle": "Jeśli zaznaczyłeś(-aś) jakiekolwiek problemy, na ile utrudniły Ci one pracę, prowadzenie domu lub kontakty z innymi ludźmi?",
"functional": {
"0": "Wcale nie utrudniły",
"1": "Trochę utrudniły",
"2": "Bardzo utrudniły",
"3": "Skrajnie utrudniły"
},
"functionalOptionalHint": "Opcjonalne — możesz wysłać bez odpowiedzi na to pytanie.",
"result": {
"title": "Twój wynik",
"totalLabel": "Wynik łączny",
@@ -47,8 +47,8 @@ export function CheckInWizard({
isError,
}: {
instrument: InstrumentId;
/** Submit the completed check-in. */
onSubmit: (items: number[]) => void;
/** Submit the completed check-in (+ the optional PHQ-9 follow-up). */
onSubmit: (items: number[], functionalDifficulty?: number) => void;
/** Return to the landing (abandons the in-progress check-in). */
onBack: () => void;
isPending: boolean;
@@ -57,15 +57,28 @@ export function CheckInWizard({
const { t } = useTranslations();
const key = lower(instrument);
const itemCount = INSTRUMENTS[instrument].itemCount;
// v1.27.8 — the PHQ-9 asks its validated functional-impairment
// follow-up as a regular LAST question (10 of 10) instead of a
// separate review-step widget. It is OPTIONAL per the instrument
// (answering is not required to submit) and never scores into the
// total — the value rides the existing `functionalDifficulty` API
// field. GAD-7 has no such item and keeps its 7 steps.
const hasFunctionalStep = instrument === "PHQ9";
const totalSteps = itemCount + (hasFunctionalStep ? 1 : 0);
const [step, setStep] = useState(1);
const [items, setItems] = useState<number[]>(() =>
Array(itemCount).fill(UNANSWERED),
);
const [functional, setFunctional] = useState<number>(UNANSWERED);
const onLast = isLastStep(step, itemCount);
const onFunctionalStep = hasFunctionalStep && step === itemCount + 1;
const onLast = isLastStep(step, totalSteps);
const questionIndex = step - 1; // 0-based item index for the current question
const complete = isComplete(items);
const questionLabel = onFunctionalStep
? t("mentalHealth.functionalTitle")
: t(`mentalHealth.items.${key}.${step}`);
// a11y: announce each step by moving focus to its heading (the question
// text). Mirrors the medication dialog's per-step title focus so a
@@ -77,6 +90,13 @@ export function CheckInWizard({
}, [step]);
function selectAnswer(value: number) {
if (onFunctionalStep) {
// Optional follow-up: selecting never advances (it IS the last
// step) and tapping the selected option again clears it — the
// answer stays skippable right up to submit.
setFunctional((prev) => (prev === value ? UNANSWERED : value));
return;
}
setItems((prev) => {
const next = [...prev];
next[questionIndex] = value;
@@ -85,7 +105,7 @@ export function CheckInWizard({
// Forward-on-select: advance to the next question. On the last question
// the selection stays visible and the footer submit arms — submitting is
// a deliberate second tap, never a side effect of answering.
if (!onLast) setStep((s) => nextStep(s, itemCount));
if (!onLast) setStep((s) => nextStep(s, totalSteps));
}
return (
@@ -123,7 +143,7 @@ export function CheckInWizard({
<p className="text-muted-foreground text-xs">
{t("mentalHealth.progress", {
current: step,
total: itemCount,
total: totalSteps,
})}
</p>
<h2
@@ -131,27 +151,39 @@ export function CheckInWizard({
tabIndex={-1}
className="scroll-mt-4 text-base font-medium outline-none"
>
{t(`mentalHealth.items.${key}.${step}`)}
{questionLabel}
</h2>
{onFunctionalStep && (
<p className="text-muted-foreground text-xs">
{t("mentalHealth.functionalOptionalHint")}
</p>
)}
<div
className="flex flex-col gap-2"
role="group"
aria-label={t(`mentalHealth.items.${key}.${step}`)}
aria-label={questionLabel}
>
{SCALE.map((v) => (
<Button
key={v}
type="button"
// 44px touch-target floor (WCAG 2.5.5).
className="min-h-11 justify-start"
variant={items[questionIndex] === v ? "default" : "outline"}
aria-pressed={items[questionIndex] === v}
onClick={() => selectAnswer(v)}
disabled={isPending}
>
{t(`mentalHealth.options.${v}`)}
</Button>
))}
{SCALE.map((v) => {
const selected = onFunctionalStep
? functional === v
: items[questionIndex] === v;
return (
<Button
key={v}
type="button"
// 44px touch-target floor (WCAG 2.5.5).
className="min-h-11 justify-start"
variant={selected ? "default" : "outline"}
aria-pressed={selected}
onClick={() => selectAnswer(v)}
disabled={isPending}
>
{onFunctionalStep
? t(`mentalHealth.functional.${v}`)
: t(`mentalHealth.options.${v}`)}
</Button>
);
})}
</div>
{isError && (
@@ -177,7 +209,7 @@ export function CheckInWizard({
<Button
type="button"
className="h-11"
onClick={() => setStep((s) => nextStep(s, itemCount))}
onClick={() => setStep((s) => nextStep(s, totalSteps))}
disabled={items[questionIndex] < 0}
data-slot="check-in-next"
>
@@ -187,7 +219,9 @@ export function CheckInWizard({
<Button
type="button"
className="h-11"
onClick={() => onSubmit(items)}
onClick={() =>
onSubmit(items, functional >= 0 ? functional : undefined)
}
disabled={!complete || isPending}
aria-busy={isPending || undefined}
data-slot="check-in-submit"
@@ -93,15 +93,15 @@ export function MentalWellbeing() {
setPhase("choose");
}
// v1.27.6 — the review step (and with it the optional functional-difficulty
// follow-up) left the web wizard; the check-in submits the scored items
// directly after the last question. The API keeps accepting
// `functionalDifficulty` for clients that still capture it.
function submit(items: number[]) {
// v1.27.8 — the PHQ-9 functional-impairment follow-up returned as the
// wizard's regular last question (optional, unscored); the value rides
// the existing `functionalDifficulty` field when the user answered it.
function submit(items: number[], functionalDifficulty?: number) {
mutation.mutate({
instrument,
items,
locale,
...(functionalDifficulty !== undefined ? { functionalDifficulty } : {}),
});
}