feat(web): two-stage questionnaires, screening result and diary by result

- Sign-in: centred ЕМИАС card without the sidebar; anonymous entry asks «Депрессия / ПТСР».
- Stage 1 is the same for everyone and follows the fixed order of the MVP questionnaire:
  PHQ-2 → trauma filter/TSQ → RUS-AUDIT-S → C-SSRS → context; stage 2 only where stage 1
  is positive. Starts without a profile.
- After stage 1 the patient sees a preliminary result (screening, not a diagnosis), stored
  with the record; it switches on the diary modules (depression / PTSD).
- Stage 2 diary: morning (sleep, sleep quality, alcohol) and evening (mood, medication,
  3 PHQ-9 items in rotation, short PCL-5); C-SSRS since last contact only under risk;
  flags on weekly means against the personal norm; scheduled full scales with change
  (remission, response, worsening).
- Questionnaire log: GET /me/{token}/results and the «Мои результаты» screen.
- Questionnaires centred without the sidebar and without scale names; «Продолжить позже»
  goes home; confirmations are toasts; free-text comments are optional.
- Demo policy config/questionnaire_demo.yaml (INTAKE_DEMO only, not a clinical approval).

ADR-012.

Refs #77

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 15:14:09 +03:00
co-authored by Claude Opus 5.5
parent 6e7c89900b
commit 290b0d6151
36 changed files with 1900 additions and 691 deletions
+27 -54
View File
@@ -2,15 +2,18 @@ import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { api } from "../api/client";
import type { Focus } from "../api/types";
import { AppShell, Notice, TopBar } from "../components/AppShell";
import { Icon, type IconName } from "../components/Icon";
import { ru } from "../content/ru";
import { useSession } from "../session/SessionContext";
const YES_ICONS: IconName[] = ["calendar", "pill", "doc"];
const NO_ICONS: IconName[] = ["user", "doc"];
const FOCUS: { id: Focus; icon: IconName }[] = [
{ id: "depression", icon: "smile" },
{ id: "ptsd", icon: "heart" },
];
// A3 · Анонимный вход
// A3 · Анонимный вход: человек выбирает, что хочет проверить — от этого зависит стартовый опрос
export function AnonymousSignIn() {
const t = ru.anonymous;
const navigate = useNavigate();
@@ -18,11 +21,11 @@ export function AnonymousSignIn() {
const [busy, setBusy] = useState(false);
const [error, setError] = useState(false);
const start = async () => {
const start = async (focus: Focus) => {
setBusy(true);
setError(false);
try {
signIn(await api.signInAnonymous());
signIn(await api.signInAnonymous(focus));
navigate("/flow", { replace: true });
} catch {
setError(true);
@@ -34,58 +37,28 @@ export function AnonymousSignIn() {
<AppShell signedIn={false} focus>
<div className="focus">
<TopBar onBack={() => navigate("/")} />
<div className="split">
<div className="split__lead">
<header className="head">
<span className="tile-icon">
<Icon name="lock" size={26} />
</span>
<div className="stack signin">
<h1 className="h1">{t.title}</h1>
<p className="lead">{t.lead}</p>
</header>
<Notice tone="warn">{t.warning}</Notice>
</div>
<div className="split__act">
<section className="panel panel--teal">
<p className="panel__title">
<span className="badge-icon badge-icon--teal">
<Icon name="check" size={16} />
</span>
{t.yesTitle}
</p>
<ul className="iconlist">
{t.yes.map((item, i) => (
<li key={item}>
<Icon name={YES_ICONS[i]} size={20} />
{item}
</li>
<div className="entry-grid">
{FOCUS.map(({ id, icon }) => (
<button key={id} type="button" className="entry" onClick={() => start(id)} disabled={busy}>
<span className="entry__top">
<span className="tile-icon">
<Icon name={icon} size={26} />
</span>
</span>
<span className="entry__title">
{t.focus[id].title}
<Icon name="chevronRight" />
</span>
<span className="entry__text">{t.focus[id].text}</span>
</button>
))}
</ul>
</section>
</div>
<section className="panel panel--red">
<p className="panel__title">
<span className="badge-icon badge-icon--red">
<Icon name="x" size={16} />
</span>
{t.noTitle}
</p>
<ul className="iconlist">
{t.no.map((item, i) => (
<li key={item}>
<Icon name={NO_ICONS[i]} size={20} />
{item}
</li>
))}
</ul>
</section>
{error && <Notice tone="danger">{ru.common.networkError}</Notice>}
<button type="button" className="btn btn--primary" onClick={start} disabled={busy}>
{busy ? t.submitting : t.submit}
</button>
</div>
{error && <Notice tone="danger">{ru.common.networkError}</Notice>}
<p className="muted small">{busy ? t.submitting : t.note}</p>
</div>
</div>
</AppShell>