import { useEffect, useMemo, useState } from "react";
import { clinicianApi } from "../../api/client";
import { asOfLabel } from "../../logic/format";
import { searchPatients, sortPatients } from "../../logic/patients";
import { needsAttention } from "../../logic/today";
import { Button, Count, EmptyState, Icon, SearchField, SectionHeader, Segmented } from "../kit";
import { useLocation } from "../router";
import Shell from "../Shell";
import { ErrorNote, Spinner, useLoad, useToast } from "../ui";
import { InvitationCard } from "./Invitations";
import { NewPatient } from "./NewPatient";
import { PatientTable } from "./PatientTable";
import { PatientTile } from "./PatientTile";
type Show = "all" | "needs";
const showFrom = (param: string | null): Show => (param === "needs-you" ? "needs" : "all");
/** PAT-2: invitations first, then patients by what needs attention, with search and a Needs you filter. */
export default function PatientsPage() {
const { query: params } = useLocation();
const invitations = useLoad(() => clinicianApi.invitations(), []);
const patients = useLoad(() => clinicianApi.patients(), []);
const [query, setQuery] = useState("");
const [show, setShow] = useState<Show>(() => showFrom(params.get("show")));
const [adding, setAdding] = useState(false);
const toast = useToast();
// Each navigation (the sidebar link, Today's "See all") sets the filter from the address again.
useEffect(() => setShow(showFrom(params.get("show"))), [params]);
useEffect(() => {
const t = setInterval(() => patients.reload(true), 30_000);
return () => clearInterval(t);
}, [patients.reload]);
const all = useMemo(() => sortPatients(patients.data?.patients ?? []), [patients.data]);
const needs = useMemo(() => all.filter(needsAttention), [all]);
const list = useMemo(() => searchPatients(show === "needs" ? needs : all, query), [all, needs, show, query]);
const pending = invitations.data?.invitations ?? [];
const pick = (next: Show) => {
setShow(next);
// The filter keeps its own address without a new history entry or a jump to the top.
history.replaceState(history.state, "", next === "needs" ? "/app/patients?show=needs-you" : "/app/patients");
};
return (
<Shell crumbs={[{ label: "Patients" }]} active="patients">
<div className="flex flex-col gap-6 pb-12 md:pt-2">
<header className="flex flex-wrap items-end justify-between gap-4">
<div className="flex min-w-0 max-w-2xl flex-col gap-1.5">
<h1 className="m-0 flex items-center gap-2.5 text-[26px] leading-8 font-semibold tracking-[-0.02em] text-ink md:text-[30px] md:leading-9">
Patients
{patients.data && <Count className="h-[22px] min-w-[22px] text-[12px]">{all.length}</Count>}
</h1>
<p className="m-0 text-[14px] leading-5 text-muted">
People who invited you from MARMAR Health, and patients you added. Those who need you most come first.
</p>
</div>
<Button onClick={() => setAdding(true)}>
<Icon name="add-line" size={16} />
Add a patient
</Button>
</header>
{toast.node}
<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} />
{pending.map((inv) => (
<InvitationCard
key={inv.id}
invitation={inv}
onDone={(message) => {
toast.show(message);
invitations.reload(true);
}}
/>
))}
</section>
)}
<section aria-label="Your patients" className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-3">
<h2 className="m-0 mr-auto flex items-center gap-2 text-[15px] leading-5 font-semibold tracking-[-0.01em] text-ink">
<Icon name="team-line" size={16} className="text-grey-icon" />
Your patients
{patients.data && <Count>{list.length}</Count>}
</h2>
<Segmented
label="Show"
value={show}
onChange={pick}
options={[
{ value: "all", label: "All" },
{ value: "needs", label: needs.length ? `Needs you · ${needs.length}` : "Needs you" },
]}
/>
<SearchField label="Search patients by name" placeholder="Search by name" value={query} onChange={setQuery} className="w-full sm:w-64" />
</div>
{patients.loading && !patients.data && <Spinner label="Loading patients" />}
<ErrorNote error={patients.error} onRetry={() => patients.reload()} />
{patients.data && !list.length && (
<EmptyState
title={query ? `No patient matches "${query}".` : show === "needs" ? "Nobody needs you right now." : "No patients yet."}
body={
query
? undefined
: show === "needs"
? "Urgent notes, unread check-in notes and new information since your last check show here."
: "Invitations from MARMAR Health appear above."
}
action={
show === "needs" ? (
<Button size="sm" onClick={() => pick("all")}>
Show all patients
</Button>
) : undefined
}
/>
)}
{list.length > 0 && (
<>
<div className="hidden md:block">
<PatientTable patients={list} />
</div>
<div className="flex flex-col gap-2.5 md:hidden">
{list.map((p) => (
<PatientTile key={p.id} patient={p} />
))}
</div>
</>
)}
{patients.data && <p className="m-0 text-[12px] text-subtle">Updated {asOfLabel(patients.data.asOf)}</p>}
</section>
</div>
<NewPatient open={adding} onClose={() => setAdding(false)} />
</Shell>
);
}