336 lines
13 KiB
TypeScript
336 lines
13 KiB
TypeScript
import { useNavigate } from "react-router-dom";
|
|
|
|
import type { TodayDose, TodayPrescription } from "../api/types";
|
|
import { AppShell, Notice } from "../components/AppShell";
|
|
import { Icon, type IconName } from "../components/Icon";
|
|
import { useCatalog } from "../content/reference";
|
|
import { ru } from "../content/ru";
|
|
import { useDialog } from "../dialog/DialogContext";
|
|
import { classify } from "../dialog/step";
|
|
import { WeekDots } from "./Meds";
|
|
import { humanDate, useToday } from "./useToday";
|
|
|
|
// Spec «Модуль Лекарства»: the expected-effect card is pushed on these days of therapy.
|
|
const EFFECT_DAYS = [1, 7, 14];
|
|
|
|
function Task({
|
|
icon,
|
|
title,
|
|
sub,
|
|
hero,
|
|
chip,
|
|
onClick,
|
|
disabled,
|
|
}: {
|
|
icon: IconName;
|
|
title: string;
|
|
sub: string;
|
|
hero?: boolean;
|
|
chip?: string;
|
|
onClick: () => void;
|
|
disabled?: boolean;
|
|
}) {
|
|
return (
|
|
<button type="button" className={hero ? "task task--hero" : "task"} onClick={onClick} disabled={disabled}>
|
|
<span className={hero ? "task__icon" : "task__icon task__icon--teal"}>
|
|
<Icon name={icon} />
|
|
</span>
|
|
<span className="task__text">
|
|
<span className="task__title">{title}</span>
|
|
<span className="task__sub">{sub}</span>
|
|
</span>
|
|
{chip ? <span className="chip chip--teal-solid">{chip}</span> : <Icon name="chevronRight" />}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
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;
|
|
const navigate = useNavigate();
|
|
const dialog = useDialog();
|
|
const { plan, failed } = useToday();
|
|
const catalog = useCatalog();
|
|
|
|
// An unanswered engine question (reminder, check-in, scale, unfinished onboarding) waits on the focus screen.
|
|
const waiting = classify(dialog.pending);
|
|
// A paused questionnaire is not an open step: the «continue the questionnaire» card below resumes it.
|
|
const paused = waiting?.kind === "notice" && waiting.primary?.data === "intake:resume";
|
|
const hasOpenStep = waiting !== null && waiting.kind !== "idle" && waiting.kind !== "ack" && !paused;
|
|
const surveyItem = waiting?.kind === "survey" ? catalog?.items[waiting.itemId] : undefined;
|
|
const surveyForm = surveyItem ? catalog?.forms[surveyItem.form] : undefined;
|
|
|
|
const go = (button: string, label: string) =>
|
|
void dialog.send({ type: "button", button }, label).then((ok) => ok && navigate("/flow"));
|
|
|
|
// Questionnaires (stage 1 intake → stage 2 diary) replace the simple check-in once they are switched on.
|
|
const q = plan?.questionnaires;
|
|
const surveys = q?.enabled === true;
|
|
const intakeOpen = surveys && q.intake !== "complete" && q.intake !== "review";
|
|
const diaryDue = surveys && q.intake === "complete" && !q.diary_done;
|
|
const effectRx = plan?.prescriptions.find((p) => p.ref_id && p.therapy_day && EFFECT_DAYS.includes(p.therapy_day));
|
|
|
|
const pendingDoses = plan?.doses.filter((d) => d.status === "pending") ?? [];
|
|
const takenDoses = plan?.doses.filter((d) => d.status === "taken") ?? [];
|
|
const doseNow = pendingDoses.some((d) => d.time <= (plan?.now ?? "00:00"));
|
|
const allDone =
|
|
plan !== null &&
|
|
pendingDoses.length === 0 &&
|
|
plan.checkin_done &&
|
|
!hasOpenStep &&
|
|
!intakeOpen &&
|
|
!diaryDue &&
|
|
q?.intake !== "review";
|
|
const nextDose = plan?.prescriptions.flatMap((p) => p.times).sort()[0];
|
|
const noDoctor = 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="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}
|
|
>
|
|
<Icon name="bell" />
|
|
{dialog.unread > 0 && <span className="dot" />}
|
|
</button>
|
|
</header>
|
|
|
|
{failed && <Notice tone="danger">{ru.common.networkError}</Notice>}
|
|
|
|
<section className="dash__main" aria-label={t.title}>
|
|
{hasOpenStep &&
|
|
(waiting?.kind === "survey" ? (
|
|
<Task
|
|
hero
|
|
icon={surveyForm?.section === "diary" ? "book" : "clipboard"}
|
|
title={surveyForm?.section === "diary" ? t.diary : t.intakeResume}
|
|
sub={t.intakeResumeHint}
|
|
chip={t.continue}
|
|
onClick={() => navigate("/flow")}
|
|
/>
|
|
) : (
|
|
<Task
|
|
hero
|
|
icon="message"
|
|
title={t.newMessage}
|
|
sub={dialog.pending[0]?.text.split("\n")[0].slice(0, 90) ?? ""}
|
|
chip={t.open}
|
|
onClick={() => navigate("/flow")}
|
|
/>
|
|
))}
|
|
|
|
{surveys && q.intake === "review" && <Notice>{t.intakeReview}</Notice>}
|
|
|
|
{effectRx && (
|
|
<button type="button" className="effect" onClick={() => navigate(`/meds/${effectRx.ref_id}`)}>
|
|
<span className="effect__day">
|
|
<span className="effect__num">{effectRx.therapy_day}</span>
|
|
<span className="effect__unit">день</span>
|
|
</span>
|
|
<span className="task__text">
|
|
<span className="task__title">{t.therapyDay(effectRx.therapy_day!)}</span>
|
|
<span className="task__sub">{t.effectHint}</span>
|
|
</span>
|
|
<Icon name="chevronRight" />
|
|
</button>
|
|
)}
|
|
|
|
{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>
|
|
))}
|
|
{intakeOpen && waiting?.kind !== "survey" && (
|
|
<Task
|
|
hero={!hasOpenStep && !doseNow}
|
|
icon="clipboard"
|
|
title={t.intakeStart}
|
|
sub={q.intake === "none" ? t.intakeStartHint : t.intakeResumeHint}
|
|
chip={q.intake === "none" ? undefined : t.continue}
|
|
disabled={dialog.busy}
|
|
onClick={() =>
|
|
q.intake === "none" ? go("intake:start", t.intakeStart) : go("intake:resume", t.intakeResume)
|
|
}
|
|
/>
|
|
)}
|
|
{diaryDue && waiting?.kind !== "survey" && (
|
|
<Task
|
|
icon="book"
|
|
title={q.diary_next ? t.diaryPart[q.diary_next] : t.diary}
|
|
sub={q.diary_next ? t.diaryPartHint[q.diary_next] : t.diaryHint}
|
|
disabled={dialog.busy}
|
|
onClick={() => go("menu:checkin", q.diary_next ? t.diaryPart[q.diary_next] : t.diary)}
|
|
/>
|
|
)}
|
|
{plan && !surveys && !plan.checkin_done && (
|
|
<Task
|
|
icon="smile"
|
|
title={t.checkin}
|
|
sub={t.checkinHint}
|
|
disabled={dialog.busy}
|
|
onClick={() => go("menu:checkin", t.checkin)}
|
|
/>
|
|
)}
|
|
</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">{surveys ? t.diaryDone : t.checkinDone}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{plan?.prescriptions[0] && <WeekCard rx={plan.prescriptions[0]} onOpen={() => navigate("/meds")} />}
|
|
|
|
{noDoctor ? (
|
|
<div className="stack stack--sm">
|
|
<Notice>{t.noDoctor}</Notice>
|
|
<button type="button" className="btn btn--primary" onClick={() => navigate("/doctor")}>
|
|
{ru.doctor.connect}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="quick">
|
|
<button
|
|
type="button"
|
|
className="quick__btn"
|
|
onClick={() => go("menu:med_question", t.medQuestion)}
|
|
disabled={dialog.busy}
|
|
>
|
|
<span className="tile-icon tile-icon--blue">
|
|
<Icon name="question" />
|
|
</span>
|
|
<span>
|
|
<span className="quick__title">{t.medQuestion}</span>
|
|
<span className="quick__sub">{t.medQuestionHint}</span>
|
|
</span>
|
|
</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>
|
|
<span className="quick__title">{t.messageDoctor}</span>
|
|
<span className="quick__sub">{t.messageDoctorHint}</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</AppShell>
|
|
);
|
|
}
|