@dainamite/billing 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.md +14 -0
- package/dist/modules/billing/api/export/account/[id]/route.js +5 -5
- package/dist/modules/billing/api/export/account/[id]/route.js.map +1 -1
- package/dist/modules/billing/api/invoices/add-line/route.js +1 -1
- package/dist/modules/billing/api/invoices/add-line/route.js.map +1 -1
- package/dist/modules/billing/api/invoices/edit-line/route.js +1 -1
- package/dist/modules/billing/api/invoices/edit-line/route.js.map +1 -1
- package/dist/modules/billing/api/invoices/remove-line/route.js +1 -1
- package/dist/modules/billing/api/invoices/remove-line/route.js.map +1 -1
- package/dist/modules/billing/api/run-outcomes/route.js +2 -2
- package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
- package/dist/modules/billing/api/runs/retry-failed/route.js +2 -2
- package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
- package/dist/modules/billing/api/runs/route.js +2 -2
- package/dist/modules/billing/api/runs/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/accounts/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/invoices/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/items/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/runs/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/statements/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/topups/page.js.map +1 -1
- package/dist/modules/billing/backend/billing/transactions/page.js.map +1 -1
- package/dist/modules/billing/ce.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +4 -4
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/runs.js +3 -3
- package/dist/modules/billing/commands/runs.js.map +2 -2
- package/dist/modules/billing/components/PrepaidPanel.js.map +1 -1
- package/dist/modules/billing/data/entities.js +56 -56
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/events.js +20 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/lib/billRunEngine.js +4 -4
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/billRunLabel.js.map +1 -1
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +1 -1
- package/dist/modules/billing/lib/tenantLock.js.map +1 -1
- package/dist/modules/billing/workers/reap-stale-bill-runs.js +2 -2
- package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +2 -2
- package/generated/entities/billing_account/index.ts +23 -0
- package/generated/entities/billing_account_balance/index.ts +13 -0
- package/generated/entities/billing_account_transaction/index.ts +21 -0
- package/generated/entities/billing_account_usage/index.ts +18 -0
- package/generated/entities/billing_draft_invoice_edit/index.ts +14 -0
- package/generated/entities/billing_item/index.ts +22 -0
- package/generated/entities/billing_run/index.ts +21 -0
- package/generated/entities/billing_run_outcome/index.ts +17 -0
- package/generated/entities/billing_statement/index.ts +21 -0
- package/generated/entities/billing_topup/index.ts +20 -0
- package/generated/entities.ids.generated.ts +28 -0
- package/package.json +11 -10
- package/src/modules/billing/__tests__/commands/invoices.test.ts +8 -8
- package/src/modules/billing/__tests__/commands/topups.test.ts +1 -1
- package/src/modules/billing/__tests__/commands/transactions.test.ts +25 -2
- package/src/modules/billing/__tests__/entityRegistry.test.ts +319 -0
- package/src/modules/billing/__tests__/events.test.ts +142 -0
- package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +3 -3
- package/src/modules/billing/api/export/account/[id]/route.ts +7 -7
- package/src/modules/billing/api/invoices/add-line/route.ts +1 -1
- package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
- package/src/modules/billing/api/invoices/remove-line/route.ts +1 -1
- package/src/modules/billing/api/run-outcomes/route.ts +2 -2
- package/src/modules/billing/api/runs/retry-failed/route.ts +2 -2
- package/src/modules/billing/api/runs/route.ts +2 -2
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +1 -1
- package/src/modules/billing/backend/billing/accounts/page.tsx +1 -1
- package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +1 -1
- package/src/modules/billing/backend/billing/invoices/page.tsx +1 -1
- package/src/modules/billing/backend/billing/items/page.tsx +1 -1
- package/src/modules/billing/backend/billing/runs/[id]/page.tsx +1 -1
- package/src/modules/billing/backend/billing/runs/page.tsx +1 -1
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +1 -1
- package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
- package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
- package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
- package/src/modules/billing/ce.ts +8 -7
- package/src/modules/billing/commands/invoices.ts +9 -9
- package/src/modules/billing/commands/runs.ts +3 -3
- package/src/modules/billing/components/PrepaidPanel.tsx +1 -1
- package/src/modules/billing/data/entities.ts +31 -10
- package/src/modules/billing/data/entityIds.ts +16 -11
- package/src/modules/billing/events.ts +26 -0
- package/src/modules/billing/lib/billRunEngine.ts +12 -12
- package/src/modules/billing/lib/billRunLabel.ts +1 -1
- package/src/modules/billing/lib/prepaidPeriodClose.ts +3 -3
- package/src/modules/billing/lib/tenantLock.ts +1 -1
- package/src/modules/billing/workers/reap-stale-bill-runs.ts +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/billing/backend/billing/accounts/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Pencil, Plus } from 'lucide-react'\nimport { useParams, useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n apiCallOrThrow,\n readApiResultOrThrow,\n} from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { AccountForm, type AccountFormValues } from '../../../../components/AccountForm'\nimport { DetailCard, DetailField } from '../../../../components/DetailFields'\nimport { PrepaidPanel } from '../../../../components/PrepaidPanel'\n\n/**\n * `/backend/billing/accounts/[id]` \u2014 Billing Account detail.\n *\n * Opens read-only (field grid + the account's items). The `Edit`\n * button swaps the grid for the `AccountForm`; Save / Cancel return\n * to the read-only view. Loads via `GET /api/billing/accounts?id=<id>`,\n * edits through `PUT`, soft-delete through `DELETE`.\n *\n * `customerId` + `currencyCode` are immutable on update \u2014 the form\n * disables both in `mode='edit'` and the schema rejects them per\n * `billingAccountUpdateSchema`.\n */\n\n// API list rows are snake_case (see `api/accounts/route.ts` `fields`).\ntype AccountRow = {\n id: string\n customer_id: string\n name: string\n currency_code: string\n bill_cycle: 'weekly' | 'monthly' | 'quarterly' | 'annually'\n bill_cycle_anchor: string\n invoice_email: string\n invoice_language: string\n tax_id: string | null\n invoice_address: {\n line1?: string\n line2?: string\n city?: string\n postal_code?: string\n country?: string\n } | null\n next_bill_date: string\n last_bill_date: string | null\n billing_mode: 'postpaid' | 'prepaid'\n credit_limit: string\n is_active: boolean\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = { items: AccountRow[] }\n\n// Minimal item shape for the embedded \"Items\" table.\ntype AccountItemRow = {\n id: string\n description: string\n type: 'one_time' | 'recurring' | 'usage'\n bill_start_date: string\n is_active: boolean\n}\n\n// Minimal usage shape for the embedded \"Usage\" table. Mirrors the\n// snake_case `fields` projection in `api/usage/route.ts`. Usage is\n// append-only \u2014 `rated_in_bill_run_id` is null until a Bill Run rates it.\ntype AccountUsageRow = {\n id: string\n uom_code: string\n quantity: string\n period_start: string\n period_end: string\n line_description: string | null\n rated_in_bill_run_id: string | null\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction toFormValues(row: AccountRow): AccountFormValues {\n return {\n customerId: row.customer_id,\n name: row.name,\n currencyCode: row.currency_code,\n billCycle: row.bill_cycle,\n billCycleAnchor: row.bill_cycle_anchor,\n invoiceEmail: row.invoice_email,\n invoiceLanguage: row.invoice_language,\n taxId: row.tax_id ?? '',\n invoiceAddress: {\n line1: row.invoice_address?.line1 ?? '',\n line2: row.invoice_address?.line2 ?? '',\n city: row.invoice_address?.city ?? '',\n postal_code: row.invoice_address?.postal_code ?? '',\n country: row.invoice_address?.country ?? '',\n },\n nextBillDate: row.next_bill_date?.slice(0, 10) ?? '',\n isActive: row.is_active,\n billingMode: row.billing_mode,\n creditLimit: row.credit_limit ?? '0',\n // The threshold lives on the balance row, not the account row \u2014 blank\n // means \"leave unchanged\" on save.\n lowBalanceThreshold: '',\n }\n}\n\n// \u2500\u2500\u2500 Read-only view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction AccountView({ row }: { row: AccountRow }) {\n const t = useT()\n const addr = row.invoice_address\n const addressText = addr\n ? [\n addr.line1,\n addr.line2,\n [addr.postal_code, addr.city].filter(Boolean).join(' '),\n addr.country,\n ]\n .map((part) => (part ?? '').trim())\n .filter(Boolean)\n .join(', ')\n : ''\n return (\n <DetailCard>\n <DetailField label={t('billing.accounts.form.customer_id', 'Customer ID')}>\n <span className=\"font-mono text-xs\">{row.customer_id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.currency', 'Currency')}>\n {row.currency_code}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.mode', 'Mode')}>\n {row.billing_mode === 'prepaid' ? (\n <Tag variant=\"brand\">{t('billing.mode.prepaid', 'Prepaid')}</Tag>\n ) : (\n <Tag variant=\"neutral\">{t('billing.mode.postpaid', 'Postpaid')}</Tag>\n )}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.credit_limit', 'Credit limit')}>\n <span className=\"font-mono tabular-nums\">{row.credit_limit}</span>\n </DetailField>\n <DetailField label={t('billing.common.id', 'ID')}>\n <span className=\"font-mono text-xs\">{row.id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.cycle', 'Cycle')}>\n {row.bill_cycle}\n <span className=\"text-muted-foreground\"> \u00B7 {row.bill_cycle_anchor}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.next_bill', 'Next bill')}>\n {formatDate(row.next_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.last_bill', 'Last bill')}>\n {formatDate(row.last_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_email', 'Invoice email')}>\n {row.invoice_email}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_language', 'Invoice language')}>\n {row.invoice_language}\n </DetailField>\n <DetailField label={t('billing.accounts.form.tax_id', 'Tax ID')}>\n {row.tax_id || '\u2014'}\n </DetailField>\n <DetailField\n label={t('billing.accounts.form.address.title', 'Invoice address')}\n fullWidth\n >\n {addressText || '\u2014'}\n </DetailField>\n </DetailCard>\n )\n}\n\n// \u2500\u2500\u2500 Embedded items table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction AccountItemsSection({ accountId }: { accountId: string }) {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<AccountItemRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountItemRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/items?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=createdAt&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountItemRow>[]>(\n () => [\n {\n accessorKey: 'description',\n header: t('billing.items.columns.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {row.original.description}\n </span>\n ),\n },\n {\n accessorKey: 'type',\n header: t('billing.items.columns.type', 'Type'),\n cell: ({ row }) => <Tag variant=\"info\">{row.original.type}</Tag>,\n },\n {\n accessorKey: 'bill_start_date',\n header: t('billing.items.columns.start', 'Start'),\n cell: ({ row }) => formatDate(row.original.bill_start_date),\n },\n {\n accessorKey: 'is_active',\n header: t('billing.items.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.items.title', 'Items')}\n columns={columns}\n data={rows}\n isLoading={loading}\n onRowClick={(row) => router.push(`/backend/billing/items/${row.id}`)}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.items.empty', 'No items for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Embedded usage table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction AccountUsageSection({ accountId }: { accountId: string }) {\n const t = useT()\n const [rows, setRows] = React.useState<AccountUsageRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountUsageRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/usage?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=periodStart&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountUsageRow>[]>(\n () => [\n {\n accessorKey: 'uom_code',\n header: t('billing.usage.columns.uom', 'Unit'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">{row.original.uom_code}</span>\n ),\n },\n {\n accessorKey: 'quantity',\n header: t('billing.usage.columns.quantity', 'Quantity'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.quantity}</span>\n ),\n },\n {\n accessorKey: 'period_start',\n header: t('billing.usage.columns.period', 'Period'),\n cell: ({ row }) =>\n `${formatDate(row.original.period_start)} \u2013 ${formatDate(row.original.period_end)}`,\n },\n {\n accessorKey: 'line_description',\n header: t('billing.usage.columns.description', 'Description'),\n cell: ({ row }) => row.original.line_description || '\u2014',\n },\n {\n accessorKey: 'rated_in_bill_run_id',\n header: t('billing.usage.columns.rated', 'Rated'),\n cell: ({ row }) =>\n row.original.rated_in_bill_run_id ? (\n // Rated records carry the Bill Run that priced them \u2014 link to it.\n // Unrated records have no destination, so they stay non-interactive\n // (no misleading row-level click affordance).\n <Link\n href={`/backend/billing/runs/${row.original.rated_in_bill_run_id}`}\n className=\"rounded-sm hover:underline\"\n >\n <Tag variant=\"success\">{t('billing.usage.rated.yes', 'Rated')}</Tag>\n </Link>\n ) : (\n <Tag variant=\"default\">{t('billing.usage.rated.no', 'Unrated')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.usage.title', 'Usage')}\n columns={columns}\n data={rows}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.usage.empty', 'No usage records for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\nexport default function BillingAccountDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n const router = useRouter()\n // OM serves backend pages through a catch-all route \u2014 the dynamic\n // `[id]` segment arrives as a page prop; `useParams()` is the fallback.\n const urlParams = useParams<{ id: string }>()\n const accountId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [row, setRow] = React.useState<AccountRow | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [editing, setEditing] = React.useState(false)\n const [saving, setSaving] = React.useState(false)\n const [deleting, setDeleting] = React.useState(false)\n\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const load = React.useCallback(async () => {\n if (!accountId) return\n setLoading(true)\n setError(null)\n try {\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/accounts?id=${accountId}&pageSize=1`,\n )\n const found = result.items?.[0]\n if (!found) {\n setError(t('billing.accounts.detail.error.not_found', 'Account not found'))\n return\n }\n setRow(found)\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setError(message || t('billing.accounts.detail.error.load', 'Failed to load account'))\n } finally {\n setLoading(false)\n }\n }, [accountId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const handleSubmit = React.useCallback(\n async (values: AccountFormValues) => {\n if (!row) return\n setSaving(true)\n try {\n const payload: Record<string, unknown> = {\n id: row.id,\n name: values.name.trim(),\n billCycle: values.billCycle,\n billCycleAnchor: values.billCycleAnchor.trim(),\n invoiceEmail: values.invoiceEmail.trim(),\n invoiceLanguage: values.invoiceLanguage.trim().toLowerCase(),\n invoiceAddress: {\n line1: values.invoiceAddress.line1.trim(),\n ...(values.invoiceAddress.line2.trim()\n ? { line2: values.invoiceAddress.line2.trim() }\n : {}),\n city: values.invoiceAddress.city.trim(),\n postal_code: values.invoiceAddress.postal_code.trim(),\n country: values.invoiceAddress.country.trim().toUpperCase(),\n },\n nextBillDate: values.nextBillDate,\n isActive: values.isActive,\n }\n payload.taxId = values.taxId.trim() === '' ? null : values.taxId.trim()\n // billingMode is immutable on update; creditLimit + threshold are editable.\n const creditLimit = Number.parseFloat(values.creditLimit)\n if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit\n if (row.billing_mode === 'prepaid' && values.lowBalanceThreshold.trim()) {\n const threshold = Number.parseFloat(values.lowBalanceThreshold)\n if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold\n }\n\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n flash(t('billing.accounts.detail.save.success', 'Account updated'), 'success')\n setEditing(false)\n void load()\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message\n ? `${message} \u2014 ${fieldDetail}`\n : fieldDetail || message\n flash(detailed || t('billing.accounts.detail.save.error', 'Save failed'), 'error')\n } finally {\n setSaving(false)\n }\n },\n [load, row, t],\n )\n\n const handleDelete = React.useCallback(async () => {\n if (!row) return\n const ok = await confirm({\n title: t('billing.accounts.detail.delete.confirm.title', 'Delete account?'),\n text: t(\n 'billing.accounts.detail.delete.confirm.text',\n \"Soft-deletes the account. Its items + usage records stay in the database for audit; the Bill Run engine will stop picking it up immediately.\",\n ),\n confirmText: t('billing.accounts.detail.delete.confirm.button', 'Soft delete'),\n variant: 'destructive',\n })\n if (!ok) return\n setDeleting(true)\n try {\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('billing.accounts.detail.delete.success', 'Account deleted'), 'success')\n router.push('/backend/billing/accounts')\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.accounts.detail.delete.error', 'Delete failed'), 'error')\n setDeleting(false)\n }\n }, [confirm, router, row, t])\n\n if (error) {\n return (\n <Page>\n <PageHeader title={t('billing.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n\n if (loading || !row) {\n return (\n <Page>\n <PageHeader title={t('billing.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <LoadingMessage\n label={t('billing.accounts.detail.loading', 'Loading account\u2026')}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/accounts\"\n entityTypeLabel={t('billing.accounts.detail.title', 'Billing Account')}\n title={row.name}\n statusBadge={\n row.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n )\n }\n actionsContent={\n <>\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/items/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_item', 'Add item')}\n </Link>\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/usage/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_usage', 'Add usage')}\n </Link>\n </Button>\n {!editing ? (\n <Button type=\"button\" onClick={() => setEditing(true)}>\n <Pencil size={16} />\n {t('billing.common.edit', 'Edit')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleDelete}\n disabled={deleting}\n >\n {deleting\n ? t('billing.accounts.detail.delete.in_progress', 'Deleting\u2026')\n : t('billing.accounts.detail.delete.action', 'Soft delete')}\n </Button>\n </>\n }\n />\n <PageBody>\n {editing ? (\n <AccountForm\n mode=\"edit\"\n initial={toFormValues(row)}\n submitting={saving}\n onSubmit={handleSubmit}\n onCancel={() => setEditing(false)}\n />\n ) : (\n <AccountView row={row} />\n )}\n {row.billing_mode === 'prepaid' ? (\n <PrepaidPanel accountId={row.id} currencyCode={row.currency_code} />\n ) : null}\n <AccountItemsSection accountId={row.id} />\n <AccountUsageSection accountId={row.id} />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Pencil, Plus } from 'lucide-react'\nimport { useParams, useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n apiCallOrThrow,\n readApiResultOrThrow,\n} from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { AccountForm, type AccountFormValues } from '../../../../components/AccountForm'\nimport { DetailCard, DetailField } from '../../../../components/DetailFields'\nimport { PrepaidPanel } from '../../../../components/PrepaidPanel'\n\n/**\n * `/backend/billing/accounts/[id]` \u2014 Billing Account detail.\n *\n * Opens read-only (field grid + the account's items). The `Edit`\n * button swaps the grid for the `AccountForm`; Save / Cancel return\n * to the read-only view. Loads via `GET /api/billing/accounts?id=<id>`,\n * edits through `PUT`, soft-delete through `DELETE`.\n *\n * `customerId` + `currencyCode` are immutable on update \u2014 the form\n * disables both in `mode='edit'` and the schema rejects them per\n * `billingAccountUpdateSchema`.\n */\n\n// API list rows are snake_case (see `api/accounts/route.ts` `fields`).\ntype AccountRow = {\n id: string\n customer_id: string\n name: string\n currency_code: string\n bill_cycle: 'weekly' | 'monthly' | 'quarterly' | 'annually'\n bill_cycle_anchor: string\n invoice_email: string\n invoice_language: string\n tax_id: string | null\n invoice_address: {\n line1?: string\n line2?: string\n city?: string\n postal_code?: string\n country?: string\n } | null\n next_bill_date: string\n last_bill_date: string | null\n billing_mode: 'postpaid' | 'prepaid'\n credit_limit: string\n is_active: boolean\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = { items: AccountRow[] }\n\n// Minimal item shape for the embedded \"Items\" table.\ntype AccountItemRow = {\n id: string\n description: string\n type: 'one_time' | 'recurring' | 'usage'\n bill_start_date: string\n is_active: boolean\n}\n\n// Minimal usage shape for the embedded \"Usage\" table. Mirrors the\n// snake_case `fields` projection in `api/usage/route.ts`. Usage is\n// append-only \u2014 `rated_in_bill_run_id` is null until a Bill Run rates it.\ntype AccountUsageRow = {\n id: string\n uom_code: string\n quantity: string\n period_start: string\n period_end: string\n line_description: string | null\n rated_in_bill_run_id: string | null\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction toFormValues(row: AccountRow): AccountFormValues {\n return {\n customerId: row.customer_id,\n name: row.name,\n currencyCode: row.currency_code,\n billCycle: row.bill_cycle,\n billCycleAnchor: row.bill_cycle_anchor,\n invoiceEmail: row.invoice_email,\n invoiceLanguage: row.invoice_language,\n taxId: row.tax_id ?? '',\n invoiceAddress: {\n line1: row.invoice_address?.line1 ?? '',\n line2: row.invoice_address?.line2 ?? '',\n city: row.invoice_address?.city ?? '',\n postal_code: row.invoice_address?.postal_code ?? '',\n country: row.invoice_address?.country ?? '',\n },\n nextBillDate: row.next_bill_date?.slice(0, 10) ?? '',\n isActive: row.is_active,\n billingMode: row.billing_mode,\n creditLimit: row.credit_limit ?? '0',\n // The threshold lives on the balance row, not the account row \u2014 blank\n // means \"leave unchanged\" on save.\n lowBalanceThreshold: '',\n }\n}\n\n// \u2500\u2500\u2500 Read-only view \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction AccountView({ row }: { row: AccountRow }) {\n const t = useT()\n const addr = row.invoice_address\n const addressText = addr\n ? [\n addr.line1,\n addr.line2,\n [addr.postal_code, addr.city].filter(Boolean).join(' '),\n addr.country,\n ]\n .map((part) => (part ?? '').trim())\n .filter(Boolean)\n .join(', ')\n : ''\n return (\n <DetailCard>\n <DetailField label={t('billing.accounts.form.customer_id', 'Customer ID')}>\n <span className=\"font-mono text-xs\">{row.customer_id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.currency', 'Currency')}>\n {row.currency_code}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.mode', 'Mode')}>\n {row.billing_mode === 'prepaid' ? (\n <Tag variant=\"brand\">{t('billing.mode.prepaid', 'Prepaid')}</Tag>\n ) : (\n <Tag variant=\"neutral\">{t('billing.mode.postpaid', 'Postpaid')}</Tag>\n )}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.credit_limit', 'Credit limit')}>\n <span className=\"font-mono tabular-nums\">{row.credit_limit}</span>\n </DetailField>\n <DetailField label={t('billing.common.id', 'ID')}>\n <span className=\"font-mono text-xs\">{row.id}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.cycle', 'Cycle')}>\n {row.bill_cycle}\n <span className=\"text-muted-foreground\"> \u00B7 {row.bill_cycle_anchor}</span>\n </DetailField>\n <DetailField label={t('billing.accounts.columns.next_bill', 'Next bill')}>\n {formatDate(row.next_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.columns.last_bill', 'Last bill')}>\n {formatDate(row.last_bill_date)}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_email', 'Invoice email')}>\n {row.invoice_email}\n </DetailField>\n <DetailField label={t('billing.accounts.form.invoice_language', 'Invoice language')}>\n {row.invoice_language}\n </DetailField>\n <DetailField label={t('billing.accounts.form.tax_id', 'Tax ID')}>\n {row.tax_id || '\u2014'}\n </DetailField>\n <DetailField\n label={t('billing.accounts.form.address.title', 'Invoice address')}\n fullWidth\n >\n {addressText || '\u2014'}\n </DetailField>\n </DetailCard>\n )\n}\n\n// \u2500\u2500\u2500 Embedded items table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction AccountItemsSection({ accountId }: { accountId: string }) {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<AccountItemRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountItemRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/items?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=createdAt&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountItemRow>[]>(\n () => [\n {\n accessorKey: 'description',\n header: t('billing.items.columns.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {row.original.description}\n </span>\n ),\n },\n {\n accessorKey: 'type',\n header: t('billing.items.columns.type', 'Type'),\n cell: ({ row }) => <Tag variant=\"info\">{row.original.type}</Tag>,\n },\n {\n accessorKey: 'bill_start_date',\n header: t('billing.items.columns.start', 'Start'),\n cell: ({ row }) => formatDate(row.original.bill_start_date),\n },\n {\n accessorKey: 'is_active',\n header: t('billing.items.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.items.title', 'Items')}\n columns={columns}\n data={rows}\n isLoading={loading}\n onRowClick={(row) => router.push(`/backend/billing/items/${row.id}`)}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.items.empty', 'No items for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Embedded usage table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction AccountUsageSection({ accountId }: { accountId: string }) {\n const t = useT()\n const [rows, setRows] = React.useState<AccountUsageRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const pageSize = 25\n\n const load = React.useCallback(async () => {\n setLoading(true)\n try {\n const result = await readApiResultOrThrow<{\n items: AccountUsageRow[]\n total: number\n totalPages: number\n }>(\n `/api/billing/usage?billAccountId=${accountId}&page=${page}` +\n `&pageSize=${pageSize}&sortField=periodStart&sortDir=desc`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [accountId, page])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const columns = React.useMemo<ColumnDef<AccountUsageRow>[]>(\n () => [\n {\n accessorKey: 'uom_code',\n header: t('billing.usage.columns.uom', 'Unit'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">{row.original.uom_code}</span>\n ),\n },\n {\n accessorKey: 'quantity',\n header: t('billing.usage.columns.quantity', 'Quantity'),\n cell: ({ row }) => (\n <span className=\"font-mono tabular-nums\">{row.original.quantity}</span>\n ),\n },\n {\n accessorKey: 'period_start',\n header: t('billing.usage.columns.period', 'Period'),\n cell: ({ row }) =>\n `${formatDate(row.original.period_start)} \u2013 ${formatDate(row.original.period_end)}`,\n },\n {\n accessorKey: 'line_description',\n header: t('billing.usage.columns.description', 'Description'),\n cell: ({ row }) => row.original.line_description || '\u2014',\n },\n {\n accessorKey: 'rated_in_bill_run_id',\n header: t('billing.usage.columns.rated', 'Rated'),\n cell: ({ row }) =>\n row.original.rated_in_bill_run_id ? (\n // Rated records carry the Bill Run that priced them \u2014 link to it.\n // Unrated records have no destination, so they stay non-interactive\n // (no misleading row-level click affordance).\n <Link\n href={`/backend/billing/runs/${row.original.rated_in_bill_run_id}`}\n className=\"rounded-sm hover:underline\"\n >\n <Tag variant=\"success\">{t('billing.usage.rated.yes', 'Rated')}</Tag>\n </Link>\n ) : (\n <Tag variant=\"default\">{t('billing.usage.rated.no', 'Unrated')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <div className=\"mt-6\">\n <DataTable\n title={t('billing.accounts.detail.usage.title', 'Usage')}\n columns={columns}\n data={rows}\n isLoading={loading}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('billing.accounts.detail.usage.empty', 'No usage records for this account yet.')}\n </div>\n }\n />\n </div>\n )\n}\n\nexport default function BillingAccountDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n const router = useRouter()\n // OM serves backend pages through a catch-all route \u2014 the dynamic\n // `[id]` segment arrives as a page prop; `useParams()` is the fallback.\n const urlParams = useParams<{ id: string }>()\n const accountId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [row, setRow] = React.useState<AccountRow | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [editing, setEditing] = React.useState(false)\n const [saving, setSaving] = React.useState(false)\n const [deleting, setDeleting] = React.useState(false)\n\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const load = React.useCallback(async () => {\n if (!accountId) return\n setLoading(true)\n setError(null)\n try {\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/accounts?id=${accountId}&pageSize=1`,\n )\n const found = result.items?.[0]\n if (!found) {\n setError(t('billing.accounts.detail.error.not_found', 'Account not found'))\n return\n }\n setRow(found)\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setError(message || t('billing.accounts.detail.error.load', 'Failed to load account'))\n } finally {\n setLoading(false)\n }\n }, [accountId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n const handleSubmit = React.useCallback(\n async (values: AccountFormValues) => {\n if (!row) return\n setSaving(true)\n try {\n const payload: Record<string, unknown> = {\n id: row.id,\n name: values.name.trim(),\n billCycle: values.billCycle,\n billCycleAnchor: values.billCycleAnchor.trim(),\n invoiceEmail: values.invoiceEmail.trim(),\n invoiceLanguage: values.invoiceLanguage.trim().toLowerCase(),\n invoiceAddress: {\n line1: values.invoiceAddress.line1.trim(),\n ...(values.invoiceAddress.line2.trim()\n ? { line2: values.invoiceAddress.line2.trim() }\n : {}),\n city: values.invoiceAddress.city.trim(),\n postal_code: values.invoiceAddress.postal_code.trim(),\n country: values.invoiceAddress.country.trim().toUpperCase(),\n },\n nextBillDate: values.nextBillDate,\n isActive: values.isActive,\n }\n payload.taxId = values.taxId.trim() === '' ? null : values.taxId.trim()\n // billingMode is immutable on update; creditLimit + threshold are editable.\n const creditLimit = Number.parseFloat(values.creditLimit)\n if (Number.isFinite(creditLimit) && creditLimit >= 0) payload.creditLimit = creditLimit\n if (row.billing_mode === 'prepaid' && values.lowBalanceThreshold.trim()) {\n const threshold = Number.parseFloat(values.lowBalanceThreshold)\n if (Number.isFinite(threshold) && threshold >= 0) payload.lowBalanceThreshold = threshold\n }\n\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n flash(t('billing.accounts.detail.save.success', 'Account updated'), 'success')\n setEditing(false)\n void load()\n } catch (err) {\n const { message, fieldErrors } = normalizeCrudServerError(err)\n const fieldDetail =\n fieldErrors && Object.keys(fieldErrors).length\n ? Object.entries(fieldErrors)\n .map(([field, msg]) => `${field}: ${msg}`)\n .join('; ')\n : null\n const detailed =\n fieldDetail && message\n ? `${message} \u2014 ${fieldDetail}`\n : fieldDetail || message\n flash(detailed || t('billing.accounts.detail.save.error', 'Save failed'), 'error')\n } finally {\n setSaving(false)\n }\n },\n [load, row, t],\n )\n\n const handleDelete = React.useCallback(async () => {\n if (!row) return\n const ok = await confirm({\n title: t('billing.accounts.detail.delete.confirm.title', 'Delete account?'),\n text: t(\n 'billing.accounts.detail.delete.confirm.text',\n \"Soft-deletes the account. Its items + usage records stay in the database for audit; the Bill Run engine will stop picking it up immediately.\",\n ),\n confirmText: t('billing.accounts.detail.delete.confirm.button', 'Soft delete'),\n variant: 'destructive',\n })\n if (!ok) return\n setDeleting(true)\n try {\n await apiCallOrThrow('/api/billing/accounts', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('billing.accounts.detail.delete.success', 'Account deleted'), 'success')\n router.push('/backend/billing/accounts')\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.accounts.detail.delete.error', 'Delete failed'), 'error')\n setDeleting(false)\n }\n }, [confirm, router, row, t])\n\n if (error) {\n return (\n <Page>\n <PageHeader title={t('billing.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n\n if (loading || !row) {\n return (\n <Page>\n <PageHeader title={t('billing.accounts.detail.title', 'Billing Account')} />\n <PageBody>\n <LoadingMessage\n label={t('billing.accounts.detail.loading', 'Loading account\u2026')}\n />\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/accounts\"\n entityTypeLabel={t('billing.accounts.detail.title', 'Billing Account')}\n title={row.name}\n statusBadge={\n row.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n )\n }\n actionsContent={\n <>\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/items/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_item', 'Add item')}\n </Link>\n </Button>\n <Button asChild variant=\"outline\">\n <Link href={`/backend/billing/usage/create?billAccountId=${row.id}`}>\n <Plus size={16} />\n {t('billing.accounts.detail.add_usage', 'Add usage')}\n </Link>\n </Button>\n {!editing ? (\n <Button type=\"button\" onClick={() => setEditing(true)}>\n <Pencil size={16} />\n {t('billing.common.edit', 'Edit')}\n </Button>\n ) : null}\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={handleDelete}\n disabled={deleting}\n >\n {deleting\n ? t('billing.accounts.detail.delete.in_progress', 'Deleting\u2026')\n : t('billing.accounts.detail.delete.action', 'Soft delete')}\n </Button>\n </>\n }\n />\n <PageBody>\n {editing ? (\n <AccountForm\n mode=\"edit\"\n initial={toFormValues(row)}\n submitting={saving}\n onSubmit={handleSubmit}\n onCancel={() => setEditing(false)}\n />\n ) : (\n <AccountView row={row} />\n )}\n {row.billing_mode === 'prepaid' ? (\n <PrepaidPanel accountId={row.id} currencyCode={row.currency_code} />\n ) : null}\n <AccountItemsSection accountId={row.id} />\n <AccountUsageSection accountId={row.id} />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/billing/backend/billing/accounts/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Plus } from 'lucide-react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\n/**\n * Billing Accounts admin list.\n *\n * Search + filter by bill cycle / currency / active state. The\n * \"New account\" CTA opens the create page; rows link into the detail\n * page for inspect + edit + soft-delete.\n */\n\n// API list rows are snake_case \u2014 the billing list endpoints project\n// raw column names (see `api/accounts/route.ts` `fields`).\ntype BillingAccountRow = {\n id: string\n customer_id: string\n name: string\n currency_code: string\n bill_cycle: 'monthly' | 'quarterly' | 'annually' | 'weekly'\n bill_cycle_anchor: string\n invoice_email: string\n next_bill_date: string\n last_bill_date: string | null\n billing_mode: 'postpaid' | 'prepaid'\n is_active: boolean\n created_at: string\n}\n\ntype ListResponse = {\n items: BillingAccountRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nexport default function BillingAccountsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<BillingAccountRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'billCycle',\n label: t('billing.accounts.filters.cycle', 'Bill cycle'),\n type: 'select',\n options: [\n { value: 'weekly', label: t('billing.cycle.weekly', 'Weekly') },\n { value: 'monthly', label: t('billing.cycle.monthly', 'Monthly') },\n { value: 'quarterly', label: t('billing.cycle.quarterly', 'Quarterly') },\n { value: 'annually', label: t('billing.cycle.annually', 'Annually') },\n ],\n },\n {\n id: 'currencyCode',\n label: t('billing.accounts.filters.currency', 'Currency'),\n type: 'text',\n },\n {\n id: 'billingMode',\n label: t('billing.accounts.columns.mode', 'Mode'),\n type: 'select',\n options: [\n { value: 'postpaid', label: t('billing.mode.postpaid', 'Postpaid') },\n { value: 'prepaid', label: t('billing.mode.prepaid', 'Prepaid') },\n ],\n },\n {\n id: 'isActive',\n label: t('billing.accounts.filters.active', 'Active'),\n type: 'select',\n options: [\n { value: 'true', label: t('billing.common.active', 'Active') },\n { value: 'false', label: t('billing.common.inactive', 'Inactive') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.billCycle === 'string' && filters.billCycle) {\n params.set('billCycle', filters.billCycle)\n }\n if (typeof filters.currencyCode === 'string' && filters.currencyCode.trim()) {\n params.set('currencyCode', filters.currencyCode.trim().toUpperCase())\n }\n if (typeof filters.billingMode === 'string' && filters.billingMode) {\n params.set('billingMode', filters.billingMode)\n }\n if (filters.isActive === 'true') params.set('isActive', 'true')\n if (filters.isActive === 'false') params.set('isActive', 'false')\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/accounts?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [\n filters.billCycle,\n filters.currencyCode,\n filters.billingMode,\n filters.isActive,\n page,\n pageSize,\n search,\n ])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<BillingAccountRow>[]>(\n () => [\n {\n accessorKey: 'name',\n header: t('billing.accounts.columns.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">{row.original.name}</span>\n ),\n },\n {\n accessorKey: 'customer_id',\n header: t('billing.accounts.columns.customer', 'Customer'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">{row.original.customer_id}</span>\n ),\n },\n {\n accessorKey: 'currency_code',\n header: t('billing.accounts.columns.currency', 'Currency'),\n },\n {\n accessorKey: 'bill_cycle',\n header: t('billing.accounts.columns.cycle', 'Cycle'),\n cell: ({ row }) => (\n <span>\n {row.original.bill_cycle}\n <span className=\"text-muted-foreground\"> \u00B7 {row.original.bill_cycle_anchor}</span>\n </span>\n ),\n },\n {\n accessorKey: 'billing_mode',\n header: t('billing.accounts.columns.mode', 'Mode'),\n cell: ({ row }) =>\n row.original.billing_mode === 'prepaid' ? (\n <Tag variant=\"brand\">{t('billing.mode.prepaid', 'Prepaid')}</Tag>\n ) : (\n <Tag variant=\"neutral\">{t('billing.mode.postpaid', 'Postpaid')}</Tag>\n ),\n },\n {\n accessorKey: 'next_bill_date',\n header: t('billing.accounts.columns.next_bill', 'Next bill'),\n cell: ({ row }) => formatDate(row.original.next_bill_date),\n },\n {\n accessorKey: 'last_bill_date',\n header: t('billing.accounts.columns.last_bill', 'Last bill'),\n cell: ({ row }) => formatDate(row.original.last_bill_date),\n },\n {\n accessorKey: 'is_active',\n header: t('billing.accounts.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.accounts.title', 'Billing Accounts')}\n actions={\n <Button asChild>\n <Link href=\"/backend/billing/accounts/create\">\n <Plus size={16} />\n {t('billing.accounts.create.action', 'New account')}\n </Link>\n </Button>\n }\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-accounts' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/accounts/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Plus } from 'lucide-react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\n/**\n * Billing Accounts admin list.\n *\n * Search + filter by bill cycle / currency / active state. The\n * \"New account\" CTA opens the create page; rows link into the detail\n * page for inspect + edit + soft-delete.\n */\n\n// API list rows are snake_case \u2014 the billing list endpoints project\n// raw column names (see `api/accounts/route.ts` `fields`).\ntype BillingAccountRow = {\n id: string\n customer_id: string\n name: string\n currency_code: string\n bill_cycle: 'monthly' | 'quarterly' | 'annually' | 'weekly'\n bill_cycle_anchor: string\n invoice_email: string\n next_bill_date: string\n last_bill_date: string | null\n billing_mode: 'postpaid' | 'prepaid'\n is_active: boolean\n created_at: string\n}\n\ntype ListResponse = {\n items: BillingAccountRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nexport default function BillingAccountsListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<BillingAccountRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'billCycle',\n label: t('billing.accounts.filters.cycle', 'Bill cycle'),\n type: 'select',\n options: [\n { value: 'weekly', label: t('billing.cycle.weekly', 'Weekly') },\n { value: 'monthly', label: t('billing.cycle.monthly', 'Monthly') },\n { value: 'quarterly', label: t('billing.cycle.quarterly', 'Quarterly') },\n { value: 'annually', label: t('billing.cycle.annually', 'Annually') },\n ],\n },\n {\n id: 'currencyCode',\n label: t('billing.accounts.filters.currency', 'Currency'),\n type: 'text',\n },\n {\n id: 'billingMode',\n label: t('billing.accounts.columns.mode', 'Mode'),\n type: 'select',\n options: [\n { value: 'postpaid', label: t('billing.mode.postpaid', 'Postpaid') },\n { value: 'prepaid', label: t('billing.mode.prepaid', 'Prepaid') },\n ],\n },\n {\n id: 'isActive',\n label: t('billing.accounts.filters.active', 'Active'),\n type: 'select',\n options: [\n { value: 'true', label: t('billing.common.active', 'Active') },\n { value: 'false', label: t('billing.common.inactive', 'Inactive') },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.billCycle === 'string' && filters.billCycle) {\n params.set('billCycle', filters.billCycle)\n }\n if (typeof filters.currencyCode === 'string' && filters.currencyCode.trim()) {\n params.set('currencyCode', filters.currencyCode.trim().toUpperCase())\n }\n if (typeof filters.billingMode === 'string' && filters.billingMode) {\n params.set('billingMode', filters.billingMode)\n }\n if (filters.isActive === 'true') params.set('isActive', 'true')\n if (filters.isActive === 'false') params.set('isActive', 'false')\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/accounts?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [\n filters.billCycle,\n filters.currencyCode,\n filters.billingMode,\n filters.isActive,\n page,\n pageSize,\n search,\n ])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<BillingAccountRow>[]>(\n () => [\n {\n accessorKey: 'name',\n header: t('billing.accounts.columns.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">{row.original.name}</span>\n ),\n },\n {\n accessorKey: 'customer_id',\n header: t('billing.accounts.columns.customer', 'Customer'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">{row.original.customer_id}</span>\n ),\n },\n {\n accessorKey: 'currency_code',\n header: t('billing.accounts.columns.currency', 'Currency'),\n },\n {\n accessorKey: 'bill_cycle',\n header: t('billing.accounts.columns.cycle', 'Cycle'),\n cell: ({ row }) => (\n <span>\n {row.original.bill_cycle}\n <span className=\"text-muted-foreground\"> \u00B7 {row.original.bill_cycle_anchor}</span>\n </span>\n ),\n },\n {\n accessorKey: 'billing_mode',\n header: t('billing.accounts.columns.mode', 'Mode'),\n cell: ({ row }) =>\n row.original.billing_mode === 'prepaid' ? (\n <Tag variant=\"brand\">{t('billing.mode.prepaid', 'Prepaid')}</Tag>\n ) : (\n <Tag variant=\"neutral\">{t('billing.mode.postpaid', 'Postpaid')}</Tag>\n ),\n },\n {\n accessorKey: 'next_bill_date',\n header: t('billing.accounts.columns.next_bill', 'Next bill'),\n cell: ({ row }) => formatDate(row.original.next_bill_date),\n },\n {\n accessorKey: 'last_bill_date',\n header: t('billing.accounts.columns.last_bill', 'Last bill'),\n cell: ({ row }) => formatDate(row.original.last_bill_date),\n },\n {\n accessorKey: 'is_active',\n header: t('billing.accounts.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.accounts.title', 'Billing Accounts')}\n actions={\n <Button asChild>\n <Link href=\"/backend/billing/accounts/create\">\n <Plus size={16} />\n {t('billing.accounts.create.action', 'New account')}\n </Link>\n </Button>\n }\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-accounts' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/accounts/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/billing/backend/billing/invoices/%5Bid%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useParams } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Pencil, Plus, Trash2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n apiCallOrThrow,\n readApiResultOrThrow,\n} from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { LineFormDialog, type LineFormValues } from '../../../../components/LineFormDialog'\nimport {\n DetailCard,\n DetailField,\n Stat,\n StatCard,\n} from '../../../../components/DetailFields'\nimport { billRunName } from '../../../../lib/billRunLabel'\n\ntype BillingInvoice = {\n id: string\n invoice_number: string\n status: 'draft' | 'posted' | 'paid' | 'void' | null\n currency_code: string\n issue_date: string | null\n due_date: string | null\n subtotal_net_amount: string\n grand_total_net_amount: string\n grand_total_gross_amount: string\n paid_total_amount: string\n outstanding_amount: string\n metadata: {\n bill_run_id?: string\n bill_account_id?: string\n bill_period_start?: string\n bill_period_end?: string\n test_run?: boolean\n } | null\n created_at: string\n updated_at: string\n}\n\ntype InvoiceLine = {\n id: string\n line_number: number\n kind: string\n description: string | null\n quantity: string\n currency_code: string\n unit_price_net: string\n total_net_amount: string\n total_gross_amount: string\n metadata: {\n billing_item_id?: string\n billing_type?: 'one_time' | 'recurring' | 'usage'\n operator_added?: boolean\n usage_tier_breakdown?: unknown\n } | null\n}\n\ntype DetailResponse = {\n invoice: BillingInvoice\n lines: InvoiceLine[]\n}\n\nfunction statusVariant(\n status: BillingInvoice['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'paid':\n return 'success'\n case 'posted':\n return 'warning'\n case 'void':\n return 'error'\n case 'draft':\n default:\n return 'default'\n }\n}\n\nfunction lineKindVariant(\n type: InvoiceLine['metadata'] extends infer M\n ? M extends { billing_type?: infer T }\n ? T\n : never\n : never,\n): 'default' | 'success' | 'warning' | 'error' {\n switch (type) {\n case 'one_time':\n return 'warning'\n case 'recurring':\n return 'success'\n case 'usage':\n return 'default'\n default:\n return 'default'\n }\n}\n\nfunction formatMoney(value: string, currency: string): string {\n const n = Number.parseFloat(value)\n if (!Number.isFinite(n)) return value\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n } catch {\n return `${value} ${currency}`\n }\n}\n\nexport default function BillingInvoiceDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n // OM serves backend pages through a catch-all route \u2014 the dynamic\n // `[id]` segment arrives as a page prop; `useParams()` is the fallback.\n const urlParams = useParams<{ id: string }>()\n const invoiceId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [invoice, setInvoice] = React.useState<BillingInvoice | null>(null)\n const [lines, setLines] = React.useState<InvoiceLine[]>([])\n // Bill Run + Billing Account are stored as FK ids in invoice metadata;\n // resolve their human names so the detail card shows names, not UUIDs.\n const [accountName, setAccountName] = React.useState<string | null>(null)\n const [runName, setRunName] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [posting, setPosting] = React.useState(false)\n const [wiping, setWiping] = React.useState(false)\n\n // Client-side pagination for invoice lines. Caps the table at 100 rows\n // per page (per the OM DataTable convention) \u2014 invoices typically have\n // <20 lines, but bulk import or a long-running usage-rated subscription\n // can exceed that.\n const [linesPage, setLinesPage] = React.useState(1)\n const linesPageSize = 100\n const visibleLines = React.useMemo(\n () => lines.slice((linesPage - 1) * linesPageSize, linesPage * linesPageSize),\n [lines, linesPage],\n )\n const linesTotalPages = Math.max(1, Math.ceil(lines.length / linesPageSize))\n\n // Inline line-edit dialog state. `dialogMode === null` \u2192 closed.\n const [dialogMode, setDialogMode] = React.useState<'add' | 'edit' | null>(null)\n const [dialogTargetLine, setDialogTargetLine] = React.useState<InvoiceLine | null>(null)\n const [dialogSubmitting, setDialogSubmitting] = React.useState(false)\n\n // Confirm flows: wipe-test + remove-line both use ConfirmDialog\n // (per UI AGENTS.md: NEVER `window.confirm`).\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const load = React.useCallback(async () => {\n if (!invoiceId) return\n setError(null)\n setLoading(true)\n try {\n const data = await readApiResultOrThrow<DetailResponse>(\n `/api/billing/invoices/${invoiceId}`,\n )\n setInvoice(data.invoice)\n setLines(data.lines ?? [])\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setError(message || t('billing.invoices.detail.error.load', 'Failed to load invoice'))\n } finally {\n setLoading(false)\n }\n }, [invoiceId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n // Resolve the FK names once the invoice is loaded (one bounded\n // request each \u2014 Bill Run name is derived from its as-of date).\n const billRunId = invoice?.metadata?.bill_run_id\n const billAccountId = invoice?.metadata?.bill_account_id\n React.useEffect(() => {\n if (billAccountId) {\n readApiResultOrThrow<{ items: Array<{ name: string }> }>(\n `/api/billing/accounts?id=${billAccountId}&pageSize=1`,\n )\n .then((r) => setAccountName(r.items?.[0]?.name ?? null))\n .catch(() => setAccountName(null))\n }\n if (billRunId) {\n readApiResultOrThrow<{ items: Array<{ as_of_date: string }> }>(\n `/api/billing/runs?id=${billRunId}&pageSize=1`,\n )\n .then((r) => {\n const asOf = r.items?.[0]?.as_of_date\n setRunName(asOf ? billRunName(asOf) : null)\n })\n .catch(() => setRunName(null))\n }\n }, [billAccountId, billRunId])\n\n const handlePost = React.useCallback(async () => {\n if (!invoice) return\n setPosting(true)\n try {\n const call = await apiCallOrThrow<{ status: string; invoiceNumber: string }>(\n '/api/billing/invoices/post',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ invoiceId: invoice.id }),\n },\n )\n flash(\n t('billing.invoices.post.success', 'Invoice posted') +\n ` (${call.result?.invoiceNumber ?? invoice.invoice_number})`,\n 'success',\n )\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.post.error', 'Post failed'), 'error')\n } finally {\n setPosting(false)\n }\n }, [invoice, load, t])\n\n const handleWipeTest = React.useCallback(async () => {\n if (!invoice) return\n const ok = await confirm({\n title: t('billing.invoices.wipe.confirm.title', 'Wipe test invoices?'),\n text: t(\n 'billing.invoices.wipe.confirm.text',\n 'Every test invoice from this Bill Run will be permanently deleted. This cannot be undone.',\n ),\n confirmText: t('billing.invoices.wipe.confirm.button', 'Delete test invoices'),\n variant: 'destructive',\n })\n if (!ok) return\n setWiping(true)\n try {\n const billRunId = invoice.metadata?.bill_run_id\n const call = await apiCallOrThrow<{ invoicesRemoved: number }>(\n '/api/billing/test-invoices/wipe',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(billRunId ? { billRunId } : {}),\n },\n )\n flash(\n t('billing.invoices.wipe.success', 'Test invoices removed') +\n ` (${call.result?.invoicesRemoved ?? 0})`,\n 'success',\n )\n // The invoice we were looking at is hard-deleted \u2014 navigate away\n // on success by reloading; the detail load will 404 and the\n // user clicks back.\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.wipe.error', 'Wipe failed'), 'error')\n } finally {\n setWiping(false)\n }\n }, [confirm, invoice, load, t])\n\n // \u2500\u2500\u2500 Line add / edit / remove \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n const handleSubmitLine = React.useCallback(\n async (values: LineFormValues) => {\n if (!invoice) return\n setDialogSubmitting(true)\n try {\n if (dialogMode === 'add') {\n await apiCallOrThrow('/api/billing/invoices/add-line', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n invoiceId: invoice.id,\n description: values.description,\n quantity: values.quantity,\n unitPriceNet: values.unitPriceNet,\n }),\n })\n flash(t('billing.invoices.lines.add.success', 'Line added'), 'success')\n } else if (dialogMode === 'edit' && dialogTargetLine) {\n await apiCallOrThrow('/api/billing/invoices/edit-line', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n invoiceId: invoice.id,\n invoiceLineId: dialogTargetLine.id,\n changes: {\n description: values.description,\n quantity: values.quantity,\n unitPriceNet: values.unitPriceNet,\n ...(values.totalNetAmount !== undefined\n ? { totalNetAmount: values.totalNetAmount }\n : {}),\n },\n }),\n })\n flash(t('billing.invoices.lines.edit.success', 'Line updated'), 'success')\n }\n setDialogMode(null)\n setDialogTargetLine(null)\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.lines.save.error', 'Save failed'), 'error')\n } finally {\n setDialogSubmitting(false)\n }\n },\n [dialogMode, dialogTargetLine, invoice, load, t],\n )\n\n const handleRemoveLine = React.useCallback(\n async (line: InvoiceLine) => {\n if (!invoice) return\n const ok = await confirm({\n title: t('billing.invoices.lines.remove.confirm.title', 'Remove line?'),\n text: t(\n 'billing.invoices.lines.remove.confirm.text',\n 'The line will be deleted and recorded in the draft-edit audit log.',\n ),\n confirmText: t('billing.invoices.lines.remove.confirm.button', 'Remove line'),\n variant: 'destructive',\n })\n if (!ok) return\n try {\n await apiCallOrThrow('/api/billing/invoices/remove-line', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n }),\n })\n flash(t('billing.invoices.lines.remove.success', 'Line removed'), 'success')\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.lines.remove.error', 'Remove failed'), 'error')\n }\n },\n [confirm, invoice, load, t],\n )\n\n const canEditLines = invoice?.status === 'draft'\n\n const lineColumns = React.useMemo<ColumnDef<InvoiceLine>[]>(\n () => [\n {\n accessorKey: 'line_number',\n header: '#',\n },\n {\n accessorKey: 'description',\n header: t('billing.invoices.lines.columns.description', 'Description'),\n cell: ({ row }) => {\n const type = row.original.metadata?.billing_type\n const operatorAdded = row.original.metadata?.operator_added === true\n return (\n <div className=\"flex flex-col gap-1\">\n <span>{row.original.description ?? '\u2014'}</span>\n <div className=\"flex items-center gap-1 flex-wrap\">\n {type ? <Tag variant={lineKindVariant(type)}>{type}</Tag> : null}\n {operatorAdded ? (\n <Tag variant=\"info\">\n {t('billing.invoices.lines.operator_added', 'Operator-added')}\n </Tag>\n ) : null}\n </div>\n </div>\n )\n },\n },\n {\n accessorKey: 'quantity',\n header: t('billing.invoices.lines.columns.quantity', 'Qty'),\n },\n {\n accessorKey: 'unit_price_net',\n header: t('billing.invoices.lines.columns.unit_price', 'Unit price'),\n cell: ({ row }) =>\n formatMoney(row.original.unit_price_net, row.original.currency_code),\n },\n {\n accessorKey: 'total_net_amount',\n header: t('billing.invoices.lines.columns.total', 'Total net'),\n cell: ({ row }) =>\n formatMoney(row.original.total_net_amount, row.original.currency_code),\n },\n {\n id: 'actions',\n header: '',\n cell: ({ row }) => {\n if (!canEditLines) return null\n return (\n <div className=\"flex items-center justify-end gap-1\">\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n aria-label={t('billing.invoices.lines.actions.edit', 'Edit line')}\n onClick={() => {\n setDialogTargetLine(row.original)\n setDialogMode('edit')\n }}\n >\n <Pencil size={16} />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n aria-label={t('billing.invoices.lines.actions.remove', 'Remove line')}\n onClick={() => void handleRemoveLine(row.original)}\n >\n <Trash2 size={16} />\n </IconButton>\n </div>\n )\n },\n },\n ],\n [canEditLines, handleRemoveLine, t],\n )\n\n const dialogInitial = React.useMemo<Partial<LineFormValues> | undefined>(() => {\n if (dialogMode !== 'edit' || !dialogTargetLine) return undefined\n return {\n description: dialogTargetLine.description ?? '',\n quantity: Number.parseFloat(dialogTargetLine.quantity),\n unitPriceNet: Number.parseFloat(dialogTargetLine.unit_price_net),\n }\n }, [dialogMode, dialogTargetLine])\n\n if (error) {\n return (\n <Page>\n <PageHeader title={t('billing.invoices.detail.title', 'Billing Invoice')} />\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n if (loading || !invoice) {\n return (\n <Page>\n <PageHeader title={t('billing.invoices.detail.title', 'Billing Invoice')} />\n <PageBody>\n <LoadingMessage label={t('billing.invoices.detail.loading', 'Loading invoice\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n const isTest = invoice.metadata?.test_run === true\n const isDraft = invoice.status === 'draft'\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/invoices\"\n entityTypeLabel={t('billing.invoices.detail.title', 'Billing Invoice')}\n title={invoice.invoice_number}\n statusBadge={\n <div className=\"flex items-center gap-2 flex-wrap\">\n <Tag variant={statusVariant(invoice.status)}>{invoice.status ?? '\u2014'}</Tag>\n {isTest ? <Tag variant=\"warning\">TEST</Tag> : null}\n </div>\n }\n actionsContent={\n <>\n {isDraft && !isTest ? (\n <Button onClick={handlePost} disabled={posting}>\n {posting\n ? t('billing.invoices.post.in_progress', 'Posting\u2026')\n : t('billing.invoices.post.action', 'Post invoice')}\n </Button>\n ) : null}\n {isTest ? (\n <Button\n variant=\"outline\"\n onClick={handleWipeTest}\n disabled={wiping}\n >\n {wiping\n ? t('billing.invoices.wipe.in_progress', 'Wiping\u2026')\n : t('billing.invoices.wipe.action', 'Wipe test invoices for this run')}\n </Button>\n ) : null}\n </>\n }\n />\n <PageBody>\n <div className=\"space-y-6\">\n <DetailCard>\n <DetailField label={t('billing.invoices.detail.field.period', 'Bill period')}>\n {invoice.metadata?.bill_period_start ?? '\u2014'} \u2192{' '}\n {invoice.metadata?.bill_period_end ?? '\u2014'}\n </DetailField>\n <DetailField label={t('billing.invoices.detail.field.bill_run', 'Bill Run')}>\n {invoice.metadata?.bill_run_id ? (\n <Link\n href={`/backend/billing/runs/${invoice.metadata.bill_run_id}`}\n className=\"text-primary hover:underline\"\n >\n {runName ?? invoice.metadata.bill_run_id}\n </Link>\n ) : (\n '\u2014'\n )}\n </DetailField>\n <DetailField\n label={t('billing.invoices.detail.field.account', 'Billing Account')}\n >\n {invoice.metadata?.bill_account_id ? (\n <Link\n href={`/backend/billing/accounts/${invoice.metadata.bill_account_id}`}\n className=\"text-primary hover:underline\"\n >\n {accountName ?? invoice.metadata.bill_account_id}\n </Link>\n ) : (\n '\u2014'\n )}\n </DetailField>\n <DetailField label={t('billing.invoices.detail.field.issued', 'Issued')}>\n {invoice.issue_date?.slice(0, 10) ?? '\u2014'}\n </DetailField>\n <DetailField label={t('billing.common.id', 'ID')}>\n <span className=\"font-mono text-xs\">{invoice.id}</span>\n </DetailField>\n </DetailCard>\n\n <StatCard>\n <Stat\n label={t('billing.invoices.detail.subtotal', 'Subtotal net')}\n value={formatMoney(invoice.subtotal_net_amount, invoice.currency_code)}\n />\n <Stat\n label={t('billing.invoices.detail.total', 'Grand total')}\n value={formatMoney(invoice.grand_total_gross_amount, invoice.currency_code)}\n />\n <Stat\n label={t('billing.invoices.detail.paid', 'Paid')}\n value={formatMoney(invoice.paid_total_amount, invoice.currency_code)}\n />\n <Stat\n label={t('billing.invoices.detail.outstanding', 'Outstanding')}\n value={formatMoney(invoice.outstanding_amount, invoice.currency_code)}\n />\n </StatCard>\n\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <h2 className=\"text-base font-semibold\">\n {t('billing.invoices.lines.title', 'Lines')}\n </h2>\n {canEditLines ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => {\n setDialogTargetLine(null)\n setDialogMode('add')\n }}\n >\n <Plus size={16} />\n {t('billing.invoices.lines.add.action', 'Add line')}\n </Button>\n ) : null}\n </div>\n <DataTable\n columns={lineColumns}\n data={visibleLines}\n isLoading={false}\n pagination={{\n page: linesPage,\n pageSize: linesPageSize,\n total: lines.length,\n totalPages: linesTotalPages,\n onPageChange: setLinesPage,\n }}\n />\n </div>\n </div>\n\n <LineFormDialog\n open={dialogMode !== null}\n mode={dialogMode ?? 'add'}\n initial={dialogInitial}\n submitting={dialogSubmitting}\n onCancel={() => {\n setDialogMode(null)\n setDialogTargetLine(null)\n }}\n onSubmit={handleSubmitLine}\n />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useParams } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Pencil, Plus, Trash2 } from 'lucide-react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Page, PageBody, PageHeader } from '@open-mercato/ui/backend/Page'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n apiCallOrThrow,\n readApiResultOrThrow,\n} from '@open-mercato/ui/backend/utils/apiCall'\nimport { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { LineFormDialog, type LineFormValues } from '../../../../components/LineFormDialog'\nimport {\n DetailCard,\n DetailField,\n Stat,\n StatCard,\n} from '../../../../components/DetailFields'\nimport { billRunName } from '../../../../lib/billRunLabel'\n\ntype BillingInvoice = {\n id: string\n invoice_number: string\n status: 'draft' | 'posted' | 'paid' | 'void' | null\n currency_code: string\n issue_date: string | null\n due_date: string | null\n subtotal_net_amount: string\n grand_total_net_amount: string\n grand_total_gross_amount: string\n paid_total_amount: string\n outstanding_amount: string\n metadata: {\n bill_run_id?: string\n bill_account_id?: string\n bill_period_start?: string\n bill_period_end?: string\n test_run?: boolean\n } | null\n created_at: string\n updated_at: string\n}\n\ntype InvoiceLine = {\n id: string\n line_number: number\n kind: string\n description: string | null\n quantity: string\n currency_code: string\n unit_price_net: string\n total_net_amount: string\n total_gross_amount: string\n metadata: {\n billing_item_id?: string\n billing_type?: 'one_time' | 'recurring' | 'usage'\n operator_added?: boolean\n usage_tier_breakdown?: unknown\n } | null\n}\n\ntype DetailResponse = {\n invoice: BillingInvoice\n lines: InvoiceLine[]\n}\n\nfunction statusVariant(\n status: BillingInvoice['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'paid':\n return 'success'\n case 'posted':\n return 'warning'\n case 'void':\n return 'error'\n case 'draft':\n default:\n return 'default'\n }\n}\n\nfunction lineKindVariant(\n type: InvoiceLine['metadata'] extends infer M\n ? M extends { billing_type?: infer T }\n ? T\n : never\n : never,\n): 'default' | 'success' | 'warning' | 'error' {\n switch (type) {\n case 'one_time':\n return 'warning'\n case 'recurring':\n return 'success'\n case 'usage':\n return 'default'\n default:\n return 'default'\n }\n}\n\nfunction formatMoney(value: string, currency: string): string {\n const n = Number.parseFloat(value)\n if (!Number.isFinite(n)) return value\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n } catch {\n return `${value} ${currency}`\n }\n}\n\nexport default function BillingInvoiceDetailPage(props: { params?: { id?: string } }) {\n const t = useT()\n // OM serves backend pages through a catch-all route \u2014 the dynamic\n // `[id]` segment arrives as a page prop; `useParams()` is the fallback.\n const urlParams = useParams<{ id: string }>()\n const invoiceId = (props.params?.id ?? urlParams?.id ?? '') as string\n\n const [invoice, setInvoice] = React.useState<BillingInvoice | null>(null)\n const [lines, setLines] = React.useState<InvoiceLine[]>([])\n // Bill Run + Billing Account are stored as FK ids in invoice metadata;\n // resolve their human names so the detail card shows names, not UUIDs.\n const [accountName, setAccountName] = React.useState<string | null>(null)\n const [runName, setRunName] = React.useState<string | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [posting, setPosting] = React.useState(false)\n const [wiping, setWiping] = React.useState(false)\n\n // Client-side pagination for invoice lines. Caps the table at 100 rows\n // per page (per the OM DataTable convention) \u2014 invoices typically have\n // <20 lines, but bulk import or a long-running usage-rated subscription\n // can exceed that.\n const [linesPage, setLinesPage] = React.useState(1)\n const linesPageSize = 100\n const visibleLines = React.useMemo(\n () => lines.slice((linesPage - 1) * linesPageSize, linesPage * linesPageSize),\n [lines, linesPage],\n )\n const linesTotalPages = Math.max(1, Math.ceil(lines.length / linesPageSize))\n\n // Inline line-edit dialog state. `dialogMode === null` \u2192 closed.\n const [dialogMode, setDialogMode] = React.useState<'add' | 'edit' | null>(null)\n const [dialogTargetLine, setDialogTargetLine] = React.useState<InvoiceLine | null>(null)\n const [dialogSubmitting, setDialogSubmitting] = React.useState(false)\n\n // Confirm flows: wipe-test + remove-line both use ConfirmDialog\n // (per UI AGENTS.md: NEVER `window.confirm`).\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n\n const load = React.useCallback(async () => {\n if (!invoiceId) return\n setError(null)\n setLoading(true)\n try {\n const data = await readApiResultOrThrow<DetailResponse>(\n `/api/billing/invoices/${invoiceId}`,\n )\n setInvoice(data.invoice)\n setLines(data.lines ?? [])\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n setError(message || t('billing.invoices.detail.error.load', 'Failed to load invoice'))\n } finally {\n setLoading(false)\n }\n }, [invoiceId, t])\n\n React.useEffect(() => {\n void load()\n }, [load])\n\n // Resolve the FK names once the invoice is loaded (one bounded\n // request each \u2014 Bill Run name is derived from its as-of date).\n const billRunId = invoice?.metadata?.bill_run_id\n const billAccountId = invoice?.metadata?.bill_account_id\n React.useEffect(() => {\n if (billAccountId) {\n readApiResultOrThrow<{ items: Array<{ name: string }> }>(\n `/api/billing/accounts?id=${billAccountId}&pageSize=1`,\n )\n .then((r) => setAccountName(r.items?.[0]?.name ?? null))\n .catch(() => setAccountName(null))\n }\n if (billRunId) {\n readApiResultOrThrow<{ items: Array<{ as_of_date: string }> }>(\n `/api/billing/runs?id=${billRunId}&pageSize=1`,\n )\n .then((r) => {\n const asOf = r.items?.[0]?.as_of_date\n setRunName(asOf ? billRunName(asOf) : null)\n })\n .catch(() => setRunName(null))\n }\n }, [billAccountId, billRunId])\n\n const handlePost = React.useCallback(async () => {\n if (!invoice) return\n setPosting(true)\n try {\n const call = await apiCallOrThrow<{ status: string; invoiceNumber: string }>(\n '/api/billing/invoices/post',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ invoiceId: invoice.id }),\n },\n )\n flash(\n t('billing.invoices.post.success', 'Invoice posted') +\n ` (${call.result?.invoiceNumber ?? invoice.invoice_number})`,\n 'success',\n )\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.post.error', 'Post failed'), 'error')\n } finally {\n setPosting(false)\n }\n }, [invoice, load, t])\n\n const handleWipeTest = React.useCallback(async () => {\n if (!invoice) return\n const ok = await confirm({\n title: t('billing.invoices.wipe.confirm.title', 'Wipe test invoices?'),\n text: t(\n 'billing.invoices.wipe.confirm.text',\n 'Every test invoice from this Bill Run will be permanently deleted. This cannot be undone.',\n ),\n confirmText: t('billing.invoices.wipe.confirm.button', 'Delete test invoices'),\n variant: 'destructive',\n })\n if (!ok) return\n setWiping(true)\n try {\n const billRunId = invoice.metadata?.bill_run_id\n const call = await apiCallOrThrow<{ invoicesRemoved: number }>(\n '/api/billing/test-invoices/wipe',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(billRunId ? { billRunId } : {}),\n },\n )\n flash(\n t('billing.invoices.wipe.success', 'Test invoices removed') +\n ` (${call.result?.invoicesRemoved ?? 0})`,\n 'success',\n )\n // The invoice we were looking at is hard-deleted \u2014 navigate away\n // on success by reloading; the detail load will 404 and the\n // user clicks back.\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.wipe.error', 'Wipe failed'), 'error')\n } finally {\n setWiping(false)\n }\n }, [confirm, invoice, load, t])\n\n // \u2500\u2500\u2500 Line add / edit / remove \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n const handleSubmitLine = React.useCallback(\n async (values: LineFormValues) => {\n if (!invoice) return\n setDialogSubmitting(true)\n try {\n if (dialogMode === 'add') {\n await apiCallOrThrow('/api/billing/invoices/add-line', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n invoiceId: invoice.id,\n description: values.description,\n quantity: values.quantity,\n unitPriceNet: values.unitPriceNet,\n }),\n })\n flash(t('billing.invoices.lines.add.success', 'Line added'), 'success')\n } else if (dialogMode === 'edit' && dialogTargetLine) {\n await apiCallOrThrow('/api/billing/invoices/edit-line', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n invoiceId: invoice.id,\n invoiceLineId: dialogTargetLine.id,\n changes: {\n description: values.description,\n quantity: values.quantity,\n unitPriceNet: values.unitPriceNet,\n ...(values.totalNetAmount !== undefined\n ? { totalNetAmount: values.totalNetAmount }\n : {}),\n },\n }),\n })\n flash(t('billing.invoices.lines.edit.success', 'Line updated'), 'success')\n }\n setDialogMode(null)\n setDialogTargetLine(null)\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.lines.save.error', 'Save failed'), 'error')\n } finally {\n setDialogSubmitting(false)\n }\n },\n [dialogMode, dialogTargetLine, invoice, load, t],\n )\n\n const handleRemoveLine = React.useCallback(\n async (line: InvoiceLine) => {\n if (!invoice) return\n const ok = await confirm({\n title: t('billing.invoices.lines.remove.confirm.title', 'Remove line?'),\n text: t(\n 'billing.invoices.lines.remove.confirm.text',\n 'The line will be deleted and recorded in the draft-edit audit log.',\n ),\n confirmText: t('billing.invoices.lines.remove.confirm.button', 'Remove line'),\n variant: 'destructive',\n })\n if (!ok) return\n try {\n await apiCallOrThrow('/api/billing/invoices/remove-line', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n }),\n })\n flash(t('billing.invoices.lines.remove.success', 'Line removed'), 'success')\n void load()\n } catch (err) {\n const { message } = normalizeCrudServerError(err)\n flash(message || t('billing.invoices.lines.remove.error', 'Remove failed'), 'error')\n }\n },\n [confirm, invoice, load, t],\n )\n\n const canEditLines = invoice?.status === 'draft'\n\n const lineColumns = React.useMemo<ColumnDef<InvoiceLine>[]>(\n () => [\n {\n accessorKey: 'line_number',\n header: '#',\n },\n {\n accessorKey: 'description',\n header: t('billing.invoices.lines.columns.description', 'Description'),\n cell: ({ row }) => {\n const type = row.original.metadata?.billing_type\n const operatorAdded = row.original.metadata?.operator_added === true\n return (\n <div className=\"flex flex-col gap-1\">\n <span>{row.original.description ?? '\u2014'}</span>\n <div className=\"flex items-center gap-1 flex-wrap\">\n {type ? <Tag variant={lineKindVariant(type)}>{type}</Tag> : null}\n {operatorAdded ? (\n <Tag variant=\"info\">\n {t('billing.invoices.lines.operator_added', 'Operator-added')}\n </Tag>\n ) : null}\n </div>\n </div>\n )\n },\n },\n {\n accessorKey: 'quantity',\n header: t('billing.invoices.lines.columns.quantity', 'Qty'),\n },\n {\n accessorKey: 'unit_price_net',\n header: t('billing.invoices.lines.columns.unit_price', 'Unit price'),\n cell: ({ row }) =>\n formatMoney(row.original.unit_price_net, row.original.currency_code),\n },\n {\n accessorKey: 'total_net_amount',\n header: t('billing.invoices.lines.columns.total', 'Total net'),\n cell: ({ row }) =>\n formatMoney(row.original.total_net_amount, row.original.currency_code),\n },\n {\n id: 'actions',\n header: '',\n cell: ({ row }) => {\n if (!canEditLines) return null\n return (\n <div className=\"flex items-center justify-end gap-1\">\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n aria-label={t('billing.invoices.lines.actions.edit', 'Edit line')}\n onClick={() => {\n setDialogTargetLine(row.original)\n setDialogMode('edit')\n }}\n >\n <Pencil size={16} />\n </IconButton>\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n aria-label={t('billing.invoices.lines.actions.remove', 'Remove line')}\n onClick={() => void handleRemoveLine(row.original)}\n >\n <Trash2 size={16} />\n </IconButton>\n </div>\n )\n },\n },\n ],\n [canEditLines, handleRemoveLine, t],\n )\n\n const dialogInitial = React.useMemo<Partial<LineFormValues> | undefined>(() => {\n if (dialogMode !== 'edit' || !dialogTargetLine) return undefined\n return {\n description: dialogTargetLine.description ?? '',\n quantity: Number.parseFloat(dialogTargetLine.quantity),\n unitPriceNet: Number.parseFloat(dialogTargetLine.unit_price_net),\n }\n }, [dialogMode, dialogTargetLine])\n\n if (error) {\n return (\n <Page>\n <PageHeader title={t('billing.invoices.detail.title', 'Billing Invoice')} />\n <PageBody>\n <ErrorMessage label={error} />\n </PageBody>\n </Page>\n )\n }\n if (loading || !invoice) {\n return (\n <Page>\n <PageHeader title={t('billing.invoices.detail.title', 'Billing Invoice')} />\n <PageBody>\n <LoadingMessage label={t('billing.invoices.detail.loading', 'Loading invoice\u2026')} />\n </PageBody>\n </Page>\n )\n }\n\n const isTest = invoice.metadata?.test_run === true\n const isDraft = invoice.status === 'draft'\n\n return (\n <Page>\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/billing/invoices\"\n entityTypeLabel={t('billing.invoices.detail.title', 'Billing Invoice')}\n title={invoice.invoice_number}\n statusBadge={\n <div className=\"flex items-center gap-2 flex-wrap\">\n <Tag variant={statusVariant(invoice.status)}>{invoice.status ?? '\u2014'}</Tag>\n {isTest ? <Tag variant=\"warning\">TEST</Tag> : null}\n </div>\n }\n actionsContent={\n <>\n {isDraft && !isTest ? (\n <Button onClick={handlePost} disabled={posting}>\n {posting\n ? t('billing.invoices.post.in_progress', 'Posting\u2026')\n : t('billing.invoices.post.action', 'Post invoice')}\n </Button>\n ) : null}\n {isTest ? (\n <Button\n variant=\"outline\"\n onClick={handleWipeTest}\n disabled={wiping}\n >\n {wiping\n ? t('billing.invoices.wipe.in_progress', 'Wiping\u2026')\n : t('billing.invoices.wipe.action', 'Wipe test invoices for this run')}\n </Button>\n ) : null}\n </>\n }\n />\n <PageBody>\n <div className=\"space-y-6\">\n <DetailCard>\n <DetailField label={t('billing.invoices.detail.field.period', 'Bill period')}>\n {invoice.metadata?.bill_period_start ?? '\u2014'} \u2192{' '}\n {invoice.metadata?.bill_period_end ?? '\u2014'}\n </DetailField>\n <DetailField label={t('billing.invoices.detail.field.bill_run', 'Bill Run')}>\n {invoice.metadata?.bill_run_id ? (\n <Link\n href={`/backend/billing/runs/${invoice.metadata.bill_run_id}`}\n className=\"text-primary hover:underline\"\n >\n {runName ?? invoice.metadata.bill_run_id}\n </Link>\n ) : (\n '\u2014'\n )}\n </DetailField>\n <DetailField\n label={t('billing.invoices.detail.field.account', 'Billing Account')}\n >\n {invoice.metadata?.bill_account_id ? (\n <Link\n href={`/backend/billing/accounts/${invoice.metadata.bill_account_id}`}\n className=\"text-primary hover:underline\"\n >\n {accountName ?? invoice.metadata.bill_account_id}\n </Link>\n ) : (\n '\u2014'\n )}\n </DetailField>\n <DetailField label={t('billing.invoices.detail.field.issued', 'Issued')}>\n {invoice.issue_date?.slice(0, 10) ?? '\u2014'}\n </DetailField>\n <DetailField label={t('billing.common.id', 'ID')}>\n <span className=\"font-mono text-xs\">{invoice.id}</span>\n </DetailField>\n </DetailCard>\n\n <StatCard>\n <Stat\n label={t('billing.invoices.detail.subtotal', 'Subtotal net')}\n value={formatMoney(invoice.subtotal_net_amount, invoice.currency_code)}\n />\n <Stat\n label={t('billing.invoices.detail.total', 'Grand total')}\n value={formatMoney(invoice.grand_total_gross_amount, invoice.currency_code)}\n />\n <Stat\n label={t('billing.invoices.detail.paid', 'Paid')}\n value={formatMoney(invoice.paid_total_amount, invoice.currency_code)}\n />\n <Stat\n label={t('billing.invoices.detail.outstanding', 'Outstanding')}\n value={formatMoney(invoice.outstanding_amount, invoice.currency_code)}\n />\n </StatCard>\n\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <h2 className=\"text-base font-semibold\">\n {t('billing.invoices.lines.title', 'Lines')}\n </h2>\n {canEditLines ? (\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => {\n setDialogTargetLine(null)\n setDialogMode('add')\n }}\n >\n <Plus size={16} />\n {t('billing.invoices.lines.add.action', 'Add line')}\n </Button>\n ) : null}\n </div>\n <DataTable\n columns={lineColumns}\n data={visibleLines}\n isLoading={false}\n pagination={{\n page: linesPage,\n pageSize: linesPageSize,\n total: lines.length,\n totalPages: linesTotalPages,\n onPageChange: setLinesPage,\n }}\n />\n </div>\n </div>\n\n <LineFormDialog\n open={dialogMode !== null}\n mode={dialogMode ?? 'add'}\n initial={dialogInitial}\n submitting={dialogSubmitting}\n onCancel={() => {\n setDialogMode(null)\n setDialogTargetLine(null)\n }}\n onSubmit={handleSubmitLine}\n />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["billRunId"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/billing/backend/billing/invoices/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\n/**\n * Billing Invoices list \u2014 what the operator actually opens every\n * morning to \"review and post drafts\".\n *\n * Scope: only invoices the billing engine emitted (`metadata.bill_run_id`\n * set). Plain `core/sales` invoices (created by sales orders, etc.)\n * are NOT shown here \u2014 billing has its own surface so the operator's\n * review queue is small and focused.\n */\n\ntype BillingInvoiceRow = {\n id: string\n invoice_number: string\n status: 'draft' | 'posted' | 'paid' | 'void' | null\n currency_code: string\n issue_date: string | null\n due_date: string | null\n subtotal_net_amount: string\n grand_total_net_amount: string\n grand_total_gross_amount: string\n paid_total_amount: string\n outstanding_amount: string\n metadata: {\n bill_run_id?: string\n bill_account_id?: string\n bill_period_start?: string\n bill_period_end?: string\n test_run?: boolean\n } | null\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = {\n items: BillingInvoiceRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction statusVariant(\n status: BillingInvoiceRow['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'paid':\n return 'success'\n case 'posted':\n return 'warning'\n case 'void':\n return 'error'\n case 'draft':\n default:\n return 'default'\n }\n}\n\nfunction formatMoney(value: string, currency: string): string {\n const n = Number.parseFloat(value)\n if (!Number.isFinite(n)) return value\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n } catch {\n return `${value} ${currency}`\n }\n}\n\nexport default function BillingInvoicesListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<BillingInvoiceRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.invoices.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'draft', label: t('billing.invoices.status.draft', 'Draft') },\n { value: 'posted', label: t('billing.invoices.status.posted', 'Posted') },\n { value: 'paid', label: t('billing.invoices.status.paid', 'Paid') },\n { value: 'void', label: t('billing.invoices.status.void', 'Void') },\n ],\n },\n {\n id: 'testRun',\n label: t('billing.invoices.filters.test_run', 'Test runs'),\n type: 'select',\n options: [\n { value: 'true', label: t('billing.invoices.filters.test_run.only', 'Only test') },\n {\n value: 'false',\n label: t('billing.invoices.filters.test_run.exclude', 'Exclude test'),\n },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.status === 'string' && filters.status) {\n params.set('status', filters.status)\n }\n if (filters.testRun === 'true' || filters.testRun === 'false') {\n params.set('testRun', String(filters.testRun))\n }\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/invoices?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [filters.status, filters.testRun, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<BillingInvoiceRow>[]>(\n () => [\n {\n accessorKey: 'invoice_number',\n header: t('billing.invoices.columns.number', 'Number'),\n cell: ({ row }) => {\n const isTest = row.original.metadata?.test_run === true\n return (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-primary\">\n {row.original.invoice_number}\n </span>\n {isTest ? <Tag variant=\"warning\">TEST</Tag> : null}\n </div>\n )\n },\n },\n {\n accessorKey: 'status',\n header: t('billing.invoices.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={statusVariant(row.original.status)}>{row.original.status ?? '\u2014'}</Tag>\n ),\n },\n {\n id: 'period',\n header: t('billing.invoices.columns.period', 'Period'),\n cell: ({ row }) => {\n const start = row.original.metadata?.bill_period_start\n const end = row.original.metadata?.bill_period_end\n if (!start || !end) return '\u2014'\n return (\n <span className=\"text-xs\">\n {start} \u2192 {end}\n </span>\n )\n },\n },\n {\n id: 'total',\n header: t('billing.invoices.columns.total', 'Total'),\n cell: ({ row }) =>\n formatMoney(row.original.grand_total_gross_amount, row.original.currency_code),\n },\n {\n accessorKey: 'outstanding_amount',\n header: t('billing.invoices.columns.outstanding', 'Outstanding'),\n cell: ({ row }) =>\n formatMoney(row.original.outstanding_amount, row.original.currency_code),\n },\n {\n accessorKey: 'issue_date',\n header: t('billing.invoices.columns.issued', 'Issued'),\n cell: ({ row }) => formatDate(row.original.issue_date),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.invoices.title', 'Billing Invoices')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-invoices' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/invoices/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\n/**\n * Billing Invoices list \u2014 what the operator actually opens every\n * morning to \"review and post drafts\".\n *\n * Scope: only invoices the billing engine emitted (`metadata.bill_run_id`\n * set). Plain `core/sales` invoices (created by sales orders, etc.)\n * are NOT shown here \u2014 billing has its own surface so the operator's\n * review queue is small and focused.\n */\n\ntype BillingInvoiceRow = {\n id: string\n invoice_number: string\n status: 'draft' | 'posted' | 'paid' | 'void' | null\n currency_code: string\n issue_date: string | null\n due_date: string | null\n subtotal_net_amount: string\n grand_total_net_amount: string\n grand_total_gross_amount: string\n paid_total_amount: string\n outstanding_amount: string\n metadata: {\n bill_run_id?: string\n bill_account_id?: string\n bill_period_start?: string\n bill_period_end?: string\n test_run?: boolean\n } | null\n created_at: string\n updated_at: string\n}\n\ntype ListResponse = {\n items: BillingInvoiceRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction statusVariant(\n status: BillingInvoiceRow['status'],\n): 'default' | 'success' | 'warning' | 'error' {\n switch (status) {\n case 'paid':\n return 'success'\n case 'posted':\n return 'warning'\n case 'void':\n return 'error'\n case 'draft':\n default:\n return 'default'\n }\n}\n\nfunction formatMoney(value: string, currency: string): string {\n const n = Number.parseFloat(value)\n if (!Number.isFinite(n)) return value\n try {\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(n)\n } catch {\n return `${value} ${currency}`\n }\n}\n\nexport default function BillingInvoicesListPage() {\n const t = useT()\n const router = useRouter()\n const [rows, setRows] = React.useState<BillingInvoiceRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [search, setSearch] = React.useState('')\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'status',\n label: t('billing.invoices.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'draft', label: t('billing.invoices.status.draft', 'Draft') },\n { value: 'posted', label: t('billing.invoices.status.posted', 'Posted') },\n { value: 'paid', label: t('billing.invoices.status.paid', 'Paid') },\n { value: 'void', label: t('billing.invoices.status.void', 'Void') },\n ],\n },\n {\n id: 'testRun',\n label: t('billing.invoices.filters.test_run', 'Test runs'),\n type: 'select',\n options: [\n { value: 'true', label: t('billing.invoices.filters.test_run.only', 'Only test') },\n {\n value: 'false',\n label: t('billing.invoices.filters.test_run.exclude', 'Exclude test'),\n },\n ],\n },\n ],\n [t],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.status === 'string' && filters.status) {\n params.set('status', filters.status)\n }\n if (filters.testRun === 'true' || filters.testRun === 'false') {\n params.set('testRun', String(filters.testRun))\n }\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/invoices?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [filters.status, filters.testRun, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<BillingInvoiceRow>[]>(\n () => [\n {\n accessorKey: 'invoice_number',\n header: t('billing.invoices.columns.number', 'Number'),\n cell: ({ row }) => {\n const isTest = row.original.metadata?.test_run === true\n return (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-primary\">\n {row.original.invoice_number}\n </span>\n {isTest ? <Tag variant=\"warning\">TEST</Tag> : null}\n </div>\n )\n },\n },\n {\n accessorKey: 'status',\n header: t('billing.invoices.columns.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={statusVariant(row.original.status)}>{row.original.status ?? '\u2014'}</Tag>\n ),\n },\n {\n id: 'period',\n header: t('billing.invoices.columns.period', 'Period'),\n cell: ({ row }) => {\n const start = row.original.metadata?.bill_period_start\n const end = row.original.metadata?.bill_period_end\n if (!start || !end) return '\u2014'\n return (\n <span className=\"text-xs\">\n {start} \u2192 {end}\n </span>\n )\n },\n },\n {\n id: 'total',\n header: t('billing.invoices.columns.total', 'Total'),\n cell: ({ row }) =>\n formatMoney(row.original.grand_total_gross_amount, row.original.currency_code),\n },\n {\n accessorKey: 'outstanding_amount',\n header: t('billing.invoices.columns.outstanding', 'Outstanding'),\n cell: ({ row }) =>\n formatMoney(row.original.outstanding_amount, row.original.currency_code),\n },\n {\n accessorKey: 'issue_date',\n header: t('billing.invoices.columns.issued', 'Issued'),\n cell: ({ row }) => formatDate(row.original.issue_date),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.invoices.title', 'Billing Invoices')}\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-invoices' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/invoices/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/billing/backend/billing/items/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Plus } from 'lucide-react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\n/**\n * Billing Items admin list.\n *\n * Filters: type (one_time / recurring / usage), bill account, active.\n * Items are typically integration-driven (CPQ connector creates them\n * on subscription events), so the most common UI use is *inspecting*\n * what's there \u2014 but the \"New item\" CTA is available for manual\n * entry / one-off adjustments.\n */\n\n// API list rows are snake_case (see `api/items/route.ts` `fields`).\ntype BillingItemRow = {\n id: string\n bill_account_id: string\n bill_account_name: string | null\n type: 'one_time' | 'recurring' | 'usage'\n description: string\n bill_start_date: string\n bill_end_date: string | null\n uom_code: string | null\n subscription_id: string | null\n source_ref: string | null\n is_active: boolean\n created_at: string\n}\n\ntype ListResponse = {\n items: BillingItemRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction typeVariant(\n type: BillingItemRow['type'],\n): 'default' | 'success' | 'warning' | 'info' {\n switch (type) {\n case 'recurring':\n return 'success'\n case 'one_time':\n return 'warning'\n case 'usage':\n return 'info'\n default:\n return 'default'\n }\n}\n\nexport default function BillingItemsListPage() {\n const t = useT()\n const router = useRouter()\n const searchParams = useSearchParams()\n // Deep-link from the account detail page: `?billAccountId=<id>` pre-\n // filters the list so the operator lands on \"items for this account\".\n const initialAccountId = searchParams.get('billAccountId') ?? ''\n\n const [rows, setRows] = React.useState<BillingItemRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>(\n initialAccountId ? { billAccountId: initialAccountId } : {},\n )\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [search, setSearch] = React.useState('')\n\n // Resolve account id \u2192 name from the loaded rows (each carries\n // `bill_account_name` from the items route's afterList hook) so the\n // active `billAccountId` filter chip shows a name, not a UUID.\n const accountNames = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const r of rows) {\n if (r.bill_account_name) map[r.bill_account_id] = r.bill_account_name\n }\n return map\n }, [rows])\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'type',\n label: t('billing.items.filters.type', 'Type'),\n type: 'select',\n options: [\n { value: 'one_time', label: t('billing.items.type.one_time', 'One-time') },\n { value: 'recurring', label: t('billing.items.type.recurring', 'Recurring') },\n { value: 'usage', label: t('billing.items.type.usage', 'Usage') },\n ],\n },\n {\n id: 'billAccountId',\n label: t('billing.items.filters.account', 'Billing Account'),\n type: 'text',\n formatValue: (id: string) => accountNames[id] ?? id,\n },\n {\n id: 'subscriptionId',\n label: t('billing.items.filters.subscription', 'Subscription ID'),\n type: 'text',\n },\n {\n id: 'isActive',\n label: t('billing.items.filters.active', 'Active'),\n type: 'select',\n options: [\n { value: 'true', label: t('billing.common.active', 'Active') },\n { value: 'false', label: t('billing.common.inactive', 'Inactive') },\n ],\n },\n ],\n [t, accountNames],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.type === 'string' && filters.type) {\n params.set('type', filters.type)\n }\n if (typeof filters.billAccountId === 'string' && filters.billAccountId.trim()) {\n params.set('billAccountId', filters.billAccountId.trim())\n }\n if (typeof filters.subscriptionId === 'string' && filters.subscriptionId.trim()) {\n params.set('subscriptionId', filters.subscriptionId.trim())\n }\n if (filters.isActive === 'true' || filters.isActive === 'false') {\n params.set('isActive', String(filters.isActive))\n }\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/items?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [filters.billAccountId, filters.isActive, filters.subscriptionId, filters.type, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<BillingItemRow>[]>(\n () => [\n {\n accessorKey: 'description',\n header: t('billing.items.columns.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {row.original.description}\n </span>\n ),\n },\n {\n accessorKey: 'type',\n header: t('billing.items.columns.type', 'Type'),\n cell: ({ row }) => (\n <Tag variant={typeVariant(row.original.type)}>{row.original.type}</Tag>\n ),\n },\n {\n accessorKey: 'bill_account_id',\n header: t('billing.items.columns.account', 'Account'),\n cell: ({ row }) => (\n <Link\n href={`/backend/billing/accounts/${row.original.bill_account_id}`}\n className=\"text-sm text-primary hover:underline\"\n onClick={(e) => e.stopPropagation()}\n >\n {row.original.bill_account_name ?? row.original.bill_account_id}\n </Link>\n ),\n },\n {\n accessorKey: 'bill_start_date',\n header: t('billing.items.columns.start', 'Start'),\n cell: ({ row }) => formatDate(row.original.bill_start_date),\n },\n {\n accessorKey: 'bill_end_date',\n header: t('billing.items.columns.end', 'End'),\n cell: ({ row }) => formatDate(row.original.bill_end_date),\n },\n {\n accessorKey: 'uom_code',\n header: t('billing.items.columns.uom', 'UoM'),\n cell: ({ row }) => row.original.uom_code ?? '\u2014',\n },\n {\n accessorKey: 'is_active',\n header: t('billing.items.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.items.title', 'Billing Items')}\n actions={\n <Button asChild>\n <Link href=\"/backend/billing/items/create\">\n <Plus size={16} />\n {t('billing.items.create.action', 'New item')}\n </Link>\n </Button>\n }\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-items' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/items/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter, useSearchParams } from 'next/navigation'\nimport { Plus } from 'lucide-react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\n/**\n * Billing Items admin list.\n *\n * Filters: type (one_time / recurring / usage), bill account, active.\n * Items are typically integration-driven (CPQ connector creates them\n * on subscription events), so the most common UI use is *inspecting*\n * what's there \u2014 but the \"New item\" CTA is available for manual\n * entry / one-off adjustments.\n */\n\n// API list rows are snake_case (see `api/items/route.ts` `fields`).\ntype BillingItemRow = {\n id: string\n bill_account_id: string\n bill_account_name: string | null\n type: 'one_time' | 'recurring' | 'usage'\n description: string\n bill_start_date: string\n bill_end_date: string | null\n uom_code: string | null\n subscription_id: string | null\n source_ref: string | null\n is_active: boolean\n created_at: string\n}\n\ntype ListResponse = {\n items: BillingItemRow[]\n total: number\n totalPages: number\n}\n\nfunction formatDate(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString()\n}\n\nfunction typeVariant(\n type: BillingItemRow['type'],\n): 'default' | 'success' | 'warning' | 'info' {\n switch (type) {\n case 'recurring':\n return 'success'\n case 'one_time':\n return 'warning'\n case 'usage':\n return 'info'\n default:\n return 'default'\n }\n}\n\nexport default function BillingItemsListPage() {\n const t = useT()\n const router = useRouter()\n const searchParams = useSearchParams()\n // Deep-link from the account detail page: `?billAccountId=<id>` pre-\n // filters the list so the operator lands on \"items for this account\".\n const initialAccountId = searchParams.get('billAccountId') ?? ''\n\n const [rows, setRows] = React.useState<BillingItemRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(25)\n const [filters, setFilters] = React.useState<FilterValues>(\n initialAccountId ? { billAccountId: initialAccountId } : {},\n )\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(true)\n const [search, setSearch] = React.useState('')\n\n // Resolve account id \u2192 name from the loaded rows (each carries\n // `bill_account_name` from the items route's afterList hook) so the\n // active `billAccountId` filter chip shows a name, not a UUID.\n const accountNames = React.useMemo(() => {\n const map: Record<string, string> = {}\n for (const r of rows) {\n if (r.bill_account_name) map[r.bill_account_id] = r.bill_account_name\n }\n return map\n }, [rows])\n\n const filterDefs = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'type',\n label: t('billing.items.filters.type', 'Type'),\n type: 'select',\n options: [\n { value: 'one_time', label: t('billing.items.type.one_time', 'One-time') },\n { value: 'recurring', label: t('billing.items.type.recurring', 'Recurring') },\n { value: 'usage', label: t('billing.items.type.usage', 'Usage') },\n ],\n },\n {\n id: 'billAccountId',\n label: t('billing.items.filters.account', 'Billing Account'),\n type: 'text',\n formatValue: (id: string) => accountNames[id] ?? id,\n },\n {\n id: 'subscriptionId',\n label: t('billing.items.filters.subscription', 'Subscription ID'),\n type: 'text',\n },\n {\n id: 'isActive',\n label: t('billing.items.filters.active', 'Active'),\n type: 'select',\n options: [\n { value: 'true', label: t('billing.common.active', 'Active') },\n { value: 'false', label: t('billing.common.inactive', 'Inactive') },\n ],\n },\n ],\n [t, accountNames],\n )\n\n const loadRows = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(pageSize),\n sortField: 'createdAt',\n sortDir: 'desc',\n })\n if (search.trim()) params.set('search', search.trim())\n if (typeof filters.type === 'string' && filters.type) {\n params.set('type', filters.type)\n }\n if (typeof filters.billAccountId === 'string' && filters.billAccountId.trim()) {\n params.set('billAccountId', filters.billAccountId.trim())\n }\n if (typeof filters.subscriptionId === 'string' && filters.subscriptionId.trim()) {\n params.set('subscriptionId', filters.subscriptionId.trim())\n }\n if (filters.isActive === 'true' || filters.isActive === 'false') {\n params.set('isActive', String(filters.isActive))\n }\n const result = await readApiResultOrThrow<ListResponse>(\n `/api/billing/items?${params.toString()}`,\n )\n setRows(result.items ?? [])\n setTotal(result.total ?? 0)\n setTotalPages(result.totalPages ?? 1)\n } finally {\n setLoading(false)\n }\n }, [filters.billAccountId, filters.isActive, filters.subscriptionId, filters.type, page, pageSize, search])\n\n React.useEffect(() => {\n void loadRows()\n }, [loadRows])\n\n const columns = React.useMemo<ColumnDef<BillingItemRow>[]>(\n () => [\n {\n accessorKey: 'description',\n header: t('billing.items.columns.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-sm font-medium text-primary\">\n {row.original.description}\n </span>\n ),\n },\n {\n accessorKey: 'type',\n header: t('billing.items.columns.type', 'Type'),\n cell: ({ row }) => (\n <Tag variant={typeVariant(row.original.type)}>{row.original.type}</Tag>\n ),\n },\n {\n accessorKey: 'bill_account_id',\n header: t('billing.items.columns.account', 'Account'),\n cell: ({ row }) => (\n <Link\n href={`/backend/billing/accounts/${row.original.bill_account_id}`}\n className=\"text-sm text-primary hover:underline\"\n onClick={(e) => e.stopPropagation()}\n >\n {row.original.bill_account_name ?? row.original.bill_account_id}\n </Link>\n ),\n },\n {\n accessorKey: 'bill_start_date',\n header: t('billing.items.columns.start', 'Start'),\n cell: ({ row }) => formatDate(row.original.bill_start_date),\n },\n {\n accessorKey: 'bill_end_date',\n header: t('billing.items.columns.end', 'End'),\n cell: ({ row }) => formatDate(row.original.bill_end_date),\n },\n {\n accessorKey: 'uom_code',\n header: t('billing.items.columns.uom', 'UoM'),\n cell: ({ row }) => row.original.uom_code ?? '\u2014',\n },\n {\n accessorKey: 'is_active',\n header: t('billing.items.columns.status', 'Status'),\n cell: ({ row }) =>\n row.original.is_active ? (\n <Tag variant=\"success\">{t('billing.common.active', 'Active')}</Tag>\n ) : (\n <Tag variant=\"default\">{t('billing.common.inactive', 'Inactive')}</Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('billing.items.title', 'Billing Items')}\n actions={\n <Button asChild>\n <Link href=\"/backend/billing/items/create\">\n <Plus size={16} />\n {t('billing.items.create.action', 'New item')}\n </Link>\n </Button>\n }\n refreshButton={{\n label: t('billing.common.refresh', 'Refresh'),\n onRefresh: loadRows,\n isRefreshing: loading,\n }}\n columnChooser={{ auto: true }}\n perspective={{ tableId: 'billing-items' }}\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/billing/items/${row.id}`)}\n isLoading={loading}\n pagination={{\n page,\n pageSize,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n filters={filterDefs}\n filterValues={filters}\n onFiltersApply={setFilters}\n onFiltersClear={() => setFilters({})}\n searchValue={search}\n onSearchChange={setSearch}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|