import type { InboxClaimRow, InboxCandidateRow, ScoreBreakdown } from "@/lib/inbox-api"; import { cn } from "@/lib/utils"; export type Accent = "oxblood" | "amber" | "ink-blue" | "muted"; const ACCENT_VAR: Record = { oxblood: "var(--tt-oxblood)", amber: "var(--tt-amber)", "ink-blue": "var(--tt-ink-blue)", muted: "var(--tt-muted)", }; const REJECT_CODES = new Set(["R", "E", "X"]); function fmtMoney(n: number | null | undefined): string { if (n == null) return "—"; return n.toLocaleString("en-US", { style: "currency", currency: "USD" }); } function fmtAgo(iso: string | null | undefined): string { if (!iso) return ""; const ms = Date.now() - new Date(iso).getTime(); if (Number.isNaN(ms)) return ""; const mins = Math.round(ms / 60_000); if (mins < 60) return `${mins}m`; const hrs = Math.round(mins / 60); if (hrs < 24) return `${hrs}h`; const days = Math.round(hrs / 24); return `${days}d`; } function Sparkline({ breakdown }: { breakdown: ScoreBreakdown | null }) { if (!breakdown) return null; const fields: Array = ["patient", "date", "amount", "provider"]; return ( {fields.map((k) => { const v = breakdown[k] as number; const h = Math.max(3, Math.round(v * 14)); return ( = 0.7 ? "bg-[color:var(--tt-amber)] shadow-[0_0_4px_-1px_var(--tt-amber)]" : v >= 0.5 ? "bg-[color:var(--tt-amber)]/70" : "bg-[color:var(--tt-muted)]" )} style={{ height: `${h}px` }} /> ); })} ); } // --------------------------------------------------------------------------- // SP29: per-AK2 chip (compact evidence row for the rejected-lane drill) // --------------------------------------------------------------------------- function AckCodeChip({ setControlNumber, code, }: { setControlNumber: string; code: string; }) { const isReject = REJECT_CODES.has(code); return ( {setControlNumber} · {code || "—"} ); } type Props = { row: InboxClaimRow | InboxCandidateRow; accent: Accent; onClick: () => void; /** SP29: per-row Resubmit gesture. Only meaningful for rejected * claim rows; the component no-ops on remit rows or other states. */ onResubmitOne?: (claimId: string) => void; }; export function InboxRow({ row, accent, onClick, onResubmitOne }: Props) { const isCandidate = row.kind === "remit"; const isRejected = !isCandidate && (row as InboxClaimRow).state === "rejected"; const id = isCandidate ? (row as InboxCandidateRow).payer_claim_control_number : row.id; const payer = (isCandidate ? (row as InboxCandidateRow).payer_id : (row as InboxClaimRow).payer_id) ?? ""; const charge = fmtMoney(row.charge_amount); const reason = !isCandidate ? (row as InboxClaimRow).rejection_reason : null; const rejectedAt = !isCandidate ? (row as InboxClaimRow).rejected_at : null; const topScore = isCandidate && row.candidates.length > 0 ? row.candidates[0].score : null; const filled = isCandidate && row.candidates.length > 0 ? row.candidates[0].breakdown : null; const claimAcks = !isCandidate ? (row as InboxClaimRow).claim_acks : null; return ( {id}
{reason ?? (topScore != null ? `score ${topScore}` : "")} {/* SP29: rejected-lane AK2 chip evidence (newest 3 + overflow). */} {isRejected && claimAcks && claimAcks.items.length > 0 && ( {claimAcks.items.slice(0, 3).map((it) => ( ))} {claimAcks.total > 3 && ( +{claimAcks.total - 3} more )} )} {/* SP29: rejected claim but no linked 999 acks (orphan). */} {isRejected && (!claimAcks || claimAcks.items.length === 0) && ( 999 not linked )}
{fmtAgo(rejectedAt)} {charge} {payer} {/* SP29: per-row Resubmit button (rejected-lane only). */} {isRejected && onResubmitOne ? ( e.stopPropagation()} > ) : ( )} ); }