{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "payables-ap",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Blocking. Duplicate payment is the most common AP loss.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatDate } from \"@/lib/ledger/fiscal-period\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { Button } from \"@/components/ledger/button\";\nimport { DocumentDuplicateIcon, ExclamationTriangleIcon } from \"@/components/ledger/icons\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type BillFingerprint = {\n  id: string;\n  vendor: string;\n  vendorInvoiceNumber: string;\n  invoiceDate: Date;\n  amount: MoneyValue;\n  postedAt?: Date;\n};\n\nexport type DuplicateMatch = {\n  existing: BillFingerprint;\n  /** Primary key: vendor + their invoice number. Secondary: fuzzy. */\n  kind: \"exact-invoice-number\" | \"fuzzy-amount-date\" | \"duplicate-vendor\";\n  detail: string;\n};\n\nexport interface DuplicateBillAlertProps {\n  candidate: BillFingerprint;\n  className?: string;\n  matches: DuplicateMatch[];\n  onCancel?: () => void;\n  onProceedAnyway?: (reason: string) => void;\n}\n\n/**\n * **Duplicate payment is the single most common AP loss.**\n *\n * Runs before save, and it is **blocking** — the suspected duplicate is\n * shown side by side with the new entry. Duplicate *vendors* are detected\n * too, because that is how duplicate payments survive duplicate-invoice\n * checks.\n */\nexport function DuplicateBillAlert({\n  candidate,\n  matches,\n  onProceedAnyway,\n  onCancel,\n  className,\n}: DuplicateBillAlertProps) {\n  const [reason, setReason] = React.useState(\"\");\n  if (matches.length === 0) return null;\n\n  return (\n    <div className={cn(\"rounded-xl border-2 border-negative bg-surface p-6\", className)} role=\"alert\">\n      <p className=\"flex items-center gap-2 text-title-sm font-medium text-negative\">\n        <ExclamationTriangleIcon aria-hidden=\"true\" className=\"size-5 shrink-0\" />\n        This looks like a duplicate. Saving is blocked.\n      </p>\n\n      {matches.map((match) => (\n        <div className=\"mt-4\" key={match.existing.id}>\n          <p className=\"flex items-center gap-1.5 text-caption text-text-secondary\">\n            <DocumentDuplicateIcon aria-hidden=\"true\" className=\"size-3.5\" />\n            {match.kind === \"exact-invoice-number\"\n              ? \"Same vendor and vendor invoice number\"\n              : match.kind === \"fuzzy-amount-date\"\n                ? \"Same vendor and amount, within five days\"\n                : \"Two vendor records look like the same vendor\"}{\" \"}\n            — {match.detail}\n          </p>\n\n          <div className=\"mt-2 grid gap-3 sm:grid-cols-2\">\n            <Side bill={candidate} title=\"New entry\" tone=\"new\" />\n            <Side bill={match.existing} title=\"Already in the ledger\" tone=\"existing\" />\n          </div>\n        </div>\n      ))}\n\n      <label className=\"mt-4 block\">\n        <span className=\"mb-1 block text-label font-medium text-text-secondary\">\n          If this genuinely is not a duplicate, say why\n        </span>\n        <input\n          className=\"h-10 w-full rounded-sm border border-border bg-surface px-3 text-body outline-none focus-visible:border-accent\"\n          onChange={(e) => setReason(e.target.value)}\n          placeholder=\"e.g. vendor reissued the invoice with a new number after a correction\"\n          value={reason}\n        />\n      </label>\n\n      <div className=\"mt-3 flex flex-wrap gap-2\">\n        <Button onClick={onCancel} variant=\"secondary\">\n          Cancel this bill\n        </Button>\n        <Button\n          disabled={reason.trim().length < 10}\n          onClick={() => onProceedAnyway?.(reason)}\n          variant=\"destructive\"\n        >\n          Save anyway — recorded with the reason\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nfunction Side({ title, bill, tone }: { title: string; bill: BillFingerprint; tone: \"new\" | \"existing\" }) {\n  return (\n    <div\n      className={cn(\n        \"rounded-md border p-3\",\n        tone === \"new\" ? \"border-accent bg-accent-subtle\" : \"border-border bg-surface-sunken\",\n      )}\n    >\n      <p className=\"text-caption font-medium text-text-secondary\">{title}</p>\n      <p className=\"mt-1 text-body font-medium\">{bill.vendor}</p>\n      <p className=\"reference-numeric text-caption text-text-secondary\">{bill.vendorInvoiceNumber}</p>\n      <p className=\"numeric text-caption text-text-secondary\">\n        {formatDate(bill.invoiceDate)}\n        {bill.postedAt ? ` · posted ${formatDate(bill.postedAt)}` : \"\"}\n      </p>\n      <Money className=\"mt-1\" size=\"md\" value={bill.amount} />\n    </div>\n  );\n}\n",
      "path": "components/ledger/duplicate-bill-alert.tsx",
      "target": "components/ledger/duplicate-bill-alert.tsx",
      "type": "registry:component"
    }
  ],
  "name": "duplicate-bill-alert",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/button",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/icons",
    "@lopifi/money",
    "@lopifi/money.types"
  ],
  "title": "DuplicateBillAlert",
  "type": "registry:component"
}
