// InvoiceDetail — the invoice page, given a hierarchy. // // It used to render a flat grid of every field the detail view returned: 29 for // Invoice Detail, 21 for Finance Review Detail. Grouped, but with no ranking, so // the reviewer's actual decision inputs — the recommendation, its confidence, // the match result, the amount — sat among twenty peers of equal weight. // // Order here follows the decision the viewer has to make: // // 1. Identity + status who/what/where in the flow // 2. AI recommendation the answer, up front // 3. 3-way match evidence why that answer is defensible // 4. Clarification Q&A the human exchange, if there was one // 5. Source document the PDF being reasoned about // 6. AI review the full reasoning trail, on demand // 7. History how it got here // 8. All fields the original grid, collapsed // // Everything derives from one fetch pass (useInstanceDetail). import { useState } from "react"; import { ChevronDown, ChevronRight, Sparkles, MessageSquare, CheckCircle2, XCircle, PauseCircle, HelpCircle, Loader2, AlertCircle, ListTree, } from "lucide-react"; import DetailViewPanel from "./DetailViewPanel"; import ThreeWayMatchCard from "./ThreeWayMatchCard"; import InvoiceDocumentCard from "./InvoiceDocumentCard"; import AIActivityPanel from "./AIActivityPanel"; import AuditTimeline from "./AuditTimeline"; import { useInstanceDetail } from "../hooks/useInstanceDetail"; import { stateMeta, TONE_PILL, TONE_DOT, fmtAmount, fmtDateTime } from "../lib/workflowMeta"; interface Props { instanceId: string; /** dv/screen uuid used for the collapsed full-field grid. */ viewId: number | string; /** Current state UUID, from the instance. */ stateId?: string | null; } const REC_STYLE: Record = { approve: { tone: "text-emerald-700 bg-emerald-50 ring-emerald-200 dark:text-emerald-300 dark:bg-emerald-950/40 dark:ring-emerald-800/60", icon: , label: "Approve" }, partial_approve: { tone: "text-amber-700 bg-amber-50 ring-amber-200 dark:text-amber-300 dark:bg-amber-950/40 dark:ring-amber-800/60", icon: , label: "Partial approve" }, reject: { tone: "text-red-700 bg-red-50 ring-red-200 dark:text-red-300 dark:bg-red-950/40 dark:ring-red-800/60", icon: , label: "Reject" }, hold: { tone: "text-slate-600 bg-slate-100 ring-slate-200 dark:text-zinc-400 dark:bg-zinc-800 dark:ring-zinc-700", icon: , label: "Hold" }, }; const str = (v: unknown): string | null => { if (v == null) return null; const s = String(v).trim(); return s === "" ? null : s; }; export default function InvoiceDetail({ instanceId, viewId, stateId }: Props) { const d = useInstanceDetail(instanceId); const [showAll, setShowAll] = useState(false); if (d.loading) { return (
Loading invoice…
); } if (d.error) { return (
{d.error}
); } const s = d.submitted; const cur = String(s.field_5 || "INR"); const st = stateMeta(stateId); const recommendation = str(s.recommendation)?.toLowerCase(); const rec = recommendation ? REC_STYLE[recommendation] : undefined; const confidence = s.ai_confidence != null ? Number(s.ai_confidence) : undefined; const question = str(s.clarification_question); const answer = str(s.response_text); return (
{/* ── 1. Identity + status ─────────────────────────────────────────── */}

{str(s.invoice_number) ?? `Invoice #${instanceId}`}

{st.label}

{str(s.vendor_name) ?? "Unknown vendor"} {str(s.vendor_id) ? ` · ${str(s.vendor_id)}` : ""}

{fmtAmount(s.total_amount, cur)}
{s.tax_amount != null && (
incl. tax {fmtAmount(s.tax_amount, cur)}
)}
{str(s.notes) && (

Vendor notes: {str(s.notes)}

)}
{/* ── 2. AI recommendation ─────────────────────────────────────────── */} {rec && (

AI recommendation

{rec.icon}{rec.label} {s.recommended_amount != null && (
Recommended {fmtAmount(s.recommended_amount, cur)}
)} {confidence != null && Number.isFinite(confidence) && ( {Math.round(confidence * 100)}% confidence )}
{str(s.ai_reasoning) && (

{str(s.ai_reasoning)}

)}
)} {/* ── 3. Match evidence ────────────────────────────────────────────── */} {/* ── 4. Clarification exchange ────────────────────────────────────── */} {(question || answer) && (

Clarification {str(s.target_department) && ( · {String(s.target_department).replace(/_/g, " ")} )}

{question && ( )} {answer && ( )} {question && !answer && (
Awaiting a response.
)}
)} {/* ── 5. Source document ───────────────────────────────────────────── */} {/* ── 6. AI reasoning trail ────────────────────────────────────────── */} {/* ── 7. History ───────────────────────────────────────────────────── */} {/* ── 8. Everything else, collapsed ────────────────────────────────── */}
{showAll && (
)}
); } function Meta({ label, value }: { label: string; value?: string | null }) { if (!value) return null; return (
{label}
{value}
); } function Bubble({ who, text, context, ai }: { who: string; text: string; context?: string | null; ai?: boolean }) { return (
{who}

{text}

{context && (

{context}

)}
); }