@dainamite/cpq 0.4.0 → 0.5.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 +22 -0
- package/MIGRATION.md +27 -0
- package/dist/modules/cpq/api/_helpers/handleArcError.js +2 -2
- package/dist/modules/cpq/api/_helpers/handleArcError.js.map +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/activate/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/route.js +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js +2 -2
- package/dist/modules/cpq/api/orders/[orderId]/status/route.js.map +2 -2
- package/dist/modules/cpq/api/orders/route.js +2 -2
- package/dist/modules/cpq/api/orders/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/clone/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js +3 -3
- package/dist/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/recalculate/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js +2 -2
- package/dist/modules/cpq/api/quotes/[quoteId]/route.js.map +2 -2
- package/dist/modules/cpq/api/quotes/route.js +2 -2
- package/dist/modules/cpq/api/quotes/route.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/inventory/AssetsTable.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/inventory/SubscriptionsTable.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/offerings/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/orders/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/price-rules/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/pricing/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/quotes/[id]/page.js.map +2 -2
- package/dist/modules/cpq/backend/cpq/quotes/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/specifications/page.js.map +1 -1
- package/dist/modules/cpq/backend/cpq/wizards/page.js.map +1 -1
- package/dist/modules/cpq/components/CpqListView.js.map +2 -2
- package/dist/modules/cpq/services/cpqOrderService.js +9 -2
- package/dist/modules/cpq/services/cpqOrderService.js.map +2 -2
- package/dist/modules/cpq/services/cpqQuotingService.js +9 -2
- package/dist/modules/cpq/services/cpqQuotingService.js.map +2 -2
- package/package.json +9 -9
- package/src/modules/cpq/__integration__/TC-CPQ-002.spec.ts +8 -5
- package/src/modules/cpq/api/_helpers/handleArcError.ts +12 -12
- package/src/modules/cpq/api/orders/[orderId]/activate/route.ts +2 -2
- package/src/modules/cpq/api/orders/[orderId]/route.ts +2 -2
- package/src/modules/cpq/api/orders/[orderId]/status/route.ts +2 -2
- package/src/modules/cpq/api/orders/route.ts +2 -2
- package/src/modules/cpq/api/quotes/[quoteId]/clone/route.ts +2 -2
- package/src/modules/cpq/api/quotes/[quoteId]/items/[lineId]/route.ts +3 -3
- package/src/modules/cpq/api/quotes/[quoteId]/recalculate/route.ts +2 -2
- package/src/modules/cpq/api/quotes/[quoteId]/route.ts +2 -2
- package/src/modules/cpq/api/quotes/route.ts +2 -2
- package/src/modules/cpq/backend/cpq/inventory/AssetsTable.tsx +1 -1
- package/src/modules/cpq/backend/cpq/inventory/SubscriptionsTable.tsx +1 -1
- package/src/modules/cpq/backend/cpq/offerings/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/orders/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/price-rules/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/pricing/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/quotes/[id]/page.tsx +4 -3
- package/src/modules/cpq/backend/cpq/quotes/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/specifications/page.tsx +1 -1
- package/src/modules/cpq/backend/cpq/wizards/page.tsx +1 -1
- package/src/modules/cpq/components/CpqListView.tsx +2 -1
- package/src/modules/cpq/services/__tests__/errorBrands.test.ts +72 -0
- package/src/modules/cpq/services/cpqOrderService.ts +10 -0
- package/src/modules/cpq/services/cpqQuotingService.ts +14 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/cpq/backend/cpq/quotes/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Plus, Search as SearchIcon } from 'lucide-react'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { CpqListView, useCpqListData } from '../../../components/CpqListView'\nimport {\n formatStatusLabel,\n quoteCpqStatusMap,\n type QuoteCpqStatus,\n} from '../../../components/statusMaps'\n\ntype QuoteConfig = {\n id: string\n quoteId: string\n /** Resolved from SalesQuote.quoteNumber by the list API. */\n quoteNumber: string | null\n customerId: string\n cpqStatus: string\n version: number\n currencyCode: string\n pricingSummary: {\n nrcTotal?: number\n mrcTotal?: number\n } | null\n createdAt: string\n updatedAt: string\n}\n\ntype Customer = {\n id: string\n display_name?: string\n displayName?: string\n name?: string\n companyName?: string\n primary_email?: string\n primaryEmail?: string\n}\n\nconst PAGE_SIZE = 50\n\nfunction fmt(amount: number | undefined, currency: string): string {\n if (amount == null || amount === 0) return '\u2014'\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)\n}\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.cpqStatus === 'string' && values.cpqStatus) {\n params.set('cpqStatus', values.cpqStatus)\n }\n if (typeof values.currencyCode === 'string' && values.currencyCode.trim()) {\n params.set('currencyCode', values.currencyCode.trim().toUpperCase())\n }\n}\n\n// \u2500\u2500\u2500 Customer picker (extracted to keep main page focused) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype CustomerPickerProps = {\n open: boolean\n onClose: () => void\n onSelect: (customer: Customer) => void | Promise<void>\n creating: boolean\n error: string | null\n}\n\nfunction getCustomerLabel(c: Customer) {\n return c.display_name || c.displayName || c.name || c.companyName || 'Unnamed'\n}\n\nfunction getCustomerEmail(c: Customer) {\n return c.primary_email || c.primaryEmail || ''\n}\n\nfunction CustomerPickerModal({ open, onClose, onSelect, creating, error }: CustomerPickerProps) {\n const [customers, setCustomers] = React.useState<Customer[]>([])\n const [customersLoading, setCustomersLoading] = React.useState(false)\n const [search, setSearch] = React.useState('')\n\n React.useEffect(() => {\n if (!open) return\n let cancelled = false\n setSearch('')\n setCustomersLoading(true)\n fetch('/api/customers/companies?pageSize=100')\n .then((r) => (r.ok ? r.json() : { items: [] }))\n .then((data) => {\n if (!cancelled) setCustomers(data.items ?? data ?? [])\n })\n .catch(() => {\n if (!cancelled) setCustomers([])\n })\n .finally(() => {\n if (!cancelled) setCustomersLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [open])\n\n if (!open) return null\n\n const filtered = customers.filter((c) => {\n if (!search.trim()) return true\n const q = search.toLowerCase()\n return (\n getCustomerLabel(c).toLowerCase().includes(q) ||\n getCustomerEmail(c).toLowerCase().includes(q) ||\n c.id.toLowerCase().includes(q)\n )\n })\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50\"\n onClick={() => !creating && onClose()}\n >\n <div\n className=\"w-full max-w-lg rounded-lg border bg-card shadow-xl\"\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"text-sm font-medium\">Select Customer for New Quote</h3>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={creating}\n className=\"text-muted-foreground hover:text-foreground text-lg font-bold leading-none cursor-pointer disabled:cursor-not-allowed\"\n >\n ×\n </button>\n </div>\n <div className=\"p-4 space-y-3\">\n {error && <Alert variant=\"destructive\">{error}</Alert>}\n <div className=\"relative\">\n <SearchIcon className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search customers\u2026\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n autoFocus\n className=\"pl-10\"\n />\n </div>\n <div className=\"max-h-72 overflow-y-auto space-y-1\">\n {customersLoading ? (\n <div className=\"py-8 text-center text-sm text-muted-foreground\">\n Loading customers\u2026\n </div>\n ) : filtered.length === 0 ? (\n <div className=\"py-8 text-center text-sm text-muted-foreground\">\n {search ? 'No customers match your search.' : 'No customers found.'}\n </div>\n ) : (\n filtered.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => onSelect(c)}\n disabled={creating}\n className=\"w-full rounded-md border px-3 py-2.5 text-left hover:border-primary hover:bg-primary/5 transition-colors disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed\"\n >\n <p className=\"text-sm font-medium\">{getCustomerLabel(c)}</p>\n {getCustomerEmail(c) && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{getCustomerEmail(c)}</p>\n )}\n <p className=\"text-xs text-muted-foreground font-mono mt-0.5\">{c.id}</p>\n </button>\n ))\n )}\n </div>\n </div>\n </div>\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Page \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default function CpqQuotesListPage() {\n const router = useRouter()\n const t = useT()\n\n const data = useCpqListData<QuoteConfig>({\n endpoint: '/api/cpq/quotes',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.quotes.list.error.load', 'Failed to load quotes'),\n })\n\n const [showCustomerPicker, setShowCustomerPicker] = React.useState(false)\n const [creating, setCreating] = React.useState(false)\n const [createError, setCreateError] = React.useState<string | null>(null)\n\n const statusOptions = React.useMemo(\n () => [\n { value: 'new', label: 'New' },\n { value: 'incomplete', label: 'Incomplete' },\n { value: 'ready', label: 'Ready' },\n { value: 'in_approval', label: 'In Approval' },\n { value: 'approved', label: 'Approved' },\n { value: 'with_customer', label: 'With Customer' },\n { value: 'accepted', label: 'Accepted' },\n { value: 'rejected', label: 'Rejected' },\n { value: 'cancelled', label: 'Cancelled' },\n ],\n [],\n )\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'cpqStatus',\n label: t('cpq.quotes.filters.status', 'Status'),\n type: 'select',\n options: statusOptions,\n },\n {\n id: 'currencyCode',\n label: t('cpq.quotes.filters.currency', 'Currency'),\n type: 'text',\n },\n ],\n [statusOptions, t],\n )\n\n const selectCustomer = React.useCallback(\n async (customer: Customer) => {\n setCreating(true)\n setCreateError(null)\n try {\n const res = await fetch('/api/cpq/quotes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ customerId: customer.id }),\n })\n const body = await res.json()\n if (res.ok) {\n setShowCustomerPicker(false)\n router.push(`/backend/cpq/quotes/${body.id}`)\n } else {\n setCreateError(body.error ?? `Failed to create quote (${res.status})`)\n }\n } catch (err) {\n setCreateError((err as Error).message)\n } finally {\n setCreating(false)\n }\n },\n [router],\n )\n\n const columns = React.useMemo<ColumnDef<QuoteConfig>[]>(\n () => [\n {\n accessorKey: 'quoteNumber',\n header: t('cpq.quotes.table.quoteNumber', 'Quote Number'),\n cell: ({ row }) => (\n <span className=\"font-medium\">\n {row.original.quoteNumber ?? (\n <span className=\"font-mono text-xs text-muted-foreground\">\n {(row.original.quoteId ?? row.original.id).slice(0, 8)}\u2026\n </span>\n )}\n </span>\n ),\n },\n {\n accessorKey: 'cpqStatus',\n header: t('cpq.quotes.table.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={quoteCpqStatusMap[row.original.cpqStatus as QuoteCpqStatus] ?? 'neutral'} dot>\n {formatStatusLabel(row.original.cpqStatus)}\n </Tag>\n ),\n },\n {\n accessorKey: 'version',\n header: t('cpq.quotes.table.version', 'Version'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">v{row.original.version}</span>,\n },\n {\n accessorKey: 'currencyCode',\n header: t('cpq.quotes.table.currency', 'Currency'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">{row.original.currencyCode}</span>,\n },\n {\n id: 'nrcTotal',\n header: t('cpq.quotes.table.nrcTotal', 'NRC Total'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">\n {fmt(row.original.pricingSummary?.nrcTotal, row.original.currencyCode)}\n </span>\n ),\n },\n {\n id: 'mrcTotal',\n header: t('cpq.quotes.table.mrcTotal', 'MRC Total'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">\n {fmt(row.original.pricingSummary?.mrcTotal, row.original.currencyCode)}\n </span>\n ),\n },\n {\n accessorKey: 'createdAt',\n header: t('cpq.quotes.table.createdAt', 'Created'),\n cell: ({ row }) => (\n <span className=\"text-xs text-muted-foreground\">\n {new Date(row.original.createdAt).toLocaleDateString()}\n </span>\n ),\n },\n ],\n [t],\n )\n\n return (\n <CpqListView<QuoteConfig>\n title={t('cpq.quotes.list.title', 'CPQ Quotes')}\n tableId=\"cpq.quotes.list\"\n data={data}\n columns={columns}\n filters={filters}\n pageSize={PAGE_SIZE}\n searchPlaceholder={t('cpq.quotes.search.placeholder', 'Search by quote / customer id...')}\n actions={\n <Button type=\"button\" onClick={() => setShowCustomerPicker(true)} className=\"gap-1.5\">\n <Plus className=\"h-4 w-4\" />\n {t('cpq.quotes.add', 'New Quote')}\n </Button>\n }\n onRowClick={(row) => router.push(`/backend/cpq/quotes/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'open',\n label: t('cpq.quotes.actions.open', 'Open'),\n href: `/backend/cpq/quotes/${row.id}`,\n },\n ]}\n />\n )}\n toolbarContent={\n <CustomerPickerModal\n open={showCustomerPicker}\n onClose={() => setShowCustomerPicker(false)}\n onSelect={selectCustomer}\n creating={creating}\n error={createError}\n />\n }\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t(\n 'cpq.quotes.empty',\n 'No CPQ quotes found. Create one to start configuring offerings for a customer.',\n )}\n </div>\n }\n />\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Plus, Search as SearchIcon } from 'lucide-react'\nimport { Alert } from '@open-mercato/ui/primitives/alert'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { CpqListView, useCpqListData } from '../../../components/CpqListView'\nimport {\n formatStatusLabel,\n quoteCpqStatusMap,\n type QuoteCpqStatus,\n} from '../../../components/statusMaps'\n\ntype QuoteConfig = {\n id: string\n quoteId: string\n /** Resolved from SalesQuote.quoteNumber by the list API. */\n quoteNumber: string | null\n customerId: string\n cpqStatus: string\n version: number\n currencyCode: string\n pricingSummary: {\n nrcTotal?: number\n mrcTotal?: number\n } | null\n createdAt: string\n updatedAt: string\n}\n\ntype Customer = {\n id: string\n display_name?: string\n displayName?: string\n name?: string\n companyName?: string\n primary_email?: string\n primaryEmail?: string\n}\n\nconst PAGE_SIZE = 50\n\nfunction fmt(amount: number | undefined, currency: string): string {\n if (amount == null || amount === 0) return '\u2014'\n return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)\n}\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.cpqStatus === 'string' && values.cpqStatus) {\n params.set('cpqStatus', values.cpqStatus)\n }\n if (typeof values.currencyCode === 'string' && values.currencyCode.trim()) {\n params.set('currencyCode', values.currencyCode.trim().toUpperCase())\n }\n}\n\n// \u2500\u2500\u2500 Customer picker (extracted to keep main page focused) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype CustomerPickerProps = {\n open: boolean\n onClose: () => void\n onSelect: (customer: Customer) => void | Promise<void>\n creating: boolean\n error: string | null\n}\n\nfunction getCustomerLabel(c: Customer) {\n return c.display_name || c.displayName || c.name || c.companyName || 'Unnamed'\n}\n\nfunction getCustomerEmail(c: Customer) {\n return c.primary_email || c.primaryEmail || ''\n}\n\nfunction CustomerPickerModal({ open, onClose, onSelect, creating, error }: CustomerPickerProps) {\n const [customers, setCustomers] = React.useState<Customer[]>([])\n const [customersLoading, setCustomersLoading] = React.useState(false)\n const [search, setSearch] = React.useState('')\n\n React.useEffect(() => {\n if (!open) return\n let cancelled = false\n setSearch('')\n setCustomersLoading(true)\n fetch('/api/customers/companies?pageSize=100')\n .then((r) => (r.ok ? r.json() : { items: [] }))\n .then((data) => {\n if (!cancelled) setCustomers(data.items ?? data ?? [])\n })\n .catch(() => {\n if (!cancelled) setCustomers([])\n })\n .finally(() => {\n if (!cancelled) setCustomersLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [open])\n\n if (!open) return null\n\n const filtered = customers.filter((c) => {\n if (!search.trim()) return true\n const q = search.toLowerCase()\n return (\n getCustomerLabel(c).toLowerCase().includes(q) ||\n getCustomerEmail(c).toLowerCase().includes(q) ||\n c.id.toLowerCase().includes(q)\n )\n })\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50\"\n onClick={() => !creating && onClose()}\n >\n <div\n className=\"w-full max-w-lg rounded-lg border bg-card shadow-xl\"\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"flex items-center justify-between border-b px-4 py-3\">\n <h3 className=\"text-sm font-medium\">Select Customer for New Quote</h3>\n <button\n type=\"button\"\n onClick={onClose}\n disabled={creating}\n className=\"text-muted-foreground hover:text-foreground text-lg font-bold leading-none cursor-pointer disabled:cursor-not-allowed\"\n >\n ×\n </button>\n </div>\n <div className=\"p-4 space-y-3\">\n {error && <Alert variant=\"destructive\">{error}</Alert>}\n <div className=\"relative\">\n <SearchIcon className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground\" />\n <Input\n placeholder=\"Search customers\u2026\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n autoFocus\n className=\"pl-10\"\n />\n </div>\n <div className=\"max-h-72 overflow-y-auto space-y-1\">\n {customersLoading ? (\n <div className=\"py-8 text-center text-sm text-muted-foreground\">\n Loading customers\u2026\n </div>\n ) : filtered.length === 0 ? (\n <div className=\"py-8 text-center text-sm text-muted-foreground\">\n {search ? 'No customers match your search.' : 'No customers found.'}\n </div>\n ) : (\n filtered.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => onSelect(c)}\n disabled={creating}\n className=\"w-full rounded-md border px-3 py-2.5 text-left hover:border-primary hover:bg-primary/5 transition-colors disabled:opacity-50 cursor-pointer disabled:cursor-not-allowed\"\n >\n <p className=\"text-sm font-medium\">{getCustomerLabel(c)}</p>\n {getCustomerEmail(c) && (\n <p className=\"text-xs text-muted-foreground mt-0.5\">{getCustomerEmail(c)}</p>\n )}\n <p className=\"text-xs text-muted-foreground font-mono mt-0.5\">{c.id}</p>\n </button>\n ))\n )}\n </div>\n </div>\n </div>\n </div>\n )\n}\n\n// \u2500\u2500\u2500 Page \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default function CpqQuotesListPage() {\n const router = useRouter()\n const t = useT()\n\n const data = useCpqListData<QuoteConfig>({\n endpoint: '/api/cpq/quotes',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.quotes.list.error.load', 'Failed to load quotes'),\n })\n\n const [showCustomerPicker, setShowCustomerPicker] = React.useState(false)\n const [creating, setCreating] = React.useState(false)\n const [createError, setCreateError] = React.useState<string | null>(null)\n\n const statusOptions = React.useMemo(\n () => [\n { value: 'new', label: 'New' },\n { value: 'incomplete', label: 'Incomplete' },\n { value: 'ready', label: 'Ready' },\n { value: 'in_approval', label: 'In Approval' },\n { value: 'approved', label: 'Approved' },\n { value: 'with_customer', label: 'With Customer' },\n { value: 'accepted', label: 'Accepted' },\n { value: 'rejected', label: 'Rejected' },\n { value: 'cancelled', label: 'Cancelled' },\n ],\n [],\n )\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'cpqStatus',\n label: t('cpq.quotes.filters.status', 'Status'),\n type: 'select',\n options: statusOptions,\n },\n {\n id: 'currencyCode',\n label: t('cpq.quotes.filters.currency', 'Currency'),\n type: 'text',\n },\n ],\n [statusOptions, t],\n )\n\n const selectCustomer = React.useCallback(\n async (customer: Customer) => {\n setCreating(true)\n setCreateError(null)\n try {\n const res = await fetch('/api/cpq/quotes', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ customerId: customer.id }),\n })\n const body = await res.json()\n if (res.ok) {\n setShowCustomerPicker(false)\n router.push(`/backend/cpq/quotes/${body.id}`)\n } else {\n setCreateError(body.error ?? `Failed to create quote (${res.status})`)\n }\n } catch (err) {\n setCreateError((err as Error).message)\n } finally {\n setCreating(false)\n }\n },\n [router],\n )\n\n const columns = React.useMemo<ColumnDef<QuoteConfig>[]>(\n () => [\n {\n accessorKey: 'quoteNumber',\n header: t('cpq.quotes.table.quoteNumber', 'Quote Number'),\n cell: ({ row }) => (\n <span className=\"font-medium\">\n {row.original.quoteNumber ?? (\n <span className=\"font-mono text-xs text-muted-foreground\">\n {(row.original.quoteId ?? row.original.id).slice(0, 8)}\u2026\n </span>\n )}\n </span>\n ),\n },\n {\n accessorKey: 'cpqStatus',\n header: t('cpq.quotes.table.status', 'Status'),\n cell: ({ row }) => (\n <Tag variant={quoteCpqStatusMap[row.original.cpqStatus as QuoteCpqStatus] ?? 'neutral'} dot>\n {formatStatusLabel(row.original.cpqStatus)}\n </Tag>\n ),\n },\n {\n accessorKey: 'version',\n header: t('cpq.quotes.table.version', 'Version'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">v{row.original.version}</span>,\n },\n {\n accessorKey: 'currencyCode',\n header: t('cpq.quotes.table.currency', 'Currency'),\n cell: ({ row }) => <span className=\"text-muted-foreground\">{row.original.currencyCode}</span>,\n },\n {\n id: 'nrcTotal',\n header: t('cpq.quotes.table.nrcTotal', 'NRC Total'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">\n {fmt(row.original.pricingSummary?.nrcTotal, row.original.currencyCode)}\n </span>\n ),\n },\n {\n id: 'mrcTotal',\n header: t('cpq.quotes.table.mrcTotal', 'MRC Total'),\n cell: ({ row }) => (\n <span className=\"font-mono text-xs\">\n {fmt(row.original.pricingSummary?.mrcTotal, row.original.currencyCode)}\n </span>\n ),\n },\n {\n accessorKey: 'createdAt',\n header: t('cpq.quotes.table.createdAt', 'Created'),\n cell: ({ row }) => (\n <span className=\"text-xs text-muted-foreground\">\n {new Date(row.original.createdAt).toLocaleDateString()}\n </span>\n ),\n },\n ],\n [t],\n )\n\n return (\n <CpqListView<QuoteConfig>\n title={t('cpq.quotes.list.title', 'CPQ Quotes')}\n tableId=\"cpq.quotes.list\"\n data={data}\n columns={columns}\n filters={filters}\n pageSize={PAGE_SIZE}\n searchPlaceholder={t('cpq.quotes.search.placeholder', 'Search by quote / customer id...')}\n actions={\n <Button type=\"button\" onClick={() => setShowCustomerPicker(true)} className=\"gap-1.5\">\n <Plus className=\"h-4 w-4\" />\n {t('cpq.quotes.add', 'New Quote')}\n </Button>\n }\n onRowClick={(row) => router.push(`/backend/cpq/quotes/${row.id}`)}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'open',\n label: t('cpq.quotes.actions.open', 'Open'),\n href: `/backend/cpq/quotes/${row.id}`,\n },\n ]}\n />\n )}\n toolbarContent={\n <CustomerPickerModal\n open={showCustomerPicker}\n onClose={() => setShowCustomerPicker(false)}\n onSelect={selectCustomer}\n creating={creating}\n error={createError}\n />\n }\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t(\n 'cpq.quotes.empty',\n 'No CPQ quotes found. Create one to start configuring offerings for a customer.',\n )}\n </div>\n }\n />\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/cpq/backend/cpq/specifications/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { CpqListView, useCpqListData } from '../../../components/CpqListView'\nimport {\n formatStatusLabel,\n specificationLifecycleStatusMap,\n type SpecificationLifecycleStatus,\n} from '../../../components/statusMaps'\n\ntype Specification = {\n id: string\n productId: string\n code: string\n name: string\n description: string | null\n specType: string\n isAssetizable: boolean\n lifecycleStatus: string\n version: number\n isActive: boolean\n createdAt: string\n}\n\nconst PAGE_SIZE = 50\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.lifecycleStatus === 'string' && values.lifecycleStatus) {\n params.set('lifecycleStatus', values.lifecycleStatus)\n }\n if (typeof values.specType === 'string' && values.specType) {\n params.set('specType', values.specType)\n }\n if (values.isActive === true) params.set('isActive', 'true')\n if (values.isActive === false) params.set('isActive', 'false')\n if (values.isAssetizable === true) params.set('isAssetizable', 'true')\n if (values.isAssetizable === false) params.set('isAssetizable', 'false')\n}\n\nexport default function SpecificationsListPage() {\n const t = useT()\n const router = useRouter()\n\n const data = useCpqListData<Specification>({\n endpoint: '/api/cpq/product-specifications',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.specifications.list.error.load', 'Failed to load specifications'),\n })\n\n const lifecycleOptions = React.useMemo(\n () => [\n { value: 'draft', label: t('cpq.specifications.lifecycle.draft', 'Draft') },\n { value: 'active', label: t('cpq.specifications.lifecycle.active', 'Active') },\n { value: 'deprecated', label: t('cpq.specifications.lifecycle.deprecated', 'Deprecated') },\n ],\n [t],\n )\n\n const specTypeOptions = React.useMemo(\n () => [\n { value: 'simple', label: t('cpq.specifications.type.simple', 'Simple') },\n { value: 'bundle', label: t('cpq.specifications.type.bundle', 'Bundle') },\n ],\n [t],\n )\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'lifecycleStatus',\n label: t('cpq.specifications.filters.lifecycleStatus', 'Lifecycle Status'),\n type: 'select',\n options: lifecycleOptions,\n },\n {\n id: 'specType',\n label: t('cpq.specifications.filters.specType', 'Type'),\n type: 'select',\n options: specTypeOptions,\n },\n {\n id: 'isActive',\n label: t('cpq.specifications.filters.isActive', 'Active'),\n type: 'checkbox',\n },\n {\n id: 'isAssetizable',\n label: t('cpq.specifications.filters.isAssetizable', 'Assetizable'),\n type: 'checkbox',\n },\n ],\n [lifecycleOptions, specTypeOptions, t],\n )\n\n const columns = React.useMemo<ColumnDef<Specification>[]>(\n () => [\n {\n accessorKey: 'code',\n header: t('cpq.specifications.code', 'Code'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('cpq.specifications.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"font-medium\">\n {row.original.name}\n {row.original.specType === 'bundle' && (\n <Tag variant=\"brand\" className=\"ml-2 px-2 text-[10px]\">\n bundle\n </Tag>\n )}\n {row.original.isAssetizable && (\n <Tag variant=\"warning\" className=\"ml-1 px-2 text-[10px]\">\n asset\n </Tag>\n )}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('cpq.specifications.isActive', 'Is Active?'),\n cell: ({ row }) => <Checkbox checked={row.original.isActive} disabled />,\n },\n {\n accessorKey: 'lifecycleStatus',\n header: t('cpq.specifications.lifecycleStatus', 'Lifecycle Status'),\n cell: ({ row }) => (\n <Tag variant={specificationLifecycleStatusMap[row.original.lifecycleStatus as SpecificationLifecycleStatus] ?? 'neutral'} dot>\n {formatStatusLabel(row.original.lifecycleStatus)}\n </Tag>\n ),\n },\n {\n accessorKey: 'version',\n header: t('cpq.specifications.version', 'Version'),\n cell: ({ row }) => <span>v{row.original.version}</span>,\n },\n {\n accessorKey: 'description',\n header: t('cpq.specifications.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-muted-foreground\">{row.original.description ?? '\u2014'}</span>\n ),\n meta: { truncate: true, maxWidth: 320 },\n },\n ],\n [t],\n )\n\n return (\n <CpqListView<Specification>\n title={t('cpq.specifications.list.title', 'Product Specifications')}\n tableId=\"cpq.specifications.list\"\n data={data}\n columns={columns}\n filters={filters}\n pageSize={PAGE_SIZE}\n searchPlaceholder={t('cpq.specifications.search.placeholder', 'Search specifications...')}\n actions={\n <Button asChild>\n <a href=\"/backend/cpq/specifications/new\">\n {t('cpq.specifications.add', 'New Specification')}\n </a>\n </Button>\n }\n crud={{\n endpoint: '/api/cpq/product-specifications',\n entityName: t('cpq.specifications.entityName', 'specification'),\n editHref: (row) => `/backend/cpq/specifications/${row.id}`,\n }}\n onRowClick={(row) => router.push(`/backend/cpq/specifications/${row.id}`)}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t(\n 'cpq.specifications.empty',\n 'No product specifications found. Create one to define your product catalogue.',\n )}\n </div>\n }\n />\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { CpqListView, useCpqListData } from '../../../components/CpqListView'\nimport {\n formatStatusLabel,\n specificationLifecycleStatusMap,\n type SpecificationLifecycleStatus,\n} from '../../../components/statusMaps'\n\ntype Specification = {\n id: string\n productId: string\n code: string\n name: string\n description: string | null\n specType: string\n isAssetizable: boolean\n lifecycleStatus: string\n version: number\n isActive: boolean\n createdAt: string\n}\n\nconst PAGE_SIZE = 50\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.lifecycleStatus === 'string' && values.lifecycleStatus) {\n params.set('lifecycleStatus', values.lifecycleStatus)\n }\n if (typeof values.specType === 'string' && values.specType) {\n params.set('specType', values.specType)\n }\n if (values.isActive === true) params.set('isActive', 'true')\n if (values.isActive === false) params.set('isActive', 'false')\n if (values.isAssetizable === true) params.set('isAssetizable', 'true')\n if (values.isAssetizable === false) params.set('isAssetizable', 'false')\n}\n\nexport default function SpecificationsListPage() {\n const t = useT()\n const router = useRouter()\n\n const data = useCpqListData<Specification>({\n endpoint: '/api/cpq/product-specifications',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.specifications.list.error.load', 'Failed to load specifications'),\n })\n\n const lifecycleOptions = React.useMemo(\n () => [\n { value: 'draft', label: t('cpq.specifications.lifecycle.draft', 'Draft') },\n { value: 'active', label: t('cpq.specifications.lifecycle.active', 'Active') },\n { value: 'deprecated', label: t('cpq.specifications.lifecycle.deprecated', 'Deprecated') },\n ],\n [t],\n )\n\n const specTypeOptions = React.useMemo(\n () => [\n { value: 'simple', label: t('cpq.specifications.type.simple', 'Simple') },\n { value: 'bundle', label: t('cpq.specifications.type.bundle', 'Bundle') },\n ],\n [t],\n )\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'lifecycleStatus',\n label: t('cpq.specifications.filters.lifecycleStatus', 'Lifecycle Status'),\n type: 'select',\n options: lifecycleOptions,\n },\n {\n id: 'specType',\n label: t('cpq.specifications.filters.specType', 'Type'),\n type: 'select',\n options: specTypeOptions,\n },\n {\n id: 'isActive',\n label: t('cpq.specifications.filters.isActive', 'Active'),\n type: 'checkbox',\n },\n {\n id: 'isAssetizable',\n label: t('cpq.specifications.filters.isAssetizable', 'Assetizable'),\n type: 'checkbox',\n },\n ],\n [lifecycleOptions, specTypeOptions, t],\n )\n\n const columns = React.useMemo<ColumnDef<Specification>[]>(\n () => [\n {\n accessorKey: 'code',\n header: t('cpq.specifications.code', 'Code'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.code}</span>,\n },\n {\n accessorKey: 'name',\n header: t('cpq.specifications.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"font-medium\">\n {row.original.name}\n {row.original.specType === 'bundle' && (\n <Tag variant=\"brand\" className=\"ml-2 px-2 text-[10px]\">\n bundle\n </Tag>\n )}\n {row.original.isAssetizable && (\n <Tag variant=\"warning\" className=\"ml-1 px-2 text-[10px]\">\n asset\n </Tag>\n )}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('cpq.specifications.isActive', 'Is Active?'),\n cell: ({ row }) => <Checkbox checked={row.original.isActive} disabled />,\n },\n {\n accessorKey: 'lifecycleStatus',\n header: t('cpq.specifications.lifecycleStatus', 'Lifecycle Status'),\n cell: ({ row }) => (\n <Tag variant={specificationLifecycleStatusMap[row.original.lifecycleStatus as SpecificationLifecycleStatus] ?? 'neutral'} dot>\n {formatStatusLabel(row.original.lifecycleStatus)}\n </Tag>\n ),\n },\n {\n accessorKey: 'version',\n header: t('cpq.specifications.version', 'Version'),\n cell: ({ row }) => <span>v{row.original.version}</span>,\n },\n {\n accessorKey: 'description',\n header: t('cpq.specifications.description', 'Description'),\n cell: ({ row }) => (\n <span className=\"text-muted-foreground\">{row.original.description ?? '\u2014'}</span>\n ),\n meta: { truncate: true, maxWidth: 320 },\n },\n ],\n [t],\n )\n\n return (\n <CpqListView<Specification>\n title={t('cpq.specifications.list.title', 'Product Specifications')}\n tableId=\"cpq.specifications.list\"\n data={data}\n columns={columns}\n filters={filters}\n pageSize={PAGE_SIZE}\n searchPlaceholder={t('cpq.specifications.search.placeholder', 'Search specifications...')}\n actions={\n <Button asChild>\n <a href=\"/backend/cpq/specifications/new\">\n {t('cpq.specifications.add', 'New Specification')}\n </a>\n </Button>\n }\n crud={{\n endpoint: '/api/cpq/product-specifications',\n entityName: t('cpq.specifications.entityName', 'specification'),\n editHref: (row) => `/backend/cpq/specifications/${row.id}`,\n }}\n onRowClick={(row) => router.push(`/backend/cpq/specifications/${row.id}`)}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t(\n 'cpq.specifications.empty',\n 'No product specifications found. Create one to define your product catalogue.',\n )}\n </div>\n }\n />\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/cpq/backend/cpq/wizards/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { CpqListView, useCpqListData } from '../../../components/CpqListView'\n\ntype WizardDefinition = {\n id: string\n code: string\n name: string\n description: string | null\n version: number\n surface: string\n isActive: boolean\n steps: Array<{ stepId: string; type: string; title: string }>\n createdAt: string\n}\n\nconst PAGE_SIZE = 50\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.surface === 'string' && values.surface.trim()) {\n params.set('surface', values.surface.trim())\n }\n if (values.isActive === true) params.set('isActive', 'true')\n if (values.isActive === false) params.set('isActive', 'false')\n}\n\nexport default function CpqWizardsPage() {\n const router = useRouter()\n const t = useT()\n\n const data = useCpqListData<WizardDefinition>({\n endpoint: '/api/cpq/wizards',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.wizards.list.error.load', 'Failed to load wizards'),\n })\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'surface',\n label: t('cpq.wizards.filters.surface', 'Surface'),\n type: 'text',\n },\n {\n id: 'isActive',\n label: t('cpq.wizards.filters.isActive', 'Active'),\n type: 'checkbox',\n },\n ],\n [t],\n )\n\n const handlePreviewWizard = React.useCallback(\n (definitionCode: string) => {\n router.push(`/backend/cpq/wizards/${definitionCode}`)\n },\n [router],\n )\n\n const columns = React.useMemo<ColumnDef<WizardDefinition>[]>(\n () => [\n {\n accessorKey: 'name',\n header: t('cpq.wizards.table.name', 'Name'),\n cell: ({ row }) => <span className=\"font-medium\">{row.original.name}</span>,\n },\n {\n accessorKey: 'code',\n header: t('cpq.wizards.table.code', 'Code'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.code}</span>,\n },\n {\n accessorKey: 'surface',\n header: t('cpq.wizards.table.surface', 'Surface'),\n },\n {\n id: 'steps',\n header: t('cpq.wizards.table.steps', 'Steps'),\n cell: ({ row }) => <span>{row.original.steps.length}</span>,\n },\n {\n id: 'version',\n header: t('cpq.wizards.table.version', 'Version'),\n cell: ({ row }) => <span>v{row.original.version}</span>,\n },\n {\n accessorKey: 'isActive',\n header: t('cpq.wizards.table.active', 'Active'),\n cell: ({ row }) => (\n <Tag variant={row.original.isActive ? 'success' : 'neutral'} dot>\n {row.original.isActive ? 'Yes' : 'No'}\n </Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <CpqListView<WizardDefinition>\n title={t('cpq.wizards.list.title', 'Wizards')}\n tableId=\"cpq.wizards.list\"\n data={data}\n columns={columns}\n filters={filters}\n pageSize={PAGE_SIZE}\n searchPlaceholder={t('cpq.wizards.search.placeholder', 'Search wizards...')}\n onRowClick={(row) => router.push(`/backend/cpq/wizards/${row.code}/detail`)}\n // Wizards are read-only in the admin UI for now: no Edit / Delete\n // entries \u2014 only `Preview` (run the wizard) and `View details`.\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'preview',\n label: t('cpq.wizards.actions.preview', 'Preview'),\n onSelect: () => handlePreviewWizard(row.code),\n },\n {\n id: 'details',\n label: t('cpq.wizards.actions.details', 'View details'),\n href: `/backend/cpq/wizards/${row.code}/detail`,\n },\n ]}\n />\n )}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizards.empty', 'No wizard definitions yet. Create one via the API.')}\n </div>\n }\n />\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { CpqListView, useCpqListData } from '../../../components/CpqListView'\n\ntype WizardDefinition = {\n id: string\n code: string\n name: string\n description: string | null\n version: number\n surface: string\n isActive: boolean\n steps: Array<{ stepId: string; type: string; title: string }>\n createdAt: string\n}\n\nconst PAGE_SIZE = 50\n\nfunction buildFilterParams(values: FilterValues, params: URLSearchParams) {\n if (typeof values.surface === 'string' && values.surface.trim()) {\n params.set('surface', values.surface.trim())\n }\n if (values.isActive === true) params.set('isActive', 'true')\n if (values.isActive === false) params.set('isActive', 'false')\n}\n\nexport default function CpqWizardsPage() {\n const router = useRouter()\n const t = useT()\n\n const data = useCpqListData<WizardDefinition>({\n endpoint: '/api/cpq/wizards',\n pageSize: PAGE_SIZE,\n buildFilterParams,\n loadErrorMessage: t('cpq.wizards.list.error.load', 'Failed to load wizards'),\n })\n\n const filters = React.useMemo<FilterDef[]>(\n () => [\n {\n id: 'surface',\n label: t('cpq.wizards.filters.surface', 'Surface'),\n type: 'text',\n },\n {\n id: 'isActive',\n label: t('cpq.wizards.filters.isActive', 'Active'),\n type: 'checkbox',\n },\n ],\n [t],\n )\n\n const handlePreviewWizard = React.useCallback(\n (definitionCode: string) => {\n router.push(`/backend/cpq/wizards/${definitionCode}`)\n },\n [router],\n )\n\n const columns = React.useMemo<ColumnDef<WizardDefinition>[]>(\n () => [\n {\n accessorKey: 'name',\n header: t('cpq.wizards.table.name', 'Name'),\n cell: ({ row }) => <span className=\"font-medium\">{row.original.name}</span>,\n },\n {\n accessorKey: 'code',\n header: t('cpq.wizards.table.code', 'Code'),\n cell: ({ row }) => <span className=\"font-mono text-xs\">{row.original.code}</span>,\n },\n {\n accessorKey: 'surface',\n header: t('cpq.wizards.table.surface', 'Surface'),\n },\n {\n id: 'steps',\n header: t('cpq.wizards.table.steps', 'Steps'),\n cell: ({ row }) => <span>{row.original.steps.length}</span>,\n },\n {\n id: 'version',\n header: t('cpq.wizards.table.version', 'Version'),\n cell: ({ row }) => <span>v{row.original.version}</span>,\n },\n {\n accessorKey: 'isActive',\n header: t('cpq.wizards.table.active', 'Active'),\n cell: ({ row }) => (\n <Tag variant={row.original.isActive ? 'success' : 'neutral'} dot>\n {row.original.isActive ? 'Yes' : 'No'}\n </Tag>\n ),\n },\n ],\n [t],\n )\n\n return (\n <CpqListView<WizardDefinition>\n title={t('cpq.wizards.list.title', 'Wizards')}\n tableId=\"cpq.wizards.list\"\n data={data}\n columns={columns}\n filters={filters}\n pageSize={PAGE_SIZE}\n searchPlaceholder={t('cpq.wizards.search.placeholder', 'Search wizards...')}\n onRowClick={(row) => router.push(`/backend/cpq/wizards/${row.code}/detail`)}\n // Wizards are read-only in the admin UI for now: no Edit / Delete\n // entries \u2014 only `Preview` (run the wizard) and `View details`.\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'preview',\n label: t('cpq.wizards.actions.preview', 'Preview'),\n onSelect: () => handlePreviewWizard(row.code),\n },\n {\n id: 'details',\n label: t('cpq.wizards.actions.details', 'View details'),\n href: `/backend/cpq/wizards/${row.code}/detail`,\n },\n ]}\n />\n )}\n emptyState={\n <div className=\"rounded-lg border bg-card p-6 text-center text-sm text-muted-foreground\">\n {t('cpq.wizards.empty', 'No wizard definitions yet. Create one via the API.')}\n </div>\n }\n />\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/cpq/components/CpqListView.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable, type BulkAction } from '@open-mercato/ui/backend/DataTable'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useCpqRowActions, type UseCpqRowActionsOptions } from './useCpqRowActions'\n\n// \u2500\u2500\u2500 Types \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype PaginatedResponse<T> = {\n items?: T[]\n total?: number\n totalPages?: number\n}\n\nexport type CpqListData<T> = {\n rows: T[]\n total: number\n totalPages: number\n page: number\n isLoading: boolean\n /** Last successful fetch duration in milliseconds. Surfaces in the\n * pagination footer as `... in 1.5s`, matching OM's standard list views. */\n durationMs: number | null\n search: string\n sorting: SortingState\n filterValues: FilterValues\n setPage: (page: number) => void\n setSearch: (value: string) => void\n setSorting: React.Dispatch<React.SetStateAction<SortingState>>\n setFilterValues: (values: FilterValues) => void\n clearFilters: () => void\n reload: () => void\n queryString: string\n}\n\nexport type UseCpqListDataOptions = {\n /** API endpoint without query string, e.g. `/api/cpq/product-offerings` */\n endpoint: string\n /** Default 50 \u2014 keep at or below 100. */\n pageSize?: number\n /** Initial sort state. Default `[{ id: 'createdAt', desc: true }]`. */\n initialSorting?: SortingState\n /** Map filter values to URL params (only call set on the params arg). */\n buildFilterParams?: (values: FilterValues, params: URLSearchParams) => void\n /** Override the flash message shown when the load fails. */\n loadErrorMessage?: string\n}\n\n/**\n * Server-side paginated list state for CPQ list views.\n *\n * Designed for 100k+ records:\n * - search / sort / filter all go to the server\n * - default pageSize stays at or below 100\n * - debouncing is handled by FilterBar's search input (1s)\n */\nexport function useCpqListData<T extends { id: string }>(\n options: UseCpqListDataOptions,\n): CpqListData<T> {\n const {\n endpoint,\n pageSize = 50,\n initialSorting = [{ id: 'createdAt', desc: true }],\n buildFilterParams,\n loadErrorMessage,\n } = options\n const t = useT()\n\n const [rows, setRows] = React.useState<T[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [page, setPageState] = React.useState(1)\n const [isLoading, setIsLoading] = React.useState(true)\n const [durationMs, setDurationMs] = React.useState<number | null>(null)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [search, setSearchState] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>(initialSorting)\n const [filterValues, setFilterValuesState] = React.useState<FilterValues>({})\n\n const queryString = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const sort = sorting[0]\n if (sort?.id) {\n params.set('sortField', sort.id)\n params.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n if (buildFilterParams) buildFilterParams(filterValues, params)\n return params.toString()\n }, [buildFilterParams, filterValues, page, pageSize, search, sorting])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n // Use `performance.now()` so the wall-clock change during the request\n // (NTP, DST, manual clock skew) can never produce a negative duration.\n const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n try {\n const fallback: PaginatedResponse<T> = { items: [], total: 0, totalPages: 1 }\n const call = await apiCall<PaginatedResponse<T>>(\n `${endpoint}?${queryString}`,\n undefined,\n { fallback },\n )\n if (cancelled) return\n if (!call.ok) {\n flash(loadErrorMessage ?? t('cpq.list.error.load', 'Failed to load'), 'error')\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n const finishedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n setDurationMs(Math.max(0, finishedAt - startedAt))\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [endpoint, loadErrorMessage, queryString, reloadToken, t])\n\n const setSearch = React.useCallback((value: string) => {\n setSearchState(value)\n setPageState(1)\n }, [])\n\n const setFilterValues = React.useCallback((values: FilterValues) => {\n setFilterValuesState(values)\n setPageState(1)\n }, [])\n\n const clearFilters = React.useCallback(() => {\n setFilterValuesState({})\n setPageState(1)\n }, [])\n\n const reload = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n return {\n rows,\n total,\n totalPages,\n page,\n isLoading,\n durationMs,\n search,\n sorting,\n filterValues,\n setPage: setPageState,\n setSearch,\n setSorting,\n setFilterValues,\n clearFilters,\n reload,\n queryString,\n }\n}\n\n// \u2500\u2500\u2500 Component \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\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Declarative shortcut for the standard CRUD-list row actions.\n *\n * When provided, CpqListView:\n * - registers `useCpqRowActions` internally\n * - renders Edit + Delete in every row's `\u22EF` menu (with optional\n * `extraRowItems` per row prepended / appended)\n * - adds a \"Delete selected\" bulk action (after any `bulkActions` you\n * pass explicitly, so module-specific bulk ops come first)\n * - mounts the confirm-dialog portal in the page footer\n *\n * Pass `rowActions` / `bulkActions` directly instead when the page needs\n * fully custom semantics (e.g. Quotes / Orders have no DELETE endpoint).\n */\nexport type CpqListViewCrudConfig<T extends { id: string }> = Omit<\n UseCpqRowActionsOptions<T>,\n 'onReload'\n> & {\n /**\n * Extra items added to each row's actions menu.\n * `prepend` renders before Edit, `append` between Edit and Delete.\n */\n extraRowItems?: (row: T) => { prepend?: RowActionItem[]; append?: RowActionItem[] }\n}\n\nexport type CpqListViewProps<T extends { id: string }> = {\n /** Page + table title */\n title: React.ReactNode\n /** Stable id for perspectives (e.g. `cpq.offerings.list`) */\n tableId: string\n /** Stable id for DataTable's selection state (defaults to tableId) */\n selectionScopeKey?: string\n /** List state \u2014 typically built from `useCpqListData` in the parent */\n data: CpqListData<T>\n /** Table columns */\n columns: ColumnDef<T>[]\n /** Filter definitions for the FilterBar overlay */\n filters?: FilterDef[]\n /** Search input placeholder */\n searchPlaceholder?: string\n /** Header actions (e.g. Create button) */\n actions?: React.ReactNode\n /** Bulk actions toolbar shown when rows are selected */\n bulkActions?: BulkAction<T>[]\n /** Per-row action menu */\n rowActions?: (row: T) => React.ReactNode\n /**\n * Declarative CRUD config. When set, CpqListView produces the standard\n * `\u22EF \u2192 Edit / Delete` menu, a \"Delete selected\" bulk action, and mounts\n * the confirm-dialog portal \u2014 replacing the ~30 lines of boilerplate\n * each list page used to repeat. Override `rowActions` / `bulkActions`\n * to take full control instead.\n */\n crud?: CpqListViewCrudConfig<T>\n /** Click handler for whole row (typically navigates to detail) */\n onRowClick?: (row: T) => void\n /** Custom empty state */\n emptyState?: React.ReactNode\n /** Pagination size (must match `useCpqListData` pageSize) */\n pageSize?: number\n /**\n * Custom content rendered ABOVE the DataTable inside the same Page wrapper.\n * Use for inline forms, customer pickers, tab switchers, alerts.\n */\n toolbarContent?: React.ReactNode\n /**\n * Custom content rendered BELOW the DataTable (e.g. confirm dialog portals).\n */\n footerContent?: React.ReactNode\n}\n\n/**\n * Generic CPQ list view. Wraps DataTable + FilterBar + Page in the\n * Open Mercato standard, with stronger row-hover affordance so users\n * always know what's clickable.\n */\nexport function CpqListView<T extends { id: string }>({\n title,\n tableId,\n selectionScopeKey,\n data,\n columns,\n filters,\n searchPlaceholder,\n actions,\n bulkActions,\n rowActions,\n crud,\n onRowClick,\n emptyState,\n pageSize = 50,\n toolbarContent,\n footerContent,\n}: CpqListViewProps<T>) {\n const t = useT()\n\n // Wire `crud` into RowActions + bulkActions + footer confirm dialog so the\n // call-site stays a single declarative config block.\n // The hook is always called (Rules of Hooks); the helpers below decide\n // whether to actually use its output based on the presence of `crud`.\n const crudActions = useCpqRowActions<T>({\n endpoint: crud?.endpoint ?? '',\n entityName: crud?.entityName ?? 'item',\n editHref: crud?.editHref ?? (() => '#'),\n onReload: data.reload,\n canDelete: crud?.canDelete,\n })\n\n const resolvedRowActions = React.useMemo(() => {\n if (rowActions) return rowActions\n if (!crud) return undefined\n return (row: T) => (\n <RowActions items={crudActions.buildItems(row, crud.extraRowItems?.(row))} />\n )\n }, [crud, crudActions, rowActions])\n\n const resolvedBulkActions = React.useMemo<BulkAction<T>[] | undefined>(() => {\n if (!crud) return bulkActions\n const deleteAction: BulkAction<T> = {\n id: 'delete',\n label: t('cpq.list.bulk.deleteSelected', 'Delete selected'),\n destructive: true,\n onExecute: crudActions.bulkDelete,\n }\n return bulkActions ? [...bulkActions, deleteAction] : [deleteAction]\n }, [bulkActions, crud, crudActions.bulkDelete, t])\n\n const resolvedFooter = crud ? (\n <>\n {footerContent}\n {crudActions.ConfirmDialogElement}\n </>\n ) : (\n footerContent\n )\n\n return (\n <Page>\n <PageBody className=\"cpq-list-view space-y-6\">\n <CpqInteractionStyles />\n {toolbarContent}\n <DataTable<T>\n title={title}\n actions={actions}\n refreshButton={{\n label: t('cpq.list.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={searchPlaceholder ?? t('cpq.list.search.placeholder', 'Search...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n bulkActions={resolvedBulkActions}\n selectionScopeKey={selectionScopeKey ?? tableId}\n onRowClick={onRowClick}\n rowActions={resolvedRowActions}\n perspective={{ tableId }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={emptyState}\n />\n {resolvedFooter}\n </PageBody>\n </Page>\n )\n}\n\n// \u2500\u2500\u2500 Shared interaction styles \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\n/**\n * Inline style block applied inside `.cpq-list-view` (and any `.cpq-page`)\n * wrappers. Strengthens the OM hover affordance so users can clearly tell\n * which row their mouse is on, and forces `cursor: pointer` on every\n * clickable element inside CPQ pages.\n *\n * Kept inline to avoid coupling the package to a global stylesheet.\n */\nexport function CpqInteractionStyles() {\n return (\n <style>{`\n.cpq-list-view tbody tr.cursor-pointer,\n.cpq-page tbody tr.cursor-pointer {\n cursor: pointer;\n}\n.cpq-list-view tbody tr.cursor-pointer:hover,\n.cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(0, 0, 0, 0.045);\n}\n.dark .cpq-list-view tbody tr.cursor-pointer:hover,\n.dark .cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(255, 255, 255, 0.06);\n}\n.cpq-list-view button:not(:disabled),\n.cpq-list-view [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-list-view a[href],\n.cpq-list-view [data-clickable=\"true\"],\n.cpq-page button:not(:disabled),\n.cpq-page [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-page a[href],\n.cpq-page [data-clickable=\"true\"] {\n cursor: pointer;\n}\n/* Row actions \\`\u22EF\\` trigger: OM's hover:bg-accent is barely visible in\n the light theme. Make hover (and the open state) clearly distinct so\n users see what they're aiming at. */\n.cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.cpq-page [aria-haspopup=\"menu\"]:hover,\n.cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(0, 0, 0, 0.08);\n}\n.dark .cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-page [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.dark .cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(255, 255, 255, 0.1);\n}\n/* Items inside the row-actions dropdown menu \u2014 strengthen hover so the\n item under the pointer is unmistakable. The menu portal renders at the\n document body, so we can't scope it to .cpq-list-view; targeting the\n role attribute is enough. */\n[role=\"menu\"] [role=\"menuitem\"]:hover,\n[role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(0, 0, 0, 0.06);\n}\n.dark [role=\"menu\"] [role=\"menuitem\"]:hover,\n.dark [role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(255, 255, 255, 0.08);\n}\n/* Align padding between <a href> menu items (OM's px-2) and the <button>\n variant used for onSelect items (OM's Button size=sm = px-3). Without\n this the Delete label visibly indents past the Edit label. */\n[role=\"menu\"] button[role=\"menuitem\"] {\n padding-left: 0.5rem;\n padding-right: 0.5rem;\n}\n`}</style>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport type { SortingState } from '@tanstack/react-table'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable, type BulkAction } from '@open-mercato/ui/backend/DataTable'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useCpqRowActions, type UseCpqRowActionsOptions } from './useCpqRowActions'\n\n// \u2500\u2500\u2500 Types \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype PaginatedResponse<T> = {\n items?: T[]\n total?: number\n totalPages?: number\n}\n\nexport type CpqListData<T> = {\n rows: T[]\n total: number\n totalPages: number\n page: number\n isLoading: boolean\n /** Last successful fetch duration in milliseconds. Surfaces in the\n * pagination footer as `... in 1.5s`, matching OM's standard list views. */\n durationMs: number | null\n search: string\n sorting: SortingState\n filterValues: FilterValues\n setPage: (page: number) => void\n setSearch: (value: string) => void\n setSorting: React.Dispatch<React.SetStateAction<SortingState>>\n setFilterValues: (values: FilterValues) => void\n clearFilters: () => void\n reload: () => void\n queryString: string\n}\n\nexport type UseCpqListDataOptions = {\n /** API endpoint without query string, e.g. `/api/cpq/product-offerings` */\n endpoint: string\n /** Default 50 \u2014 keep at or below 100. */\n pageSize?: number\n /** Initial sort state. Default `[{ id: 'createdAt', desc: true }]`. */\n initialSorting?: SortingState\n /** Map filter values to URL params (only call set on the params arg). */\n buildFilterParams?: (values: FilterValues, params: URLSearchParams) => void\n /** Override the flash message shown when the load fails. */\n loadErrorMessage?: string\n}\n\n/**\n * Server-side paginated list state for CPQ list views.\n *\n * Designed for 100k+ records:\n * - search / sort / filter all go to the server\n * - default pageSize stays at or below 100\n * - debouncing is handled by FilterBar's search input (1s)\n */\nexport function useCpqListData<T extends { id: string }>(\n options: UseCpqListDataOptions,\n): CpqListData<T> {\n const {\n endpoint,\n pageSize = 50,\n initialSorting = [{ id: 'createdAt', desc: true }],\n buildFilterParams,\n loadErrorMessage,\n } = options\n const t = useT()\n\n const [rows, setRows] = React.useState<T[]>([])\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [page, setPageState] = React.useState(1)\n const [isLoading, setIsLoading] = React.useState(true)\n const [durationMs, setDurationMs] = React.useState<number | null>(null)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [search, setSearchState] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>(initialSorting)\n const [filterValues, setFilterValuesState] = React.useState<FilterValues>({})\n\n const queryString = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', String(pageSize))\n if (search.trim()) params.set('search', search.trim())\n const sort = sorting[0]\n if (sort?.id) {\n params.set('sortField', sort.id)\n params.set('sortDir', sort.desc ? 'desc' : 'asc')\n }\n if (buildFilterParams) buildFilterParams(filterValues, params)\n return params.toString()\n }, [buildFilterParams, filterValues, page, pageSize, search, sorting])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n // Use `performance.now()` so the wall-clock change during the request\n // (NTP, DST, manual clock skew) can never produce a negative duration.\n const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n try {\n const fallback: PaginatedResponse<T> = { items: [], total: 0, totalPages: 1 }\n const call = await apiCall<PaginatedResponse<T>>(\n `${endpoint}?${queryString}`,\n undefined,\n { fallback },\n )\n if (cancelled) return\n if (!call.ok) {\n flash(loadErrorMessage ?? t('cpq.list.error.load', 'Failed to load'), 'error')\n return\n }\n const payload = call.result ?? fallback\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)\n const finishedAt = typeof performance !== 'undefined' ? performance.now() : Date.now()\n setDurationMs(Math.max(0, finishedAt - startedAt))\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => {\n cancelled = true\n }\n }, [endpoint, loadErrorMessage, queryString, reloadToken, t])\n\n const setSearch = React.useCallback((value: string) => {\n setSearchState(value)\n setPageState(1)\n }, [])\n\n const setFilterValues = React.useCallback((values: FilterValues) => {\n setFilterValuesState(values)\n setPageState(1)\n }, [])\n\n const clearFilters = React.useCallback(() => {\n setFilterValuesState({})\n setPageState(1)\n }, [])\n\n const reload = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n return {\n rows,\n total,\n totalPages,\n page,\n isLoading,\n durationMs,\n search,\n sorting,\n filterValues,\n setPage: setPageState,\n setSearch,\n setSorting,\n setFilterValues,\n clearFilters,\n reload,\n queryString,\n }\n}\n\n// \u2500\u2500\u2500 Component \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\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Declarative shortcut for the standard CRUD-list row actions.\n *\n * When provided, CpqListView:\n * - registers `useCpqRowActions` internally\n * - renders Edit + Delete in every row's `\u22EF` menu (with optional\n * `extraRowItems` per row prepended / appended)\n * - adds a \"Delete selected\" bulk action (after any `bulkActions` you\n * pass explicitly, so module-specific bulk ops come first)\n * - mounts the confirm-dialog portal in the page footer\n *\n * Pass `rowActions` / `bulkActions` directly instead when the page needs\n * fully custom semantics (e.g. Quotes / Orders have no DELETE endpoint).\n */\nexport type CpqListViewCrudConfig<T extends { id: string }> = Omit<\n UseCpqRowActionsOptions<T>,\n 'onReload'\n> & {\n /**\n * Extra items added to each row's actions menu.\n * `prepend` renders before Edit, `append` between Edit and Delete.\n */\n extraRowItems?: (row: T) => { prepend?: RowActionItem[]; append?: RowActionItem[] }\n}\n\nexport type CpqListViewProps<T extends { id: string }> = {\n /** Page + table title */\n title: React.ReactNode\n /** Stable id for perspectives (e.g. `cpq.offerings.list`) */\n tableId: string\n /** Stable id for DataTable's selection state (defaults to tableId) */\n selectionScopeKey?: string\n /** List state \u2014 typically built from `useCpqListData` in the parent */\n data: CpqListData<T>\n /** Table columns */\n columns: ColumnDef<T>[]\n /** Filter definitions for the FilterBar overlay */\n filters?: FilterDef[]\n /** Search input placeholder */\n searchPlaceholder?: string\n /** Header actions (e.g. Create button) */\n actions?: React.ReactNode\n /** Bulk actions toolbar shown when rows are selected */\n bulkActions?: BulkAction<T>[]\n /** Per-row action menu */\n rowActions?: (row: T) => React.ReactNode\n /**\n * Declarative CRUD config. When set, CpqListView produces the standard\n * `\u22EF \u2192 Edit / Delete` menu, a \"Delete selected\" bulk action, and mounts\n * the confirm-dialog portal \u2014 replacing the ~30 lines of boilerplate\n * each list page used to repeat. Override `rowActions` / `bulkActions`\n * to take full control instead.\n */\n crud?: CpqListViewCrudConfig<T>\n /** Click handler for whole row (typically navigates to detail) */\n onRowClick?: (row: T) => void\n /** Custom empty state */\n emptyState?: React.ReactNode\n /** Pagination size (must match `useCpqListData` pageSize) */\n pageSize?: number\n /**\n * Custom content rendered ABOVE the DataTable inside the same Page wrapper.\n * Use for inline forms, customer pickers, tab switchers, alerts.\n */\n toolbarContent?: React.ReactNode\n /**\n * Custom content rendered BELOW the DataTable (e.g. confirm dialog portals).\n */\n footerContent?: React.ReactNode\n}\n\n/**\n * Generic CPQ list view. Wraps DataTable + FilterBar + Page in the\n * Open Mercato standard, with stronger row-hover affordance so users\n * always know what's clickable.\n */\nexport function CpqListView<T extends { id: string }>({\n title,\n tableId,\n selectionScopeKey,\n data,\n columns,\n filters,\n searchPlaceholder,\n actions,\n bulkActions,\n rowActions,\n crud,\n onRowClick,\n emptyState,\n pageSize = 50,\n toolbarContent,\n footerContent,\n}: CpqListViewProps<T>) {\n const t = useT()\n\n // Wire `crud` into RowActions + bulkActions + footer confirm dialog so the\n // call-site stays a single declarative config block.\n // The hook is always called (Rules of Hooks); the helpers below decide\n // whether to actually use its output based on the presence of `crud`.\n const crudActions = useCpqRowActions<T>({\n endpoint: crud?.endpoint ?? '',\n entityName: crud?.entityName ?? 'item',\n editHref: crud?.editHref ?? (() => '#'),\n onReload: data.reload,\n canDelete: crud?.canDelete,\n })\n\n const resolvedRowActions = React.useMemo(() => {\n if (rowActions) return rowActions\n if (!crud) return undefined\n return (row: T) => (\n <RowActions items={crudActions.buildItems(row, crud.extraRowItems?.(row))} />\n )\n }, [crud, crudActions, rowActions])\n\n const resolvedBulkActions = React.useMemo<BulkAction<T>[] | undefined>(() => {\n if (!crud) return bulkActions\n const deleteAction: BulkAction<T> = {\n id: 'delete',\n label: t('cpq.list.bulk.deleteSelected', 'Delete selected'),\n destructive: true,\n onExecute: crudActions.bulkDelete,\n }\n return bulkActions ? [...bulkActions, deleteAction] : [deleteAction]\n }, [bulkActions, crud, crudActions.bulkDelete, t])\n\n const resolvedFooter = crud ? (\n <>\n {footerContent}\n {crudActions.ConfirmDialogElement}\n </>\n ) : (\n footerContent\n )\n\n return (\n <Page>\n <PageBody className=\"cpq-list-view space-y-6\">\n <CpqInteractionStyles />\n {toolbarContent}\n <DataTable<T>\n title={title}\n actions={actions}\n refreshButton={{\n label: t('cpq.list.actions.refresh', 'Refresh'),\n onRefresh: data.reload,\n isRefreshing: data.isLoading,\n }}\n columns={columns}\n data={data.rows}\n searchValue={data.search}\n onSearchChange={data.setSearch}\n searchPlaceholder={searchPlaceholder ?? t('cpq.list.search.placeholder', 'Search...')}\n filters={filters}\n filterValues={data.filterValues}\n onFiltersApply={data.setFilterValues}\n onFiltersClear={data.clearFilters}\n sorting={data.sorting}\n onSortingChange={data.setSorting}\n bulkActions={resolvedBulkActions}\n selectionScopeKey={selectionScopeKey ?? tableId}\n onRowClick={onRowClick}\n rowActions={resolvedRowActions}\n perspective={{ tableId }}\n columnChooser={{ auto: true }}\n pagination={{\n page: data.page,\n pageSize,\n total: data.total,\n totalPages: data.totalPages,\n onPageChange: data.setPage,\n durationMs: data.durationMs,\n }}\n isLoading={data.isLoading}\n emptyState={emptyState}\n />\n {resolvedFooter}\n </PageBody>\n </Page>\n )\n}\n\n// \u2500\u2500\u2500 Shared interaction styles \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\n/**\n * Inline style block applied inside `.cpq-list-view` (and any `.cpq-page`)\n * wrappers. Strengthens the OM hover affordance so users can clearly tell\n * which row their mouse is on, and forces `cursor: pointer` on every\n * clickable element inside CPQ pages.\n *\n * Kept inline to avoid coupling the package to a global stylesheet.\n */\nexport function CpqInteractionStyles() {\n return (\n <style>{`\n.cpq-list-view tbody tr.cursor-pointer,\n.cpq-page tbody tr.cursor-pointer {\n cursor: pointer;\n}\n.cpq-list-view tbody tr.cursor-pointer:hover,\n.cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(0, 0, 0, 0.045);\n}\n.dark .cpq-list-view tbody tr.cursor-pointer:hover,\n.dark .cpq-page tbody tr.cursor-pointer:hover {\n background-color: rgba(255, 255, 255, 0.06);\n}\n.cpq-list-view button:not(:disabled),\n.cpq-list-view [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-list-view a[href],\n.cpq-list-view [data-clickable=\"true\"],\n.cpq-page button:not(:disabled),\n.cpq-page [role=\"button\"]:not([aria-disabled=\"true\"]),\n.cpq-page a[href],\n.cpq-page [data-clickable=\"true\"] {\n cursor: pointer;\n}\n/* Row actions \\`\u22EF\\` trigger: OM's hover:bg-accent is barely visible in\n the light theme. Make hover (and the open state) clearly distinct so\n users see what they're aiming at. */\n.cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.cpq-page [aria-haspopup=\"menu\"]:hover,\n.cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(0, 0, 0, 0.08);\n}\n.dark .cpq-list-view [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-page [aria-haspopup=\"menu\"]:hover,\n.dark .cpq-list-view [aria-haspopup=\"menu\"][aria-expanded=\"true\"],\n.dark .cpq-page [aria-haspopup=\"menu\"][aria-expanded=\"true\"] {\n background-color: rgba(255, 255, 255, 0.1);\n}\n/* Items inside the row-actions dropdown menu \u2014 strengthen hover so the\n item under the pointer is unmistakable. The menu portal renders at the\n document body, so we can't scope it to .cpq-list-view; targeting the\n role attribute is enough. */\n[role=\"menu\"] [role=\"menuitem\"]:hover,\n[role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(0, 0, 0, 0.06);\n}\n.dark [role=\"menu\"] [role=\"menuitem\"]:hover,\n.dark [role=\"menu\"] [role=\"menuitem\"]:focus-visible {\n background-color: rgba(255, 255, 255, 0.08);\n}\n/* Align padding between <a href> menu items (OM's px-2) and the <button>\n variant used for onSelect items (OM's Button size=sm = px-3). Without\n this the Delete label visibly indents past the Edit label. */\n[role=\"menu\"] button[role=\"menuitem\"] {\n padding-left: 0.5rem;\n padding-right: 0.5rem;\n}\n`}</style>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
var _a, _b;
|
|
1
2
|
import { SalesOrder, SalesOrderLine } from "@open-mercato/core/modules/sales/data/entities";
|
|
2
3
|
import {
|
|
3
4
|
CpqQuoteConfiguration,
|
|
@@ -765,13 +766,18 @@ function pickArcSourceFromConfig(cfg, map) {
|
|
|
765
766
|
quantity: item.quantity
|
|
766
767
|
};
|
|
767
768
|
}
|
|
768
|
-
|
|
769
|
+
const ORDER_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@dainamite/cpq/OrderError");
|
|
770
|
+
class OrderError extends (_b = Error, _a = ORDER_ERROR_BRAND, _b) {
|
|
769
771
|
constructor(status, message) {
|
|
770
772
|
super(message);
|
|
771
773
|
this.status = status;
|
|
774
|
+
this[_a] = true;
|
|
772
775
|
this.name = "OrderError";
|
|
773
776
|
}
|
|
774
777
|
}
|
|
778
|
+
function isOrderError(err) {
|
|
779
|
+
return !!err && typeof err === "object" && err[ORDER_ERROR_BRAND] === true;
|
|
780
|
+
}
|
|
775
781
|
function buildArcLineChange(line, specMap) {
|
|
776
782
|
const action = line.action;
|
|
777
783
|
if (action === "add") {
|
|
@@ -950,6 +956,7 @@ async function emitSupersededEvent(sourceLog, effectiveDate, envelope) {
|
|
|
950
956
|
}
|
|
951
957
|
export {
|
|
952
958
|
DefaultCpqOrderService,
|
|
953
|
-
OrderError
|
|
959
|
+
OrderError,
|
|
960
|
+
isOrderError
|
|
954
961
|
};
|
|
955
962
|
//# sourceMappingURL=cpqOrderService.js.map
|