@open-mercato/core 0.6.8-develop.6911.1.f853dd04a9 → 0.6.8-develop.6912.1.36161dc9ac
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/dist/modules/api_keys/backend/api-keys/page.js.map +1 -1
- package/dist/modules/attachments/components/AttachmentLibrary.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +1 -1
- package/dist/modules/audit_logs/components/AccessLogsTable.js.map +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +1 -1
- package/dist/modules/auth/backend/roles/page.js.map +2 -2
- package/dist/modules/auth/backend/users/page.js.map +2 -2
- package/dist/modules/business_rules/backend/logs/page.js.map +1 -1
- package/dist/modules/business_rules/backend/rules/page.js.map +1 -1
- package/dist/modules/business_rules/backend/sets/page.js.map +1 -1
- package/dist/modules/catalog/components/PriceKindSettings.js.map +1 -1
- package/dist/modules/catalog/components/categories/CategoriesDataTable.js.map +1 -1
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/communication_channels/backend/communication_channels/channels/page.js.map +1 -1
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +1 -1
- package/dist/modules/currencies/backend/currencies/page.js.map +1 -1
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +1 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +1 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +1 -1
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +1 -1
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/CustomerTodosTable.js.map +1 -1
- package/dist/modules/data_sync/backend/data-sync/page.js.map +1 -1
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js.map +1 -1
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/entities/components/SystemEntitiesTable.js.map +2 -2
- package/dist/modules/entities/components/UserEntitiesTable.js.map +2 -2
- package/dist/modules/feature_toggles/components/FeatureTogglesTable.js.map +2 -2
- package/dist/modules/feature_toggles/components/OverridesTable.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/log/page.js.map +1 -1
- package/dist/modules/inbox_ops/backend/inbox-ops/page.js.map +1 -1
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +1 -1
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +1 -1
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/page.js.map +1 -1
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +1 -1
- package/dist/modules/sales/components/PaymentMethodsSettings.js.map +1 -1
- package/dist/modules/sales/components/ShippingMethodsSettings.js.map +1 -1
- package/dist/modules/sales/components/TaxRatesSettings.js.map +1 -1
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +1 -1
- package/dist/modules/sales/components/documents/PaymentsSection.js.map +1 -1
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +1 -1
- package/dist/modules/staff/backend/staff/leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-roles/page.js.map +1 -1
- package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/teams/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/timesheets/projects/page.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +1 -1
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +1 -1
- package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +1 -1
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +1 -1
- package/dist/modules/workflows/backend/definitions/page.js.map +1 -1
- package/dist/modules/workflows/backend/events/page.js.map +1 -1
- package/dist/modules/workflows/backend/instances/page.js.map +1 -1
- package/dist/modules/workflows/backend/tasks/page.js.map +1 -1
- package/jest.config.cjs +1 -1
- package/package.json +7 -7
- package/src/modules/api_keys/backend/api-keys/page.tsx +1 -1
- package/src/modules/attachments/components/AttachmentLibrary.tsx +2 -1
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +1 -1
- package/src/modules/audit_logs/components/AccessLogsTable.tsx +1 -1
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +1 -1
- package/src/modules/auth/backend/roles/page.tsx +2 -1
- package/src/modules/auth/backend/users/page.tsx +2 -1
- package/src/modules/business_rules/backend/logs/page.tsx +1 -1
- package/src/modules/business_rules/backend/rules/page.tsx +1 -1
- package/src/modules/business_rules/backend/sets/page.tsx +1 -1
- package/src/modules/catalog/components/PriceKindSettings.tsx +1 -1
- package/src/modules/catalog/components/categories/CategoriesDataTable.tsx +1 -1
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +2 -1
- package/src/modules/communication_channels/backend/communication_channels/channels/page.tsx +1 -1
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +1 -1
- package/src/modules/currencies/backend/currencies/page.tsx +1 -1
- package/src/modules/currencies/backend/exchange-rates/page.tsx +1 -1
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +1 -1
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +1 -1
- package/src/modules/customers/backend/customers/companies/page.tsx +2 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +1 -1
- package/src/modules/customers/backend/customers/people/page.tsx +2 -1
- package/src/modules/customers/components/CustomerTodosTable.tsx +1 -1
- package/src/modules/data_sync/backend/data-sync/page.tsx +1 -1
- package/src/modules/dictionaries/components/DictionaryTable.tsx +2 -1
- package/src/modules/directory/backend/directory/organizations/page.tsx +1 -1
- package/src/modules/directory/backend/directory/tenants/page.tsx +2 -1
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +2 -1
- package/src/modules/entities/components/SystemEntitiesTable.tsx +2 -1
- package/src/modules/entities/components/UserEntitiesTable.tsx +2 -1
- package/src/modules/feature_toggles/components/FeatureTogglesTable.tsx +2 -1
- package/src/modules/feature_toggles/components/OverridesTable.tsx +2 -1
- package/src/modules/inbox_ops/backend/inbox-ops/log/page.tsx +1 -1
- package/src/modules/inbox_ops/backend/inbox-ops/page.tsx +1 -1
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +1 -1
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +1 -1
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +2 -1
- package/src/modules/query_index/components/QueryIndexesTable.tsx +2 -1
- package/src/modules/resources/backend/resources/resource-types/page.tsx +2 -1
- package/src/modules/resources/backend/resources/resources/page.tsx +1 -1
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +2 -1
- package/src/modules/sales/backend/sales/channels/page.tsx +2 -1
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +1 -1
- package/src/modules/sales/components/PaymentMethodsSettings.tsx +1 -1
- package/src/modules/sales/components/ShippingMethodsSettings.tsx +1 -1
- package/src/modules/sales/components/TaxRatesSettings.tsx +1 -1
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +2 -1
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +1 -1
- package/src/modules/sales/components/documents/PaymentsSection.tsx +1 -1
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +1 -1
- package/src/modules/staff/backend/staff/leave-requests/page.tsx +2 -1
- package/src/modules/staff/backend/staff/my-leave-requests/page.tsx +2 -1
- package/src/modules/staff/backend/staff/team-members/page.tsx +3 -2
- package/src/modules/staff/backend/staff/team-roles/page.tsx +1 -1
- package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +2 -1
- package/src/modules/staff/backend/staff/teams/page.tsx +2 -1
- package/src/modules/staff/backend/staff/timesheets/projects/page.tsx +2 -1
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +2 -1
- package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +4 -3
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +1 -1
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +1 -1
- package/src/modules/wms/components/backend/WmsLotsListPage.tsx +2 -1
- package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +1 -1
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +1 -1
- package/src/modules/workflows/backend/definitions/page.tsx +1 -1
- package/src/modules/workflows/backend/events/page.tsx +1 -1
- package/src/modules/workflows/backend/instances/page.tsx +1 -1
- package/src/modules/workflows/backend/tasks/page.tsx +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/sales/backend/sales/channels/offers/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport type { FilterOption } from '@open-mercato/ui/backend/FilterOverlay'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { mapOfferRow, renderOfferPriceSummary, type OfferRow } from '@open-mercato/core/modules/sales/components/channels/offerTableUtils'\nimport { useSalesChannelsEnabled } from '@open-mercato/core/modules/sales/components/useSalesChannelsEnabled'\nimport { SalesChannelsDisabledNotice } from '@open-mercato/core/modules/sales/components/SalesChannelsDisabledNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype OffersResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n totalPages?: number\n}\n\nconst PAGE_SIZE = 25\n\nconst SAVE_CONTEXT_ID = 'sales-channel-offers-list'\n\nexport default function SalesChannelOffersListPage() {\n const t = useT()\n const { enabled: channelsEnabled, isLoading: channelsEnabledLoading } = useSalesChannelsEnabled()\n const router = useRouter()\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.channels.offers',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n const scopeVersion = useOrganizationScopeVersion()\n const channelOptionsRef = React.useRef<Map<string, FilterOption>>(new Map())\n const [rows, setRows] = React.useState<OfferRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'updatedAt', desc: true }])\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [channelOptions, setChannelOptions] = React.useState<Map<string, FilterOption>>(channelOptionsRef.current)\n\n const selectedChannelIds = React.useMemo(() => {\n if (!Array.isArray(filterValues.channelIds)) return []\n return filterValues.channelIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value): value is string => value.length > 0)\n }, [filterValues])\n\n const upsertChannelOptions = React.useCallback((options: FilterOption[]) => {\n if (!options.length) return\n setChannelOptions((prev) => {\n const next = new Map(prev)\n options.forEach((option) => {\n if (!option.value) return\n next.set(option.value, option)\n })\n channelOptionsRef.current = next\n return next\n })\n }, [])\n\n const loadChannelOptions = React.useCallback(async (term?: string): Promise<FilterOption[]> => {\n try {\n const params = new URLSearchParams({ pageSize: '100', isActive: 'true' })\n if (term && term.trim().length) params.set('search', term.trim())\n const payload = await readApiResultOrThrow<{ items?: Array<{ id?: string; name?: string; code?: string; description?: string | null }> }>(\n `/api/sales/channels?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.filters.channelsLoadError', 'Failed to load channels') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n const options = items\n .map((entry) => {\n const value = typeof entry.id === 'string' ? entry.id : null\n if (!value) return null\n const label =\n typeof entry.name === 'string'\n ? entry.name\n : typeof entry.code === 'string'\n ? entry.code\n : value\n const description =\n typeof entry.code === 'string' && entry.code !== label\n ? entry.code\n : typeof entry.description === 'string' && entry.description.trim().length\n ? entry.description\n : null\n return { value, label, description }\n })\n .filter((option) => !!option) as FilterOption[]\n upsertChannelOptions(options)\n return options\n } catch (err) {\n logger.warn('sales.channels.offers failed to load channel options', { err })\n return []\n }\n }, [t, upsertChannelOptions])\n\n const ensureChannelMetadata = React.useCallback(async (ids: string[]) => {\n const missing = ids.filter((id) => !channelOptionsRef.current.has(id))\n if (!missing.length) return\n try {\n const params = new URLSearchParams({\n ids: missing.join(','),\n pageSize: String(Math.min(Math.max(missing.length, 1), 100)),\n })\n const payload = await readApiResultOrThrow<{ items?: Array<{ id?: string; name?: string; code?: string; description?: string | null }> }>(\n `/api/sales/channels?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.filters.channelsLoadError', 'Failed to load channels') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n const options = items\n .map((entry) => {\n const value = typeof entry.id === 'string' ? entry.id : null\n if (!value) return null\n const label =\n typeof entry.name === 'string'\n ? entry.name\n : typeof entry.code === 'string'\n ? entry.code\n : value\n const description =\n typeof entry.code === 'string' && entry.code !== label\n ? entry.code\n : typeof entry.description === 'string' && entry.description.trim().length\n ? entry.description\n : null\n return { value, label, description }\n })\n .filter((option) => !!option) as FilterOption[]\n upsertChannelOptions(options)\n } catch (err) {\n logger.warn('sales.channels.offers failed to hydrate channel metadata', { err })\n }\n }, [t, upsertChannelOptions])\n\n const columns = React.useMemo<ColumnDef<OfferRow>[]>(() => [\n {\n accessorKey: 'title',\n header: t('sales.channels.offers.table.offer', 'Offer'),\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n {row.original.productMediaUrl ? (\n <img\n src={row.original.productMediaUrl}\n alt={row.original.productTitle ?? row.original.title}\n className=\"h-12 w-12 rounded border object-cover\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted\" />\n )}\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.title}</span>\n <div className=\"text-xs text-muted-foreground\">\n {row.original.productTitle ?? t('sales.channels.offers.table.emptyProduct', 'Unlinked product')}\n </div>\n </div>\n </div>\n </div>\n ),\n meta: { sticky: true },\n },\n {\n accessorKey: 'pricing',\n header: t('sales.channels.offers.table.pricing', 'Prices'),\n cell: ({ row }) => (\n <div className=\"text-sm\">{renderOfferPriceSummary(row.original, t as any)}</div>\n ),\n },\n {\n accessorKey: 'channelId',\n header: t('sales.channels.offers.table.channel', 'Channel'),\n cell: ({ row }) => {\n const channelId = row.original.channelId\n if (!channelId) {\n return <span className=\"text-xs text-muted-foreground\">{t('sales.channels.offers.table.channelUnassigned', 'Unassigned')}</span>\n }\n const label = channelOptions.get(channelId)?.label ?? channelId\n const description = channelOptions.get(channelId)?.description ?? null\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-sm font-medium\">{label}</span>\n {description ? <span className=\"text-xs text-muted-foreground\">{description}</span> : null}\n </div>\n )\n },\n },\n {\n accessorKey: 'isActive',\n header: t('sales.channels.offers.table.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isActive} />,\n },\n {\n accessorKey: 'updatedAt',\n header: t('sales.channels.offers.table.updated', 'Updated'),\n cell: ({ row }) =>\n row.original.updatedAt\n ? <span className=\"text-xs text-muted-foreground\">{new Date(row.original.updatedAt).toLocaleDateString()}</span>\n : <span className=\"text-xs text-muted-foreground\">\u2014</span>,\n },\n ], [channelOptions, t])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'channelIds',\n label: t('sales.channels.offers.filters.channels', 'Sales channels'),\n type: 'tags',\n placeholder: t('sales.channels.offers.filters.channelsPlaceholder', 'Filter by channels\u2026'),\n loadOptions: loadChannelOptions,\n formatValue: (value: string) => channelOptions.get(value)?.label ?? value,\n formatDescription: (value: string) => channelOptions.get(value)?.description ?? null,\n },\n ], [channelOptions, loadChannelOptions, t])\n\n const loadOffers = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(PAGE_SIZE),\n })\n if (search.trim().length) {\n params.set('search', search.trim())\n }\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 (selectedChannelIds.length) {\n params.set('channelIds', selectedChannelIds.join(','))\n }\n const payload = await readApiResultOrThrow<OffersResponse>(\n `/api/catalog/offers?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.errors.load', 'Failed to load offers.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n const mapped = items.map(mapOfferRow)\n setRows(mapped)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))\n const ids = mapped\n .map((row) => row.channelId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n if (ids.length) void ensureChannelMetadata(Array.from(new Set(ids)))\n } catch (err) {\n logger.error('sales.channels.offers.list', { err })\n flash(t('sales.channels.offers.errors.load', 'Failed to load offers.'), 'error')\n } finally {\n setLoading(false)\n }\n }, [ensureChannelMetadata, page, search, selectedChannelIds, sorting, t])\n\n React.useEffect(() => {\n void loadOffers()\n }, [loadOffers, scopeVersion, reloadToken])\n\n const handleSearchChange = React.useCallback((value: string) => {\n setSearch(value)\n setPage(1)\n }, [])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const handleRefresh = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n const handleDelete = React.useCallback(async (row: OfferRow) => {\n try {\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(\n buildOptimisticLockHeader(row.updatedAt),\n () => deleteCrud('catalog/offers', row.id, {\n errorMessage: t('sales.channels.offers.errors.delete', 'Failed to delete offer.'),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'delete', id: row.id },\n })\n flash(t('sales.channels.offers.messages.deleted', 'Offer deleted.'), 'success')\n handleRefresh()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) { handleRefresh(); return }\n logger.error('sales.channels.offers.delete', { err })\n }\n }, [handleRefresh, mutationContext, runMutation, t])\n\n const tableTitle = (\n <div className=\"flex flex-col gap-1\">\n <span>{t('sales.channels.offers.listTitle', 'Sales channel offers')}</span>\n <span className=\"text-sm font-normal text-muted-foreground\">\n {t('sales.channels.offers.listSubtitle', 'Review product overrides across every sales channel.')}\n </span>\n </div>\n )\n\n if (!channelsEnabled && !channelsEnabledLoading) {\n return <SalesChannelsDisabledNotice />\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable<OfferRow>\n title={tableTitle}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('sales.channels.offers.table.search', 'Search offers\u2026')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={{\n label: t('sales.channels.offers.table.refresh', 'Refresh'),\n onRefresh: handleRefresh,\n isRefreshing: isLoading,\n }}\n rowActions={(row) => {\n if (!row.channelId) return null\n return (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('sales.channels.offers.actions.edit', 'Edit'),\n href: `/backend/sales/channels/${row.channelId}/offers/${row.id}/edit`,\n },\n {\n id: 'delete',\n label: t('sales.channels.offers.actions.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ]}\n />\n )\n }}\n onRowClick={(row) => {\n if (!row.channelId) return\n router.push(`/backend/sales/channels/${row.channelId}/offers/${row.id}/edit`)\n }}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('sales.channels.offers.table.emptyAll', 'No offers available yet.')}\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport type { SortingState } from '@tanstack/react-table'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport type { FilterOption } from '@open-mercato/ui/backend/FilterOverlay'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { mapOfferRow, renderOfferPriceSummary, type OfferRow } from '@open-mercato/core/modules/sales/components/channels/offerTableUtils'\nimport { useSalesChannelsEnabled } from '@open-mercato/core/modules/sales/components/useSalesChannelsEnabled'\nimport { SalesChannelsDisabledNotice } from '@open-mercato/core/modules/sales/components/SalesChannelsDisabledNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype OffersResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n totalPages?: number\n}\n\nconst PAGE_SIZE = 25\n\nconst SAVE_CONTEXT_ID = 'sales-channel-offers-list'\n\nexport default function SalesChannelOffersListPage() {\n const t = useT()\n const { enabled: channelsEnabled, isLoading: channelsEnabledLoading } = useSalesChannelsEnabled()\n const router = useRouter()\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.channels.offers',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n const scopeVersion = useOrganizationScopeVersion()\n const channelOptionsRef = React.useRef<Map<string, FilterOption>>(new Map())\n const [rows, setRows] = React.useState<OfferRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'updatedAt', desc: true }])\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const [channelOptions, setChannelOptions] = React.useState<Map<string, FilterOption>>(channelOptionsRef.current)\n\n const selectedChannelIds = React.useMemo(() => {\n if (!Array.isArray(filterValues.channelIds)) return []\n return filterValues.channelIds\n .map((value) => (typeof value === 'string' ? value.trim() : ''))\n .filter((value): value is string => value.length > 0)\n }, [filterValues])\n\n const upsertChannelOptions = React.useCallback((options: FilterOption[]) => {\n if (!options.length) return\n setChannelOptions((prev) => {\n const next = new Map(prev)\n options.forEach((option) => {\n if (!option.value) return\n next.set(option.value, option)\n })\n channelOptionsRef.current = next\n return next\n })\n }, [])\n\n const loadChannelOptions = React.useCallback(async (term?: string): Promise<FilterOption[]> => {\n try {\n const params = new URLSearchParams({ pageSize: '100', isActive: 'true' })\n if (term && term.trim().length) params.set('search', term.trim())\n const payload = await readApiResultOrThrow<{ items?: Array<{ id?: string; name?: string; code?: string; description?: string | null }> }>(\n `/api/sales/channels?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.filters.channelsLoadError', 'Failed to load channels') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n const options = items\n .map((entry) => {\n const value = typeof entry.id === 'string' ? entry.id : null\n if (!value) return null\n const label =\n typeof entry.name === 'string'\n ? entry.name\n : typeof entry.code === 'string'\n ? entry.code\n : value\n const description =\n typeof entry.code === 'string' && entry.code !== label\n ? entry.code\n : typeof entry.description === 'string' && entry.description.trim().length\n ? entry.description\n : null\n return { value, label, description }\n })\n .filter((option) => !!option) as FilterOption[]\n upsertChannelOptions(options)\n return options\n } catch (err) {\n logger.warn('sales.channels.offers failed to load channel options', { err })\n return []\n }\n }, [t, upsertChannelOptions])\n\n const ensureChannelMetadata = React.useCallback(async (ids: string[]) => {\n const missing = ids.filter((id) => !channelOptionsRef.current.has(id))\n if (!missing.length) return\n try {\n const params = new URLSearchParams({\n ids: missing.join(','),\n pageSize: String(Math.min(Math.max(missing.length, 1), 100)),\n })\n const payload = await readApiResultOrThrow<{ items?: Array<{ id?: string; name?: string; code?: string; description?: string | null }> }>(\n `/api/sales/channels?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.filters.channelsLoadError', 'Failed to load channels') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n const options = items\n .map((entry) => {\n const value = typeof entry.id === 'string' ? entry.id : null\n if (!value) return null\n const label =\n typeof entry.name === 'string'\n ? entry.name\n : typeof entry.code === 'string'\n ? entry.code\n : value\n const description =\n typeof entry.code === 'string' && entry.code !== label\n ? entry.code\n : typeof entry.description === 'string' && entry.description.trim().length\n ? entry.description\n : null\n return { value, label, description }\n })\n .filter((option) => !!option) as FilterOption[]\n upsertChannelOptions(options)\n } catch (err) {\n logger.warn('sales.channels.offers failed to hydrate channel metadata', { err })\n }\n }, [t, upsertChannelOptions])\n\n const columns = React.useMemo<ColumnDef<OfferRow>[]>(() => [\n {\n accessorKey: 'title',\n header: t('sales.channels.offers.table.offer', 'Offer'),\n cell: ({ row }) => (\n <div className=\"flex items-center gap-3\">\n {row.original.productMediaUrl ? (\n <img\n src={row.original.productMediaUrl}\n alt={row.original.productTitle ?? row.original.title}\n className=\"h-12 w-12 rounded border object-cover\"\n />\n ) : (\n <div className=\"h-12 w-12 rounded border bg-muted\" />\n )}\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.title}</span>\n <div className=\"text-xs text-muted-foreground\">\n {row.original.productTitle ?? t('sales.channels.offers.table.emptyProduct', 'Unlinked product')}\n </div>\n </div>\n </div>\n </div>\n ),\n meta: { sticky: true },\n },\n {\n accessorKey: 'pricing',\n header: t('sales.channels.offers.table.pricing', 'Prices'),\n cell: ({ row }) => (\n <div className=\"text-sm\">{renderOfferPriceSummary(row.original, t as any)}</div>\n ),\n },\n {\n accessorKey: 'channelId',\n header: t('sales.channels.offers.table.channel', 'Channel'),\n cell: ({ row }) => {\n const channelId = row.original.channelId\n if (!channelId) {\n return <span className=\"text-xs text-muted-foreground\">{t('sales.channels.offers.table.channelUnassigned', 'Unassigned')}</span>\n }\n const label = channelOptions.get(channelId)?.label ?? channelId\n const description = channelOptions.get(channelId)?.description ?? null\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-sm font-medium\">{label}</span>\n {description ? <span className=\"text-xs text-muted-foreground\">{description}</span> : null}\n </div>\n )\n },\n },\n {\n accessorKey: 'isActive',\n header: t('sales.channels.offers.table.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isActive} />,\n },\n {\n accessorKey: 'updatedAt',\n header: t('sales.channels.offers.table.updated', 'Updated'),\n cell: ({ row }) =>\n row.original.updatedAt\n ? <span className=\"text-xs text-muted-foreground\">{new Date(row.original.updatedAt).toLocaleDateString()}</span>\n : <span className=\"text-xs text-muted-foreground\">\u2014</span>,\n },\n ], [channelOptions, t])\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'channelIds',\n label: t('sales.channels.offers.filters.channels', 'Sales channels'),\n type: 'tags',\n placeholder: t('sales.channels.offers.filters.channelsPlaceholder', 'Filter by channels\u2026'),\n loadOptions: loadChannelOptions,\n formatValue: (value: string) => channelOptions.get(value)?.label ?? value,\n formatDescription: (value: string) => channelOptions.get(value)?.description ?? null,\n },\n ], [channelOptions, loadChannelOptions, t])\n\n const loadOffers = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(PAGE_SIZE),\n })\n if (search.trim().length) {\n params.set('search', search.trim())\n }\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 (selectedChannelIds.length) {\n params.set('channelIds', selectedChannelIds.join(','))\n }\n const payload = await readApiResultOrThrow<OffersResponse>(\n `/api/catalog/offers?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.offers.errors.load', 'Failed to load offers.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n const mapped = items.map(mapOfferRow)\n setRows(mapped)\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))\n const ids = mapped\n .map((row) => row.channelId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n if (ids.length) void ensureChannelMetadata(Array.from(new Set(ids)))\n } catch (err) {\n logger.error('sales.channels.offers.list', { err })\n flash(t('sales.channels.offers.errors.load', 'Failed to load offers.'), 'error')\n } finally {\n setLoading(false)\n }\n }, [ensureChannelMetadata, page, search, selectedChannelIds, sorting, t])\n\n React.useEffect(() => {\n void loadOffers()\n }, [loadOffers, scopeVersion, reloadToken])\n\n const handleSearchChange = React.useCallback((value: string) => {\n setSearch(value)\n setPage(1)\n }, [])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n setFilterValues(values)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const handleRefresh = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n const handleDelete = React.useCallback(async (row: OfferRow) => {\n try {\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(\n buildOptimisticLockHeader(row.updatedAt),\n () => deleteCrud('catalog/offers', row.id, {\n errorMessage: t('sales.channels.offers.errors.delete', 'Failed to delete offer.'),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'delete', id: row.id },\n })\n flash(t('sales.channels.offers.messages.deleted', 'Offer deleted.'), 'success')\n handleRefresh()\n } catch (err) {\n if (surfaceRecordConflict(err, t)) { handleRefresh(); return }\n logger.error('sales.channels.offers.delete', { err })\n }\n }, [handleRefresh, mutationContext, runMutation, t])\n\n const tableTitle = (\n <div className=\"flex flex-col gap-1\">\n <span>{t('sales.channels.offers.listTitle', 'Sales channel offers')}</span>\n <span className=\"text-sm font-normal text-muted-foreground\">\n {t('sales.channels.offers.listSubtitle', 'Review product overrides across every sales channel.')}\n </span>\n </div>\n )\n\n if (!channelsEnabled && !channelsEnabledLoading) {\n return <SalesChannelsDisabledNotice />\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable<OfferRow>\n title={tableTitle}\n columns={columns}\n data={rows}\n isLoading={isLoading}\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('sales.channels.offers.table.search', 'Search offers\u2026')}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={{\n label: t('sales.channels.offers.table.refresh', 'Refresh'),\n onRefresh: handleRefresh,\n isRefreshing: isLoading,\n }}\n rowActions={(row) => {\n if (!row.channelId) return null\n return (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('sales.channels.offers.actions.edit', 'Edit'),\n href: `/backend/sales/channels/${row.channelId}/offers/${row.id}/edit`,\n },\n {\n id: 'delete',\n label: t('sales.channels.offers.actions.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ]}\n />\n )\n }}\n onRowClick={(row) => {\n if (!row.channelId) return\n router.push(`/backend/sales/channels/${row.channelId}/offers/${row.id}/edit`)\n }}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('sales.channels.offers.table.emptyAll', 'No offers available yet.')}\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
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/sales/backend/sales/channels/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useSalesChannelsEnabled } from '../../../components/useSalesChannelsEnabled'\nimport { SalesChannelsDisabledNotice } from '../../../components/SalesChannelsDisabledNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype ChannelRow = {\n id: string\n name: string\n code: string | null\n description: string | null\n offerCount: number\n isActive: boolean\n updatedAt: string | null\n}\n\ntype ChannelsResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n totalPages?: number\n}\n\nconst PAGE_SIZE = 25\n\nconst SAVE_CONTEXT_ID = 'sales-channels-list'\n\nexport default function SalesChannelsPage() {\n const t = useT()\n const { enabled: channelsEnabled, isLoading: channelsEnabledLoading } = useSalesChannelsEnabled()\n const router = useRouter()\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.channels',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n const scopeVersion = useOrganizationScopeVersion()\n const [rows, setRows] = React.useState<ChannelRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [search, setSearch] = React.useState('')\n const [isLoading, setLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const columns = React.useMemo<ColumnDef<ChannelRow>[]>(() => [\n {\n accessorKey: 'name',\n header: t('sales.channels.table.name', 'Name'),\n cell: ({ row }) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.name}</span>\n {row.original.description ? (\n <span className=\"text-xs text-muted-foreground\">{row.original.description}</span>\n ) : null}\n </div>\n ),\n meta: { sticky: true },\n },\n {\n accessorKey: 'code',\n header: t('sales.channels.table.code', 'Code'),\n cell: ({ row }) => row.original.code ? (\n <span className=\"font-mono text-xs\">{row.original.code}</span>\n ) : (\n <span className=\"text-xs text-muted-foreground\">\u2014</span>\n ),\n },\n {\n accessorKey: 'offerCount',\n header: t('sales.channels.table.offers', 'Product offers'),\n cell: ({ row }) => (\n <span className=\"text-sm font-semibold\">{row.original.offerCount}</span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('sales.channels.table.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isActive} />,\n },\n {\n accessorKey: 'updatedAt',\n header: t('sales.channels.table.updated', 'Updated'),\n cell: ({ row }) =>\n row.original.updatedAt\n ? <span className=\"text-xs text-muted-foreground\">{new Date(row.original.updatedAt).toLocaleDateString()}</span>\n : <span className=\"text-xs text-muted-foreground\">\u2014</span>,\n },\n ], [t])\n\n const loadChannels = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(PAGE_SIZE),\n })\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 (search.trim().length) {\n params.set('search', search.trim())\n }\n const payload = await readApiResultOrThrow<ChannelsResponse>(\n `/api/sales/channels?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.table.errors.load', 'Failed to load channels.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items.map(mapApiChannel))\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))\n } catch (err) {\n logger.error('sales.channels.list', { err })\n flash(t('sales.channels.table.errors.load', 'Failed to load channels.'), 'error')\n } finally {\n setLoading(false)\n }\n }, [page, search, sorting, t])\n\n React.useEffect(() => {\n void loadChannels()\n }, [loadChannels, scopeVersion, reloadToken])\n\n const handleSearchChange = React.useCallback((value: string) => {\n setSearch(value)\n setPage(1)\n }, [])\n\n const handleRefresh = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n const handleDelete = React.useCallback(async (row: ChannelRow) => {\n try {\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(\n buildOptimisticLockHeader(row.updatedAt),\n () => deleteCrud('sales/channels', row.id, {\n errorMessage: t('sales.channels.table.errors.delete', 'Failed to delete channel.'),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'delete', id: row.id },\n })\n flash(t('sales.channels.table.messages.deleted', 'Channel deleted.'), 'success')\n handleRefresh()\n } catch (err) {\n if (extractOptimisticLockConflict(err)) {\n // Someone edited the channel after this list loaded \u2014 refuse the\n // stale delete, surface the conflict, and reload so the row reflects\n // the latest server state (QA #2055 channel edit/delete broken state).\n flash(\n t('ui.forms.flash.recordModified', 'This record was modified by someone else. Refresh and try again.'),\n 'error',\n )\n handleRefresh()\n return\n }\n logger.error('sales.channels.delete', { err })\n }\n }, [handleRefresh, mutationContext, runMutation, t])\n\n if (!channelsEnabled && !channelsEnabledLoading) {\n return <SalesChannelsDisabledNotice />\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable<ChannelRow>\n title={(\n <div className=\"flex flex-col\">\n <span>{t('sales.channels.nav.title', 'Sales channels')}</span>\n <span className=\"text-sm font-normal text-muted-foreground\">\n {t('sales.channels.table.subtitle', 'Organize catalog offers per marketplace or storefront.')}\n </span>\n </div>\n )}\n actions={(\n <Button asChild>\n <Link href=\"/backend/sales/channels/create\">\n {t('sales.channels.actions.create', 'Add channel')}\n </Link>\n </Button>\n )}\n columns={columns}\n data={rows}\n sorting={sorting}\n onSortingChange={setSorting}\n isLoading={isLoading}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('sales.channels.table.search', 'Search channels\u2026')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={{\n label: t('sales.channels.table.refresh', 'Refresh'),\n onRefresh: handleRefresh,\n isRefreshing: isLoading,\n }}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('sales.channels.table.actions.edit', 'Edit'),\n href: `/backend/sales/channels/${row.id}/edit`,\n },\n {\n id: 'delete',\n label: t('sales.channels.table.actions.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n },\n ]}\n />\n )}\n onRowClick={(row) => router.push(`/backend/sales/channels/${row.id}/edit`)}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('sales.channels.table.empty', 'No channels yet.')}\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n\nfunction mapApiChannel(item: Record<string, unknown>): ChannelRow {\n const id = typeof item.id === 'string' ? item.id : ''\n return withDataTableNamespaces({\n id,\n name: typeof item.name === 'string' ? item.name : id,\n code: typeof item.code === 'string' && item.code.length ? item.code : null,\n description: typeof item.description === 'string' && item.description.length ? item.description : null,\n offerCount: typeof item.offerCount === 'number'\n ? item.offerCount\n : typeof item.offer_count === 'number'\n ? item.offer_count\n : 0,\n isActive: item.isActive === true || item.is_active === true,\n updatedAt: typeof item.updatedAt === 'string'\n ? item.updatedAt\n : typeof item.updated_at === 'string'\n ? item.updated_at\n : null,\n }, item)\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport type { SortingState } from '@tanstack/react-table'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useSalesChannelsEnabled } from '../../../components/useSalesChannelsEnabled'\nimport { SalesChannelsDisabledNotice } from '../../../components/SalesChannelsDisabledNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype ChannelRow = {\n id: string\n name: string\n code: string | null\n description: string | null\n offerCount: number\n isActive: boolean\n updatedAt: string | null\n}\n\ntype ChannelsResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n totalPages?: number\n}\n\nconst PAGE_SIZE = 25\n\nconst SAVE_CONTEXT_ID = 'sales-channels-list'\n\nexport default function SalesChannelsPage() {\n const t = useT()\n const { enabled: channelsEnabled, isLoading: channelsEnabledLoading } = useSalesChannelsEnabled()\n const router = useRouter()\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.channels',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n const scopeVersion = useOrganizationScopeVersion()\n const [rows, setRows] = React.useState<ChannelRow[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [sorting, setSorting] = React.useState<SortingState>([])\n const [search, setSearch] = React.useState('')\n const [isLoading, setLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n\n const columns = React.useMemo<ColumnDef<ChannelRow>[]>(() => [\n {\n accessorKey: 'name',\n header: t('sales.channels.table.name', 'Name'),\n cell: ({ row }) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.name}</span>\n {row.original.description ? (\n <span className=\"text-xs text-muted-foreground\">{row.original.description}</span>\n ) : null}\n </div>\n ),\n meta: { sticky: true },\n },\n {\n accessorKey: 'code',\n header: t('sales.channels.table.code', 'Code'),\n cell: ({ row }) => row.original.code ? (\n <span className=\"font-mono text-xs\">{row.original.code}</span>\n ) : (\n <span className=\"text-xs text-muted-foreground\">\u2014</span>\n ),\n },\n {\n accessorKey: 'offerCount',\n header: t('sales.channels.table.offers', 'Product offers'),\n cell: ({ row }) => (\n <span className=\"text-sm font-semibold\">{row.original.offerCount}</span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: t('sales.channels.table.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isActive} />,\n },\n {\n accessorKey: 'updatedAt',\n header: t('sales.channels.table.updated', 'Updated'),\n cell: ({ row }) =>\n row.original.updatedAt\n ? <span className=\"text-xs text-muted-foreground\">{new Date(row.original.updatedAt).toLocaleDateString()}</span>\n : <span className=\"text-xs text-muted-foreground\">\u2014</span>,\n },\n ], [t])\n\n const loadChannels = React.useCallback(async () => {\n setLoading(true)\n try {\n const params = new URLSearchParams({\n page: String(page),\n pageSize: String(PAGE_SIZE),\n })\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 (search.trim().length) {\n params.set('search', search.trim())\n }\n const payload = await readApiResultOrThrow<ChannelsResponse>(\n `/api/sales/channels?${params.toString()}`,\n undefined,\n { errorMessage: t('sales.channels.table.errors.load', 'Failed to load channels.') },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setRows(items.map(mapApiChannel))\n setTotal(typeof payload.total === 'number' ? payload.total : items.length)\n setTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : Math.max(1, Math.ceil(items.length / PAGE_SIZE)))\n } catch (err) {\n logger.error('sales.channels.list', { err })\n flash(t('sales.channels.table.errors.load', 'Failed to load channels.'), 'error')\n } finally {\n setLoading(false)\n }\n }, [page, search, sorting, t])\n\n React.useEffect(() => {\n void loadChannels()\n }, [loadChannels, scopeVersion, reloadToken])\n\n const handleSearchChange = React.useCallback((value: string) => {\n setSearch(value)\n setPage(1)\n }, [])\n\n const handleRefresh = React.useCallback(() => {\n setReloadToken((token) => token + 1)\n }, [])\n\n const handleDelete = React.useCallback(async (row: ChannelRow) => {\n try {\n await runMutation({\n operation: () =>\n withScopedApiRequestHeaders(\n buildOptimisticLockHeader(row.updatedAt),\n () => deleteCrud('sales/channels', row.id, {\n errorMessage: t('sales.channels.table.errors.delete', 'Failed to delete channel.'),\n }),\n ),\n context: mutationContext,\n mutationPayload: { action: 'delete', id: row.id },\n })\n flash(t('sales.channels.table.messages.deleted', 'Channel deleted.'), 'success')\n handleRefresh()\n } catch (err) {\n if (extractOptimisticLockConflict(err)) {\n // Someone edited the channel after this list loaded \u2014 refuse the\n // stale delete, surface the conflict, and reload so the row reflects\n // the latest server state (QA #2055 channel edit/delete broken state).\n flash(\n t('ui.forms.flash.recordModified', 'This record was modified by someone else. Refresh and try again.'),\n 'error',\n )\n handleRefresh()\n return\n }\n logger.error('sales.channels.delete', { err })\n }\n }, [handleRefresh, mutationContext, runMutation, t])\n\n if (!channelsEnabled && !channelsEnabledLoading) {\n return <SalesChannelsDisabledNotice />\n }\n\n return (\n <Page>\n <PageBody>\n <DataTable<ChannelRow>\n title={(\n <div className=\"flex flex-col\">\n <span>{t('sales.channels.nav.title', 'Sales channels')}</span>\n <span className=\"text-sm font-normal text-muted-foreground\">\n {t('sales.channels.table.subtitle', 'Organize catalog offers per marketplace or storefront.')}\n </span>\n </div>\n )}\n actions={(\n <Button asChild>\n <Link href=\"/backend/sales/channels/create\">\n {t('sales.channels.actions.create', 'Add channel')}\n </Link>\n </Button>\n )}\n columns={columns}\n data={rows}\n sorting={sorting}\n onSortingChange={setSorting}\n isLoading={isLoading}\n searchValue={search}\n onSearchChange={handleSearchChange}\n searchPlaceholder={t('sales.channels.table.search', 'Search channels\u2026')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={{\n label: t('sales.channels.table.refresh', 'Refresh'),\n onRefresh: handleRefresh,\n isRefreshing: isLoading,\n }}\n rowActions={(row) => (\n <RowActions\n items={[\n {\n id: 'edit',\n label: t('sales.channels.table.actions.edit', 'Edit'),\n href: `/backend/sales/channels/${row.id}/edit`,\n },\n {\n id: 'delete',\n label: t('sales.channels.table.actions.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n },\n ]}\n />\n )}\n onRowClick={(row) => router.push(`/backend/sales/channels/${row.id}/edit`)}\n emptyState={\n <div className=\"py-10 text-center text-sm text-muted-foreground\">\n {t('sales.channels.table.empty', 'No channels yet.')}\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n\nfunction mapApiChannel(item: Record<string, unknown>): ChannelRow {\n const id = typeof item.id === 'string' ? item.id : ''\n return withDataTableNamespaces({\n id,\n name: typeof item.name === 'string' ? item.name : id,\n code: typeof item.code === 'string' && item.code.length ? item.code : null,\n description: typeof item.description === 'string' && item.description.length ? item.description : null,\n offerCount: typeof item.offerCount === 'number'\n ? item.offerCount\n : typeof item.offer_count === 'number'\n ? item.offer_count\n : 0,\n isActive: item.isActive === true || item.is_active === true,\n updatedAt: typeof item.updatedAt === 'string'\n ? item.updatedAt\n : typeof item.updated_at === 'string'\n ? item.updated_at\n : null,\n }, item)\n}\n"],
|
|
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/sales/components/AdjustmentKindSettings.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { AppearanceSelector } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'\nimport { renderDictionaryColor, renderDictionaryIcon } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype AdjustmentKind = {\n id: string\n value: string\n label: string\n color: string | null\n icon: string | null\n createdAt: string | null\n updatedAt: string | null\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: AdjustmentKind }\n\ntype FormState = {\n value: string\n label: string\n color: string | null\n icon: string | null\n}\n\nconst DEFAULT_FORM: FormState = {\n value: '',\n label: '',\n color: null,\n icon: null,\n}\n\nconst PAGE_SIZE = 100\n\nconst normalizeEntry = (raw: any): AdjustmentKind | null => {\n if (!raw || typeof raw !== 'object') return null\n const value = typeof raw.value === 'string' ? raw.value.trim() : ''\n const id = typeof raw.id === 'string' ? raw.id : ''\n if (!value || !id) return null\n const label = typeof raw.label === 'string' && raw.label.trim().length ? raw.label.trim() : value\n const color =\n typeof raw.color === 'string' && /^#([0-9a-fA-F]{6})$/.test(raw.color) ? `#${raw.color.slice(1).toLowerCase()}` : null\n const icon = typeof raw.icon === 'string' && raw.icon.trim().length ? raw.icon.trim() : null\n return {\n id,\n value,\n label,\n color,\n icon,\n createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : null,\n updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : null,\n }\n}\n\nconst SAVE_CONTEXT_ID = 'sales-adjustment-kind-settings'\n\nexport function AdjustmentKindSettings() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.adjustmentKinds',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n const [items, setItems] = React.useState<AdjustmentKind[]>([])\n const [search, setSearch] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [form, setForm] = React.useState<FormState>(DEFAULT_FORM)\n const [submitting, setSubmitting] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const labels = React.useMemo(\n () => ({\n title: t('sales.config.adjustmentKinds.title', 'Adjustment kinds'),\n description: t(\n 'sales.config.adjustmentKinds.description',\n 'Manage reusable adjustment kinds for taxes, surcharges, shipping, and discounts.'\n ),\n add: t('sales.config.adjustmentKinds.actions.add', 'Add adjustment kind'),\n refresh: t('sales.config.adjustmentKinds.actions.refresh', 'Refresh'),\n edit: t('sales.config.adjustmentKinds.actions.edit', 'Edit'),\n delete: t('sales.config.adjustmentKinds.actions.delete', 'Delete'),\n deleteConfirm: t('sales.config.adjustmentKinds.confirm.delete', 'Delete adjustment kind \"{{code}}\"?'),\n createTitle: t('sales.config.adjustmentKinds.dialog.createTitle', 'Create adjustment kind'),\n editTitle: t('sales.config.adjustmentKinds.dialog.editTitle', 'Edit adjustment kind'),\n createDescription: t(\n 'sales.config.adjustmentKinds.dialog.createDescription',\n 'Define a reusable adjustment kind shown in document adjustment dialogs.'\n ),\n editDescription: t(\n 'sales.config.adjustmentKinds.dialog.editDescription',\n 'Update the code, label, or appearance for this adjustment kind.'\n ),\n codeLabel: t('sales.config.adjustmentKinds.form.codeLabel', 'Code'),\n codePlaceholder: t('sales.config.adjustmentKinds.form.codePlaceholder', 'e.g. discount'),\n labelLabel: t('sales.config.adjustmentKinds.form.labelLabel', 'Label'),\n labelPlaceholder: t('sales.config.adjustmentKinds.form.labelPlaceholder', 'e.g. Discount'),\n appearanceLabel: t('sales.config.adjustmentKinds.form.appearanceLabel', 'Appearance'),\n saveCreate: t('sales.config.adjustmentKinds.actions.create', 'Create'),\n saveEdit: t('sales.config.adjustmentKinds.actions.save', 'Save changes'),\n cancel: t('ui.actions.cancel', 'Cancel'),\n required: t('sales.config.adjustmentKinds.errors.required', 'Code is required.'),\n loadError: t('sales.config.adjustmentKinds.errors.load', 'Failed to load adjustment kinds.'),\n saveError: t('sales.config.adjustmentKinds.errors.save', 'Failed to save adjustment kind.'),\n deleteError: t('sales.config.adjustmentKinds.errors.delete', 'Failed to delete adjustment kind.'),\n created: t('sales.config.adjustmentKinds.messages.created', 'Adjustment kind created.'),\n updated: t('sales.config.adjustmentKinds.messages.updated', 'Adjustment kind updated.'),\n deleted: t('sales.config.adjustmentKinds.messages.deleted', 'Adjustment kind deleted.'),\n empty: t('sales.config.adjustmentKinds.table.empty', 'No adjustment kinds yet.'),\n searchPlaceholder: t('sales.config.adjustmentKinds.search.placeholder', 'Search by code or label\u2026'),\n columns: {\n code: t('sales.config.adjustmentKinds.table.code', 'Code'),\n label: t('sales.config.adjustmentKinds.table.label', 'Label'),\n appearance: t('sales.config.adjustmentKinds.table.appearance', 'Appearance'),\n },\n appearanceLabels: {\n colorLabel: t('dictionaries.form.colorLabel', 'Color'),\n colorHelp: t('dictionaries.form.colorHelp', 'Pick a highlight color.'),\n colorClearLabel: t('dictionaries.form.colorClear', 'Remove color'),\n iconLabel: t('dictionaries.form.iconLabel', 'Icon or emoji'),\n iconPlaceholder: t('dictionaries.form.iconPlaceholder', 'Type an emoji or icon token.'),\n iconPickerTriggerLabel: t('dictionaries.form.iconPickerTriggerLabel', 'Browse icons and emoji'),\n iconSearchPlaceholder: t('dictionaries.form.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('dictionaries.form.iconSearchEmptyLabel', 'No icons match your search.'),\n iconSuggestionsLabel: t('dictionaries.form.iconSuggestionsLabel', 'Suggestions'),\n iconClearLabel: t('dictionaries.form.iconClearLabel', 'Remove icon'),\n previewEmptyLabel: t('dictionaries.form.previewEmptyLabel', 'No appearance selected'),\n },\n }),\n [t]\n )\n\n const loadItems = React.useCallback(async () => {\n setLoading(true)\n try {\n const payload = await readApiResultOrThrow<{ items?: any[] }>(\n `/api/sales/adjustment-kinds?pageSize=${PAGE_SIZE}`,\n undefined,\n { errorMessage: labels.loadError }\n )\n const mapped = Array.isArray(payload.items)\n ? payload.items\n .map((entry) => normalizeEntry(entry))\n .filter((entry): entry is AdjustmentKind => Boolean(entry))\n : []\n setItems(mapped)\n } catch (err) {\n logger.error('sales.adjustment-kinds.list failed', { err })\n flash(labels.loadError, 'error')\n } finally {\n setLoading(false)\n }\n }, [labels.loadError])\n\n React.useEffect(() => {\n loadItems().catch(() => {})\n }, [loadItems, scopeVersion])\n\n const openDialog = React.useCallback((state: DialogState) => {\n if (state.mode === 'edit') {\n setForm({\n value: state.entry.value,\n label: state.entry.label,\n color: state.entry.color,\n icon: state.entry.icon,\n })\n } else {\n setForm(DEFAULT_FORM)\n }\n setError(null)\n setDialog(state)\n }, [])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n setSubmitting(false)\n setError(null)\n setForm(DEFAULT_FORM)\n }, [])\n\n const handleSubmit = React.useCallback(async () => {\n if (!dialog) return\n const trimmedValue = form.value.trim()\n const trimmedLabel = form.label.trim()\n if (!trimmedValue) {\n setError(labels.required)\n return\n }\n setSubmitting(true)\n setError(null)\n try {\n const payload: Record<string, unknown> = {\n value: trimmedValue,\n }\n if (trimmedLabel) payload.label = trimmedLabel\n if (form.color !== undefined) payload.color = form.color\n if (form.icon !== undefined) payload.icon = form.icon\n\n const method = dialog.mode === 'create' ? 'POST' : 'PUT'\n const body =\n dialog.mode === 'create'\n ? JSON.stringify(payload)\n : JSON.stringify({ id: dialog.entry.id, ...payload })\n\n const lockHeader =\n dialog.mode === 'edit' ? buildOptimisticLockHeader(dialog.entry.updatedAt) : {}\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(lockHeader, () =>\n apiCall('/api/sales/adjustment-kinds', {\n method,\n headers: { 'content-type': 'application/json' },\n body,\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, labels.saveError)\n }\n return call\n },\n context: mutationContext,\n mutationPayload:\n dialog.mode === 'create'\n ? { action: 'create', ...payload }\n : { action: 'update', id: dialog.entry.id, ...payload },\n })\n flash(dialog.mode === 'create' ? labels.created : labels.updated, 'success')\n closeDialog()\n await loadItems()\n } catch (err) {\n logger.error('sales.adjustment-kinds.save failed', { err })\n const message = err instanceof Error ? err.message : labels.saveError\n setError(message)\n } finally {\n setSubmitting(false)\n }\n }, [closeDialog, dialog, form.color, form.icon, form.label, form.value, labels, loadItems, mutationContext, runMutation])\n\n const handleDelete = React.useCallback(\n async (entry: AdjustmentKind) => {\n const message = labels.deleteConfirm.replace('{{code}}', entry.label || entry.value)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall('/api/sales/adjustment-kinds', {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id }),\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, labels.deleteError)\n }\n return call\n },\n context: mutationContext,\n mutationPayload: { action: 'delete', id: entry.id },\n })\n flash(labels.deleted, 'success')\n await loadItems()\n } catch (err) {\n logger.error('sales.adjustment-kinds.delete failed', { err })\n const message = err instanceof Error ? err.message : labels.deleteError\n flash(message, 'error')\n }\n },\n [confirm, labels, loadItems, mutationContext, runMutation]\n )\n\n const formKeyHandler = React.useCallback(\n (event: React.KeyboardEvent<HTMLFormElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit]\n )\n\n const columns = React.useMemo<ColumnDef<AdjustmentKind>[]>(() => [\n {\n accessorKey: 'value',\n header: labels.columns.code,\n cell: ({ row }) => <span className=\"font-mono uppercase\">{row.original.value}</span>,\n },\n {\n accessorKey: 'label',\n header: labels.columns.label,\n cell: ({ row }) => <span className=\"font-medium\">{row.original.label}</span>,\n },\n {\n id: 'appearance',\n header: labels.columns.appearance,\n cell: ({ row }) => (\n <div className=\"flex items-center gap-2\">\n {renderDictionaryIcon(row.original.icon, 'h-4 w-4')}\n {renderDictionaryColor(row.original.color, 'h-4 w-4 rounded-full')}\n </div>\n ),\n },\n ], [labels.columns.appearance, labels.columns.code, labels.columns.label])\n\n const filteredItems = React.useMemo(() => {\n const term = search.trim().toLowerCase()\n if (!term) return items\n return items.filter((item) => item.value.toLowerCase().includes(term) || item.label.toLowerCase().includes(term))\n }, [items, search])\n\n const onRowClick = React.useCallback((entry: AdjustmentKind) => {\n openDialog({ mode: 'edit', entry })\n }, [openDialog])\n\n return (\n <section className=\"border bg-card text-card-foreground shadow-sm\" id=\"adjustment-kinds\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-semibold\">{labels.title}</h2>\n <p className=\"text-sm text-muted-foreground\">{labels.description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DataTable<AdjustmentKind>\n data={filteredItems}\n columns={columns}\n embedded\n isLoading={loading}\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder={labels.searchPlaceholder}\n emptyState={<p className=\"py-8 text-center text-sm text-muted-foreground\">{labels.empty}</p>}\n actions={(\n <Button size=\"sm\" onClick={() => openDialog({ mode: 'create' })}>\n {labels.add}\n </Button>\n )}\n refreshButton={{\n label: labels.refresh,\n onRefresh: () => { void loadItems() },\n isRefreshing: loading,\n }}\n rowActions={(entry) => (\n <RowActions\n items={[\n { id: 'edit', label: labels.edit, onSelect: () => openDialog({ mode: 'edit', entry }) },\n { id: 'delete', label: labels.delete, destructive: true, onSelect: () => { void handleDelete(entry) } },\n ]}\n />\n )}\n onRowClick={onRowClick}\n />\n </div>\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit' ? labels.editTitle : labels.createTitle}\n </DialogTitle>\n <DialogDescription>\n {dialog?.mode === 'edit' ? labels.editDescription : labels.createDescription}\n </DialogDescription>\n </DialogHeader>\n <form\n className=\"space-y-4\"\n onKeyDown={formKeyHandler}\n onSubmit={(event) => {\n event.preventDefault()\n void handleSubmit()\n }}\n >\n <div className=\"space-y-2\">\n <Label htmlFor=\"adjustment-kind-code\">{labels.codeLabel}</Label>\n <Input\n id=\"adjustment-kind-code\"\n value={form.value}\n onChange={(event) => setForm((prev) => ({ ...prev, value: event.target.value }))}\n placeholder={labels.codePlaceholder}\n className=\"font-mono uppercase\"\n disabled={dialog?.mode === 'edit'}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"adjustment-kind-label\">{labels.labelLabel}</Label>\n <Input\n id=\"adjustment-kind-label\"\n value={form.label}\n onChange={(event) => setForm((prev) => ({ ...prev, label: event.target.value }))}\n placeholder={labels.labelPlaceholder}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>{labels.appearanceLabel}</Label>\n <AppearanceSelector\n color={form.color}\n icon={form.icon}\n onColorChange={(next) => setForm((prev) => ({ ...prev, color: next }))}\n onIconChange={(next) => setForm((prev) => ({ ...prev, icon: next }))}\n labels={labels.appearanceLabels}\n />\n </div>\n {error ? <p className=\"text-sm text-destructive\">{error}</p> : null}\n </form>\n <DialogFooter>\n <Button variant=\"ghost\" onClick={closeDialog}>\n {labels.cancel}\n </Button>\n <Button onClick={() => void handleSubmit()} disabled={submitting}>\n {dialog?.mode === 'edit' ? labels.saveEdit : labels.saveCreate}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </section>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { AppearanceSelector } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'\nimport { renderDictionaryColor, renderDictionaryIcon } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype AdjustmentKind = {\n id: string\n value: string\n label: string\n color: string | null\n icon: string | null\n createdAt: string | null\n updatedAt: string | null\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: AdjustmentKind }\n\ntype FormState = {\n value: string\n label: string\n color: string | null\n icon: string | null\n}\n\nconst DEFAULT_FORM: FormState = {\n value: '',\n label: '',\n color: null,\n icon: null,\n}\n\nconst PAGE_SIZE = 100\n\nconst normalizeEntry = (raw: any): AdjustmentKind | null => {\n if (!raw || typeof raw !== 'object') return null\n const value = typeof raw.value === 'string' ? raw.value.trim() : ''\n const id = typeof raw.id === 'string' ? raw.id : ''\n if (!value || !id) return null\n const label = typeof raw.label === 'string' && raw.label.trim().length ? raw.label.trim() : value\n const color =\n typeof raw.color === 'string' && /^#([0-9a-fA-F]{6})$/.test(raw.color) ? `#${raw.color.slice(1).toLowerCase()}` : null\n const icon = typeof raw.icon === 'string' && raw.icon.trim().length ? raw.icon.trim() : null\n return {\n id,\n value,\n label,\n color,\n icon,\n createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : null,\n updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : null,\n }\n}\n\nconst SAVE_CONTEXT_ID = 'sales-adjustment-kind-settings'\n\nexport function AdjustmentKindSettings() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: SAVE_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({\n formId: SAVE_CONTEXT_ID,\n resourceKind: 'sales.adjustmentKinds',\n retryLastMutation,\n }),\n [retryLastMutation],\n )\n const [items, setItems] = React.useState<AdjustmentKind[]>([])\n const [search, setSearch] = React.useState('')\n const [loading, setLoading] = React.useState(false)\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [form, setForm] = React.useState<FormState>(DEFAULT_FORM)\n const [submitting, setSubmitting] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n\n const labels = React.useMemo(\n () => ({\n title: t('sales.config.adjustmentKinds.title', 'Adjustment kinds'),\n description: t(\n 'sales.config.adjustmentKinds.description',\n 'Manage reusable adjustment kinds for taxes, surcharges, shipping, and discounts.'\n ),\n add: t('sales.config.adjustmentKinds.actions.add', 'Add adjustment kind'),\n refresh: t('sales.config.adjustmentKinds.actions.refresh', 'Refresh'),\n edit: t('sales.config.adjustmentKinds.actions.edit', 'Edit'),\n delete: t('sales.config.adjustmentKinds.actions.delete', 'Delete'),\n deleteConfirm: t('sales.config.adjustmentKinds.confirm.delete', 'Delete adjustment kind \"{{code}}\"?'),\n createTitle: t('sales.config.adjustmentKinds.dialog.createTitle', 'Create adjustment kind'),\n editTitle: t('sales.config.adjustmentKinds.dialog.editTitle', 'Edit adjustment kind'),\n createDescription: t(\n 'sales.config.adjustmentKinds.dialog.createDescription',\n 'Define a reusable adjustment kind shown in document adjustment dialogs.'\n ),\n editDescription: t(\n 'sales.config.adjustmentKinds.dialog.editDescription',\n 'Update the code, label, or appearance for this adjustment kind.'\n ),\n codeLabel: t('sales.config.adjustmentKinds.form.codeLabel', 'Code'),\n codePlaceholder: t('sales.config.adjustmentKinds.form.codePlaceholder', 'e.g. discount'),\n labelLabel: t('sales.config.adjustmentKinds.form.labelLabel', 'Label'),\n labelPlaceholder: t('sales.config.adjustmentKinds.form.labelPlaceholder', 'e.g. Discount'),\n appearanceLabel: t('sales.config.adjustmentKinds.form.appearanceLabel', 'Appearance'),\n saveCreate: t('sales.config.adjustmentKinds.actions.create', 'Create'),\n saveEdit: t('sales.config.adjustmentKinds.actions.save', 'Save changes'),\n cancel: t('ui.actions.cancel', 'Cancel'),\n required: t('sales.config.adjustmentKinds.errors.required', 'Code is required.'),\n loadError: t('sales.config.adjustmentKinds.errors.load', 'Failed to load adjustment kinds.'),\n saveError: t('sales.config.adjustmentKinds.errors.save', 'Failed to save adjustment kind.'),\n deleteError: t('sales.config.adjustmentKinds.errors.delete', 'Failed to delete adjustment kind.'),\n created: t('sales.config.adjustmentKinds.messages.created', 'Adjustment kind created.'),\n updated: t('sales.config.adjustmentKinds.messages.updated', 'Adjustment kind updated.'),\n deleted: t('sales.config.adjustmentKinds.messages.deleted', 'Adjustment kind deleted.'),\n empty: t('sales.config.adjustmentKinds.table.empty', 'No adjustment kinds yet.'),\n searchPlaceholder: t('sales.config.adjustmentKinds.search.placeholder', 'Search by code or label\u2026'),\n columns: {\n code: t('sales.config.adjustmentKinds.table.code', 'Code'),\n label: t('sales.config.adjustmentKinds.table.label', 'Label'),\n appearance: t('sales.config.adjustmentKinds.table.appearance', 'Appearance'),\n },\n appearanceLabels: {\n colorLabel: t('dictionaries.form.colorLabel', 'Color'),\n colorHelp: t('dictionaries.form.colorHelp', 'Pick a highlight color.'),\n colorClearLabel: t('dictionaries.form.colorClear', 'Remove color'),\n iconLabel: t('dictionaries.form.iconLabel', 'Icon or emoji'),\n iconPlaceholder: t('dictionaries.form.iconPlaceholder', 'Type an emoji or icon token.'),\n iconPickerTriggerLabel: t('dictionaries.form.iconPickerTriggerLabel', 'Browse icons and emoji'),\n iconSearchPlaceholder: t('dictionaries.form.iconSearchPlaceholder', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('dictionaries.form.iconSearchEmptyLabel', 'No icons match your search.'),\n iconSuggestionsLabel: t('dictionaries.form.iconSuggestionsLabel', 'Suggestions'),\n iconClearLabel: t('dictionaries.form.iconClearLabel', 'Remove icon'),\n previewEmptyLabel: t('dictionaries.form.previewEmptyLabel', 'No appearance selected'),\n },\n }),\n [t]\n )\n\n const loadItems = React.useCallback(async () => {\n setLoading(true)\n try {\n const payload = await readApiResultOrThrow<{ items?: any[] }>(\n `/api/sales/adjustment-kinds?pageSize=${PAGE_SIZE}`,\n undefined,\n { errorMessage: labels.loadError }\n )\n const mapped = Array.isArray(payload.items)\n ? payload.items\n .map((entry) => normalizeEntry(entry))\n .filter((entry): entry is AdjustmentKind => Boolean(entry))\n : []\n setItems(mapped)\n } catch (err) {\n logger.error('sales.adjustment-kinds.list failed', { err })\n flash(labels.loadError, 'error')\n } finally {\n setLoading(false)\n }\n }, [labels.loadError])\n\n React.useEffect(() => {\n loadItems().catch(() => {})\n }, [loadItems, scopeVersion])\n\n const openDialog = React.useCallback((state: DialogState) => {\n if (state.mode === 'edit') {\n setForm({\n value: state.entry.value,\n label: state.entry.label,\n color: state.entry.color,\n icon: state.entry.icon,\n })\n } else {\n setForm(DEFAULT_FORM)\n }\n setError(null)\n setDialog(state)\n }, [])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n setSubmitting(false)\n setError(null)\n setForm(DEFAULT_FORM)\n }, [])\n\n const handleSubmit = React.useCallback(async () => {\n if (!dialog) return\n const trimmedValue = form.value.trim()\n const trimmedLabel = form.label.trim()\n if (!trimmedValue) {\n setError(labels.required)\n return\n }\n setSubmitting(true)\n setError(null)\n try {\n const payload: Record<string, unknown> = {\n value: trimmedValue,\n }\n if (trimmedLabel) payload.label = trimmedLabel\n if (form.color !== undefined) payload.color = form.color\n if (form.icon !== undefined) payload.icon = form.icon\n\n const method = dialog.mode === 'create' ? 'POST' : 'PUT'\n const body =\n dialog.mode === 'create'\n ? JSON.stringify(payload)\n : JSON.stringify({ id: dialog.entry.id, ...payload })\n\n const lockHeader =\n dialog.mode === 'edit' ? buildOptimisticLockHeader(dialog.entry.updatedAt) : {}\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(lockHeader, () =>\n apiCall('/api/sales/adjustment-kinds', {\n method,\n headers: { 'content-type': 'application/json' },\n body,\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, labels.saveError)\n }\n return call\n },\n context: mutationContext,\n mutationPayload:\n dialog.mode === 'create'\n ? { action: 'create', ...payload }\n : { action: 'update', id: dialog.entry.id, ...payload },\n })\n flash(dialog.mode === 'create' ? labels.created : labels.updated, 'success')\n closeDialog()\n await loadItems()\n } catch (err) {\n logger.error('sales.adjustment-kinds.save failed', { err })\n const message = err instanceof Error ? err.message : labels.saveError\n setError(message)\n } finally {\n setSubmitting(false)\n }\n }, [closeDialog, dialog, form.color, form.icon, form.label, form.value, labels, loadItems, mutationContext, runMutation])\n\n const handleDelete = React.useCallback(\n async (entry: AdjustmentKind) => {\n const message = labels.deleteConfirm.replace('{{code}}', entry.label || entry.value)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n await runMutation({\n operation: async () => {\n const call = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(entry.updatedAt),\n () =>\n apiCall('/api/sales/adjustment-kinds', {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id }),\n })\n )\n if (!call.ok) {\n await raiseCrudError(call.response, labels.deleteError)\n }\n return call\n },\n context: mutationContext,\n mutationPayload: { action: 'delete', id: entry.id },\n })\n flash(labels.deleted, 'success')\n await loadItems()\n } catch (err) {\n logger.error('sales.adjustment-kinds.delete failed', { err })\n const message = err instanceof Error ? err.message : labels.deleteError\n flash(message, 'error')\n }\n },\n [confirm, labels, loadItems, mutationContext, runMutation]\n )\n\n const formKeyHandler = React.useCallback(\n (event: React.KeyboardEvent<HTMLFormElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void handleSubmit()\n }\n },\n [handleSubmit]\n )\n\n const columns = React.useMemo<ColumnDef<AdjustmentKind>[]>(() => [\n {\n accessorKey: 'value',\n header: labels.columns.code,\n cell: ({ row }) => <span className=\"font-mono uppercase\">{row.original.value}</span>,\n },\n {\n accessorKey: 'label',\n header: labels.columns.label,\n cell: ({ row }) => <span className=\"font-medium\">{row.original.label}</span>,\n },\n {\n id: 'appearance',\n header: labels.columns.appearance,\n cell: ({ row }) => (\n <div className=\"flex items-center gap-2\">\n {renderDictionaryIcon(row.original.icon, 'h-4 w-4')}\n {renderDictionaryColor(row.original.color, 'h-4 w-4 rounded-full')}\n </div>\n ),\n },\n ], [labels.columns.appearance, labels.columns.code, labels.columns.label])\n\n const filteredItems = React.useMemo(() => {\n const term = search.trim().toLowerCase()\n if (!term) return items\n return items.filter((item) => item.value.toLowerCase().includes(term) || item.label.toLowerCase().includes(term))\n }, [items, search])\n\n const onRowClick = React.useCallback((entry: AdjustmentKind) => {\n openDialog({ mode: 'edit', entry })\n }, [openDialog])\n\n return (\n <section className=\"border bg-card text-card-foreground shadow-sm\" id=\"adjustment-kinds\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-semibold\">{labels.title}</h2>\n <p className=\"text-sm text-muted-foreground\">{labels.description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DataTable<AdjustmentKind>\n data={filteredItems}\n columns={columns}\n embedded\n isLoading={loading}\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder={labels.searchPlaceholder}\n emptyState={<p className=\"py-8 text-center text-sm text-muted-foreground\">{labels.empty}</p>}\n actions={(\n <Button size=\"sm\" onClick={() => openDialog({ mode: 'create' })}>\n {labels.add}\n </Button>\n )}\n refreshButton={{\n label: labels.refresh,\n onRefresh: () => { void loadItems() },\n isRefreshing: loading,\n }}\n rowActions={(entry) => (\n <RowActions\n items={[\n { id: 'edit', label: labels.edit, onSelect: () => openDialog({ mode: 'edit', entry }) },\n { id: 'delete', label: labels.delete, destructive: true, onSelect: () => { void handleDelete(entry) } },\n ]}\n />\n )}\n onRowClick={onRowClick}\n />\n </div>\n <Dialog open={dialog !== null} onOpenChange={(open) => { if (!open) closeDialog() }}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit' ? labels.editTitle : labels.createTitle}\n </DialogTitle>\n <DialogDescription>\n {dialog?.mode === 'edit' ? labels.editDescription : labels.createDescription}\n </DialogDescription>\n </DialogHeader>\n <form\n className=\"space-y-4\"\n onKeyDown={formKeyHandler}\n onSubmit={(event) => {\n event.preventDefault()\n void handleSubmit()\n }}\n >\n <div className=\"space-y-2\">\n <Label htmlFor=\"adjustment-kind-code\">{labels.codeLabel}</Label>\n <Input\n id=\"adjustment-kind-code\"\n value={form.value}\n onChange={(event) => setForm((prev) => ({ ...prev, value: event.target.value }))}\n placeholder={labels.codePlaceholder}\n className=\"font-mono uppercase\"\n disabled={dialog?.mode === 'edit'}\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"adjustment-kind-label\">{labels.labelLabel}</Label>\n <Input\n id=\"adjustment-kind-label\"\n value={form.label}\n onChange={(event) => setForm((prev) => ({ ...prev, label: event.target.value }))}\n placeholder={labels.labelPlaceholder}\n />\n </div>\n <div className=\"space-y-2\">\n <Label>{labels.appearanceLabel}</Label>\n <AppearanceSelector\n color={form.color}\n icon={form.icon}\n onColorChange={(next) => setForm((prev) => ({ ...prev, color: next }))}\n onIconChange={(next) => setForm((prev) => ({ ...prev, icon: next }))}\n labels={labels.appearanceLabels}\n />\n </div>\n {error ? <p className=\"text-sm text-destructive\">{error}</p> : null}\n </form>\n <DialogFooter>\n <Button variant=\"ghost\" onClick={closeDialog}>\n {labels.cancel}\n </Button>\n <Button onClick={() => void handleSubmit()} disabled={submitting}>\n {dialog?.mode === 'edit' ? labels.saveEdit : labels.saveCreate}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </section>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["message"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/sales/components/PaymentMethodsSettings.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { CrudForm, type CrudField, type CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n listPaymentProviders,\n type PaymentProvider,\n} from '../lib/providers'\nimport { isRecord } from '@open-mercato/shared/lib/utils'\nimport { renderProviderFieldInput } from './ProviderFieldInput'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype PaymentMethodRow = {\n id: string\n name: string\n code: string\n providerKey: string | null\n providerLabel: string | null\n description: string | null\n terms: string | null\n isActive: boolean\n updatedAt: string | null\n providerSettings: Record<string, unknown> | null\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: PaymentMethodRow }\n\nconst paymentFormSchema = z.object({\n name: z.string().trim().min(1),\n code: z.string().trim().min(1),\n providerKey: z.string().trim().optional(),\n description: z.string().optional(),\n terms: z.string().optional(),\n isActive: z.boolean().optional(),\n providerSettings: z.record(z.string(), z.unknown()).optional(),\n})\n\ntype PaymentFormValues = z.infer<typeof paymentFormSchema>\n\nconst DEFAULT_FORM: PaymentFormValues = {\n name: '',\n code: '',\n providerKey: '',\n description: '',\n terms: '',\n isActive: true,\n providerSettings: {},\n}\n\nfunction createPaymentProviderSettingsRenderer(params: {\n providers: PaymentProvider[]\n selectPrompt: string\n noFieldsLabel: string\n}) {\n const { providers, selectPrompt, noFieldsLabel } = params\n return function PaymentProviderSettingsField({\n value,\n setValue,\n values,\n }: CrudCustomFieldRenderProps) {\n const providerKey =\n values && typeof values.providerKey === 'string' && values.providerKey.trim().length\n ? values.providerKey\n : null\n const provider = providers.find((entry) => entry.key === providerKey)\n const previousKey = React.useRef<string | null>(providerKey)\n\n React.useEffect(() => {\n if (providerKey !== previousKey.current) {\n const defaults = provider?.settings?.defaults ?? null\n setValue(defaults && typeof defaults === 'object' ? defaults : {})\n previousKey.current = providerKey\n }\n }, [providerKey, provider, setValue])\n\n if (!provider) {\n return <p className=\"text-sm text-muted-foreground\">{selectPrompt}</p>\n }\n\n const settings = isRecord(value) ? value : {}\n const fields = provider.settings?.fields ?? []\n if (!fields.length) {\n return <p className=\"text-sm text-muted-foreground\">{noFieldsLabel}</p>\n }\n\n return (\n <div className=\"space-y-4\">\n {fields.map((field) => {\n const fieldValue = settings[field.key]\n return (\n <div key={field.key} className=\"space-y-1.5\">\n <Label htmlFor={field.key} className=\"text-sm font-medium\">\n {field.label}\n {field.required ? ' *' : ''}\n </Label>\n {field.description ? (\n <p className=\"text-xs text-muted-foreground\">{field.description}</p>\n ) : null}\n {renderProviderFieldInput({\n field,\n value: fieldValue,\n onChange: (next) => setValue({ ...settings, [field.key]: next }),\n })}\n </div>\n )\n })}\n </div>\n )\n }\n}\n\nexport function PaymentMethodsSettings() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const providers = React.useMemo(() => listPaymentProviders(), [])\n const providerOptions = React.useMemo(\n () => providers.map((provider) => ({ value: provider.key, label: provider.label })),\n [providers]\n )\n const ProviderSettingsField = React.useMemo(\n () =>\n createPaymentProviderSettingsRenderer({\n providers,\n selectPrompt: t(\n 'sales.config.paymentMethods.form.selectProvider',\n 'Choose a provider to configure settings.'\n ),\n noFieldsLabel: t('sales.config.paymentMethods.form.noSettings', 'No configurable settings.'),\n }),\n [providers, t]\n )\n\n const translations = React.useMemo(() => ({\n title: t('sales.config.paymentMethods.title', 'Payment methods'),\n description: t(\n 'sales.config.paymentMethods.description',\n 'Configure payment providers and any checkout fees.'\n ),\n actions: {\n add: t('sales.config.paymentMethods.actions.add', 'Add payment method'),\n edit: t('sales.config.paymentMethods.actions.edit', 'Edit'),\n delete: t('sales.config.paymentMethods.actions.delete', 'Delete'),\n deleteConfirm: t('sales.config.paymentMethods.actions.deleteConfirm', 'Delete \"{{name}}\"?'),\n refresh: t('sales.config.paymentMethods.actions.refresh', 'Refresh'),\n },\n table: {\n name: t('sales.config.paymentMethods.table.name', 'Name'),\n code: t('sales.config.paymentMethods.table.code', 'Code'),\n provider: t('sales.config.paymentMethods.table.provider', 'Provider'),\n active: t('sales.config.paymentMethods.table.active', 'Active'),\n statusActive: t('sales.config.paymentMethods.table.statusActive', 'Active'),\n statusInactive: t('sales.config.paymentMethods.table.statusInactive', 'Inactive'),\n updatedAt: t('sales.config.paymentMethods.table.updatedAt', 'Updated'),\n empty: t('sales.config.paymentMethods.table.empty', 'No payment methods yet.'),\n search: t('sales.config.paymentMethods.table.search', 'Search payment methods\u2026'),\n },\n form: {\n createTitle: t('sales.config.paymentMethods.form.createTitle', 'Add payment method'),\n editTitle: t('sales.config.paymentMethods.form.editTitle', 'Edit payment method'),\n name: t('sales.config.paymentMethods.form.name', 'Name'),\n code: t('sales.config.paymentMethods.form.code', 'Code'),\n provider: t('sales.config.paymentMethods.form.provider', 'Provider'),\n description: t('sales.config.paymentMethods.form.description', 'Description'),\n terms: t('sales.config.paymentMethods.form.terms', 'Terms'),\n isActive: t('sales.config.paymentMethods.form.isActive', 'Active'),\n providerSettings: t('sales.config.paymentMethods.form.providerSettings', 'Provider settings'),\n save: t('sales.config.paymentMethods.form.save', 'Save'),\n cancel: t('sales.config.paymentMethods.form.cancel', 'Cancel'),\n },\n messages: {\n saved: t('sales.config.paymentMethods.messages.saved', 'Payment method saved.'),\n deleted: t('sales.config.paymentMethods.messages.deleted', 'Payment method deleted.'),\n },\n errors: {\n load: t('sales.config.paymentMethods.errors.load', 'Failed to load payment methods.'),\n save: t('sales.config.paymentMethods.errors.save', 'Failed to save payment method.'),\n delete: t('sales.config.paymentMethods.errors.delete', 'Failed to delete payment method.'),\n },\n }), [t])\n\n const [entries, setEntries] = React.useState<PaymentMethodRow[]>([])\n const [loading, setLoading] = React.useState(false)\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [formValues, setFormValues] = React.useState<PaymentFormValues>(DEFAULT_FORM)\n const [search, setSearch] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n\n const loadEntries = React.useCallback(async () => {\n setLoading(true)\n try {\n const payload = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/sales/payment-methods?pageSize=100',\n undefined,\n { errorMessage: translations.errors.load, fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setEntries(\n items.map((item) => {\n const providerKey = typeof (item as any).providerKey === 'string'\n ? (item as any).providerKey\n : typeof (item as any).provider_key === 'string'\n ? (item as any).provider_key\n : null\n const provider = providers.find((entry) => entry.key === providerKey)\n return {\n id: String(item.id ?? ''),\n name: typeof item.name === 'string' && item.name.length ? item.name : '\u2014',\n code: typeof item.code === 'string' ? item.code : '',\n providerKey,\n providerLabel: provider?.label ?? null,\n description: typeof item.description === 'string' ? item.description : null,\n terms: typeof item.terms === 'string' ? item.terms : null,\n isActive:\n typeof item.isActive === 'boolean'\n ? item.isActive\n : typeof (item as any)?.is_active === 'boolean'\n ? (item as any).is_active\n : false,\n updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,\n providerSettings:\n item && typeof (item as any).providerSettings === 'object'\n ? ((item as any).providerSettings as Record<string, unknown>)\n : null,\n }\n })\n )\n } catch (err) {\n logger.error('sales.payment-methods.list failed', { err })\n flash(translations.errors.load, 'error')\n } finally {\n setLoading(false)\n }\n }, [providers, translations.errors.load])\n\n React.useEffect(() => {\n void loadEntries()\n }, [loadEntries, scopeVersion])\n\n const openCreate = React.useCallback(() => {\n const defaultProvider = providerOptions[0]?.value ?? ''\n setFormValues({\n ...DEFAULT_FORM,\n providerKey: defaultProvider,\n providerSettings: {},\n })\n setDialog({ mode: 'create' })\n }, [providerOptions])\n\n const openEdit = React.useCallback((entry: PaymentMethodRow) => {\n setFormValues({\n name: entry.name,\n code: entry.code,\n providerKey: entry.providerKey ?? '',\n description: entry.description ?? '',\n terms: entry.terms ?? '',\n isActive: entry.isActive,\n providerSettings: entry.providerSettings ?? {},\n })\n setDialog({ mode: 'edit', entry })\n }, [])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n }, [])\n\n const deleteEntry = React.useCallback(async (entry: PaymentMethodRow) => {\n const message = translations.actions.deleteConfirm.replace('{{name}}', entry.name)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n const headers = buildOptimisticLockHeader(entry.updatedAt)\n const call = await withScopedApiRequestHeaders(headers, () => apiCall('/api/sales/payment-methods', {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id }),\n }))\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return\n await raiseCrudError(call.response, translations.errors.delete)\n }\n flash(translations.messages.deleted, 'success')\n await loadEntries()\n } catch (err) {\n logger.error('sales.payment-methods.delete failed', { err })\n const message =\n err instanceof Error ? err.message : translations.errors.delete\n flash(message, 'error')\n }\n }, [confirm, loadEntries, translations.actions.deleteConfirm, translations.errors.delete, translations.messages.deleted])\n\n const handleSubmit = React.useCallback(async (values: PaymentFormValues) => {\n if (!dialog) return\n setSubmitting(true)\n const payload: Record<string, unknown> = {\n name: values.name.trim(),\n code: values.code.trim().toLowerCase(),\n description: values.description?.trim() || undefined,\n providerKey: values.providerKey?.trim() || undefined,\n terms: values.terms?.trim() || undefined,\n isActive: values.isActive ?? true,\n providerSettings: isRecord(values.providerSettings) ? values.providerSettings : undefined,\n }\n const path = '/api/sales/payment-methods'\n const method = dialog.mode === 'create' ? 'POST' : 'PUT'\n if (dialog.mode === 'edit') payload.id = dialog.entry.id\n try {\n const savePaymentMethod = () => apiCall(path, {\n method,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const headers = buildOptimisticLockHeader(dialog.mode === 'edit' ? dialog.entry.updatedAt : null)\n const call = await withScopedApiRequestHeaders(headers, savePaymentMethod)\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return\n await raiseCrudError(call.response, translations.errors.save)\n }\n flash(translations.messages.saved, 'success')\n await loadEntries()\n closeDialog()\n } catch (err) {\n logger.error('sales.payment-methods.save failed', { err })\n const message = err instanceof Error ? err.message : translations.errors.save\n flash(message, 'error')\n } finally {\n setSubmitting(false)\n }\n }, [closeDialog, dialog, loadEntries, translations.errors.save, translations.messages.saved])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'name', label: translations.form.name, type: 'text', required: true },\n { id: 'code', label: translations.form.code, type: 'text', required: true },\n {\n id: 'providerKey',\n label: translations.form.provider,\n type: 'select',\n required: false,\n options: providerOptions,\n },\n { id: 'description', label: translations.form.description, type: 'textarea' },\n { id: 'terms', label: translations.form.terms, type: 'textarea' },\n { id: 'isActive', label: translations.form.isActive, type: 'checkbox' },\n {\n id: 'providerSettings',\n label: translations.form.providerSettings,\n type: 'custom',\n component: ProviderSettingsField,\n },\n ], [ProviderSettingsField, providerOptions, translations.form.code, translations.form.description, translations.form.isActive, translations.form.name, translations.form.provider, translations.form.providerSettings, translations.form.terms])\n\n const columns = React.useMemo<ColumnDef<PaymentMethodRow>[]>(() => [\n {\n accessorKey: 'name',\n header: translations.table.name,\n cell: ({ row }) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.name}</span>\n <span className=\"text-xs text-muted-foreground\">{row.original.code}</span>\n </div>\n ),\n },\n {\n accessorKey: 'providerKey',\n header: translations.table.provider,\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.providerLabel ?? row.original.providerKey ?? '\u2014'}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: translations.table.active,\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.isActive ? translations.table.statusActive : translations.table.statusInactive}\n </span>\n ),\n },\n {\n accessorKey: 'updatedAt',\n header: translations.table.updatedAt,\n cell: ({ row }) => (\n <span className=\"text-xs text-muted-foreground\">\n {row.original.updatedAt ? new Date(row.original.updatedAt).toLocaleString() : '\u2014'}\n </span>\n ),\n },\n ], [translations])\n\n const filteredEntries = React.useMemo(() => {\n if (!search.trim()) return entries\n const term = search.trim().toLowerCase()\n return entries.filter(\n (entry) =>\n entry.name.toLowerCase().includes(term) ||\n entry.code.toLowerCase().includes(term) ||\n (entry.providerLabel ?? '').toLowerCase().includes(term) ||\n (entry.providerKey ?? '').toLowerCase().includes(term)\n )\n }, [entries, search])\n\n return (\n <section className=\"rounded border bg-card text-card-foreground shadow-sm\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-medium\">{translations.title}</h2>\n <p className=\"text-sm text-muted-foreground\">{translations.description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DataTable\n embedded\n isLoading={loading}\n columns={columns}\n data={filteredEntries}\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder={translations.table.search}\n emptyState={<p className=\"py-8 text-center text-sm text-muted-foreground\">{translations.table.empty}</p>}\n actions={(\n <Button size=\"sm\" onClick={openCreate}>\n {translations.actions.add}\n </Button>\n )}\n refreshButton={{\n label: translations.actions.refresh,\n onRefresh: () => { void loadEntries() },\n isRefreshing: loading,\n }}\n rowActions={(row) => (\n <RowActions\n items={[\n { id: 'edit', label: translations.actions.edit, onSelect: () => openEdit(row) },\n { id: 'delete', label: translations.actions.delete, destructive: true, onSelect: () => deleteEntry(row) },\n ]}\n />\n )}\n />\n </div>\n\n <Dialog open={dialog !== null} onOpenChange={(next) => !next && closeDialog()}>\n <DialogContent className=\"max-w-3xl\">\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit' ? translations.form.editTitle : translations.form.createTitle}\n </DialogTitle>\n </DialogHeader>\n <CrudForm\n schema={paymentFormSchema}\n fields={fields}\n initialValues={formValues}\n optimisticLockUpdatedAt={dialog?.mode === 'edit' ? dialog.entry.updatedAt : null}\n submitLabel={translations.form.save}\n onSubmit={handleSubmit}\n cancelHref={undefined}\n embedded\n isLoading={submitting}\n twoColumn\n extraActions={(\n <Button type=\"button\" variant=\"ghost\" onClick={closeDialog}>\n {translations.form.cancel}\n </Button>\n )}\n />\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </section>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { z } from 'zod'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { CrudForm, type CrudField, type CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport {\n listPaymentProviders,\n type PaymentProvider,\n} from '../lib/providers'\nimport { isRecord } from '@open-mercato/shared/lib/utils'\nimport { renderProviderFieldInput } from './ProviderFieldInput'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\ntype PaymentMethodRow = {\n id: string\n name: string\n code: string\n providerKey: string | null\n providerLabel: string | null\n description: string | null\n terms: string | null\n isActive: boolean\n updatedAt: string | null\n providerSettings: Record<string, unknown> | null\n}\n\ntype DialogState =\n | { mode: 'create' }\n | { mode: 'edit'; entry: PaymentMethodRow }\n\nconst paymentFormSchema = z.object({\n name: z.string().trim().min(1),\n code: z.string().trim().min(1),\n providerKey: z.string().trim().optional(),\n description: z.string().optional(),\n terms: z.string().optional(),\n isActive: z.boolean().optional(),\n providerSettings: z.record(z.string(), z.unknown()).optional(),\n})\n\ntype PaymentFormValues = z.infer<typeof paymentFormSchema>\n\nconst DEFAULT_FORM: PaymentFormValues = {\n name: '',\n code: '',\n providerKey: '',\n description: '',\n terms: '',\n isActive: true,\n providerSettings: {},\n}\n\nfunction createPaymentProviderSettingsRenderer(params: {\n providers: PaymentProvider[]\n selectPrompt: string\n noFieldsLabel: string\n}) {\n const { providers, selectPrompt, noFieldsLabel } = params\n return function PaymentProviderSettingsField({\n value,\n setValue,\n values,\n }: CrudCustomFieldRenderProps) {\n const providerKey =\n values && typeof values.providerKey === 'string' && values.providerKey.trim().length\n ? values.providerKey\n : null\n const provider = providers.find((entry) => entry.key === providerKey)\n const previousKey = React.useRef<string | null>(providerKey)\n\n React.useEffect(() => {\n if (providerKey !== previousKey.current) {\n const defaults = provider?.settings?.defaults ?? null\n setValue(defaults && typeof defaults === 'object' ? defaults : {})\n previousKey.current = providerKey\n }\n }, [providerKey, provider, setValue])\n\n if (!provider) {\n return <p className=\"text-sm text-muted-foreground\">{selectPrompt}</p>\n }\n\n const settings = isRecord(value) ? value : {}\n const fields = provider.settings?.fields ?? []\n if (!fields.length) {\n return <p className=\"text-sm text-muted-foreground\">{noFieldsLabel}</p>\n }\n\n return (\n <div className=\"space-y-4\">\n {fields.map((field) => {\n const fieldValue = settings[field.key]\n return (\n <div key={field.key} className=\"space-y-1.5\">\n <Label htmlFor={field.key} className=\"text-sm font-medium\">\n {field.label}\n {field.required ? ' *' : ''}\n </Label>\n {field.description ? (\n <p className=\"text-xs text-muted-foreground\">{field.description}</p>\n ) : null}\n {renderProviderFieldInput({\n field,\n value: fieldValue,\n onChange: (next) => setValue({ ...settings, [field.key]: next }),\n })}\n </div>\n )\n })}\n </div>\n )\n }\n}\n\nexport function PaymentMethodsSettings() {\n const t = useT()\n const scopeVersion = useOrganizationScopeVersion()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const providers = React.useMemo(() => listPaymentProviders(), [])\n const providerOptions = React.useMemo(\n () => providers.map((provider) => ({ value: provider.key, label: provider.label })),\n [providers]\n )\n const ProviderSettingsField = React.useMemo(\n () =>\n createPaymentProviderSettingsRenderer({\n providers,\n selectPrompt: t(\n 'sales.config.paymentMethods.form.selectProvider',\n 'Choose a provider to configure settings.'\n ),\n noFieldsLabel: t('sales.config.paymentMethods.form.noSettings', 'No configurable settings.'),\n }),\n [providers, t]\n )\n\n const translations = React.useMemo(() => ({\n title: t('sales.config.paymentMethods.title', 'Payment methods'),\n description: t(\n 'sales.config.paymentMethods.description',\n 'Configure payment providers and any checkout fees.'\n ),\n actions: {\n add: t('sales.config.paymentMethods.actions.add', 'Add payment method'),\n edit: t('sales.config.paymentMethods.actions.edit', 'Edit'),\n delete: t('sales.config.paymentMethods.actions.delete', 'Delete'),\n deleteConfirm: t('sales.config.paymentMethods.actions.deleteConfirm', 'Delete \"{{name}}\"?'),\n refresh: t('sales.config.paymentMethods.actions.refresh', 'Refresh'),\n },\n table: {\n name: t('sales.config.paymentMethods.table.name', 'Name'),\n code: t('sales.config.paymentMethods.table.code', 'Code'),\n provider: t('sales.config.paymentMethods.table.provider', 'Provider'),\n active: t('sales.config.paymentMethods.table.active', 'Active'),\n statusActive: t('sales.config.paymentMethods.table.statusActive', 'Active'),\n statusInactive: t('sales.config.paymentMethods.table.statusInactive', 'Inactive'),\n updatedAt: t('sales.config.paymentMethods.table.updatedAt', 'Updated'),\n empty: t('sales.config.paymentMethods.table.empty', 'No payment methods yet.'),\n search: t('sales.config.paymentMethods.table.search', 'Search payment methods\u2026'),\n },\n form: {\n createTitle: t('sales.config.paymentMethods.form.createTitle', 'Add payment method'),\n editTitle: t('sales.config.paymentMethods.form.editTitle', 'Edit payment method'),\n name: t('sales.config.paymentMethods.form.name', 'Name'),\n code: t('sales.config.paymentMethods.form.code', 'Code'),\n provider: t('sales.config.paymentMethods.form.provider', 'Provider'),\n description: t('sales.config.paymentMethods.form.description', 'Description'),\n terms: t('sales.config.paymentMethods.form.terms', 'Terms'),\n isActive: t('sales.config.paymentMethods.form.isActive', 'Active'),\n providerSettings: t('sales.config.paymentMethods.form.providerSettings', 'Provider settings'),\n save: t('sales.config.paymentMethods.form.save', 'Save'),\n cancel: t('sales.config.paymentMethods.form.cancel', 'Cancel'),\n },\n messages: {\n saved: t('sales.config.paymentMethods.messages.saved', 'Payment method saved.'),\n deleted: t('sales.config.paymentMethods.messages.deleted', 'Payment method deleted.'),\n },\n errors: {\n load: t('sales.config.paymentMethods.errors.load', 'Failed to load payment methods.'),\n save: t('sales.config.paymentMethods.errors.save', 'Failed to save payment method.'),\n delete: t('sales.config.paymentMethods.errors.delete', 'Failed to delete payment method.'),\n },\n }), [t])\n\n const [entries, setEntries] = React.useState<PaymentMethodRow[]>([])\n const [loading, setLoading] = React.useState(false)\n const [dialog, setDialog] = React.useState<DialogState | null>(null)\n const [formValues, setFormValues] = React.useState<PaymentFormValues>(DEFAULT_FORM)\n const [search, setSearch] = React.useState('')\n const [submitting, setSubmitting] = React.useState(false)\n\n const loadEntries = React.useCallback(async () => {\n setLoading(true)\n try {\n const payload = await readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(\n '/api/sales/payment-methods?pageSize=100',\n undefined,\n { errorMessage: translations.errors.load, fallback: { items: [] } },\n )\n const items = Array.isArray(payload.items) ? payload.items : []\n setEntries(\n items.map((item) => {\n const providerKey = typeof (item as any).providerKey === 'string'\n ? (item as any).providerKey\n : typeof (item as any).provider_key === 'string'\n ? (item as any).provider_key\n : null\n const provider = providers.find((entry) => entry.key === providerKey)\n return {\n id: String(item.id ?? ''),\n name: typeof item.name === 'string' && item.name.length ? item.name : '\u2014',\n code: typeof item.code === 'string' ? item.code : '',\n providerKey,\n providerLabel: provider?.label ?? null,\n description: typeof item.description === 'string' ? item.description : null,\n terms: typeof item.terms === 'string' ? item.terms : null,\n isActive:\n typeof item.isActive === 'boolean'\n ? item.isActive\n : typeof (item as any)?.is_active === 'boolean'\n ? (item as any).is_active\n : false,\n updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : null,\n providerSettings:\n item && typeof (item as any).providerSettings === 'object'\n ? ((item as any).providerSettings as Record<string, unknown>)\n : null,\n }\n })\n )\n } catch (err) {\n logger.error('sales.payment-methods.list failed', { err })\n flash(translations.errors.load, 'error')\n } finally {\n setLoading(false)\n }\n }, [providers, translations.errors.load])\n\n React.useEffect(() => {\n void loadEntries()\n }, [loadEntries, scopeVersion])\n\n const openCreate = React.useCallback(() => {\n const defaultProvider = providerOptions[0]?.value ?? ''\n setFormValues({\n ...DEFAULT_FORM,\n providerKey: defaultProvider,\n providerSettings: {},\n })\n setDialog({ mode: 'create' })\n }, [providerOptions])\n\n const openEdit = React.useCallback((entry: PaymentMethodRow) => {\n setFormValues({\n name: entry.name,\n code: entry.code,\n providerKey: entry.providerKey ?? '',\n description: entry.description ?? '',\n terms: entry.terms ?? '',\n isActive: entry.isActive,\n providerSettings: entry.providerSettings ?? {},\n })\n setDialog({ mode: 'edit', entry })\n }, [])\n\n const closeDialog = React.useCallback(() => {\n setDialog(null)\n }, [])\n\n const deleteEntry = React.useCallback(async (entry: PaymentMethodRow) => {\n const message = translations.actions.deleteConfirm.replace('{{name}}', entry.name)\n const confirmed = await confirm({\n title: message,\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n const headers = buildOptimisticLockHeader(entry.updatedAt)\n const call = await withScopedApiRequestHeaders(headers, () => apiCall('/api/sales/payment-methods', {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: entry.id }),\n }))\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return\n await raiseCrudError(call.response, translations.errors.delete)\n }\n flash(translations.messages.deleted, 'success')\n await loadEntries()\n } catch (err) {\n logger.error('sales.payment-methods.delete failed', { err })\n const message =\n err instanceof Error ? err.message : translations.errors.delete\n flash(message, 'error')\n }\n }, [confirm, loadEntries, translations.actions.deleteConfirm, translations.errors.delete, translations.messages.deleted])\n\n const handleSubmit = React.useCallback(async (values: PaymentFormValues) => {\n if (!dialog) return\n setSubmitting(true)\n const payload: Record<string, unknown> = {\n name: values.name.trim(),\n code: values.code.trim().toLowerCase(),\n description: values.description?.trim() || undefined,\n providerKey: values.providerKey?.trim() || undefined,\n terms: values.terms?.trim() || undefined,\n isActive: values.isActive ?? true,\n providerSettings: isRecord(values.providerSettings) ? values.providerSettings : undefined,\n }\n const path = '/api/sales/payment-methods'\n const method = dialog.mode === 'create' ? 'POST' : 'PUT'\n if (dialog.mode === 'edit') payload.id = dialog.entry.id\n try {\n const savePaymentMethod = () => apiCall(path, {\n method,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const headers = buildOptimisticLockHeader(dialog.mode === 'edit' ? dialog.entry.updatedAt : null)\n const call = await withScopedApiRequestHeaders(headers, savePaymentMethod)\n if (!call.ok) {\n if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return\n await raiseCrudError(call.response, translations.errors.save)\n }\n flash(translations.messages.saved, 'success')\n await loadEntries()\n closeDialog()\n } catch (err) {\n logger.error('sales.payment-methods.save failed', { err })\n const message = err instanceof Error ? err.message : translations.errors.save\n flash(message, 'error')\n } finally {\n setSubmitting(false)\n }\n }, [closeDialog, dialog, loadEntries, translations.errors.save, translations.messages.saved])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'name', label: translations.form.name, type: 'text', required: true },\n { id: 'code', label: translations.form.code, type: 'text', required: true },\n {\n id: 'providerKey',\n label: translations.form.provider,\n type: 'select',\n required: false,\n options: providerOptions,\n },\n { id: 'description', label: translations.form.description, type: 'textarea' },\n { id: 'terms', label: translations.form.terms, type: 'textarea' },\n { id: 'isActive', label: translations.form.isActive, type: 'checkbox' },\n {\n id: 'providerSettings',\n label: translations.form.providerSettings,\n type: 'custom',\n component: ProviderSettingsField,\n },\n ], [ProviderSettingsField, providerOptions, translations.form.code, translations.form.description, translations.form.isActive, translations.form.name, translations.form.provider, translations.form.providerSettings, translations.form.terms])\n\n const columns = React.useMemo<ColumnDef<PaymentMethodRow>[]>(() => [\n {\n accessorKey: 'name',\n header: translations.table.name,\n cell: ({ row }) => (\n <div className=\"flex flex-col\">\n <span className=\"font-medium\">{row.original.name}</span>\n <span className=\"text-xs text-muted-foreground\">{row.original.code}</span>\n </div>\n ),\n },\n {\n accessorKey: 'providerKey',\n header: translations.table.provider,\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.providerLabel ?? row.original.providerKey ?? '\u2014'}\n </span>\n ),\n },\n {\n accessorKey: 'isActive',\n header: translations.table.active,\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.isActive ? translations.table.statusActive : translations.table.statusInactive}\n </span>\n ),\n },\n {\n accessorKey: 'updatedAt',\n header: translations.table.updatedAt,\n cell: ({ row }) => (\n <span className=\"text-xs text-muted-foreground\">\n {row.original.updatedAt ? new Date(row.original.updatedAt).toLocaleString() : '\u2014'}\n </span>\n ),\n },\n ], [translations])\n\n const filteredEntries = React.useMemo(() => {\n if (!search.trim()) return entries\n const term = search.trim().toLowerCase()\n return entries.filter(\n (entry) =>\n entry.name.toLowerCase().includes(term) ||\n entry.code.toLowerCase().includes(term) ||\n (entry.providerLabel ?? '').toLowerCase().includes(term) ||\n (entry.providerKey ?? '').toLowerCase().includes(term)\n )\n }, [entries, search])\n\n return (\n <section className=\"rounded border bg-card text-card-foreground shadow-sm\">\n <div className=\"border-b px-6 py-4 space-y-1\">\n <h2 className=\"text-lg font-medium\">{translations.title}</h2>\n <p className=\"text-sm text-muted-foreground\">{translations.description}</p>\n </div>\n <div className=\"px-2 py-4 sm:px-4\">\n <DataTable\n embedded\n isLoading={loading}\n columns={columns}\n data={filteredEntries}\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder={translations.table.search}\n emptyState={<p className=\"py-8 text-center text-sm text-muted-foreground\">{translations.table.empty}</p>}\n actions={(\n <Button size=\"sm\" onClick={openCreate}>\n {translations.actions.add}\n </Button>\n )}\n refreshButton={{\n label: translations.actions.refresh,\n onRefresh: () => { void loadEntries() },\n isRefreshing: loading,\n }}\n rowActions={(row) => (\n <RowActions\n items={[\n { id: 'edit', label: translations.actions.edit, onSelect: () => openEdit(row) },\n { id: 'delete', label: translations.actions.delete, destructive: true, onSelect: () => deleteEntry(row) },\n ]}\n />\n )}\n />\n </div>\n\n <Dialog open={dialog !== null} onOpenChange={(next) => !next && closeDialog()}>\n <DialogContent className=\"max-w-3xl\">\n <DialogHeader>\n <DialogTitle>\n {dialog?.mode === 'edit' ? translations.form.editTitle : translations.form.createTitle}\n </DialogTitle>\n </DialogHeader>\n <CrudForm\n schema={paymentFormSchema}\n fields={fields}\n initialValues={formValues}\n optimisticLockUpdatedAt={dialog?.mode === 'edit' ? dialog.entry.updatedAt : null}\n submitLabel={translations.form.save}\n onSubmit={handleSubmit}\n cancelHref={undefined}\n embedded\n isLoading={submitting}\n twoColumn\n extraActions={(\n <Button type=\"button\" variant=\"ghost\" onClick={closeDialog}>\n {translations.form.cancel}\n </Button>\n )}\n />\n </DialogContent>\n </Dialog>\n {ConfirmDialogElement}\n </section>\n )\n}\n"],
|
|
5
5
|
"mappings": ";AAoGa,cAeC,YAfD;AAlGb,YAAY,WAAW;AACvB,SAAS,SAAS;AAElB,SAAS,iBAAiB;AAC1B,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,gBAAiE;AAC1E,SAAS,aAAa;AACtB,SAAS,SAAS,sBAAsB,mCAAmC;AAC3E,SAAS,iCAAiC;AAC1C,SAAS,6BAA6B;AACtC,SAAS,sBAAsB;AAC/B,SAAS,mCAAmC;AAC5C,SAAS,YAAY;AACrB,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,OAEK;AACP,SAAS,gBAAgB;AACzB,SAAS,gCAAgC;AACzC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,OAAO;AAmBnC,MAAM,oBAAoB,EAAE,OAAO;AAAA,EACjC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC;AAAA,EAC7B,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EACxC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC/B,kBAAkB,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAC/D,CAAC;AAID,MAAM,eAAkC;AAAA,EACtC,MAAM;AAAA,EACN,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aAAa;AAAA,EACb,OAAO;AAAA,EACP,UAAU;AAAA,EACV,kBAAkB,CAAC;AACrB;AAEA,SAAS,sCAAsC,QAI5C;AACD,QAAM,EAAE,WAAW,cAAc,cAAc,IAAI;AACnD,SAAO,SAAS,6BAA6B;AAAA,IAC3C;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAA+B;AAC7B,UAAM,cACJ,UAAU,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,EAAE,SAC1E,OAAO,cACP;AACN,UAAM,WAAW,UAAU,KAAK,CAAC,UAAU,MAAM,QAAQ,WAAW;AACpE,UAAM,cAAc,MAAM,OAAsB,WAAW;AAE3D,UAAM,UAAU,MAAM;AACpB,UAAI,gBAAgB,YAAY,SAAS;AACvC,cAAM,WAAW,UAAU,UAAU,YAAY;AACjD,iBAAS,YAAY,OAAO,aAAa,WAAW,WAAW,CAAC,CAAC;AACjE,oBAAY,UAAU;AAAA,MACxB;AAAA,IACF,GAAG,CAAC,aAAa,UAAU,QAAQ,CAAC;AAEpC,QAAI,CAAC,UAAU;AACb,aAAO,oBAAC,OAAE,WAAU,iCAAiC,wBAAa;AAAA,IACpE;AAEA,UAAM,WAAW,SAAS,KAAK,IAAI,QAAQ,CAAC;AAC5C,UAAM,SAAS,SAAS,UAAU,UAAU,CAAC;AAC7C,QAAI,CAAC,OAAO,QAAQ;AAClB,aAAO,oBAAC,OAAE,WAAU,iCAAiC,yBAAc;AAAA,IACrE;AAEA,WACE,oBAAC,SAAI,WAAU,aACZ,iBAAO,IAAI,CAAC,UAAU;AACrB,YAAM,aAAa,SAAS,MAAM,GAAG;AACrC,aACE,qBAAC,SAAoB,WAAU,eAC7B;AAAA,6BAAC,SAAM,SAAS,MAAM,KAAK,WAAU,uBAClC;AAAA,gBAAM;AAAA,UACN,MAAM,WAAW,OAAO;AAAA,WAC3B;AAAA,QACC,MAAM,cACL,oBAAC,OAAE,WAAU,iCAAiC,gBAAM,aAAY,IAC9D;AAAA,QACH,yBAAyB;AAAA,UACxB;AAAA,UACA,OAAO;AAAA,UACP,UAAU,CAAC,SAAS,SAAS,EAAE,GAAG,UAAU,CAAC,MAAM,GAAG,GAAG,KAAK,CAAC;AAAA,QACjE,CAAC;AAAA,WAZO,MAAM,GAahB;AAAA,IAEJ,CAAC,GACH;AAAA,EAEJ;AACF;AAEO,SAAS,yBAAyB;AACvC,QAAM,IAAI,KAAK;AACf,QAAM,eAAe,4BAA4B;AACjD,QAAM,EAAE,SAAS,qBAAqB,IAAI,iBAAiB;AAC3D,QAAM,YAAY,MAAM,QAAQ,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAChE,QAAM,kBAAkB,MAAM;AAAA,IAC5B,MAAM,UAAU,IAAI,CAAC,cAAc,EAAE,OAAO,SAAS,KAAK,OAAO,SAAS,MAAM,EAAE;AAAA,IAClF,CAAC,SAAS;AAAA,EACZ;AACA,QAAM,wBAAwB,MAAM;AAAA,IAClC,MACE,sCAAsC;AAAA,MACpC;AAAA,MACA,cAAc;AAAA,QACZ;AAAA,QACA;AAAA,MACF;AAAA,MACA,eAAe,EAAE,+CAA+C,2BAA2B;AAAA,IAC7F,CAAC;AAAA,IACH,CAAC,WAAW,CAAC;AAAA,EACf;AAEA,QAAM,eAAe,MAAM,QAAQ,OAAO;AAAA,IACxC,OAAO,EAAE,qCAAqC,iBAAiB;AAAA,IAC/D,aAAa;AAAA,MACX;AAAA,MACA;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP,KAAK,EAAE,2CAA2C,oBAAoB;AAAA,MACtE,MAAM,EAAE,4CAA4C,MAAM;AAAA,MAC1D,QAAQ,EAAE,8CAA8C,QAAQ;AAAA,MAChE,eAAe,EAAE,qDAAqD,oBAAoB;AAAA,MAC1F,SAAS,EAAE,+CAA+C,SAAS;AAAA,IACrE;AAAA,IACA,OAAO;AAAA,MACL,MAAM,EAAE,0CAA0C,MAAM;AAAA,MACxD,MAAM,EAAE,0CAA0C,MAAM;AAAA,MACxD,UAAU,EAAE,8CAA8C,UAAU;AAAA,MACpE,QAAQ,EAAE,4CAA4C,QAAQ;AAAA,MAC9D,cAAc,EAAE,kDAAkD,QAAQ;AAAA,MAC1E,gBAAgB,EAAE,oDAAoD,UAAU;AAAA,MAChF,WAAW,EAAE,+CAA+C,SAAS;AAAA,MACrE,OAAO,EAAE,2CAA2C,yBAAyB;AAAA,MAC7E,QAAQ,EAAE,4CAA4C,8BAAyB;AAAA,IACjF;AAAA,IACA,MAAM;AAAA,MACJ,aAAa,EAAE,gDAAgD,oBAAoB;AAAA,MACnF,WAAW,EAAE,8CAA8C,qBAAqB;AAAA,MAChF,MAAM,EAAE,yCAAyC,MAAM;AAAA,MACvD,MAAM,EAAE,yCAAyC,MAAM;AAAA,MACvD,UAAU,EAAE,6CAA6C,UAAU;AAAA,MACnE,aAAa,EAAE,gDAAgD,aAAa;AAAA,MAC5E,OAAO,EAAE,0CAA0C,OAAO;AAAA,MAC1D,UAAU,EAAE,6CAA6C,QAAQ;AAAA,MACjE,kBAAkB,EAAE,qDAAqD,mBAAmB;AAAA,MAC5F,MAAM,EAAE,yCAAyC,MAAM;AAAA,MACvD,QAAQ,EAAE,2CAA2C,QAAQ;AAAA,IAC/D;AAAA,IACA,UAAU;AAAA,MACR,OAAO,EAAE,8CAA8C,uBAAuB;AAAA,MAC9E,SAAS,EAAE,gDAAgD,yBAAyB;AAAA,IACtF;AAAA,IACA,QAAQ;AAAA,MACN,MAAM,EAAE,2CAA2C,iCAAiC;AAAA,MACpF,MAAM,EAAE,2CAA2C,gCAAgC;AAAA,MACnF,QAAQ,EAAE,6CAA6C,kCAAkC;AAAA,IAC3F;AAAA,EACF,IAAI,CAAC,CAAC,CAAC;AAEP,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAA6B,CAAC,CAAC;AACnE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAA6B,IAAI;AACnE,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAA4B,YAAY;AAClF,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,EAAE;AAC7C,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AAExD,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,eAAW,IAAI;AACf,QAAI;AACF,YAAM,UAAU,MAAM;AAAA,QACpB;AAAA,QACA;AAAA,QACA,EAAE,cAAc,aAAa,OAAO,MAAM,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE;AAAA,MACpE;AACA,YAAM,QAAQ,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC9D;AAAA,QACE,MAAM,IAAI,CAAC,SAAS;AAClB,gBAAM,cAAc,OAAQ,KAAa,gBAAgB,WACpD,KAAa,cACd,OAAQ,KAAa,iBAAiB,WACnC,KAAa,eACd;AACN,gBAAM,WAAW,UAAU,KAAK,CAAC,UAAU,MAAM,QAAQ,WAAW;AACpE,iBAAO;AAAA,YACL,IAAI,OAAO,KAAK,MAAM,EAAE;AAAA,YACxB,MAAM,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,KAAK,OAAO;AAAA,YACtE,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAAA,YAClD;AAAA,YACA,eAAe,UAAU,SAAS;AAAA,YAClC,aAAa,OAAO,KAAK,gBAAgB,WAAW,KAAK,cAAc;AAAA,YACvE,OAAO,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAAA,YACrD,UACE,OAAO,KAAK,aAAa,YACrB,KAAK,WACL,OAAQ,MAAc,cAAc,YACjC,KAAa,YACd;AAAA,YACR,WAAW,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;AAAA,YACjE,kBACE,QAAQ,OAAQ,KAAa,qBAAqB,WAC5C,KAAa,mBACf;AAAA,UACR;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,qCAAqC,EAAE,IAAI,CAAC;AACzD,YAAM,aAAa,OAAO,MAAM,OAAO;AAAA,IACzC,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,WAAW,aAAa,OAAO,IAAI,CAAC;AAExC,QAAM,UAAU,MAAM;AACpB,SAAK,YAAY;AAAA,EACnB,GAAG,CAAC,aAAa,YAAY,CAAC;AAE9B,QAAM,aAAa,MAAM,YAAY,MAAM;AACzC,UAAM,kBAAkB,gBAAgB,CAAC,GAAG,SAAS;AACrD,kBAAc;AAAA,MACZ,GAAG;AAAA,MACH,aAAa;AAAA,MACb,kBAAkB,CAAC;AAAA,IACrB,CAAC;AACD,cAAU,EAAE,MAAM,SAAS,CAAC;AAAA,EAC9B,GAAG,CAAC,eAAe,CAAC;AAEpB,QAAM,WAAW,MAAM,YAAY,CAAC,UAA4B;AAC9D,kBAAc;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,aAAa,MAAM,eAAe;AAAA,MAClC,aAAa,MAAM,eAAe;AAAA,MAClC,OAAO,MAAM,SAAS;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,kBAAkB,MAAM,oBAAoB,CAAC;AAAA,IAC/C,CAAC;AACD,cAAU,EAAE,MAAM,QAAQ,MAAM,CAAC;AAAA,EACnC,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc,MAAM,YAAY,MAAM;AAC1C,cAAU,IAAI;AAAA,EAChB,GAAG,CAAC,CAAC;AAEL,QAAM,cAAc,MAAM,YAAY,OAAO,UAA4B;AACvE,UAAM,UAAU,aAAa,QAAQ,cAAc,QAAQ,YAAY,MAAM,IAAI;AACjF,UAAM,YAAY,MAAM,QAAQ;AAAA,MAC9B,OAAO;AAAA,MACP,SAAS;AAAA,IACX,CAAC;AACD,QAAI,CAAC,UAAW;AAChB,QAAI;AACF,YAAM,UAAU,0BAA0B,MAAM,SAAS;AACzD,YAAM,OAAO,MAAM,4BAA4B,SAAS,MAAM,QAAQ,8BAA8B;AAAA,QAClG,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,IAAI,MAAM,GAAG,CAAC;AAAA,MACvC,CAAC,CAAC;AACF,UAAI,CAAC,KAAK,IAAI;AACZ,YAAI,sBAAsB,EAAE,QAAQ,KAAK,QAAQ,MAAM,KAAK,OAAO,GAAG,CAAC,EAAG;AAC1E,cAAM,eAAe,KAAK,UAAU,aAAa,OAAO,MAAM;AAAA,MAChE;AACA,YAAM,aAAa,SAAS,SAAS,SAAS;AAC9C,YAAM,YAAY;AAAA,IACpB,SAAS,KAAK;AACZ,aAAO,MAAM,uCAAuC,EAAE,IAAI,CAAC;AAC3D,YAAMA,WACJ,eAAe,QAAQ,IAAI,UAAU,aAAa,OAAO;AAC3D,YAAMA,UAAS,OAAO;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,SAAS,aAAa,aAAa,QAAQ,eAAe,aAAa,OAAO,QAAQ,aAAa,SAAS,OAAO,CAAC;AAExH,QAAM,eAAe,MAAM,YAAY,OAAO,WAA8B;AAC1E,QAAI,CAAC,OAAQ;AACb,kBAAc,IAAI;AAClB,UAAM,UAAmC;AAAA,MACvC,MAAM,OAAO,KAAK,KAAK;AAAA,MACvB,MAAM,OAAO,KAAK,KAAK,EAAE,YAAY;AAAA,MACrC,aAAa,OAAO,aAAa,KAAK,KAAK;AAAA,MAC3C,aAAa,OAAO,aAAa,KAAK,KAAK;AAAA,MAC3C,OAAO,OAAO,OAAO,KAAK,KAAK;AAAA,MAC/B,UAAU,OAAO,YAAY;AAAA,MAC7B,kBAAkB,SAAS,OAAO,gBAAgB,IAAI,OAAO,mBAAmB;AAAA,IAClF;AACA,UAAM,OAAO;AACb,UAAM,SAAS,OAAO,SAAS,WAAW,SAAS;AACnD,QAAI,OAAO,SAAS,OAAQ,SAAQ,KAAK,OAAO,MAAM;AACtD,QAAI;AACF,YAAM,oBAAoB,MAAM,QAAQ,MAAM;AAAA,QAC5C;AAAA,QACA,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,MAC9B,CAAC;AACD,YAAM,UAAU,0BAA0B,OAAO,SAAS,SAAS,OAAO,MAAM,YAAY,IAAI;AAChG,YAAM,OAAO,MAAM,4BAA4B,SAAS,iBAAiB;AACzE,UAAI,CAAC,KAAK,IAAI;AACZ,YAAI,sBAAsB,EAAE,QAAQ,KAAK,QAAQ,MAAM,KAAK,OAAO,GAAG,CAAC,EAAG;AAC1E,cAAM,eAAe,KAAK,UAAU,aAAa,OAAO,IAAI;AAAA,MAC9D;AACA,YAAM,aAAa,SAAS,OAAO,SAAS;AAC5C,YAAM,YAAY;AAClB,kBAAY;AAAA,IACd,SAAS,KAAK;AACZ,aAAO,MAAM,qCAAqC,EAAE,IAAI,CAAC;AACzD,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,aAAa,OAAO;AACzE,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,aAAa,QAAQ,aAAa,aAAa,OAAO,MAAM,aAAa,SAAS,KAAK,CAAC;AAE5F,QAAM,SAAS,MAAM,QAAqB,MAAM;AAAA,IAC9C,EAAE,IAAI,QAAQ,OAAO,aAAa,KAAK,MAAM,MAAM,QAAQ,UAAU,KAAK;AAAA,IAC1E,EAAE,IAAI,QAAQ,OAAO,aAAa,KAAK,MAAM,MAAM,QAAQ,UAAU,KAAK;AAAA,IAC1E;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,aAAa,KAAK;AAAA,MACzB,MAAM;AAAA,MACN,UAAU;AAAA,MACV,SAAS;AAAA,IACX;AAAA,IACA,EAAE,IAAI,eAAe,OAAO,aAAa,KAAK,aAAa,MAAM,WAAW;AAAA,IAC5E,EAAE,IAAI,SAAS,OAAO,aAAa,KAAK,OAAO,MAAM,WAAW;AAAA,IAChE,EAAE,IAAI,YAAY,OAAO,aAAa,KAAK,UAAU,MAAM,WAAW;AAAA,IACtE;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,aAAa,KAAK;AAAA,MACzB,MAAM;AAAA,MACN,WAAW;AAAA,IACb;AAAA,EACF,GAAG,CAAC,uBAAuB,iBAAiB,aAAa,KAAK,MAAM,aAAa,KAAK,aAAa,aAAa,KAAK,UAAU,aAAa,KAAK,MAAM,aAAa,KAAK,UAAU,aAAa,KAAK,kBAAkB,aAAa,KAAK,KAAK,CAAC;AAE/O,QAAM,UAAU,MAAM,QAAuC,MAAM;AAAA,IACjE;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,aAAa,MAAM;AAAA,MAC3B,MAAM,CAAC,EAAE,IAAI,MACX,qBAAC,SAAI,WAAU,iBACb;AAAA,4BAAC,UAAK,WAAU,eAAe,cAAI,SAAS,MAAK;AAAA,QACjD,oBAAC,UAAK,WAAU,iCAAiC,cAAI,SAAS,MAAK;AAAA,SACrE;AAAA,IAEJ;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,aAAa,MAAM;AAAA,MAC3B,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAU,WACb,cAAI,SAAS,iBAAiB,IAAI,SAAS,eAAe,UAC7D;AAAA,IAEJ;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,aAAa,MAAM;AAAA,MAC3B,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAU,WACb,cAAI,SAAS,WAAW,aAAa,MAAM,eAAe,aAAa,MAAM,gBAChF;AAAA,IAEJ;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,QAAQ,aAAa,MAAM;AAAA,MAC3B,MAAM,CAAC,EAAE,IAAI,MACX,oBAAC,UAAK,WAAU,iCACb,cAAI,SAAS,YAAY,IAAI,KAAK,IAAI,SAAS,SAAS,EAAE,eAAe,IAAI,UAChF;AAAA,IAEJ;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAEjB,QAAM,kBAAkB,MAAM,QAAQ,MAAM;AAC1C,QAAI,CAAC,OAAO,KAAK,EAAG,QAAO;AAC3B,UAAM,OAAO,OAAO,KAAK,EAAE,YAAY;AACvC,WAAO,QAAQ;AAAA,MACb,CAAC,UACC,MAAM,KAAK,YAAY,EAAE,SAAS,IAAI,KACtC,MAAM,KAAK,YAAY,EAAE,SAAS,IAAI,MACrC,MAAM,iBAAiB,IAAI,YAAY,EAAE,SAAS,IAAI,MACtD,MAAM,eAAe,IAAI,YAAY,EAAE,SAAS,IAAI;AAAA,IACzD;AAAA,EACF,GAAG,CAAC,SAAS,MAAM,CAAC;AAEpB,SACE,qBAAC,aAAQ,WAAU,yDACjB;AAAA,yBAAC,SAAI,WAAU,gCACb;AAAA,0BAAC,QAAG,WAAU,uBAAuB,uBAAa,OAAM;AAAA,MACxD,oBAAC,OAAE,WAAU,iCAAiC,uBAAa,aAAY;AAAA,OACzE;AAAA,IACA,oBAAC,SAAI,WAAU,qBACb;AAAA,MAAC;AAAA;AAAA,QACC,UAAQ;AAAA,QACR,WAAW;AAAA,QACX;AAAA,QACA,MAAM;AAAA,QACN,aAAa;AAAA,QACb,gBAAgB;AAAA,QAChB,mBAAmB,aAAa,MAAM;AAAA,QACtC,YAAY,oBAAC,OAAE,WAAU,kDAAkD,uBAAa,MAAM,OAAM;AAAA,QACpG,SACE,oBAAC,UAAO,MAAK,MAAK,SAAS,YACxB,uBAAa,QAAQ,KACxB;AAAA,QAEF,eAAe;AAAA,UACb,OAAO,aAAa,QAAQ;AAAA,UAC5B,WAAW,MAAM;AAAE,iBAAK,YAAY;AAAA,UAAE;AAAA,UACtC,cAAc;AAAA,QAChB;AAAA,QACA,YAAY,CAAC,QACX;AAAA,UAAC;AAAA;AAAA,YACC,OAAO;AAAA,cACL,EAAE,IAAI,QAAQ,OAAO,aAAa,QAAQ,MAAM,UAAU,MAAM,SAAS,GAAG,EAAE;AAAA,cAC9E,EAAE,IAAI,UAAU,OAAO,aAAa,QAAQ,QAAQ,aAAa,MAAM,UAAU,MAAM,YAAY,GAAG,EAAE;AAAA,YAC1G;AAAA;AAAA,QACF;AAAA;AAAA,IAEJ,GACF;AAAA,IAEA,oBAAC,UAAO,MAAM,WAAW,MAAM,cAAc,CAAC,SAAS,CAAC,QAAQ,YAAY,GAC1E,+BAAC,iBAAc,WAAU,aACvB;AAAA,0BAAC,gBACC,8BAAC,eACE,kBAAQ,SAAS,SAAS,aAAa,KAAK,YAAY,aAAa,KAAK,aAC7E,GACF;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR;AAAA,UACA,eAAe;AAAA,UACf,yBAAyB,QAAQ,SAAS,SAAS,OAAO,MAAM,YAAY;AAAA,UAC5E,aAAa,aAAa,KAAK;AAAA,UAC/B,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,UAAQ;AAAA,UACR,WAAW;AAAA,UACX,WAAS;AAAA,UACT,cACE,oBAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,SAAS,aAC5C,uBAAa,KAAK,QACrB;AAAA;AAAA,MAEJ;AAAA,OACF,GACF;AAAA,IACC;AAAA,KACH;AAEJ;",
|
|
6
6
|
"names": ["message"]
|
|
7
7
|
}
|