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:
2026-10-04 08:53:13 +03:00
co-authored by Claude Opus 5.5
parent 40c242bb1a
commit 464b474abd
22 changed files with 3393 additions and 58 deletions
+394 -4
View File
@@ -782,7 +782,7 @@ button.row-card:hover:not(:disabled) {
.numscale__row {
display: grid;
grid-template-columns: repeat(11, 1fr);
grid-template-columns: repeat(var(--n, 11), 1fr);
gap: 4px;
}
@@ -1900,14 +1900,16 @@ button.row-card:hover:not(:disabled) {
/* Wide content (consent text, 0–10 scale) reads better in one centred column. */
.split--consent,
.split--number {
.split--number,
.split--in-number {
grid-template-columns: minmax(0, 1fr);
max-width: 760px;
gap: var(--s-6);
}
.split--consent .split__lead,
.split--number .split__lead {
.split--number .split__lead,
.split--in-number .split__lead {
position: static;
}
@@ -1915,7 +1917,8 @@ button.row-card:hover:not(:disabled) {
grid-template-columns: 1fr 1fr;
}
.split--number .numscale__row {
.split--number .numscale__row,
.split--in-number .numscale__row {
gap: var(--s-2);
}
@@ -2035,6 +2038,393 @@ button.row-card:hover:not(:disabled) {
}
}
/* ------------------------------------------------------------------ questionnaires (stage 1 / stage 2) */
.formchip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--s-2) var(--s-3);
}
.formchip__title {
font-size: var(--fs-13);
color: var(--c-text-2);
}
.chip--sec {
gap: 6px;
padding: 5px var(--s-3) 5px 10px;
background: var(--c-blue-soft);
color: var(--c-blue);
}
.chip--mood,
.chip--diary {
background: var(--c-teal-soft);
color: var(--c-teal-strong);
}
.chip--trauma {
background: var(--c-amber-soft);
color: #a86a00;
}
.chip--alcohol,
.chip--about {
background: var(--c-navy-soft);
color: var(--c-navy);
}
.survey {
gap: var(--s-3);
}
.survey__instr {
padding-left: var(--s-3);
border-left: 3px solid var(--c-border-strong);
color: var(--c-text-2);
}
.survey__instr .body-text {
font-size: var(--fs-16);
}
.survey__q {
font-size: var(--fs-24);
line-height: 1.3;
}
.survey__asof {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--fs-13);
color: var(--c-text-2);
}
.survey__aux {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--s-3);
padding-top: var(--s-2);
border-top: 1px solid var(--c-border);
}
.survey__saved {
margin: 0;
}
.btn--sm {
width: auto;
min-height: 40px;
padding: 0 var(--s-3);
font-size: var(--fs-14);
}
.linkbtn {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 44px;
padding: 0 var(--s-1);
border: 0;
background: none;
font-weight: 600;
font-size: var(--fs-14);
text-decoration: underline;
text-underline-offset: 3px;
}
.linkbtn--danger {
color: var(--c-red-strong);
}
.btn--ghost-red {
background: transparent;
color: var(--c-red-strong);
}
.btn--outline-red {
background: var(--c-surface);
border-color: var(--c-red);
color: var(--c-red-strong);
}
.center-self {
align-self: center;
justify-self: center;
}
/* RUS-AUDIT item 2: standard-drink table from the approved form */
.drinks {
display: grid;
gap: var(--s-2);
padding: var(--s-4);
border: 1px solid var(--c-border);
border-radius: var(--r-tile);
background: var(--c-surface);
}
.drinks__scroll {
overflow-x: auto;
margin: 0 calc(-1 * var(--s-2));
}
.drinks table {
width: 100%;
min-width: 420px;
border-collapse: collapse;
font-size: var(--fs-13);
font-variant-numeric: tabular-nums;
}
.drinks th,
.drinks td {
padding: 8px;
text-align: left;
border-bottom: 1px solid var(--c-border);
vertical-align: top;
}
.drinks thead th {
color: var(--c-text-2);
font-weight: 600;
}
.drinks tbody th {
white-space: nowrap;
color: var(--c-navy);
}
/* diary: hours of sleep */
.hours {
display: grid;
gap: var(--s-2);
}
.hours__label {
font-weight: 600;
}
.hours__field {
display: flex;
align-items: center;
gap: var(--s-2);
width: 180px;
padding: 0 var(--s-4);
border: 1.5px solid var(--c-border-strong);
border-radius: var(--r-btn);
background: var(--c-surface);
}
.hours__field:focus-within {
border-color: var(--c-focus);
box-shadow: 0 0 0 3px var(--c-blue-soft);
}
.hours__field input {
flex: 1;
min-width: 0;
height: var(--tap);
border: 0;
outline: none;
background: none;
font-size: var(--fs-28);
font-weight: 700;
color: var(--c-navy);
}
.hours__field span {
font-size: var(--fs-18);
color: var(--c-text-2);
}
.hours .btn {
margin-top: var(--s-2);
}
/* ------------------------------------------------------------------ medication: drug card, therapy day */
.rx__foot {
display: flex;
flex-wrap: wrap;
gap: var(--s-2);
padding-top: var(--s-3);
border-top: 1px solid var(--c-border);
}
.effect {
display: flex;
align-items: center;
gap: var(--s-4);
width: 100%;
padding: var(--s-3) var(--s-4);
border: 1px solid #cfe0f5;
border-radius: var(--r-tile);
background: var(--c-blue-soft);
text-align: left;
color: var(--c-text);
}
.effect > svg:last-child {
flex: none;
color: var(--c-blue);
}
.effect__day {
flex: none;
display: grid;
place-items: center;
width: 56px;
height: 56px;
border-radius: 14px;
background: var(--c-surface);
color: var(--c-blue);
line-height: 1;
}
.effect__num {
font-size: var(--fs-24);
font-weight: 700;
}
.effect__unit {
font-size: var(--fs-12);
color: var(--c-text-2);
}
.drug {
display: grid;
gap: var(--s-6);
}
.drug__head {
display: flex;
align-items: center;
gap: var(--s-4);
}
.drug__head .tile-icon--xl {
width: 72px;
height: 72px;
}
.drug__rx {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: var(--s-2);
padding: 6px var(--s-3);
border-radius: var(--r-chip);
background: var(--c-teal-soft);
color: var(--c-teal-strong);
font-size: var(--fs-14);
font-weight: 600;
}
.drug__grid,
.drug__col {
display: grid;
gap: var(--s-4);
align-content: start;
}
.drug__sec {
display: grid;
gap: var(--s-3);
padding: var(--s-5);
border: 1px solid var(--c-border);
border-radius: var(--r-tile);
background: var(--c-surface);
}
.drug__sec p {
margin: 0;
line-height: 1.5;
}
.drug__sec--red {
border-color: var(--c-red-line);
background: var(--c-red-soft);
}
.drug__h {
display: flex;
align-items: center;
gap: var(--s-3);
margin: 0;
font-size: var(--fs-18);
font-weight: 700;
}
.drug__sub {
font-weight: 600;
}
.badge-icon--blue {
background: var(--c-blue);
}
.timeline {
display: grid;
gap: var(--s-2);
}
.timeline__bar {
position: relative;
height: 10px;
border-radius: 999px;
background: var(--c-tile);
border: 1px solid var(--c-border);
}
.timeline__fill {
position: absolute;
inset: 0 auto 0 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--c-teal-soft), var(--c-teal));
}
.timeline__mark {
position: absolute;
top: 50%;
width: 18px;
height: 18px;
border: 3px solid var(--c-surface);
border-radius: 50%;
background: var(--c-teal);
box-shadow: 0 1px 3px rgb(15 39 71 / 25%);
transform: translate(-50%, -50%);
}
.timeline__legend {
display: flex;
justify-content: space-between;
gap: var(--s-3);
font-size: var(--fs-13);
color: var(--c-text-2);
}
@media (max-width: 1023px) {
.btn-row--stack-phone {
grid-template-columns: 1fr;
}
}
@media (min-width: 1024px) {
.drug__grid {
grid-template-columns: 1fr 1fr;
gap: var(--s-5);
}
.survey__q {
font-size: var(--fs-28);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,