import type { KeyboardEvent, ReactNode } from "react";
import { useEffect, useId, useRef, useState } from "react";
import type { PatientDetail } from "../../api/types";
import { details } from "../../logic/patient-detail";
import { cn, Icon, type IconName, type Tone } from "../kit";
import { ActivityList } from "./Activity";
export type RailTab = "details" | "activity";
const TABS: { id: RailTab; label: string }[] = [
{ id: "details", label: "Details" },
{ id: "activity", label: "Activity" },
];
/** The right rail: Details (what is shared and on file) and Activity (who did what), as two tabs. */
export function PatientRail({ patient, initial = "details" }: { patient: PatientDetail; initial?: RailTab }) {
const [tab, setTab] = useState<RailTab>(initial);
const base = useId();
const refs = useRef<Record<RailTab, HTMLButtonElement | null>>({ details: null, activity: null });
const aside = useRef<HTMLElement>(null);
// Opened on Activity while the rail sits under the page (a phone): bring it into view.
useEffect(() => {
const el = aside.current;
if (initial === "activity" && el && el.getBoundingClientRect().top > window.innerHeight) el.scrollIntoView({ block: "start" });
}, [initial]);
const onKey = (e: KeyboardEvent) => {
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return;
e.preventDefault();
const i = TABS.findIndex((t) => t.id === tab);
const next = e.key === "Home" ? 0 : e.key === "End" ? TABS.length - 1 : (i + (e.key === "ArrowRight" ? 1 : -1) + TABS.length) % TABS.length;
setTab(TABS[next].id);
refs.current[TABS[next].id]?.focus();
};
return (
<aside
ref={aside}
aria-label="About this patient"
className={cn(
"flex min-w-0 flex-col gap-[18px] pt-6 shadow-[inset_0_1px_0_var(--color-hairline)]",
"@min-[960px]/pd:sticky @min-[960px]/pd:top-[72px] @min-[960px]/pd:ml-7 @min-[960px]/pd:max-h-[calc(100dvh-96px)] @min-[960px]/pd:self-start @min-[960px]/pd:overflow-y-auto @min-[960px]/pd:pt-[46px] @min-[960px]/pd:pl-7 @min-[960px]/pd:shadow-[inset_1px_0_0_var(--color-hairline)]",
)}
>
<div role="tablist" aria-label="About this patient" onKeyDown={onKey} className="flex gap-[22px] shadow-[inset_0_-1px_0_var(--color-hairline)]">
{TABS.map((t) => {
const on = tab === t.id;
return (
<button
key={t.id}
ref={(el) => {
refs.current[t.id] = el;
}}
type="button"
role="tab"
id={`${base}-${t.id}-tab`}
aria-selected={on}
aria-controls={`${base}-${t.id}`}
tabIndex={on ? 0 : -1}
onClick={() => setTab(t.id)}
className={cn(
"relative h-[38px] cursor-pointer rounded-sm border-0 bg-transparent px-0.5 font-sans text-[14px] font-medium transition-colors outline-none focus-visible:shadow-[inset_0_0_0_2px_var(--color-brand)]",
on ? "text-ink after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-full after:bg-ink" : "text-muted hover:text-ink",
)}
>
{t.label}
</button>
);
})}
</div>
<div role="tabpanel" id={`${base}-${tab}`} aria-labelledby={`${base}-${tab}-tab`} tabIndex={0} className="rounded-md outline-none focus-visible:shadow-[0_0_0_2px_var(--color-brand)]">
{tab === "details" ? <DetailsList patient={patient} /> : <ActivityList patient={patient} />}
</div>
</aside>
);
}
// Literal strings so Tailwind finds every class.
const tile: Record<Tone, string> = {
red: "bg-red-fill text-red-icon",
amber: "bg-amber-fill text-amber-icon",
green: "bg-green-fill text-green-icon",
blue: "bg-blue-fill text-blue-icon",
indigo: "bg-indigo-fill text-indigo-icon",
purple: "bg-purple-fill text-purple-icon",
pink: "bg-pink-fill text-pink-icon",
grey: "bg-grey-fill text-grey-icon",
};
/** One detail: its name, a small tinted icon coloured by what it means, and the value with a quiet line under it. */
function Row({ icon, tone, label, children }: { icon: IconName; tone: Tone; label: string; children: ReactNode }) {
return (
<div className="grid grid-cols-[96px_minmax(0,1fr)] items-start gap-2 py-[9px]">
<dt className="text-[13.5px] leading-[22px] text-muted">{label}</dt>
<dd className="m-0 flex min-w-0 items-start gap-2">
<span aria-hidden="true" className={cn("flex size-[22px] shrink-0 items-center justify-center rounded-md", tile[tone])}>
<Icon name={icon} size={14} />
</span>
<span className="flex min-w-0 flex-col gap-0.5 pt-px text-[14px] leading-5 font-medium text-ink">{children}</span>
</dd>
</div>
);
}
const Sub = ({ children }: { children: ReactNode }) => <small className="text-[12.5px] leading-[18px] font-normal text-subtle">{children}</small>;
function DetailsList({ patient }: { patient: PatientDetail }) {
const d = details(patient);
const s = patient.sharing;
const shareTone: Tone = !s ? "grey" : s.status === "connected" ? "green" : s.status === "paused" ? "amber" : "grey";
const callTone: Tone = d.calls === "Yasmar may call" ? "green" : "grey";
return (
<>
<dl className="m-0 flex flex-col">
<Row icon={s ? "user-heart-line" : "hospital-line"} tone={shareTone} label={d.sharedBy.label}>
<span>{d.sharedBy.value}</span>
<Sub>{d.sharedBy.note}</Sub>
</Row>
<Row icon="link" tone="blue" label="What is shared">
{d.scopes ? (
<ul className="m-0 flex list-none flex-col gap-1 p-0">
{d.scopes.map((sc) => (
<li key={sc.scope} className={cn("flex items-center gap-1.5", !sc.shared && "font-normal text-muted")}>
<Icon name={sc.shared ? "check-line" : "subtract-line"} size={14} className={sc.shared ? "text-green-icon" : "text-subtle"} />
<span>
{sc.label}
{!sc.shared && <span className="text-subtle"> · Not shared</span>}
</span>
</li>
))}
</ul>
) : (
<span>{s ? "Nothing now" : "Nothing from the app"}</span>
)}
{d.scopesNote && <Sub>{d.scopesNote}</Sub>}
</Row>
<Row icon="phone-line" tone={callTone} label="Check-in calls">
<span>{d.calls}</span>
</Row>
<Row icon="moon-line" tone="indigo" label="Quiet hours">
<span>{d.quietHours}</span>
</Row>
<Row icon="id-card-line" tone={patient.identityOnFile === "none" ? "amber" : "pink"} label="Identity on file">
<span>{d.identity}</span>
</Row>
{d.withheldSome && (
<Row icon="eye-off-line" tone="amber" label="Held back">
<span>Some medicines or conditions were not shared.</span>
</Row>
)}
</dl>
<p className="m-0 mt-4 pt-3 text-[12px] leading-[18px] text-muted shadow-[inset_0_1px_0_var(--color-hairline)]">
What comes from MARMAR Health is read-only here. Your notes and checks stay in Context.
</p>
</>
);
}