mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
1 parent
620b26a1e9
commit
133b84fa72
8 files changed
+110
-28
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user