feat(web): приложение пациента — двухэтапные опросы, результат скрининга, дневник по результату #79
@@ -48,6 +48,12 @@ export const ru = {
|
||||
text: "Не нужны имя, полис и телефон, врач ничего не видит, история только на этом устройстве.",
|
||||
},
|
||||
disclaimer: "Сервис не ставит диагнозы и не назначает лечение. Решения о лечении принимает ваш врач.",
|
||||
howTitle: "Как это работает",
|
||||
how: [
|
||||
{ title: "Врач подключает", text: "На приёме вы получаете код или входите анонимно" },
|
||||
{ title: "Минута в день", text: "Напоминание о лекарстве и короткая отметка самочувствия" },
|
||||
{ title: "Врач видит динамику", text: "И получает сигнал, если состоянию нужно внимание" },
|
||||
],
|
||||
},
|
||||
emias: {
|
||||
title: "Введите код от врача",
|
||||
@@ -90,7 +96,11 @@ export const ru = {
|
||||
viewMeds: "Посмотреть лекарства",
|
||||
visit: "Следующий визит к врачу",
|
||||
medQuestion: "Вопрос о лекарстве",
|
||||
medQuestionHint: "Ответ по официальной инструкции",
|
||||
messageDoctor: "Сообщение врачу",
|
||||
messageDoctorHint: "Врач ответит в рабочее время",
|
||||
week: "Приём на этой неделе",
|
||||
weekTaken: (taken: number, total: number) => (total ? `Принято ${taken} из ${total}` : "Отметок пока нет"),
|
||||
noDoctor: "Врач не подключён. Подключите ЕМИАС в профиле, чтобы назначения и визиты появились здесь.",
|
||||
newMessage: "Новое сообщение",
|
||||
open: "Открыть",
|
||||
|
||||
@@ -32,13 +32,20 @@ export function AnonymousSignIn() {
|
||||
|
||||
return (
|
||||
<AppShell signedIn={false} focus>
|
||||
<div className="stack narrow">
|
||||
<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>
|
||||
<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">
|
||||
@@ -73,12 +80,13 @@ export function AnonymousSignIn() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Notice tone="warn">{t.warning}</Notice>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export function Doctor() {
|
||||
{plan && !plan.has_doctor ? (
|
||||
<Notice>{t.noDoctor}</Notice>
|
||||
) : (
|
||||
<>
|
||||
<div className="cols">
|
||||
<section className="card-list" aria-label={t.visit}>
|
||||
<div className="card-list__row">
|
||||
<span className="tile-icon tile-icon--blue">
|
||||
@@ -65,7 +65,7 @@ export function Doctor() {
|
||||
))}
|
||||
</div>
|
||||
<Notice>{t.writeNote}</Notice>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AppShell>
|
||||
|
||||
@@ -59,13 +59,20 @@ export function EmiasSignIn() {
|
||||
|
||||
return (
|
||||
<AppShell signedIn={false} focus>
|
||||
<div className="stack narrow">
|
||||
<div className="focus">
|
||||
<TopBar onBack={() => navigate("/")} />
|
||||
<div className="split">
|
||||
<div className="split__lead">
|
||||
<header className="head">
|
||||
<span className="tile-icon tile-icon--teal">
|
||||
<Icon name="docPlus" size={26} />
|
||||
</span>
|
||||
<h1 className="h1">{t.title}</h1>
|
||||
<p className="lead">{t.lead}</p>
|
||||
</header>
|
||||
|
||||
<Notice>{t.demoNotice}</Notice>
|
||||
</div>
|
||||
<div className="split__act">
|
||||
<form className="stack stack--sm" onSubmit={submit} noValidate>
|
||||
<label htmlFor="code" className="visually-hidden">
|
||||
{t.codeLabel}
|
||||
@@ -93,8 +100,6 @@ export function EmiasSignIn() {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Notice>{t.demoNotice}</Notice>
|
||||
|
||||
<div className="divider" role="separator">
|
||||
<span>{t.or}</span>
|
||||
</div>
|
||||
@@ -115,6 +120,8 @@ export function EmiasSignIn() {
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
+53
-59
@@ -322,70 +322,59 @@ export function Flow() {
|
||||
const top = (
|
||||
<TopBar onBack={home}>{step.kind === "choice" && step.progress && <Progress {...step.progress} />}</TopBar>
|
||||
);
|
||||
let body: ReactNode;
|
||||
// Phone: one column. Desktop: question and context on the left, answers and actions on the right.
|
||||
let lead: ReactNode;
|
||||
let act: ReactNode;
|
||||
switch (step.kind) {
|
||||
case "consent":
|
||||
body = (
|
||||
lead = (
|
||||
<>
|
||||
<Head title={ru.flow.consentTitle} lead={[]} icon="shield" />
|
||||
<div className="panel panel--scroll">
|
||||
<Body texts={[...step.lead, step.title].filter(Boolean)} />
|
||||
</div>
|
||||
<div className="btn-row">
|
||||
{step.buttons.map((b, i) => (
|
||||
<button
|
||||
key={b.data}
|
||||
type="button"
|
||||
className={i === 0 ? "btn btn--primary" : "btn btn--outline"}
|
||||
disabled={dialog.busy}
|
||||
onClick={() => press(b)}
|
||||
>
|
||||
{b.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
act = (
|
||||
<div className="btn-row btn-row--stack-desk">
|
||||
{step.buttons.map((b, i) => (
|
||||
<button
|
||||
key={b.data}
|
||||
type="button"
|
||||
className={i === 0 ? "btn btn--primary" : "btn btn--outline"}
|
||||
disabled={dialog.busy}
|
||||
onClick={() => press(b)}
|
||||
>
|
||||
{b.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case "number":
|
||||
body = (
|
||||
<>
|
||||
<Head title={step.title} lead={step.lead} />
|
||||
<NumberScale key={stepKey} options={step.options} busy={dialog.busy} onSubmit={press} />
|
||||
</>
|
||||
);
|
||||
lead = <Head title={step.title} lead={step.lead} icon="smile" />;
|
||||
act = <NumberScale key={stepKey} options={step.options} busy={dialog.busy} onSubmit={press} />;
|
||||
break;
|
||||
case "actions":
|
||||
body = (
|
||||
<>
|
||||
<div className="reminder">
|
||||
<span className="tile-icon tile-icon--teal tile-icon--xl">
|
||||
<Icon name="pill" size={34} />
|
||||
</span>
|
||||
<h1 className="h1 center">{step.title || ru.meds.reminderTitle}</h1>
|
||||
<Body texts={step.lead} />
|
||||
</div>
|
||||
<MedActions options={step.options} busy={dialog.busy} onPress={press} />
|
||||
</>
|
||||
lead = (
|
||||
<div className="reminder">
|
||||
<span className="tile-icon tile-icon--teal tile-icon--xl">
|
||||
<Icon name="pill" size={34} />
|
||||
</span>
|
||||
<h1 className="h1 center">{step.title || ru.meds.reminderTitle}</h1>
|
||||
<Body texts={step.lead} />
|
||||
</div>
|
||||
);
|
||||
act = <MedActions options={step.options} busy={dialog.busy} onPress={press} />;
|
||||
break;
|
||||
case "text":
|
||||
body = (
|
||||
<>
|
||||
<Head title={step.title} lead={step.lead} />
|
||||
<FreeText
|
||||
key={stepKey}
|
||||
skip={step.skip}
|
||||
busy={dialog.busy}
|
||||
onSend={say}
|
||||
onSkip={press}
|
||||
onExit={home}
|
||||
/>
|
||||
</>
|
||||
lead = <Head title={step.title} lead={step.lead} icon="edit" />;
|
||||
act = (
|
||||
<FreeText key={stepKey} skip={step.skip} busy={dialog.busy} onSend={say} onSkip={press} onExit={home} />
|
||||
);
|
||||
break;
|
||||
case "crisis":
|
||||
body = (
|
||||
lead = (
|
||||
<>
|
||||
<header className="head">
|
||||
<span className="tile-icon tile-icon--teal tile-icon--xl">
|
||||
@@ -393,15 +382,19 @@ export function Flow() {
|
||||
</span>
|
||||
<h1 className="h1">{ru.flow.supportTitle}</h1>
|
||||
</header>
|
||||
<div className="panel">
|
||||
<Body texts={[step.text]} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
act = (
|
||||
<>
|
||||
<div className="hotlines">
|
||||
{resources.map((r) => (
|
||||
<HotlineCard key={r.phone} r={r} />
|
||||
))}
|
||||
</div>
|
||||
<Breathing />
|
||||
<div className="panel">
|
||||
<Body texts={[step.text]} />
|
||||
</div>
|
||||
{step.buttons.map((b) => (
|
||||
<button key={b.data} type="button" className="btn btn--outline" disabled={dialog.busy} onClick={() => press(b)}>
|
||||
{b.label}
|
||||
@@ -423,9 +416,9 @@ export function Flow() {
|
||||
);
|
||||
break;
|
||||
case "plan":
|
||||
body = (
|
||||
lead = <Head title={step.title || ru.flow.planTitle} lead={step.lead} icon="shield" />;
|
||||
act = (
|
||||
<>
|
||||
<Head title={step.title || ru.flow.planTitle} lead={step.lead} />
|
||||
<ul className="plan">
|
||||
{step.lines.map((l) => (
|
||||
<li key={l} className="plan__item">
|
||||
@@ -441,19 +434,20 @@ export function Flow() {
|
||||
);
|
||||
break;
|
||||
case "choice":
|
||||
body = (
|
||||
<>
|
||||
<Head title={step.title} lead={step.lead} />
|
||||
<Choice key={stepKey} options={step.options} busy={dialog.busy} onSubmit={press} />
|
||||
</>
|
||||
);
|
||||
lead = <Head title={step.title} lead={step.lead} />;
|
||||
act = <Choice key={stepKey} options={step.options} busy={dialog.busy} onSubmit={press} />;
|
||||
break;
|
||||
}
|
||||
content = (
|
||||
<div className="stack narrow" key={stepKey}>
|
||||
<div className="focus" key={stepKey}>
|
||||
{top}
|
||||
{body}
|
||||
{dialog.error && <Notice tone="danger">{ru.common.networkError}</Notice>}
|
||||
<div className={`split split--${step.kind}`}>
|
||||
<div className="split__lead">{lead}</div>
|
||||
<div className="split__act">
|
||||
{act}
|
||||
{dialog.error && <Notice tone="danger">{ru.common.networkError}</Notice>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+27
-11
@@ -1,5 +1,7 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import type { TodayPrescription } from "../api/types";
|
||||
|
||||
import { AppShell, Notice, TopBar } from "../components/AppShell";
|
||||
import { Icon, type IconName } from "../components/Icon";
|
||||
import { ru } from "../content/ru";
|
||||
@@ -13,6 +15,21 @@ const DOT: Record<string, { cls: string; icon?: IconName }> = {
|
||||
none: { cls: "wk" },
|
||||
};
|
||||
|
||||
export function WeekDots({ week }: { week: TodayPrescription["week"] }) {
|
||||
return (
|
||||
<ol className="week" aria-label="Отметки за неделю">
|
||||
{week.map((w, i) => (
|
||||
<li key={w.day} className="week__day">
|
||||
<span className="week__label">{ru.meds.week[i]}</span>
|
||||
<span className={DOT[w.status].cls} aria-label={w.status}>
|
||||
{DOT[w.status].icon && <Icon name={DOT[w.status].icon!} size={14} />}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
// E4 · Моя схема приёма
|
||||
export function Meds() {
|
||||
const t = ru.meds;
|
||||
@@ -33,6 +50,7 @@ export function Meds() {
|
||||
|
||||
{plan && plan.prescriptions.length === 0 && <Notice>{t.empty}</Notice>}
|
||||
|
||||
<div className="grid2">
|
||||
{plan?.prescriptions.map((p) => (
|
||||
<article key={p.drug} className="rx">
|
||||
<div className="rx__head">
|
||||
@@ -55,18 +73,10 @@ export function Meds() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ol className="week" aria-label="Отметки за неделю">
|
||||
{p.week.map((w, i) => (
|
||||
<li key={w.day} className="week__day">
|
||||
<span className="week__label">{t.week[i]}</span>
|
||||
<span className={DOT[w.status].cls} aria-label={w.status}>
|
||||
{DOT[w.status].icon && <Icon name={DOT[w.status].icon!} size={14} />}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<WeekDots week={p.week} />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{plan && plan.prescriptions.length > 0 && <Notice>{t.onlyDoctor}</Notice>}
|
||||
|
||||
@@ -95,8 +105,10 @@ export function DoseReminder() {
|
||||
|
||||
return (
|
||||
<AppShell focus>
|
||||
<div className="stack narrow">
|
||||
<div className="focus">
|
||||
<TopBar onBack={() => navigate("/today")} />
|
||||
<div className="split">
|
||||
<div className="split__lead">
|
||||
<div className="reminder">
|
||||
<span className="tile-icon tile-icon--teal tile-icon--xl">
|
||||
<Icon name="pill" size={34} />
|
||||
@@ -115,6 +127,8 @@ export function DoseReminder() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="split__act">
|
||||
<div className="acts">
|
||||
<button type="button" className="act act--teal" disabled={dialog.busy} onClick={() => answer("med:taken", t.taken)}>
|
||||
<span className="act__icon">
|
||||
@@ -141,6 +155,8 @@ export function DoseReminder() {
|
||||
</button>
|
||||
</div>
|
||||
{dialog.error && <Notice tone="danger">{ru.common.networkError}</Notice>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ export function Profile() {
|
||||
<h1 className="h1">{t.title}</h1>
|
||||
</header>
|
||||
|
||||
<div className="cols">
|
||||
<section className="stack stack--sm">
|
||||
<div className="row-card row-card--static">
|
||||
<span className="avatar avatar--lg">
|
||||
@@ -107,6 +108,7 @@ export function Profile() {
|
||||
<Icon name="chevronRight" />
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog ref={ref} className="modal" aria-labelledby="revoke-title" onClose={() => setConfirm(false)}>
|
||||
|
||||
+165
-103
@@ -1,12 +1,13 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { TodayDose } from "../api/types";
|
||||
import type { TodayDose, TodayPrescription } from "../api/types";
|
||||
import { AppShell, Notice } from "../components/AppShell";
|
||||
import { Icon } from "../components/Icon";
|
||||
import { ru } from "../content/ru";
|
||||
import { useDialog } from "../dialog/DialogContext";
|
||||
import { classify } from "../dialog/step";
|
||||
import { useSession } from "../session/SessionContext";
|
||||
import { WeekDots } from "./Meds";
|
||||
import { humanDate, useToday } from "./useToday";
|
||||
|
||||
function VisitCard({ date, onOpen }: { date: string; onOpen: () => void }) {
|
||||
@@ -24,6 +25,26 @@ function VisitCard({ date, onOpen }: { date: string; onOpen: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function WeekCard({ rx, onOpen }: { rx: TodayPrescription; onOpen: () => void }) {
|
||||
const taken = rx.week.filter((w) => w.status === "taken").length;
|
||||
const counted = rx.week.filter((w) => w.status !== "none").length;
|
||||
return (
|
||||
<button type="button" className="card weekcard" onClick={onOpen}>
|
||||
<span className="card__head">
|
||||
<span className="tile-icon tile-icon--teal">
|
||||
<Icon name="pill" />
|
||||
</span>
|
||||
<span className="row-card__text">
|
||||
<span className="row-card__sub">{ru.today.week}</span>
|
||||
<span className="row-card__title">{ru.today.weekTaken(taken, counted)}</span>
|
||||
</span>
|
||||
<Icon name="chevronRight" />
|
||||
</span>
|
||||
<WeekDots week={rx.week} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// C1 / C2 · Сегодня
|
||||
export function Today() {
|
||||
const t = ru.today;
|
||||
@@ -43,20 +64,27 @@ export function Today() {
|
||||
const takenDoses = plan?.doses.filter((d) => d.status === "taken") ?? [];
|
||||
const allDone = plan !== null && pendingDoses.length === 0 && plan.checkin_done && !hasOpenStep;
|
||||
const nextDose = plan?.prescriptions.flatMap((p) => p.times).sort()[0];
|
||||
const noDoctor = session?.mode === "anonymous" || (plan !== null && !plan.has_doctor);
|
||||
|
||||
const now = plan?.now ?? "00:00";
|
||||
const openDose = (d: TodayDose) => navigate(`/dose/${d.id}`);
|
||||
const visit = plan?.next_visit ? <VisitCard date={plan.next_visit} onOpen={() => navigate("/doctor")} /> : null;
|
||||
const hasDone = takenDoses.length > 0 || plan?.checkin_done;
|
||||
|
||||
return (
|
||||
<AppShell aside={visit}>
|
||||
<div className="stack">
|
||||
<header className="today-head">
|
||||
<div className="dash">
|
||||
<header className="today-head dash__head">
|
||||
<div>
|
||||
<h1 className="h1">{t.title}</h1>
|
||||
{plan && <p className="muted">{humanDate(plan.today)}</p>}
|
||||
</div>
|
||||
<button type="button" className="icon-btn icon-btn--bell" onClick={() => navigate("/messages")} aria-label={ru.nav.messages}>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn icon-btn--bell"
|
||||
onClick={() => navigate("/messages")}
|
||||
aria-label={ru.nav.messages}
|
||||
>
|
||||
<Icon name="bell" />
|
||||
{dialog.unread > 0 && <span className="dot" />}
|
||||
</button>
|
||||
@@ -64,113 +92,147 @@ export function Today() {
|
||||
|
||||
{failed && <Notice tone="danger">{ru.common.networkError}</Notice>}
|
||||
|
||||
{hasOpenStep && (
|
||||
<button type="button" className="task task--hero" onClick={() => navigate("/flow")}>
|
||||
<span className="task__icon">
|
||||
<Icon name="message" />
|
||||
</span>
|
||||
<span className="task__text">
|
||||
<span className="task__title">{t.newMessage}</span>
|
||||
<span className="task__sub">{dialog.pending[0]?.text.split("\n")[0].slice(0, 80)}</span>
|
||||
</span>
|
||||
<span className="chip chip--teal-solid">{t.open}</span>
|
||||
</button>
|
||||
)}
|
||||
<section className="dash__main" aria-label={t.title}>
|
||||
{hasOpenStep && (
|
||||
<button type="button" className="task task--hero" onClick={() => navigate("/flow")}>
|
||||
<span className="task__icon">
|
||||
<Icon name="message" />
|
||||
</span>
|
||||
<span className="task__text">
|
||||
<span className="task__title">{t.newMessage}</span>
|
||||
<span className="task__sub">{dialog.pending[0]?.text.split("\n")[0].slice(0, 90)}</span>
|
||||
</span>
|
||||
<span className="chip chip--teal-solid">{t.open}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{allDone ? (
|
||||
<section className="alldone">
|
||||
<span className="done__badge" aria-hidden="true">
|
||||
<Icon name="check" size={36} />
|
||||
</span>
|
||||
<h2 className="h2 center">{t.allDoneTitle}</h2>
|
||||
<p className="muted center">{nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}</p>
|
||||
{plan && plan.prescriptions.length > 0 && (
|
||||
<button type="button" className="btn btn--primary" onClick={() => navigate("/meds")}>
|
||||
{t.viewMeds}
|
||||
<Icon name="chevronRight" size={20} />
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<div className="tasks">
|
||||
{pendingDoses.map((d) => (
|
||||
{allDone ? (
|
||||
<div className="alldone">
|
||||
<span className="done__badge" aria-hidden="true">
|
||||
<Icon name="check" size={36} />
|
||||
</span>
|
||||
<h2 className="h2 center">{t.allDoneTitle}</h2>
|
||||
<p className="muted center">{nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}</p>
|
||||
{plan && plan.prescriptions.length > 0 && (
|
||||
<button type="button" className="btn btn--primary btn--auto" onClick={() => navigate("/meds")}>
|
||||
{t.viewMeds}
|
||||
<Icon name="chevronRight" size={20} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="tasks">
|
||||
{pendingDoses.map((d) => (
|
||||
<button
|
||||
key={d.id}
|
||||
type="button"
|
||||
className={d.time <= now && !hasOpenStep ? "task task--hero" : "task"}
|
||||
onClick={() => openDose(d)}
|
||||
>
|
||||
<span className="task__icon task__icon--teal">
|
||||
<Icon name="pill" />
|
||||
</span>
|
||||
<span className="task__text">
|
||||
<span className="task__title">{t.takeMed}</span>
|
||||
<span className="task__sub">
|
||||
{d.time} · {d.drug}, {d.dose}
|
||||
</span>
|
||||
</span>
|
||||
{d.time <= now && <span className="chip chip--teal-solid">{t.now}</span>}
|
||||
<Icon name="chevronRight" />
|
||||
</button>
|
||||
))}
|
||||
{plan && !plan.checkin_done && (
|
||||
<button
|
||||
type="button"
|
||||
className="task"
|
||||
onClick={() => go("menu:checkin", t.checkin)}
|
||||
disabled={dialog.busy}
|
||||
>
|
||||
<span className="task__icon task__icon--teal">
|
||||
<Icon name="smile" />
|
||||
</span>
|
||||
<span className="task__text">
|
||||
<span className="task__title">{t.checkin}</span>
|
||||
<span className="task__sub">{t.checkinHint}</span>
|
||||
</span>
|
||||
<Icon name="chevronRight" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</section>
|
||||
|
||||
<section className="dash__side">
|
||||
{plan?.next_visit && (
|
||||
<div className="only-phone">
|
||||
<VisitCard date={plan.next_visit} onOpen={() => navigate("/doctor")} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!allDone && hasDone && (
|
||||
<div className="card">
|
||||
<h2 className="h3">{t.done}</h2>
|
||||
{takenDoses.map((d) => (
|
||||
<div key={d.id} className="done-row">
|
||||
<span className="badge-icon badge-icon--teal">
|
||||
<Icon name="check" size={16} />
|
||||
</span>
|
||||
<span>
|
||||
<span className="done-row__title">{t.medTaken}</span>
|
||||
<span className="done-row__sub">{t.medTakenAt(d.time)}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{plan?.checkin_done && (
|
||||
<div className="done-row">
|
||||
<span className="badge-icon badge-icon--teal">
|
||||
<Icon name="check" size={16} />
|
||||
</span>
|
||||
<span className="done-row__title">{t.checkinDone}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plan?.prescriptions[0] && <WeekCard rx={plan.prescriptions[0]} onOpen={() => navigate("/meds")} />}
|
||||
|
||||
{noDoctor ? (
|
||||
<Notice>{t.noDoctor}</Notice>
|
||||
) : (
|
||||
<div className="quick">
|
||||
<button
|
||||
key={d.id}
|
||||
type="button"
|
||||
className={d.time <= now && !hasOpenStep ? "task task--hero" : "task"}
|
||||
onClick={() => openDose(d)}
|
||||
className="quick__btn"
|
||||
onClick={() => go("menu:med_question", t.medQuestion)}
|
||||
disabled={dialog.busy}
|
||||
>
|
||||
<span className="task__icon task__icon--teal">
|
||||
<Icon name="pill" />
|
||||
</span>
|
||||
<span className="task__text">
|
||||
<span className="task__title">{t.takeMed}</span>
|
||||
<span className="task__sub">{d.time}</span>
|
||||
</span>
|
||||
{d.time <= now && <span className="chip chip--teal-solid">{t.now}</span>}
|
||||
<Icon name="chevronRight" />
|
||||
</button>
|
||||
))}
|
||||
{plan && !plan.checkin_done && (
|
||||
<button type="button" className="task" onClick={() => go("menu:checkin", t.checkin)} disabled={dialog.busy}>
|
||||
<span className="task__icon task__icon--teal">
|
||||
<Icon name="smile" />
|
||||
</span>
|
||||
<span className="task__text">
|
||||
<span className="task__title">{t.checkin}</span>
|
||||
<span className="task__sub">{t.checkinHint}</span>
|
||||
</span>
|
||||
<Icon name="chevronRight" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!allDone && (takenDoses.length > 0 || plan?.checkin_done) && (
|
||||
<section className="stack stack--sm">
|
||||
<h2 className="h3">{t.done}</h2>
|
||||
{takenDoses.map((d) => (
|
||||
<div key={d.id} className="done-row">
|
||||
<span className="badge-icon badge-icon--teal">
|
||||
<Icon name="check" size={16} />
|
||||
<span className="tile-icon tile-icon--blue">
|
||||
<Icon name="question" />
|
||||
</span>
|
||||
<span>
|
||||
<span className="done-row__title">{t.medTaken}</span>
|
||||
<span className="done-row__sub">{t.medTakenAt(d.time)}</span>
|
||||
<span className="quick__title">{t.medQuestion}</span>
|
||||
<span className="quick__sub">{t.medQuestionHint}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{plan?.checkin_done && (
|
||||
<div className="done-row">
|
||||
<span className="badge-icon badge-icon--teal">
|
||||
<Icon name="check" size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="quick__btn"
|
||||
onClick={() => go("menu:contact_doctor", t.messageDoctor)}
|
||||
disabled={dialog.busy}
|
||||
>
|
||||
<span className="tile-icon tile-icon--blue">
|
||||
<Icon name="message" />
|
||||
</span>
|
||||
<span className="done-row__title">{t.checkinDone}</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{plan?.next_visit && (
|
||||
<div className="only-phone">
|
||||
<VisitCard date={plan.next_visit} onOpen={() => navigate("/doctor")} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{session?.mode === "anonymous" || (plan && !plan.has_doctor) ? (
|
||||
<Notice>{t.noDoctor}</Notice>
|
||||
) : (
|
||||
<div className="quick">
|
||||
<button type="button" className="quick__btn" onClick={() => go("menu:med_question", t.medQuestion)} disabled={dialog.busy}>
|
||||
<Icon name="question" />
|
||||
{t.medQuestion}
|
||||
</button>
|
||||
<button type="button" className="quick__btn" onClick={() => go("menu:contact_doctor", t.messageDoctor)} disabled={dialog.busy}>
|
||||
<Icon name="message" />
|
||||
{t.messageDoctor}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<span>
|
||||
<span className="quick__title">{t.messageDoctor}</span>
|
||||
<span className="quick__sub">{t.messageDoctorHint}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@@ -46,6 +46,23 @@ export function Welcome() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section className="how" aria-labelledby="how-title">
|
||||
<h2 id="how-title" className="h3">
|
||||
{t.howTitle}
|
||||
</h2>
|
||||
<ol className="how__list">
|
||||
{t.how.map((step, i) => (
|
||||
<li key={step.title} className="how__item">
|
||||
<span className="how__num">{i + 1}</span>
|
||||
<span>
|
||||
<span className="how__title">{step.title}</span>
|
||||
<span className="how__text">{step.text}</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<p className="muted small">{t.disclaimer}</p>
|
||||
</div>
|
||||
</AppShell>
|
||||
|
||||
+260
-10
@@ -788,7 +788,9 @@ button.row-card:hover:not(:disabled) {
|
||||
|
||||
.num {
|
||||
aspect-ratio: 1;
|
||||
min-height: 30px;
|
||||
width: 100%;
|
||||
max-width: 52px;
|
||||
justify-self: center;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
@@ -1170,22 +1172,128 @@ button.row-card:hover:not(:disabled) {
|
||||
.quick__btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
min-height: 64px;
|
||||
gap: var(--s-3);
|
||||
min-height: 72px;
|
||||
padding: var(--s-3);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--r-tile);
|
||||
background: var(--c-surface);
|
||||
text-align: left;
|
||||
font-size: var(--fs-14);
|
||||
font-weight: 600;
|
||||
color: var(--c-navy);
|
||||
}
|
||||
|
||||
.quick__title {
|
||||
display: block;
|
||||
font-size: var(--fs-14);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.quick__sub {
|
||||
display: none;
|
||||
font-size: var(--fs-13);
|
||||
color: var(--c-text-2);
|
||||
}
|
||||
|
||||
.quick__btn svg {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- shared blocks: focus, dashboard, grids */
|
||||
|
||||
.focus,
|
||||
.split,
|
||||
.split__lead,
|
||||
.split__act {
|
||||
display: grid;
|
||||
gap: var(--s-5);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.dash,
|
||||
.dash__main,
|
||||
.dash__side {
|
||||
display: grid;
|
||||
gap: var(--s-4);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
gap: var(--s-3);
|
||||
width: 100%;
|
||||
padding: var(--s-4);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--r-tile);
|
||||
background: var(--c-surface);
|
||||
text-align: left;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.card__head > svg:last-child {
|
||||
flex: none;
|
||||
color: var(--c-text-3);
|
||||
}
|
||||
|
||||
.grid2,
|
||||
.cols {
|
||||
display: grid;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.btn--auto {
|
||||
width: auto;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.how {
|
||||
display: grid;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.how__list {
|
||||
display: grid;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.how__item {
|
||||
display: flex;
|
||||
gap: var(--s-3);
|
||||
align-items: flex-start;
|
||||
padding: var(--s-4);
|
||||
border-radius: var(--r-tile);
|
||||
background: var(--c-tile);
|
||||
}
|
||||
|
||||
.how__num {
|
||||
flex: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--c-navy);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
font-size: var(--fs-14);
|
||||
}
|
||||
|
||||
.how__title {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.how__text {
|
||||
display: block;
|
||||
font-size: var(--fs-13);
|
||||
color: var(--c-text-2);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- meds */
|
||||
|
||||
.rx {
|
||||
@@ -1717,19 +1825,152 @@ button.row-card:hover:not(:disabled) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) var(--rail-w);
|
||||
gap: var(--s-6);
|
||||
max-width: 1180px;
|
||||
max-width: 1320px;
|
||||
padding: var(--s-8) var(--s-8) var(--s-8) var(--s-10);
|
||||
}
|
||||
|
||||
.screen {
|
||||
max-width: 760px;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: var(--s-8);
|
||||
padding: 0;
|
||||
gap: var(--s-6);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
/* Tab screens and sign-in: the page content sits on one wide white card. */
|
||||
.screen > .stack {
|
||||
padding: var(--s-8) var(--s-10);
|
||||
border-radius: 24px;
|
||||
background: var(--c-surface);
|
||||
box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
|
||||
align-self: start;
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
/* Focus screens: question on the left, answers on the right. */
|
||||
.focus,
|
||||
.done {
|
||||
padding: var(--s-8) var(--s-10) var(--s-10);
|
||||
border-radius: 24px;
|
||||
background: var(--c-surface);
|
||||
box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
|
||||
}
|
||||
|
||||
.done {
|
||||
max-width: none;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.done > .panel,
|
||||
.done > .btn {
|
||||
width: min(100%, 520px);
|
||||
}
|
||||
|
||||
.split {
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
|
||||
gap: var(--s-12);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.split__lead {
|
||||
position: sticky;
|
||||
top: var(--s-8);
|
||||
}
|
||||
|
||||
.split__act {
|
||||
padding: var(--s-6);
|
||||
border-radius: 20px;
|
||||
background: var(--c-tile);
|
||||
}
|
||||
|
||||
.split__act .option,
|
||||
.split__act .row-card,
|
||||
.split__act .textarea textarea,
|
||||
.split__act .code-input {
|
||||
background: var(--c-surface);
|
||||
}
|
||||
|
||||
.split--crisis .split__act,
|
||||
.split--plan .split__act {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-row--stack-desk {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Wide content (consent text, 0–10 scale) reads better in one centred column. */
|
||||
.split--consent,
|
||||
.split--number {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
max-width: 760px;
|
||||
gap: var(--s-6);
|
||||
}
|
||||
|
||||
.split--consent .split__lead,
|
||||
.split--number .split__lead {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.split--consent .btn-row--stack-desk {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.split--number .numscale__row {
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.num {
|
||||
font-size: var(--fs-16);
|
||||
}
|
||||
|
||||
.focus .h1 {
|
||||
font-size: var(--fs-28);
|
||||
}
|
||||
|
||||
.panel--scroll {
|
||||
max-height: 56vh;
|
||||
}
|
||||
|
||||
/* Today: dashboard. */
|
||||
.dash {
|
||||
grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
|
||||
gap: var(--s-6);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.dash__head {
|
||||
grid-column: 1 / -1;
|
||||
padding: 0 var(--s-2);
|
||||
}
|
||||
|
||||
.dash__main {
|
||||
padding: var(--s-6);
|
||||
border-radius: 24px;
|
||||
background: var(--c-surface);
|
||||
box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
|
||||
}
|
||||
|
||||
.dash__side .card {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 1px 2px rgb(15 39 71 / 0.06);
|
||||
}
|
||||
|
||||
.grid2,
|
||||
.cols {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.dash__side .quick {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cols > .notice {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.how__list {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.rail {
|
||||
@@ -1769,6 +2010,15 @@ button.row-card:hover:not(:disabled) {
|
||||
font-size: var(--fs-34);
|
||||
}
|
||||
|
||||
.quick__sub {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.quick__btn {
|
||||
min-height: 84px;
|
||||
padding: var(--s-4);
|
||||
}
|
||||
|
||||
.entry-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user