feat(web): questionnaires, diary and medication module in the patient app
- Focus screen renders versioned questionnaire items (iq: buttons): section chip, position (adaptive forms without "of N"), recall date, pause / help-now actions, optional skip, sleep-hours input, 0-10 scale anchors, RUS-AUDIT item 2 standard-drink table. - Paused / held-for-review questionnaires get their own screen. - Today: intake and diary tasks, review notice, therapy-day card on days 1/7/14. - Drug card /meds/:ref from data/meds/meds_reference.json (draft built from the Minzdrav guidelines: depression 2024, PTSD 2023) — expected effect, course, side effects, red flags; no doses shown. "Report a side effect" always available (med:side_effects:<ref>). - API: /instruments/catalog, /meds/reference; today gains questionnaire state, reference id, indication and therapy day; demo persona seeds the dashboard dose history. - INTAKE_DEMO=1 (demo clock only) opens the questionnaires on synthetic data without touching the clinical release gate in config/intake_runtime.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { api } from "../api/client";
|
||||
import type { Catalog, MedsReference } from "../api/types";
|
||||
|
||||
/*
|
||||
* Static reference data from the server (questionnaire catalog, drug reference): fetched once per app run and
|
||||
* shared by every screen. A failed fetch is retried on the next mount; screens work without it, only less rich.
|
||||
*/
|
||||
|
||||
function cached<T>(load: () => Promise<T>) {
|
||||
let value: T | null = null;
|
||||
let inflight: Promise<T> | null = null;
|
||||
const get = () => {
|
||||
inflight ??= load().then(
|
||||
(v) => (value = v),
|
||||
(e) => {
|
||||
inflight = null;
|
||||
throw e;
|
||||
},
|
||||
);
|
||||
return inflight;
|
||||
};
|
||||
return function useCached(): T | null {
|
||||
const [v, setV] = useState<T | null>(value);
|
||||
useEffect(() => {
|
||||
if (value) return;
|
||||
let alive = true;
|
||||
get()
|
||||
.then((x) => alive && setV(x))
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, []);
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
export const useCatalog = cached<Catalog>(api.catalog);
|
||||
export const useMedsReference = cached<MedsReference>(api.medsReference);
|
||||
@@ -104,6 +104,20 @@ export const ru = {
|
||||
noDoctor: "Врач не подключён. Подключите ЕМИАС в профиле, чтобы назначения и визиты появились здесь.",
|
||||
newMessage: "Новое сообщение",
|
||||
open: "Открыть",
|
||||
continue: "Продолжить",
|
||||
surveyStep: (section: string, n: number) => `${section} · вопрос ${n}`,
|
||||
intakeStart: "Первичный опрос",
|
||||
intakeStartHint: "15–20 минут, можно прерваться и продолжить позже",
|
||||
intakeResume: "Продолжить первичный опрос",
|
||||
intakeResumeHint: "Ответы сохранены, можно продолжить",
|
||||
intakeReview:
|
||||
"Опрос приостановлен: продолжить можно после оценки специалиста, врач уже получил уведомление. Если нужна помощь сейчас — 112.",
|
||||
intakeDone: "Первичный опрос пройден",
|
||||
diary: "Дневник самочувствия",
|
||||
diaryHint: "2–3 минуты: настроение, сон, лекарства",
|
||||
diaryDone: "Дневник заполнен",
|
||||
therapyDay: (day: number) => `День ${day} лечения`,
|
||||
effectHint: "Когда ждать эффекта и что важно знать",
|
||||
},
|
||||
meds: {
|
||||
title: "Моя схема приёма",
|
||||
@@ -119,6 +133,32 @@ export const ru = {
|
||||
taken: "Принял(а)",
|
||||
skip: "Пропущу",
|
||||
sideEffects: "Есть побочные эффекты",
|
||||
about: "О препарате",
|
||||
reportSideEffect: "Сообщить о побочном эффекте",
|
||||
therapyDay: (day: number) => `День лечения: ${day}`,
|
||||
card: {
|
||||
back: "К схеме приёма",
|
||||
prescribed: "Назначено",
|
||||
effect: "Когда ждать эффекта",
|
||||
effectAt: (when: string) => `Эффект оценивают через ${when}`,
|
||||
progress: (day: number) => `Вы принимаете препарат ${day} ${day % 10 === 1 && day % 100 !== 11 ? "день" : [2, 3, 4].includes(day % 10) && ![12, 13, 14].includes(day % 100) ? "дня" : "дней"}`,
|
||||
course: "Сколько принимать",
|
||||
courseShort: (days: number) => `Обычно не дольше ${Math.round(days / 7)} недель — срок определяет врач`,
|
||||
sideEffects: "Что может беспокоить",
|
||||
sideEffectsHint:
|
||||
"Побочные эффекты часто ослабевают в первые недели. Если они мешают жить, сообщите врачу — он может изменить дозу или подобрать другой препарат. Не меняйте схему сами.",
|
||||
sideEffectsNone: "Частые побочные эффекты в клинических рекомендациях не описаны — спросите врача.",
|
||||
redFlags: "Когда срочно к врачу",
|
||||
important: "Важно",
|
||||
withdrawal: "Резкая отмена этого препарата может вызвать неприятные ощущения: головокружение, раздражительность, нарушения сна. Отменяют его постепенно и только с врачом.",
|
||||
overdose: "Храните препарат в недоступном для других месте и принимайте строго в назначенной дозе.",
|
||||
monitoring: "Врач может назначить обследования",
|
||||
instruction: "Как принимать, что делать при пропуске, алкоголь и вождение",
|
||||
ask: "Задать вопрос о лекарстве",
|
||||
source: (sources: string) => `Источник: ${sources}. Черновик справочника, проверяется врачом-психиатром.`,
|
||||
offLabel: "Назначение вне зарегистрированных показаний — так бывает, решение принимает врач.",
|
||||
unknown: "Справки по этому препарату пока нет. Уточните у врача или в инструкции к препарату.",
|
||||
},
|
||||
},
|
||||
doctor: {
|
||||
title: "Врач",
|
||||
@@ -163,6 +203,25 @@ export const ru = {
|
||||
exit: "К главной",
|
||||
planTitle: "Мой план безопасности",
|
||||
consentTitle: "Согласие на обработку данных о здоровье",
|
||||
sections: {
|
||||
safety: "Безопасность",
|
||||
mood: "Настроение",
|
||||
trauma: "Пережитый опыт",
|
||||
alcohol: "Алкоголь",
|
||||
about: "О вас",
|
||||
diary: "Дневник",
|
||||
scale: "Опросник",
|
||||
},
|
||||
questionN: (n: number) => `Вопрос ${n}`,
|
||||
recall: (period: string) => `Период: ${period}`,
|
||||
asOf: (date: string) => `Считайте от ${date}`,
|
||||
saved: "Ответы сохраняются после каждого вопроса",
|
||||
hoursLabel: "Сколько часов",
|
||||
hoursUnit: "ч",
|
||||
hoursHint: "От 0 до 24, можно с половиной: 6,5",
|
||||
drinks: "Таблица стандартных порций",
|
||||
drinksLead: "Посчитайте, сколько порций выходит в обычный день, когда вы выпиваете.",
|
||||
noticeTitle: "Опрос приостановлен",
|
||||
},
|
||||
messages: {
|
||||
title: "Сообщения",
|
||||
|
||||
Reference in New Issue
Block a user