import { useEffect, useMemo } from "react";
import { clinicianApi } from "../../api/client";
import type { ClinicProfile, Invitation } from "../../api/types";
import { asOfLabel } from "../../logic/format";
import { clinicHoursLabel, greeting, longDate, needsYou, summaryLine, todayStats, type Todo } from "../../logic/today";
import { zoneOffset } from "../../logic/windows";
import { Card, Dot, Icon, SectionHeader, StatusPill } from "../kit";
import { InvitationCard } from "../patients/Invitations";
import { displayName, useClinician } from "../session";
import Shell from "../Shell";
import { useShellAlerts } from "../shell/context";
import { ErrorNote, Spinner, useLoad, useToast } from "../ui";
import { DosesThisWeek, RecentCheckIns } from "./Lists";
import { NeedsYou } from "./NeedsYou";
/** Today: what needs the clinician first, then doses and check-ins across their patients. */
export default function TodayPage() {
return (
<Shell crumbs={[{ label: "Today" }]} active="today">
<TodayBody />
</Shell>
);
}
function hoursPill(clinic: ClinicProfile, now: Date) {
if (!clinic.openingHours.length) return null;
try {
return clinicHoursLabel(clinic.openingHours, now, zoneOffset(clinic.timeZone, now));
} catch {
return null;
}
}
function TodayBody() {
const { me, clinic } = useClinician();
const alerts = useShellAlerts();
const patients = useLoad(() => clinicianApi.patients(), []);
const invitations = useLoad(() => clinicianApi.invitations(), []);
const toast = useToast();
useEffect(() => {
const t = setInterval(() => patients.reload(true), 30_000);
return () => clearInterval(t);
}, [patients.reload]);
const now = new Date();
const list = patients.data?.patients;
const pending = invitations.data?.invitations ?? [];
const todos = useMemo(() => needsYou(list ?? [], alerts), [list, alerts]);
const hours = hoursPill(clinic, now);
return (
<div className="mx-auto flex w-full max-w-[920px] flex-col gap-6 pt-2 pb-12 md:pt-6">
<header className="flex flex-wrap items-end justify-between gap-x-4 gap-y-3">
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-[13.5px] text-muted">
{greeting(now)} · {longDate(now)}
</span>
<h1 className="m-0 text-[26px] leading-8 font-semibold tracking-[-0.02em] text-ink md:text-[30px] md:leading-9">{displayName(me)}</h1>
</div>
<span className="inline-flex flex-wrap items-center gap-2 text-[13px] text-cell">
<Icon name="hospital-line" size={16} className="text-blue-icon" />
{clinic.name}
{hours && (
<StatusPill tone={hours.open ? "green" : "grey"} size="sm">
<Dot tone={hours.open ? "green" : "grey"} />
{hours.label}
</StatusPill>
)}
</span>
</header>
{toast.node}
<ErrorNote error={patients.error} onRetry={() => patients.reload()} />
{!list && patients.loading && <Spinner label="Loading today" />}
{list && (
<>
<Summary todos={todos} pending={pending} stats={todayStats(list, now)} />
<NeedsYou todos={todos} />
</>
)}
<ErrorNote error={invitations.error} onRetry={() => invitations.reload()} />
{pending.length > 0 && (
<section aria-label="Invitations" className="flex flex-col gap-3">
<SectionHeader title="Invitations" count={pending.length} to="/app/patients" />
{pending.map((inv) => (
<InvitationCard
key={inv.id}
invitation={inv}
onDone={(message) => {
toast.show(message);
invitations.reload(true);
}}
/>
))}
</section>
)}
{list && (
<>
<div className="grid gap-6 lg:grid-cols-2">
<DosesThisWeek patients={list} />
<RecentCheckIns patients={list} now={now} />
</div>
<p className="m-0 text-[12px] text-subtle">Updated {asOfLabel(patients.data!.asOf)}</p>
</>
)}
</div>
);
}
const plural = (n: number, one: string) => `${n} ${n === 1 ? one : `${one}s`}`;
/** The big number: who needs you, of how many, the one plain line, and four small counts. */
function Summary({ todos, pending, stats }: { todos: Todo[]; pending: Invitation[]; stats: ReturnType<typeof todayStats> }) {
const mini = [
{ label: "Sharing with you", value: stats.sharing, tail: stats.total ? `of ${stats.total}` : "", tone: "text-subtle" },
{ label: "Check-ins today", value: stats.checkedInToday, tail: "", tone: "text-subtle" },
{ label: "Unread notes", value: stats.unreadNotes, tail: "", tone: "text-subtle" },
{ label: "Invitations", value: pending.length, tail: pending.length ? "waiting" : "", tone: "text-indigo-text" },
];
return (
<Card className="flex flex-wrap justify-between gap-6 p-6">
<div className="flex min-w-0 flex-1 basis-64 flex-col gap-1.5">
<span className="text-[13.5px] text-muted">Needs you</span>
<span className="text-[44px] leading-[48px] font-semibold tracking-[-0.03em] text-ink tabular-nums">
{todos.length}
{stats.total > 0 && <span className="ml-2 text-[16px] font-medium tracking-normal text-subtle">of {plural(stats.total, "patient")}</span>}
</span>
<p className="m-0 mt-1 max-w-md text-[14px] leading-5 text-body">{summaryLine(todos, pending)}</p>
</div>
<dl className="m-0 grid min-w-0 flex-1 basis-64 grid-cols-2 content-start gap-2.5">
{mini.map((m) => (
<div key={m.label} className="flex min-w-0 flex-col gap-1 rounded-xl bg-tile px-3.5 py-3">
<dt className="truncate text-[12.5px] text-muted">{m.label}</dt>
<dd className="m-0 text-[20px] leading-6 font-semibold text-ink tabular-nums">
{m.value}
{m.tail && <span className={`ml-1.5 text-[12.5px] font-medium ${m.tone}`}>{m.tail}</span>}
</dd>
</div>
))}
</dl>
</Card>
);
}