{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "receivables-ar",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "With the revenue-recognition preview per line.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { Account } from \"@/lib/ledger/accounting.types\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatDate } from \"@/lib/ledger/fiscal-period\";\nimport { formatMoneyInput, parseMoneyInput } from \"@/lib/ledger/format-money\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { AccountPicker } from \"@/components/ledger/account-picker\";\nimport { Button } from \"@/components/ledger/button\";\nimport { InformationCircleIcon, PlusIcon, TrashIcon } from \"@/components/ledger/icons\";\nimport { Money } from \"@/components/ledger/money\";\nimport { Rule } from \"@/components/ledger/rule\";\n\nexport type InvoiceDraftLine = {\n  key: string;\n  description: string;\n  account: Account | null;\n  amount: number | null;\n  /** Whether this line credits revenue now or deferred revenue on a schedule. */\n  recognition: \"immediate\" | \"ratable\";\n  ratableMonths?: number;\n};\n\nexport type TaxLine = {\n  jurisdiction: string;\n  rate: number;\n  amount: MoneyValue;\n};\n\nexport interface InvoiceFormProps {\n  accounts: Account[];\n  className?: string;\n  currency: string;\n  /** System-assigned and gapless. The user cannot pick it. */\n  invoiceNumber: string;\n  issueDate: Date;\n  taxes: TaxLine[];\n  /** \"2/10 net 30\" — rendered in full, with both amounts computed. */\n  terms: { discountPercent: number; discountDays: number; netDays: number };\n  totals: { subtotal: MoneyValue; tax: MoneyValue; total: MoneyValue; discounted: MoneyValue };\n}\n\n/**\n * Shows the revenue-recognition preview per line **before** posting — whether\n * each line credits revenue or deferred revenue, and on what schedule.\n */\nexport function InvoiceForm({\n  accounts,\n  currency,\n  invoiceNumber,\n  issueDate,\n  terms,\n  taxes,\n  totals,\n  className,\n}: InvoiceFormProps) {\n  const [lines, setLines] = React.useState<InvoiceDraftLine[]>([\n    {\n      account: null,\n      amount: 1_200_000,\n      description: \"Platform subscription, annual\",\n      key: \"1\",\n      ratableMonths: 12,\n      recognition: \"ratable\",\n    },\n    {\n      account: null,\n      amount: 350_000,\n      description: \"Implementation services\",\n      key: \"2\",\n      recognition: \"immediate\",\n    },\n  ]);\n\n  const discountDeadline = new Date(\n    issueDate.getFullYear(),\n    issueDate.getMonth(),\n    issueDate.getDate() + terms.discountDays,\n  );\n  const dueDate = new Date(\n    issueDate.getFullYear(),\n    issueDate.getMonth(),\n    issueDate.getDate() + terms.netDays,\n  );\n\n  const update = (key: string, patch: Partial<InvoiceDraftLine>) =>\n    setLines((prev) => prev.map((l) => (l.key === key ? { ...l, ...patch } : l)));\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      <div className=\"flex flex-wrap items-center gap-4 rounded-md bg-surface-sunken px-4 py-3\">\n        <span className=\"text-caption text-text-secondary\">\n          Invoice number\n          <span className=\"reference-numeric ml-2 text-body font-medium text-text-primary\">\n            {invoiceNumber}\n          </span>\n        </span>\n        <span className=\"flex items-center gap-1 text-caption text-text-tertiary\">\n          <InformationCircleIcon aria-hidden=\"true\" className=\"size-3.5\" />\n          System-assigned and gapless — many VAT regimes require a sequence.\n        </span>\n      </div>\n\n      <div className=\"overflow-x-auto rounded-xl border border-border bg-surface\">\n        <table className=\"w-full min-w-[860px]\">\n          <caption className=\"sr-only\">Invoice lines</caption>\n          <thead>\n            <tr className=\"border-b border-border-subtle\">\n              {[\"Description\", \"Account\", \"Recognition\", \"Amount\", \"\"].map((h, i) => (\n                <th\n                  className={cn(\n                    \"px-3 py-3 text-label font-medium text-text-secondary\",\n                    i === 3 ? \"text-right\" : \"text-left\",\n                    i === 0 && \"pl-6\",\n                  )}\n                  key={h || i}\n                  scope=\"col\"\n                >\n                  {h || <span className=\"sr-only\">Remove</span>}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody>\n            {lines.map((l) => (\n              <tr className=\"border-b border-border-subtle align-top\" key={l.key}>\n                <td className=\"py-3 pl-6 pr-3\">\n                  <input\n                    aria-label=\"Line description\"\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) => update(l.key, { description: e.target.value })}\n                    value={l.description}\n                  />\n                </td>\n                <td className=\"w-[240px] px-3 py-3\">\n                  <AccountPicker\n                    accounts={accounts}\n                    onValueChange={(account) => update(l.key, { account })}\n                    value={l.account?.id ?? null}\n                  />\n                </td>\n                <td className=\"px-3 py-3\">\n                  <select\n                    aria-label=\"Revenue recognition\"\n                    className=\"h-10 rounded-sm border border-border bg-surface px-2 text-body outline-none focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent-subtle\"\n                    onChange={(e) =>\n                      update(l.key, {\n                        recognition: e.target.value as InvoiceDraftLine[\"recognition\"],\n                      })\n                    }\n                    value={l.recognition}\n                  >\n                    <option value=\"immediate\">Recognize now</option>\n                    <option value=\"ratable\">Ratable</option>\n                  </select>\n                  {/* The recognition preview, before posting. */}\n                  <p className=\"mt-1 max-w-[220px] text-caption text-text-secondary\">\n                    {l.recognition === \"immediate\"\n                      ? \"Credits revenue on issue.\"\n                      : `Credits deferred revenue, recognized over ${l.ratableMonths ?? 12} months from ${formatDate(issueDate)}.`}\n                  </p>\n                </td>\n                <AmountCell\n                  currency={currency}\n                  onChange={(v) => update(l.key, { amount: v })}\n                  value={l.amount}\n                />\n                <td className=\"px-3 py-3\">\n                  <button\n                    aria-label=\"Remove line\"\n                    className=\"press grid size-8 place-items-center rounded-sm text-text-tertiary hover:bg-surface-hover\"\n                    onClick={() => setLines((prev) => prev.filter((x) => x.key !== l.key))}\n                    type=\"button\"\n                  >\n                    <TrashIcon aria-hidden=\"true\" className=\"size-4\" />\n                  </button>\n                </td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n\n      <Button\n        onClick={() =>\n          setLines((prev) => [\n            ...prev,\n            {\n              account: null,\n              amount: null,\n              description: \"\",\n              key: Math.random().toString(36).slice(2),\n              recognition: \"immediate\",\n            },\n          ])\n        }\n        size=\"sm\"\n        variant=\"secondary\"\n      >\n        <PlusIcon aria-hidden=\"true\" className=\"size-3.5\" />\n        Add line\n      </Button>\n\n      <div className=\"grid gap-4 lg:grid-cols-2\">\n        <section className=\"rounded-xl border border-border bg-surface p-6\">\n          <h3 className=\"text-title-sm font-medium\">Terms</h3>\n          <p className=\"mt-2 text-body\">\n            <span className=\"numeric\">{terms.discountPercent}%</span> discount if paid within{\" \"}\n            <span className=\"numeric\">{terms.discountDays}</span> days, otherwise due in{\" \"}\n            <span className=\"numeric\">{terms.netDays}</span> days.\n          </p>\n          <dl className=\"mt-3 space-y-1\">\n            <Row label={`Pay by ${formatDate(discountDeadline)}`} value={totals.discounted} />\n            <Row label={`Pay by ${formatDate(dueDate)}`} value={totals.total} />\n          </dl>\n        </section>\n\n        <section className=\"rounded-xl border border-border bg-surface p-6\">\n          <h3 className=\"text-title-sm font-medium\">Totals</h3>\n          <table className=\"mt-2 w-full\">\n            <tbody>\n              <TotalRow label=\"Subtotal\" value={totals.subtotal} />\n              {taxes.map((t) => (\n                <TotalRow key={t.jurisdiction} label={`${t.jurisdiction} @ ${t.rate}%`} value={t.amount} />\n              ))}\n              <tr>\n                <td />\n                <td>\n                  <Rule variant=\"subtotal\" />\n                </td>\n              </tr>\n              <TotalRow emphasis label=\"Total\" value={totals.total} />\n              <tr>\n                <td />\n                <td>\n                  <Rule variant=\"total\" />\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </section>\n      </div>\n    </div>\n  );\n}\n\nfunction AmountCell({\n  value,\n  currency,\n  onChange,\n}: {\n  value: number | null;\n  currency: string;\n  onChange: (v: number | null) => void;\n}) {\n  const [draft, setDraft] = React.useState(\"\");\n  const [focused, setFocused] = React.useState(false);\n  React.useEffect(() => {\n    if (focused) return;\n    setDraft(value == null ? \"\" : formatMoneyInput(value, currency));\n  }, [value, currency, focused]);\n  return (\n    <td className=\"px-3 py-3\">\n      <input\n        aria-label=\"Line amount\"\n        className=\"numeric h-10 w-32 rounded-sm border border-border bg-surface px-2 text-right text-body outline-none focus-visible:border-accent\"\n        inputMode=\"decimal\"\n        onBlur={() => {\n          setFocused(false);\n          onChange(parseMoneyInput(draft, currency));\n        }}\n        onChange={(e) => {\n          setDraft(e.target.value);\n          onChange(parseMoneyInput(e.target.value, currency));\n        }}\n        onFocus={() => setFocused(true)}\n        type=\"text\"\n        value={draft}\n      />\n    </td>\n  );\n}\n\nfunction Row({ label, value }: { label: string; value: MoneyValue }) {\n  return (\n    <div className=\"flex items-baseline justify-between gap-4\">\n      <dt className=\"text-body text-text-secondary\">{label}</dt>\n      <dd>\n        <Money accounting value={value} />\n      </dd>\n    </div>\n  );\n}\n\nfunction TotalRow({ label, value, emphasis }: { label: string; value: MoneyValue; emphasis?: boolean }) {\n  return (\n    <tr>\n      <th\n        className={cn(\n          \"py-1 pr-6 text-left text-body font-normal\",\n          emphasis ? \"font-medium text-text-primary\" : \"text-text-secondary\",\n        )}\n        scope=\"row\"\n      >\n        {label}\n      </th>\n      <td className=\"py-1 text-right\">\n        <Money accounting size={emphasis ? \"md\" : \"sm\"} value={value} />\n      </td>\n    </tr>\n  );\n}\n",
      "path": "components/ledger/invoice-form.tsx",
      "target": "components/ledger/invoice-form.tsx",
      "type": "registry:component"
    }
  ],
  "name": "invoice-form",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/account-picker",
    "@lopifi/accounting.types",
    "@lopifi/button",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/format-money",
    "@lopifi/icons",
    "@lopifi/money",
    "@lopifi/money.types",
    "@lopifi/rule"
  ],
  "title": "InvoiceForm",
  "type": "registry:component"
}
