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:
@@ -10,6 +10,7 @@ import { Icon, LogoMark, type IconName } from "./Icon";
|
||||
* Layout from docs/mock (series 2).
|
||||
* phone: content → «Экстренная помощь» block → bottom tab bar (signed in)
|
||||
* desktop: navy sidebar | content | right rail (optional `aside` + emergency panel)
|
||||
* bare: one centred column on every width (questionnaires)
|
||||
*/
|
||||
|
||||
const TABS: { to: string; label: string; icon: IconName }[] = [
|
||||
@@ -127,6 +128,7 @@ export function AppShell({
|
||||
aside,
|
||||
focus = false,
|
||||
sos = true,
|
||||
bare = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
signedIn?: boolean;
|
||||
@@ -135,7 +137,20 @@ export function AppShell({
|
||||
focus?: boolean;
|
||||
/** The crisis screen shows the hotlines itself, so it drops the bottom block. */
|
||||
sos?: boolean;
|
||||
/** Questionnaires: no sidebar, rail or tab bar — one centred column, so nothing distracts from the answers. */
|
||||
bare?: boolean;
|
||||
}) {
|
||||
if (bare)
|
||||
return (
|
||||
<div className="app app--bare">
|
||||
<div className="app__main">
|
||||
<main className="screen" id="main">
|
||||
{children}
|
||||
{sos && <EmergencyBlock />}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={`app${signedIn && !focus ? " app--tabs" : ""}`}>
|
||||
<Sidebar signedIn={signedIn} />
|
||||
|
||||
@@ -28,6 +28,7 @@ const paths = {
|
||||
</>
|
||||
),
|
||||
chevronRight: <path d="M9.5 6l6 6-6 6" />,
|
||||
chevronDown: <path d="M6 9.5l6 6 6-6" />,
|
||||
arrowLeft: <path d="M19 12H5m6-6l-6 6 6 6" />,
|
||||
close: <path d="M6 6l12 12M18 6L6 18" />,
|
||||
check: <path d="M5 12.5l4.5 4.5L19 7.5" />,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
/*
|
||||
* Short confirmations («отметил приём», «дневник сохранён») as a toast in the top-right corner instead of a separate
|
||||
* confirmation page: the person lands on the home screen right away and still sees what happened.
|
||||
*/
|
||||
|
||||
interface Toast {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const Ctx = createContext<(text: string) => void>(() => undefined);
|
||||
const LIFETIME_MS = 6000;
|
||||
|
||||
export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
const next = useRef(0);
|
||||
|
||||
const close = useCallback((id: number) => setToasts((all) => all.filter((t) => t.id !== id)), []);
|
||||
const show = useCallback(
|
||||
(text: string) => {
|
||||
const id = ++next.current;
|
||||
setToasts((all) => [...all.slice(-2), { id, text }]);
|
||||
window.setTimeout(() => close(id), LIFETIME_MS);
|
||||
},
|
||||
[close],
|
||||
);
|
||||
|
||||
const value = useMemo(() => show, [show]);
|
||||
return (
|
||||
<Ctx.Provider value={value}>
|
||||
{children}
|
||||
<div className="toasts" role="status" aria-live="polite">
|
||||
{toasts.map((t) => (
|
||||
<div key={t.id} className="toast">
|
||||
<span className="toast__icon" aria-hidden="true">
|
||||
<Icon name="check" size={18} />
|
||||
</span>
|
||||
<p className="toast__text">{t.text}</p>
|
||||
<button type="button" className="toast__close" onClick={() => close(t.id)} aria-label="Закрыть">
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Ctx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useToast(): (text: string) => void {
|
||||
return useContext(Ctx);
|
||||
}
|
||||
Reference in New Issue
Block a user