import { useState, type ReactNode } from "react";
import { clinicianApi } from "../../api/client";
import type { CheckRun, FindingMark, FindingMarkKind, PatientDetail } from "../../api/types";
import { formatDate } from "../../flow";
import { markLabels } from "../../logic/checks";
import { draftWordingPill } from "../../logic/pills";
import { allergyReactionLine, findingExtras, findingWording, ruleReviewLabel } from "../../logic/engine-extras";
import { tierOf, whenLabel } from "../../logic/format";
import { lookOf } from "../charts";
import { Button, cn, DialogActions, Drawer, FieldError, Icon, type IconName, labelClass, Modal, StatusPill, Tag, Textarea, type Tone } from "../kit";
import { AskYasmar } from "../patient/links";
import { Pill } from "../Pill";
import type { SourceRef } from "../patient/SourceDrawer";
import { errorText } from "../ui";
type Finding = CheckRun["assessment"]["contextual"]["findings"][number];
const markLook: Record<FindingMarkKind, { tone: Tone; icon: IconName }> = {
reviewed: { tone: "green", icon: "checkbox-circle-fill" },
not_relevant: { tone: "grey", icon: "forbid-fill" },
problem: { tone: "red", icon: "flag-fill" },
};
const chip =
"inline-flex max-w-full cursor-pointer flex-wrap items-center gap-x-1.5 gap-y-0.5 rounded-md border-0 bg-page px-2 py-1 text-left font-sans text-[12.5px] leading-[18px] text-body shadow-[inset_0_0_0_1px_var(--color-line)] outline-none hover:bg-tile focus-visible:shadow-[0_0_0_2px_var(--color-brand)]";
const Term = ({ term, children }: { term: string; children: ReactNode }) => (
<div className="flex flex-col gap-1 py-2.5 shadow-[inset_0_-1px_0_var(--color-hairline)]">
<dt className="text-[12.5px] text-muted">{term}</dt>
<dd className="m-0 text-[13.5px] leading-5 text-ink">{children}</dd>
</div>
);
const DrawerSub = ({ children }: { children: ReactNode }) => <h4 className="m-0 mt-5 mb-1 text-[13px] font-medium text-muted">{children}</h4>;
const refRow = "flex flex-col gap-1 py-2.5 text-[13px] leading-[19px] shadow-[inset_0_-1px_0_var(--color-hairline)]";
/** FND-1 to FND-3, FND-9: tier with its time, clinician wording, sources, why, marks, Ask Yasmar. */
export function FindingCard({
finding,
check,
patient,
mark,
canAskYasmar,
onSource,
onMarked,
}: {
finding: Finding;
check: CheckRun;
patient: PatientDetail;
mark: FindingMark | undefined;
canAskYasmar: boolean;
onSource: (s: SourceRef) => void;
onMarked: (m: FindingMark) => void;
}) {
const detail = check.details[finding.id];
const tier = tierOf(finding.tier);
const look = lookOf(finding.tier);
const isNew = check.assessment.addedRuleIds.includes(finding.ruleId);
const [why, setWhy] = useState(false);
const [marking, setMarking] = useState<FindingMarkKind | null>(null);
const wording = findingWording(finding, detail);
const title = wording.title;
const review = ruleReviewLabel(finding, detail?.ruleReviewed);
const reaction = allergyReactionLine(finding);
const specific = findingExtras(finding).specific_references ?? [];
const urgent = finding.tier === "get_help_now";
return (
<article
aria-label={title}
className={cn("flex flex-col gap-3 rounded-2xl bg-page p-5 shadow-card", urgent && "shadow-[inset_3px_0_0_var(--color-red-icon),0_0_0_1px_var(--color-red-line)]")}
>
<div className="flex flex-wrap items-center gap-2">
<StatusPill tone={look.tone} icon={look.icon}>
{tier.label}
</StatusPill>
<Tag lead="time-line">{tier.when}</Tag>
{isNew && (
<StatusPill tone="blue" size="sm" icon="sparkling-line">
New with patient context
</StatusPill>
)}
<Tag>For clinician review</Tag>
{review && <Tag>{review}</Tag>}
{wording.draft && <Pill pill={draftWordingPill} />}
</div>
<h3 className="m-0 text-[16px] leading-6 font-semibold tracking-[-0.01em] text-ink">{title}</h3>
<p className="m-0 text-[13.5px] leading-5 text-body">{wording.why}</p>
{reaction && <p className="m-0 text-[13px] leading-[19px] text-muted">{reaction}</p>}
{detail && detail.factsUsed.length > 0 && (
<div className="flex flex-wrap items-center gap-2">
<small className="text-[12.5px] text-muted">Based on</small>
{detail.factsUsed.map((f) => (
<button key={f.factId} type="button" className={chip} onClick={() => onSource({ recordId: f.recordId, title: f.recordTitle, lines: [f.line] })}>
<Icon name="file-text-line" size={14} className="text-muted" />
<span className="font-medium text-ink">{f.label}</span>
<span className="font-mono text-[11.5px] text-muted">
{f.recordTitle} · line {f.line}
</span>
</button>
))}
</div>
)}
{mark && (
<p className="m-0 flex flex-wrap items-center gap-2">
<StatusPill tone={markLook[mark.kind].tone} icon={markLook[mark.kind].icon} size="sm">
{markLabels[mark.kind]}
</StatusPill>
<small className="text-[12.5px] text-muted">
{mark.by} · {whenLabel(mark.at)}
{mark.reason ? ` · ${mark.reason}` : ""}
{mark.note ? ` · ${mark.note}` : ""}
</small>
</p>
)}
<div className="mt-1 flex flex-wrap items-center gap-2 pt-3 shadow-[inset_0_1px_0_var(--color-hairline)]">
<Button onClick={() => setWhy(true)}>
<Icon name="book-open-line" /> Why this finding
</Button>
<Button aria-pressed={mark?.kind === "reviewed"} className="aria-pressed:bg-green-fill aria-pressed:text-green-text" onClick={() => setMarking("reviewed")}>
<Icon name="check-line" /> Reviewed
</Button>
<Button aria-pressed={mark?.kind === "not_relevant"} className="aria-pressed:bg-quiet-hover aria-pressed:text-ink" onClick={() => setMarking("not_relevant")}>
<Icon name="forbid-line" /> Not relevant
</Button>
<Button variant="ghost" onClick={() => setMarking("problem")}>
<Icon name="flag-fill" className="text-muted" /> Report a problem
</Button>
{canAskYasmar ? <AskYasmar patient={patient} findingId={finding.id} checkId={check.id} /> : null}
</div>
<Drawer open={why} title="Why this finding" onClose={() => setWhy(false)}>
<div className="flex flex-col">
<StatusPill tone={look.tone} icon={look.icon} size="sm" className="self-start">
{tier.label}
</StatusPill>
<h3 className="m-0 mt-3 text-[16px] leading-6 font-semibold text-ink">{title}</h3>
<dl className="m-0 mt-2">
<Term term="Rule">
<code className="font-mono text-[12.5px]">{finding.ruleId}</code> · {review ? review.replace(/^Rule /, "").toLowerCase() : "signed off by the clinical safety lead"}
{wording.draft ? ". The clinician wording is not yet signed off for clinicians." : ""}
</Term>
<Term term="Tier">
{tier.label}: {tier.when.toLowerCase()}.
</Term>
<Term term="What the rule says">{finding.clinical || wording.why}</Term>
{reaction && <Term term="Reaction">{reaction}</Term>}
</dl>
<DrawerSub>References</DrawerSub>
<ul className="m-0 list-none p-0">
{specific.map((r) => (
<li key={`${r.sourceId}-${r.section}`} className={refRow}>
<a href={r.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 font-medium text-ink underline decoration-line underline-offset-4 hover:decoration-ink">
{r.document} <Icon name="external-link-line" size={13} className="text-subtle" />
</a>
<small className="font-mono text-[11.5px] text-muted">
Section {r.section.replace(/^Section\s+/i, "")} · {r.version} · read {formatDate(r.checkedOn)}
</small>
<q className="text-body">{r.says}</q>
</li>
))}
{(detail?.references ?? []).filter((r) => !specific.length || !r.url?.includes("search")).map((r) => (
<li key={r.name} className={refRow}>
{r.url ? (
<a href={r.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 font-medium text-ink underline decoration-line underline-offset-4 hover:decoration-ink">
{r.name} <Icon name="external-link-line" size={13} className="text-subtle" />
</a>
) : (
<span className="font-medium text-ink">{r.name}</span>
)}
<small className="font-mono text-[11.5px] text-muted">
{[r.version, r.section, r.date && formatDate(r.date)].filter(Boolean).join(" · ") || "Version and section not recorded yet"}
</small>
</li>
))}
</ul>
<DrawerSub>Facts it used</DrawerSub>
<ul className="m-0 list-none p-0">
{(detail?.factsUsed ?? []).map((f) => (
<li key={f.factId} className={refRow}>
<button
type="button"
className={cn(chip, "self-start")}
onClick={() => {
setWhy(false);
onSource({ recordId: f.recordId, title: f.recordTitle, lines: [f.line] });
}}
>
<Icon name="file-text-line" size={14} className="text-muted" />
{f.label} · {f.recordTitle}, line {f.line}
</button>
<q className="text-body">{f.quote}</q>
</li>
))}
</ul>
<p className="m-0 mt-4 text-[12.5px] leading-[18px] text-muted">The finding is information for your review. It is not a decision and not an instruction.</p>
</div>
</Drawer>
<MarkDialog
kind={marking}
checkId={check.id}
findingId={finding.id}
title={title}
onClose={() => setMarking(null)}
onSaved={(m) => {
setMarking(null);
onMarked(m);
}}
/>
</article>
);
}
const markCopy: Record<FindingMarkKind, { title: string; field: string; required: boolean; help: string }> = {
reviewed: { title: "Mark as reviewed", field: "Note (optional)", required: false, help: "This records your review. It does not change the rule." },
not_relevant: { title: "Mark as not relevant", field: "Why is it not relevant?", required: true, help: "A reason is required. The clinical safety lead reviews these each month." },
problem: { title: "Report a problem", field: "What is wrong?", required: true, help: "This goes to MARMAR's clinical team. It does not change the rule." },
};
/** FND-3: Reviewed (optional note), Not relevant (reason required), Report a problem. */
function MarkDialog({ kind, checkId, findingId, title, onClose, onSaved }: { kind: FindingMarkKind | null; checkId: string; findingId: string; title: string; onClose: () => void; onSaved: (m: FindingMark) => void }) {
const [text, setText] = useState("");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
const copy = kind ? markCopy[kind] : null;
const save = async () => {
if (!kind || !copy) return;
if (copy.required && text.trim().length < 3) return setError(kind === "not_relevant" ? "Say why this finding is not relevant." : "Describe the problem.");
setBusy(true);
try {
const m = await clinicianApi.markFinding(checkId, findingId, {
kind,
reason: kind === "not_relevant" ? text.trim() : null,
note: kind === "not_relevant" ? null : text.trim() || null,
});
setText("");
setError("");
onSaved(m);
} catch (e) {
setError(errorText(e));
} finally {
setBusy(false);
}
};
return (
<Modal open={!!kind} title={copy?.title ?? ""} onClose={onClose}>
<p className="m-0 text-[14px] font-semibold text-ink">{title}</p>
<p className="m-0 text-[13px] text-muted">{copy?.help}</p>
<label className={labelClass}>
{copy?.field}
<Textarea rows={3} maxLength={1000} value={text} required={copy?.required} aria-invalid={error ? true : undefined} onChange={(e) => setText(e.target.value)} />
</label>
{error && <FieldError>{error}</FieldError>}
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button variant="primary" disabled={busy} onClick={() => void save()}>
{busy ? "Saving…" : copy?.title}
</Button>
</DialogActions>
</Modal>
);
}