{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "payables-ap",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Segregation of duties enforced. Approvals bind to a version.",
  "files": [
    {
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatTimestamp } from \"@/lib/ledger/fiscal-period\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { CheckIcon, ExclamationTriangleIcon, ShieldExclamationIcon, UserIcon } from \"@/components/ledger/icons\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type ApprovalStep = {\n  id: string;\n  role: string;\n  approver: string;\n  threshold?: MoneyValue;\n  state: \"approved\" | \"pending\" | \"blocked\" | \"not-required\";\n  at?: Date;\n  /** Approvals bind to a version. */\n  approvedVersion?: number;\n};\n\nexport interface ApprovalChainProps {\n  amount: MoneyValue;\n  className?: string;\n  currentVersion: number;\n  preparer: string;\n  steps: ApprovalStep[];\n  /** The current viewer, so segregation of duties can be enforced. */\n  viewer: { name: string; role: string };\n}\n\n/**\n * Who, in what order, what's done, what's blocking — **visible before\n * submission**. Threshold escalation shows while the amount is being typed,\n * not after submitting.\n *\n * Segregation of duties is enforced, not advisory: for the preparer, the\n * approve action is **absent**, not disabled.\n *\n * Approvals bind to a version. Editing an approved bill voids the approvals\n * and re-requests them with the change shown — this closes the classic\n * post-approval-edit fraud path.\n */\nexport function ApprovalChain({\n  amount,\n  steps,\n  viewer,\n  preparer,\n  currentVersion,\n  className,\n}: ApprovalChainProps) {\n  const viewerIsPreparer = viewer.name === preparer;\n  const staleApprovals = steps.filter(\n    (s) => s.state === \"approved\" && s.approvedVersion !== undefined && s.approvedVersion < currentVersion,\n  );\n  const blocking = steps.find((s) => s.state === \"pending\" || s.state === \"blocked\");\n\n  return (\n    <div className={cn(\"rounded-xl border border-border bg-surface p-6\", className)}>\n      <div className=\"flex flex-wrap items-baseline justify-between gap-3\">\n        <h3 className=\"text-title-sm font-medium\">Approval chain</h3>\n        <span className=\"text-caption text-text-secondary\">\n          For <Money size=\"xs\" tone=\"inherit\" value={amount} /> · version{\" \"}\n          <span className=\"numeric\">{currentVersion}</span>\n        </span>\n      </div>\n\n      {staleApprovals.length > 0 ? (\n        <p\n          className=\"mt-3 flex items-start gap-2 rounded-sm bg-negative-subtle px-3 py-2 text-body text-negative\"\n          role=\"alert\"\n        >\n          <ShieldExclamationIcon aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n          This bill changed after approval. {staleApprovals.length} approval\n          {staleApprovals.length === 1 ? \" was\" : \"s were\"} voided and must be re-requested, with the change\n          shown to the re-approver.\n        </p>\n      ) : null}\n\n      <ol className=\"mt-4 space-y-3\">\n        {steps.map((step, i) => {\n          const last = i === steps.length - 1;\n          const stale = staleApprovals.includes(step);\n          const canAct = step.state === \"pending\" && viewer.role === step.role && !viewerIsPreparer;\n\n          return (\n            <li className=\"flex gap-3\" key={step.id}>\n              <div className=\"flex flex-col items-center\">\n                <span\n                  aria-hidden=\"true\"\n                  className={cn(\n                    \"grid size-6 shrink-0 place-items-center rounded-full border\",\n                    step.state === \"approved\" && !stale && \"border-positive bg-positive-subtle text-positive\",\n                    stale && \"border-negative bg-negative-subtle text-negative\",\n                    step.state === \"pending\" && \"border-accent text-accent\",\n                    step.state === \"blocked\" && \"border-border-strong text-text-tertiary\",\n                    step.state === \"not-required\" && \"border-border text-text-disabled\",\n                  )}\n                >\n                  {step.state === \"approved\" && !stale ? (\n                    <CheckIcon className=\"size-3\" />\n                  ) : stale ? (\n                    <ExclamationTriangleIcon className=\"size-3\" />\n                  ) : (\n                    <span aria-hidden=\"true\" className=\"size-2 rounded-full bg-current\" />\n                  )}\n                </span>\n                {!last ? (\n                  <span\n                    aria-hidden=\"true\"\n                    className={cn(\n                      \"w-px flex-1\",\n                      step.state === \"approved\" && !stale ? \"bg-positive\" : \"bg-border\",\n                    )}\n                  />\n                ) : null}\n              </div>\n\n              <div className={cn(\"flex-1\", last ? \"pb-0\" : \"pb-3\")}>\n                <p className=\"flex flex-wrap items-baseline gap-2 text-body\">\n                  <span className=\"font-medium\">{step.role}</span>\n                  <span className=\"text-text-secondary\">{step.approver}</span>\n                  {step.threshold ? (\n                    <span className=\"text-caption text-text-tertiary\">\n                      required above <Money size=\"xs\" tone=\"inherit\" value={step.threshold} />\n                    </span>\n                  ) : null}\n                </p>\n                {step.at ? (\n                  <p className=\"numeric text-caption text-text-tertiary\">\n                    {stale ? \"Voided — approved \" : \"Approved \"}\n                    {formatTimestamp(step.at)}\n                    {step.approvedVersion !== undefined ? ` on version ${step.approvedVersion}` : \"\"}\n                  </p>\n                ) : (\n                  <p className=\"text-caption text-text-tertiary\">\n                    {step.state === \"not-required\"\n                      ? \"Not required at this amount\"\n                      : step.state === \"blocked\"\n                        ? \"Blocked until the previous approval completes\"\n                        : \"Awaiting approval\"}\n                  </p>\n                )}\n\n                {/* Segregation of duties: absent for the preparer, not disabled. */}\n                {canAct ? (\n                  <div className=\"mt-2 flex gap-2\">\n                    <button\n                      className=\"press h-8 rounded-sm bg-accent px-3 text-label font-medium text-accent-fg\"\n                      type=\"button\"\n                    >\n                      Approve\n                    </button>\n                    <button\n                      className=\"press h-8 rounded-sm border border-border px-3 text-label font-medium\"\n                      type=\"button\"\n                    >\n                      Reject\n                    </button>\n                  </div>\n                ) : null}\n              </div>\n            </li>\n          );\n        })}\n      </ol>\n\n      {viewerIsPreparer ? (\n        <p className=\"mt-4 flex items-start gap-2 rounded-sm bg-surface-sunken px-3 py-2 text-caption text-text-secondary\">\n          <UserIcon aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0\" />\n          You prepared this bill, so you cannot approve it. That separation is structural, not a setting.\n        </p>\n      ) : null}\n\n      {blocking ? (\n        <p className=\"mt-2 text-caption text-text-secondary\">\n          Currently blocking: {blocking.role} ({blocking.approver}).\n        </p>\n      ) : null}\n    </div>\n  );\n}\n",
      "path": "components/ledger/approval-chain.tsx",
      "target": "components/ledger/approval-chain.tsx",
      "type": "registry:component"
    }
  ],
  "meta": {
    "license": "MIT"
  },
  "name": "approval-chain",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/icons",
    "@lopifi/money",
    "@lopifi/money.types"
  ],
  "title": "ApprovalChain",
  "type": "registry:component"
}
