feat(web): screens from docs/mock — focus flow, today, meds, doctor, profile
- /flow renders the dialog engine's current step as a mock screen (consent,
age, onboarding and scales with progress, mood 0–10, free text, dose
reminder, crisis with hotlines and breathing, safety plan), so every
engine scenario gets the design without duplicating its logic
- today / meds / doctor / profile / messages tabs; navy sidebar and
emergency rail on desktop, emergency block and tab bar on phones
- API: GET /me/{token}/today (today's doses, week marks, next visit, demo
clock time); demo ЕМИАС sign-in seeds the persona's synthetic profile and
prescription from the dashboard fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import type { TodayDose } 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 { humanDate, useToday } from "./useToday";
|
||||
|
||||
function VisitCard({ date, onOpen }: { date: string; onOpen: () => void }) {
|
||||
return (
|
||||
<button type="button" className="row-card" onClick={onOpen}>
|
||||
<span className="tile-icon tile-icon--blue">
|
||||
<Icon name="calendar" />
|
||||
</span>
|
||||
<span className="row-card__text">
|
||||
<span className="row-card__sub">{ru.today.visit}</span>
|
||||
<span className="row-card__title">{humanDate(date)}</span>
|
||||
</span>
|
||||
<Icon name="chevronRight" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// C1 / C2 · Сегодня
|
||||
export function Today() {
|
||||
const t = ru.today;
|
||||
const navigate = useNavigate();
|
||||
const { session } = useSession();
|
||||
const dialog = useDialog();
|
||||
const { plan, failed } = useToday();
|
||||
|
||||
// An unanswered engine question (reminder, check-in, scale, unfinished onboarding) waits on the focus screen.
|
||||
const waiting = classify(dialog.pending);
|
||||
const hasOpenStep = waiting !== null && waiting.kind !== "idle";
|
||||
|
||||
const go = (button: string, label: string) =>
|
||||
void dialog.send({ type: "button", button }, label).then((ok) => ok && navigate("/flow"));
|
||||
|
||||
const pendingDoses = plan?.doses.filter((d) => d.status === "pending") ?? [];
|
||||
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 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;
|
||||
|
||||
return (
|
||||
<AppShell aside={visit}>
|
||||
<div className="stack">
|
||||
<header className="today-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}>
|
||||
<Icon name="bell" />
|
||||
{dialog.unread > 0 && <span className="dot" />}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{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) => (
|
||||
<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}</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>
|
||||
<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>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user