// ThreeWayMatchCard — makes the AI's recommendation auditable. // // The employee reconciles invoice vs purchase order vs goods receipt vs payment // history through four agentic tools, then writes its findings as prose into two // fields. Both were previously invisible in the UI: `match_summary` was buried // among ~29 flat fields, and `analysis_so_far` was explicitly dropped by // DetailViewPanel's SKIP_KEYS because it looked like raw JSON. // // They're actually semi-structured, straight from the deployed data: // // match_summary "Price: MATCH (₹550/unit per contract amendment …). // Quantity: MATCH (200 units invoiced = 200 units received // per GR-3001). Goods Receipt: VALID (…). Duplicate Check: // CLEAN (₹0 prior payments). Contract: ACTIVE with …" // // analysis_so_far "PO-3001: 200 units @ ₹500/unit = ₹100,000. GR-3001: 200 // units received, QC passed. Contract CNT-300: active, // ₹500/unit, 2% tolerance. Invoice: 200 units @ ₹550/unit = // ₹110,000. Payment history: clean (₹0 paid). Issue: 10% // price variance exceeds 2% tolerance." // // So each parses into label → value pairs, with match_summary additionally // carrying a verdict keyword the reviewer can scan in one pass. import { CheckCircle2, XCircle, AlertTriangle, MinusCircle, Scale } from "lucide-react"; import { fmtAmount } from "../lib/workflowMeta"; import { parseMatchSummary, parseEvidence, overallTone, type Tone } from "../lib/matchSummary"; const TONE_STYLE: Record = { good: { chip: "text-emerald-700 bg-emerald-50 ring-1 ring-emerald-200 dark:text-emerald-300 dark:bg-emerald-950/40 dark:ring-emerald-800/60", icon: }, bad: { chip: "text-red-700 bg-red-50 ring-1 ring-red-200 dark:text-red-300 dark:bg-red-950/40 dark:ring-red-800/60", icon: }, warn: { chip: "text-amber-700 bg-amber-50 ring-1 ring-amber-200 dark:text-amber-300 dark:bg-amber-950/40 dark:ring-amber-800/60", icon: }, neutral: { chip: "text-slate-600 bg-slate-100 ring-1 ring-slate-200 dark:text-zinc-400 dark:bg-zinc-800 dark:ring-zinc-700", icon: }, }; interface Props { matchSummary?: string; analysis?: string; /** The invoice's own submitted figures, for the reference row. */ invoice?: { invoice_number?: unknown; po_number?: unknown; total_amount?: unknown; tax_amount?: unknown; currency?: unknown; }; } export default function ThreeWayMatchCard({ matchSummary, analysis, invoice }: Props) { const verdicts = parseMatchSummary(matchSummary); const evidence = parseEvidence(analysis); if (verdicts.length === 0 && evidence.length === 0) { return (

3-Way Match

The AI hasn't reconciled this invoice against its PO and goods receipt yet.

); } const worst = overallTone(verdicts); const headline = worst === "bad" ? "Discrepancy found" : worst === "warn" ? "Needs attention" : "All checks passed"; const cur = String(invoice?.currency || "INR"); return (

3-Way Match

{verdicts.length > 0 && ( {TONE_STYLE[worst].icon}{headline} )}
{/* Reference figures from the invoice itself */} {invoice && (invoice.invoice_number || invoice.po_number || invoice.total_amount != null) && (
{invoice.invoice_number ? ( ) : null} {invoice.po_number ? : null} {invoice.total_amount != null ? ( ) : null} {invoice.tax_amount != null ? : null}
)} {/* Per-dimension verdicts */} {verdicts.length > 0 && (
    {verdicts.map((v, i) => (
  • {TONE_STYLE[v.tone].icon}{v.verdict || "—"}
    {v.label}
    {v.detail && (
    {v.detail}
    )}
  • ))}
)} {/* Supporting evidence the AI gathered from PO / GR / contract / payments */} {evidence.length > 0 && (
0 ? "mt-3 pt-3 border-t border-slate-100 dark:border-zinc-800" : ""}>
Evidence gathered
{evidence.map((e, i) => (
{e.label}
{e.value}
))}
)}
); } function Ref({ label, value, strong }: { label: string; value: string; strong?: boolean }) { return (
{label}
{value}
); }