@open-mercato/core 0.6.8-develop.6910.1.560e304de2 → 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/data_sync/backend/data-sync/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/data_sync/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Separator } from '@open-mercato/ui/primitives/separator'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall, 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 { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n ArrowRightLeft,\n Boxes,\n CalendarClock,\n Clock3,\n Gauge,\n Play,\n PlugZap,\n Repeat,\n Settings2,\n ShieldCheck,\n} from 'lucide-react'\nimport { getSyncRunStatusVariant, getSyncSummaryVariant } from '../../lib/syncRunStatus'\n\ntype SyncRunRow = {\n id: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'\n createdCount: number\n updatedCount: number\n failedCount: number\n createdAt: string\n}\n\ntype ResponsePayload = {\n items: SyncRunRow[]\n total: number\n page: number\n totalPages: number\n}\n\ntype SyncOption = {\n integrationId: string\n title: string\n description?: string | null\n providerKey?: string | null\n direction: 'import' | 'export' | 'bidirectional'\n runMode?: 'generic' | 'provider'\n canStartRun?: boolean\n supportedEntities: string[]\n hasCredentials: boolean\n isEnabled: boolean\n settingsPath: string\n}\n\ntype SyncOptionsResponse = {\n items: SyncOption[]\n}\n\ntype SyncScheduleRecord = {\n id: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n fullSync: boolean\n isEnabled: boolean\n lastRunAt: string | null\n updatedAt?: string | null\n}\n\ntype SyncSchedulesResponse = {\n items?: SyncScheduleRecord[]\n}\n\ntype SyncScheduleEditorState = {\n id?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n fullSync: boolean\n isEnabled: boolean\n lastRunAt: string | null\n updatedAt?: string | null\n}\n\nconst DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n\nfunction formatEntityTypeLabel(entityType: string): string {\n return entityType\n .replace(/[_-]+/g, ' ')\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase())\n}\n\nfunction buildDefaultScheduleState(entityType: string): SyncScheduleEditorState {\n const normalized = entityType.trim().toLowerCase()\n const longerInterval = normalized === 'categories' || normalized === 'attributes'\n return {\n scheduleType: 'interval',\n scheduleValue: longerInterval ? '6h' : '1h',\n timezone: DEFAULT_TIMEZONE,\n fullSync: normalized !== 'products',\n isEnabled: true,\n lastRunAt: null,\n updatedAt: null,\n }\n}\n\nexport default function SyncRunsDashboardPage() {\n const router = useRouter()\n const [rows, setRows] = React.useState<SyncRunRow[]>([])\n const [options, setOptions] = React.useState<SyncOption[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [isLoadingOptions, setIsLoadingOptions] = React.useState(true)\n const [selectedIntegrationId, setSelectedIntegrationId] = React.useState('')\n const [selectedEntityType, setSelectedEntityType] = React.useState('')\n const [selectedDirection, setSelectedDirection] = React.useState<'import' | 'export'>('import')\n const [batchSize, setBatchSize] = React.useState('100')\n const [fullSync, setFullSync] = React.useState(false)\n const [scheduleEditor, setScheduleEditor] = React.useState<SyncScheduleEditorState>(() => buildDefaultScheduleState(''))\n const [isLoadingSchedule, setIsLoadingSchedule] = React.useState(false)\n const [isSavingSchedule, setIsSavingSchedule] = React.useState(false)\n const [isDeletingSchedule, setIsDeletingSchedule] = React.useState(false)\n const [reloadToken, setReloadToken] = React.useState(0)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const { runMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'data_sync.dashboard',\n })\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '20')\n if (filterValues.status) params.set('status', filterValues.status as string)\n if (filterValues.direction) params.set('direction', filterValues.direction as string)\n if (search.trim()) params.set('search', search.trim())\n const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(\n `/api/data_sync/runs?${params.toString()}`,\n undefined,\n { fallback },\n )\n if (!call.ok) {\n flash(t('data_sync.dashboard.loadError'), 'error')\n if (!cancelled) setIsLoading(false)\n return\n }\n const payload = call.result ?? fallback\n if (!cancelled) {\n setRows(Array.isArray(payload.items) ? payload.items : [])\n setTotal(payload.total || 0)\n setTotalPages(payload.totalPages || 1)\n setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [page, filterValues, search, reloadToken, scopeVersion, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadOptions() {\n setIsLoadingOptions(true)\n const fallback: SyncOptionsResponse = { items: [] }\n const call = await apiCall<SyncOptionsResponse>('/api/data_sync/options', undefined, { fallback })\n if (!cancelled) {\n if (!call.ok) {\n flash(t('data_sync.dashboard.loadError'), 'error')\n setOptions([])\n setIsLoadingOptions(false)\n return\n }\n\n const nextItems = Array.isArray(call.result?.items) ? call.result.items : []\n setOptions(nextItems)\n setSelectedIntegrationId((current) => {\n if (current && nextItems.some((item) => item.integrationId === current)) return current\n return nextItems[0]?.integrationId ?? ''\n })\n setIsLoadingOptions(false)\n }\n }\n\n void loadOptions()\n return () => { cancelled = true }\n }, [scopeVersion, t])\n\n const selectedIntegration = React.useMemo(\n () => options.find((item) => item.integrationId === selectedIntegrationId) ?? null,\n [options, selectedIntegrationId],\n )\n\n const entityOptions = React.useMemo(\n () => selectedIntegration?.supportedEntities ?? [],\n [selectedIntegration],\n )\n\n React.useEffect(() => {\n if (!selectedIntegration) {\n setSelectedEntityType('')\n return\n }\n setSelectedEntityType((current) => (\n current && selectedIntegration.supportedEntities.includes(current)\n ? current\n : (selectedIntegration.supportedEntities[0] ?? '')\n ))\n setSelectedDirection(selectedIntegration.direction === 'export' ? 'export' : 'import')\n }, [selectedIntegration])\n\n React.useEffect(() => {\n if (!selectedIntegration || !selectedEntityType) {\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n return\n }\n\n const currentIntegration = selectedIntegration\n let cancelled = false\n async function loadSchedule() {\n setIsLoadingSchedule(true)\n const integrationId = currentIntegration.integrationId\n const params = new URLSearchParams({\n integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n page: '1',\n pageSize: '1',\n })\n const fallback: SyncSchedulesResponse = { items: [] }\n const call = await apiCall<SyncSchedulesResponse>(`/api/data_sync/schedules?${params.toString()}`, undefined, { fallback })\n\n if (cancelled) return\n\n if (!call.ok) {\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n setIsLoadingSchedule(false)\n return\n }\n\n const record = Array.isArray(call.result?.items) ? call.result?.items[0] : undefined\n if (!record) {\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n setIsLoadingSchedule(false)\n return\n }\n\n setScheduleEditor({\n id: record.id,\n scheduleType: record.scheduleType,\n scheduleValue: record.scheduleValue,\n timezone: record.timezone,\n fullSync: record.fullSync,\n isEnabled: record.isEnabled,\n lastRunAt: record.lastRunAt,\n updatedAt: record.updatedAt ?? null,\n })\n setIsLoadingSchedule(false)\n }\n\n void loadSchedule()\n return () => { cancelled = true }\n }, [selectedDirection, selectedEntityType, selectedIntegration, scopeVersion])\n\n const updateScheduleEditor = React.useCallback((changes: Partial<SyncScheduleEditorState>) => {\n setScheduleEditor((current) => ({ ...current, ...changes }))\n }, [])\n\n const handleCancel = React.useCallback(async (row: SyncRunRow) => {\n // optimistic-lock-exempt: run lifecycle action endpoint (cancel), not a concurrent record edit\n const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/cancel`, {\n method: 'POST',\n }, { fallback: null })\n if (call.ok) {\n flash(t('data_sync.runs.detail.cancelSuccess'), 'success')\n setReloadToken((token) => token + 1)\n } else {\n flash(t('data_sync.runs.detail.cancelError'), 'error')\n }\n }, [t])\n\n const handleRetry = React.useCallback(async (row: SyncRunRow) => {\n // optimistic-lock-exempt: run lifecycle action endpoint (retry), not a concurrent record edit\n const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/retry`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ fromBeginning: false }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('data_sync.runs.detail.retrySuccess'), 'success')\n setReloadToken((token) => token + 1)\n } else {\n flash(t('data_sync.runs.detail.retryError'), 'error')\n }\n }, [t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n const next: FilterValues = {}\n Object.entries(values).forEach(([key, value]) => {\n if (value !== undefined && value !== '') next[key] = value\n })\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const handleStartSync = React.useCallback(async () => {\n if (!selectedIntegration || !selectedEntityType) return\n\n const parsedBatchSize = Number.parseInt(batchSize, 10)\n if (!Number.isFinite(parsedBatchSize) || parsedBatchSize < 1 || parsedBatchSize > 1000) {\n flash(t('data_sync.dashboard.start.invalidBatchSize', 'Batch size must be between 1 and 1000.'), 'error')\n return\n }\n\n try {\n const call = await runMutation({\n // optimistic-lock-exempt: starts a new sync run (create), not a concurrent record edit\n operation: () => apiCall<{ id: string }>('/api/data_sync/run', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n batchSize: parsedBatchSize,\n fullSync,\n }),\n }, { fallback: null }),\n mutationPayload: {\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n batchSize: parsedBatchSize,\n fullSync,\n },\n context: {\n operation: 'create',\n actionId: 'start-sync-run',\n integrationId: selectedIntegration.integrationId,\n },\n })\n\n if (!call.ok || !call.result?.id) {\n flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.start.error', 'Failed to start sync run'), 'error')\n return\n }\n\n flash(t('data_sync.dashboard.start.success', 'Sync run started'), 'success')\n setReloadToken((token) => token + 1)\n router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)\n } catch (error) {\n const message = error instanceof Error ? error.message : t('data_sync.dashboard.start.error', 'Failed to start sync run')\n flash(message, 'error')\n }\n }, [batchSize, fullSync, router, runMutation, selectedDirection, selectedEntityType, selectedIntegration, t])\n\n const handleSaveSchedule = React.useCallback(async () => {\n if (!selectedIntegration || !selectedEntityType) return\n if (scheduleEditor.scheduleValue.trim().length === 0) {\n flash(t('data_sync.dashboard.schedule.invalidValue', 'Provide a schedule value before saving.'), 'error')\n return\n }\n\n setIsSavingSchedule(true)\n try {\n const call = await runMutation({\n // Keyed upsert (POST). When the editor holds an existing schedule's\n // `updatedAt`, the lock header version-checks the resolved row on the\n // server; a brand-new schedule has a null `updatedAt`, so the header is\n // empty and the create path stays unaffected.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(scheduleEditor.updatedAt),\n () => apiCall<SyncScheduleRecord>('/api/data_sync/schedules', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n scheduleType: scheduleEditor.scheduleType,\n scheduleValue: scheduleEditor.scheduleValue.trim(),\n timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,\n fullSync: scheduleEditor.fullSync,\n isEnabled: scheduleEditor.isEnabled,\n }),\n }, { fallback: null }),\n ),\n mutationPayload: {\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n scheduleType: scheduleEditor.scheduleType,\n scheduleValue: scheduleEditor.scheduleValue.trim(),\n timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,\n fullSync: scheduleEditor.fullSync,\n isEnabled: scheduleEditor.isEnabled,\n },\n context: {\n operation: 'update',\n actionId: 'save-sync-schedule',\n integrationId: selectedIntegration.integrationId,\n },\n })\n\n if (!call.ok || !call.result) {\n const conflictError = Object.assign(\n new Error((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule')),\n {\n status: call.status,\n ...(call.result && typeof call.result === 'object' ? call.result : {}),\n },\n )\n if (surfaceRecordConflict(conflictError, t)) {\n return\n }\n flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule'), 'error')\n return\n }\n\n setScheduleEditor({\n id: call.result.id,\n scheduleType: call.result.scheduleType,\n scheduleValue: call.result.scheduleValue,\n timezone: call.result.timezone,\n fullSync: call.result.fullSync,\n isEnabled: call.result.isEnabled,\n lastRunAt: call.result.lastRunAt,\n updatedAt: call.result.updatedAt ?? null,\n })\n flash(t('data_sync.dashboard.schedule.success', 'Recurring schedule saved'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule')\n flash(message, 'error')\n } finally {\n setIsSavingSchedule(false)\n }\n }, [runMutation, scheduleEditor, selectedDirection, selectedEntityType, selectedIntegration, t])\n\n const handleDeleteSchedule = React.useCallback(async () => {\n if (!scheduleEditor.id) return\n\n setIsDeletingSchedule(true)\n try {\n const call = await runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(scheduleEditor.updatedAt),\n () => apiCall(`/api/data_sync/schedules/${encodeURIComponent(scheduleEditor.id as string)}`, {\n method: 'DELETE',\n }, { fallback: null }),\n ),\n mutationPayload: {\n scheduleId: scheduleEditor.id,\n },\n context: {\n operation: 'delete',\n actionId: 'delete-sync-schedule',\n },\n })\n\n if (!call.ok) {\n flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.deleteError', 'Failed to remove recurring schedule'), 'error')\n return\n }\n\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n flash(t('data_sync.dashboard.schedule.deleteSuccess', 'Recurring schedule removed'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('data_sync.dashboard.schedule.deleteError', 'Failed to remove recurring schedule')\n flash(message, 'error')\n } finally {\n setIsDeletingSchedule(false)\n }\n }, [runMutation, scheduleEditor.id, scheduleEditor.updatedAt, selectedEntityType, t])\n\n const filters: FilterDef[] = [\n {\n id: 'status',\n type: 'select',\n label: t('data_sync.dashboard.filters.status'),\n options: [\n { label: t('data_sync.dashboard.filters.allStatuses'), value: '' },\n { label: t('data_sync.dashboard.status.pending'), value: 'pending' },\n { label: t('data_sync.dashboard.status.running'), value: 'running' },\n { label: t('data_sync.dashboard.status.completed'), value: 'completed' },\n { label: t('data_sync.dashboard.status.failed'), value: 'failed' },\n { label: t('data_sync.dashboard.status.cancelled'), value: 'cancelled' },\n ],\n },\n {\n id: 'direction',\n type: 'select',\n label: t('data_sync.dashboard.columns.direction'),\n options: [\n { label: t('data_sync.dashboard.filters.allDirections'), value: '' },\n { label: t('data_sync.dashboard.direction.import'), value: 'import' },\n { label: t('data_sync.dashboard.direction.export'), value: 'export' },\n ],\n },\n ]\n\n const columns = React.useMemo<ColumnDef<SyncRunRow>[]>(() => [\n {\n accessorKey: 'integrationId',\n header: t('data_sync.dashboard.columns.integration'),\n cell: ({ row }) => <span className=\"font-medium text-sm\">{row.original.integrationId}</span>,\n },\n {\n accessorKey: 'entityType',\n header: t('data_sync.dashboard.columns.entityType'),\n },\n {\n accessorKey: 'direction',\n header: t('data_sync.dashboard.columns.direction'),\n cell: ({ row }) => (\n <Badge variant=\"outline\">\n {t(`data_sync.dashboard.direction.${row.original.direction}`)}\n </Badge>\n ),\n },\n {\n accessorKey: 'status',\n header: t('data_sync.dashboard.columns.status'),\n cell: ({ row }) => (\n <StatusBadge variant={getSyncRunStatusVariant(row.original.status)}>\n {t(`data_sync.dashboard.status.${row.original.status}`)}\n </StatusBadge>\n ),\n },\n {\n accessorKey: 'createdCount',\n header: t('data_sync.dashboard.columns.created'),\n },\n {\n accessorKey: 'updatedCount',\n header: t('data_sync.dashboard.columns.updated'),\n },\n {\n accessorKey: 'failedCount',\n header: t('data_sync.dashboard.columns.failed'),\n },\n {\n accessorKey: 'createdAt',\n header: t('data_sync.dashboard.columns.createdAt'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ], [t])\n\n const canStartSelectedIntegration = Boolean(\n selectedIntegration\n && selectedEntityType\n && selectedIntegration.isEnabled\n && selectedIntegration.canStartRun !== false\n && selectedIntegration.hasCredentials,\n )\n const hasSavedSchedule = Boolean(scheduleEditor.id)\n const selectedEntityLabel = selectedEntityType ? formatEntityTypeLabel(selectedEntityType) : t('data_sync.dashboard.columns.entityType')\n const integrationStateVariant = getSyncSummaryVariant(selectedIntegration?.isEnabled ? 'enabled' : 'disabled')\n const credentialsVariant = getSyncSummaryVariant(selectedIntegration?.hasCredentials ? 'ready' : 'missing')\n const scheduleVariant = getSyncSummaryVariant(\n hasSavedSchedule\n ? (scheduleEditor.isEnabled ? 'scheduled' : 'paused')\n : 'none',\n )\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <Card>\n <CardHeader className=\"space-y-4\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-xs font-medium uppercase tracking-widest text-muted-foreground\">\n <Repeat className=\"size-4\" />\n <span>{t('data_sync.dashboard.start.eyebrow', 'Run once or keep it recurring')}</span>\n </div>\n <div className=\"space-y-1\">\n <CardTitle>{t('data_sync.dashboard.start.title', 'Start or schedule a sync')}</CardTitle>\n <p className=\"max-w-3xl text-sm text-muted-foreground\">\n {t('data_sync.dashboard.start.description', 'Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.')}\n </p>\n </div>\n </div>\n {selectedIntegration ? (\n <Button asChild variant=\"outline\">\n <Link href={selectedIntegration.settingsPath}>\n <Settings2 className=\"mr-2 size-4\" />\n {t('integrations.marketplace.configure')}\n </Link>\n </Button>\n ) : null}\n </div>\n\n {selectedIntegration ? (\n <div className=\"flex flex-wrap gap-2\">\n <Badge variant=\"outline\" className=\"gap-1.5\">\n <PlugZap className=\"size-3.5\" />\n {selectedIntegration.title}\n </Badge>\n <Badge variant=\"outline\" className=\"gap-1.5\">\n <ArrowRightLeft className=\"size-3.5\" />\n {t(`data_sync.dashboard.direction.${selectedDirection}`)}\n </Badge>\n <Badge variant={integrationStateVariant} className=\"gap-1.5\">\n <ShieldCheck className=\"size-3.5\" />\n {selectedIntegration.isEnabled\n ? t('data_sync.dashboard.start.status.enabled', 'Integration enabled')\n : t('data_sync.dashboard.start.status.disabled', 'Integration disabled')}\n </Badge>\n <Badge variant={credentialsVariant} className=\"gap-1.5\">\n <PlugZap className=\"size-3.5\" />\n {selectedIntegration.hasCredentials\n ? t('data_sync.dashboard.start.status.credentialsReady', 'Credentials ready')\n : t('data_sync.dashboard.start.status.credentialsMissing', 'Credentials missing')}\n </Badge>\n <Badge variant={scheduleVariant} className=\"gap-1.5\">\n <CalendarClock className=\"size-3.5\" />\n {hasSavedSchedule\n ? (scheduleEditor.isEnabled\n ? t('data_sync.dashboard.schedule.status.enabled', 'Recurring schedule active')\n : t('data_sync.dashboard.schedule.status.disabled', 'Recurring schedule paused'))\n : t('data_sync.dashboard.schedule.status.none', 'No recurring schedule')}\n </Badge>\n </div>\n ) : null}\n </CardHeader>\n <CardContent className=\"space-y-6\">\n <div className=\"grid gap-4 xl:grid-cols-3\">\n <div className=\"space-y-2 xl:col-span-1\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <PlugZap className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.columns.integration')}</span>\n </Label>\n <Select\n value={selectedIntegrationId || undefined}\n onValueChange={(value) => setSelectedIntegrationId(value ?? '')}\n disabled={isLoadingOptions || options.length === 0}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue\n placeholder={\n options.length === 0\n ? t('integrations.marketplace.noResults', 'No integrations found')\n : undefined\n }\n />\n </SelectTrigger>\n <SelectContent>\n {options.map((item) => (\n <SelectItem key={item.integrationId} value={item.integrationId}>\n {item.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Boxes className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.columns.entityType')}</span>\n </Label>\n <Select\n value={selectedEntityType || undefined}\n onValueChange={(value) => setSelectedEntityType(value ?? '')}\n disabled={entityOptions.length === 0}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {entityOptions.map((entityType) => (\n <SelectItem key={entityType} value={entityType}>\n {formatEntityTypeLabel(entityType)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <ArrowRightLeft className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.columns.direction')}</span>\n </Label>\n <Select\n value={selectedDirection}\n onValueChange={(value) => setSelectedDirection(value === 'export' ? 'export' : 'import')}\n disabled={selectedIntegration?.direction !== 'bidirectional'}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"import\">{t('data_sync.dashboard.direction.import')}</SelectItem>\n {(selectedIntegration?.direction === 'bidirectional' || selectedIntegration?.direction === 'export') ? (\n <SelectItem value=\"export\">{t('data_sync.dashboard.direction.export')}</SelectItem>\n ) : null}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {selectedIntegration?.description ? (\n <p className=\"text-sm text-muted-foreground\">{selectedIntegration.description}</p>\n ) : null}\n\n <div className=\"grid gap-4 xl:grid-cols-2\">\n <div className=\"rounded-xl border bg-muted/30 p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <Play className=\"size-4 text-primary\" />\n <h3 className=\"text-sm font-semibold\">{t('data_sync.dashboard.start.runNowTitle', 'Run once now')}</h3>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t('data_sync.dashboard.start.runNowDescription', 'Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.')}\n </p>\n </div>\n <Badge variant=\"outline\">{selectedEntityLabel}</Badge>\n </div>\n\n <Separator className=\"my-4\" />\n\n <div className=\"grid gap-4 sm:grid-cols-[minmax(0,180px)_1fr]\">\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Gauge className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.start.batchSize', 'Batch size')}</span>\n </Label>\n <Input\n value={batchSize}\n onChange={(event) => setBatchSize(event.target.value)}\n inputMode=\"numeric\"\n />\n </div>\n <div className=\"rounded-lg border bg-background p-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"space-y-1\">\n <Label className=\"text-sm font-medium\">{t('data_sync.dashboard.start.fullSync', 'Run as full sync')}</Label>\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.start.fullSyncHelp', 'Ignore the saved cursor and process the entire source again for this run.')}\n </p>\n </div>\n <Switch checked={fullSync} onCheckedChange={setFullSync} />\n </div>\n </div>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3\">\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.start.runNowFootnote', 'Manual runs show progress immediately and land on the run detail page after launch.')}\n </p>\n <Button\n type=\"button\"\n onClick={() => void handleStartSync()}\n disabled={!canStartSelectedIntegration}\n >\n <Play className=\"mr-2 size-4\" />\n {t('data_sync.dashboard.start.submit', 'Start sync')}\n </Button>\n </div>\n </div>\n\n <div className=\"rounded-xl border bg-muted/30 p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <CalendarClock className=\"size-4 text-primary\" />\n <h3 className=\"text-sm font-semibold\">{t('data_sync.dashboard.schedule.title', 'Recurring schedule')}</h3>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t('data_sync.dashboard.schedule.description', 'Save a repeating schedule for the selected integration, entity, and direction without leaving this dashboard.')}\n </p>\n </div>\n <Badge variant=\"outline\">\n {hasSavedSchedule\n ? (scheduleEditor.isEnabled\n ? t('data_sync.dashboard.schedule.status.shortEnabled', 'Scheduled')\n : t('data_sync.dashboard.schedule.status.shortDisabled', 'Paused'))\n : t('data_sync.dashboard.schedule.status.shortNone', 'One-time only')}\n </Badge>\n </div>\n\n <Separator className=\"my-4\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Clock3 className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.schedule.type', 'Schedule type')}</span>\n </Label>\n <Select\n value={scheduleEditor.scheduleType}\n onValueChange={(value) => updateScheduleEditor({\n scheduleType: value === 'cron' ? 'cron' : 'interval',\n })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"interval\">{t('data_sync.dashboard.schedule.interval', 'Interval')}</SelectItem>\n <SelectItem value=\"cron\">{t('data_sync.dashboard.schedule.cron', 'Cron')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <CalendarClock className=\"size-4 text-muted-foreground\" />\n <span>\n {scheduleEditor.scheduleType === 'cron'\n ? t('data_sync.dashboard.schedule.cronValue', 'Cron expression')\n : t('data_sync.dashboard.schedule.intervalValue', 'Interval')}\n </span>\n </Label>\n <Input\n value={scheduleEditor.scheduleValue}\n onChange={(event) => updateScheduleEditor({ scheduleValue: event.target.value })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n placeholder={scheduleEditor.scheduleType === 'cron' ? '0 * * * *' : '1h'}\n />\n <p className=\"text-xs text-muted-foreground\">\n {scheduleEditor.scheduleType === 'cron'\n ? t('data_sync.dashboard.schedule.cronHelp', 'Example: `0 * * * *` runs at the start of every hour.')\n : t('data_sync.dashboard.schedule.intervalHelp', 'Example: `1h`, `6h`, or `24h` for repeating intervals.')}\n </p>\n </div>\n <div className=\"space-y-2 sm:col-span-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Clock3 className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.schedule.timezone', 'Timezone')}</span>\n </Label>\n <Input\n value={scheduleEditor.timezone}\n onChange={(event) => updateScheduleEditor({ timezone: event.target.value })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n />\n </div>\n </div>\n\n <div className=\"mt-4 grid gap-3\">\n <div className=\"rounded-lg border bg-background p-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"space-y-1\">\n <Label className=\"text-sm font-medium\">{t('data_sync.dashboard.schedule.fullSync', 'Run scheduled jobs as full sync')}</Label>\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.schedule.fullSyncHelp', 'When enabled, every recurring run starts from the beginning instead of the saved cursor.')}\n </p>\n </div>\n <Switch\n checked={scheduleEditor.fullSync}\n onCheckedChange={(checked) => updateScheduleEditor({ fullSync: checked })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n />\n </div>\n </div>\n <div className=\"rounded-lg border bg-background p-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"space-y-1\">\n <Label className=\"text-sm font-medium\">{t('data_sync.dashboard.schedule.enabled', 'Schedule enabled')}</Label>\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.schedule.enabledHelp', 'Pause the recurring job without deleting the schedule definition.')}\n </p>\n </div>\n <Switch\n checked={scheduleEditor.isEnabled}\n onCheckedChange={(checked) => updateScheduleEditor({ isEnabled: checked })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n />\n </div>\n </div>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3\">\n <div className=\"space-y-1 text-xs text-muted-foreground\">\n <div>\n {hasSavedSchedule\n ? (scheduleEditor.lastRunAt\n ? t('data_sync.dashboard.schedule.lastRun', 'Last scheduled run: {value}', {\n value: new Date(scheduleEditor.lastRunAt).toLocaleString(),\n })\n : t('data_sync.dashboard.schedule.neverRun', 'Saved, but no scheduled execution has completed yet.'))\n : t('data_sync.dashboard.schedule.none', 'No recurring schedule saved for this target yet.')}\n </div>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => void handleDeleteSchedule()}\n disabled={!hasSavedSchedule || isDeletingSchedule}\n >\n {isDeletingSchedule\n ? t('data_sync.dashboard.schedule.deleting', 'Removing...')\n : t('data_sync.dashboard.schedule.delete', 'Remove schedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => void handleSaveSchedule()}\n disabled={isSavingSchedule || !selectedIntegration || !selectedEntityType}\n >\n <CalendarClock className=\"mr-2 size-4\" />\n {isSavingSchedule\n ? t('data_sync.dashboard.schedule.saving', 'Saving...')\n : t('data_sync.dashboard.schedule.save', 'Save recurring schedule')}\n </Button>\n </div>\n </div>\n </div>\n </div>\n\n {selectedIntegration && !selectedIntegration.isEnabled ? (\n <Alert status=\"warning\">\n <AlertDescription>\n {t('integrations.detail.state.disabled', 'This integration is disabled. Enable it on the integration settings page before starting a sync.')}\n </AlertDescription>\n </Alert>\n ) : null}\n {selectedIntegration && !selectedIntegration.hasCredentials ? (\n <Alert status=\"warning\">\n <AlertDescription>\n {t('integrations.detail.credentials.notConfigured', 'Credentials are not configured yet. Save the integration credentials before starting a sync.')}\n </AlertDescription>\n </Alert>\n ) : null}\n {selectedIntegration && selectedIntegration.canStartRun === false ? (\n <Alert status=\"information\">\n <AlertDescription>\n {t('data_sync.dashboard.start.providerManaged', 'This integration starts sync runs from its own setup flow. Open the integration settings page to continue.')}\n </AlertDescription>\n </Alert>\n ) : null}\n </CardContent>\n </Card>\n\n <DataTable\n stickyActionsColumn\n title={t('data_sync.dashboard.title')}\n columns={columns}\n data={rows}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('data_sync.dashboard.searchPlaceholder')}\n perspective={{ tableId: extensionPoints.hosts.runsTable.tableId }}\n onRowClick={(row) => {\n router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`)\n }}\n rowActions={(row) => (\n <RowActions items={[\n {\n id: 'view',\n label: t('data_sync.dashboard.actions.view'),\n onSelect: () => { router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`) },\n },\n ...(row.status === 'running' ? [{\n id: 'cancel',\n label: t('data_sync.runs.detail.cancel'),\n destructive: true,\n onSelect: () => { void handleCancel(row) },\n }] : []),\n ...(row.status === 'failed' ? [{\n id: 'retry',\n label: t('data_sync.runs.detail.retry'),\n onSelect: () => { void handleRetry(row) },\n }] : []),\n ]} />\n )}\n pagination={{ page, pageSize: 20, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/data_sync/extension-points'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { Separator } from '@open-mercato/ui/primitives/separator'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall, 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 { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n ArrowRightLeft,\n Boxes,\n CalendarClock,\n Clock3,\n Gauge,\n Play,\n PlugZap,\n Repeat,\n Settings2,\n ShieldCheck,\n} from 'lucide-react'\nimport { getSyncRunStatusVariant, getSyncSummaryVariant } from '../../lib/syncRunStatus'\n\ntype SyncRunRow = {\n id: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'paused'\n createdCount: number\n updatedCount: number\n failedCount: number\n createdAt: string\n}\n\ntype ResponsePayload = {\n items: SyncRunRow[]\n total: number\n page: number\n totalPages: number\n}\n\ntype SyncOption = {\n integrationId: string\n title: string\n description?: string | null\n providerKey?: string | null\n direction: 'import' | 'export' | 'bidirectional'\n runMode?: 'generic' | 'provider'\n canStartRun?: boolean\n supportedEntities: string[]\n hasCredentials: boolean\n isEnabled: boolean\n settingsPath: string\n}\n\ntype SyncOptionsResponse = {\n items: SyncOption[]\n}\n\ntype SyncScheduleRecord = {\n id: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n fullSync: boolean\n isEnabled: boolean\n lastRunAt: string | null\n updatedAt?: string | null\n}\n\ntype SyncSchedulesResponse = {\n items?: SyncScheduleRecord[]\n}\n\ntype SyncScheduleEditorState = {\n id?: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n fullSync: boolean\n isEnabled: boolean\n lastRunAt: string | null\n updatedAt?: string | null\n}\n\nconst DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n\nfunction formatEntityTypeLabel(entityType: string): string {\n return entityType\n .replace(/[_-]+/g, ' ')\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase())\n}\n\nfunction buildDefaultScheduleState(entityType: string): SyncScheduleEditorState {\n const normalized = entityType.trim().toLowerCase()\n const longerInterval = normalized === 'categories' || normalized === 'attributes'\n return {\n scheduleType: 'interval',\n scheduleValue: longerInterval ? '6h' : '1h',\n timezone: DEFAULT_TIMEZONE,\n fullSync: normalized !== 'products',\n isEnabled: true,\n lastRunAt: null,\n updatedAt: null,\n }\n}\n\nexport default function SyncRunsDashboardPage() {\n const router = useRouter()\n const [rows, setRows] = React.useState<SyncRunRow[]>([])\n const [options, setOptions] = React.useState<SyncOption[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [isLoading, setIsLoading] = React.useState(true)\n const [isLoadingOptions, setIsLoadingOptions] = React.useState(true)\n const [selectedIntegrationId, setSelectedIntegrationId] = React.useState('')\n const [selectedEntityType, setSelectedEntityType] = React.useState('')\n const [selectedDirection, setSelectedDirection] = React.useState<'import' | 'export'>('import')\n const [batchSize, setBatchSize] = React.useState('100')\n const [fullSync, setFullSync] = React.useState(false)\n const [scheduleEditor, setScheduleEditor] = React.useState<SyncScheduleEditorState>(() => buildDefaultScheduleState(''))\n const [isLoadingSchedule, setIsLoadingSchedule] = React.useState(false)\n const [isSavingSchedule, setIsSavingSchedule] = React.useState(false)\n const [isDeletingSchedule, setIsDeletingSchedule] = React.useState(false)\n const [reloadToken, setReloadToken] = React.useState(0)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const { runMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: 'data_sync.dashboard',\n })\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '20')\n if (filterValues.status) params.set('status', filterValues.status as string)\n if (filterValues.direction) params.set('direction', filterValues.direction as string)\n if (search.trim()) params.set('search', search.trim())\n const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(\n `/api/data_sync/runs?${params.toString()}`,\n undefined,\n { fallback },\n )\n if (!call.ok) {\n flash(t('data_sync.dashboard.loadError'), 'error')\n if (!cancelled) setIsLoading(false)\n return\n }\n const payload = call.result ?? fallback\n if (!cancelled) {\n setRows(Array.isArray(payload.items) ? payload.items : [])\n setTotal(payload.total || 0)\n setTotalPages(payload.totalPages || 1)\n setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [page, filterValues, search, reloadToken, scopeVersion, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadOptions() {\n setIsLoadingOptions(true)\n const fallback: SyncOptionsResponse = { items: [] }\n const call = await apiCall<SyncOptionsResponse>('/api/data_sync/options', undefined, { fallback })\n if (!cancelled) {\n if (!call.ok) {\n flash(t('data_sync.dashboard.loadError'), 'error')\n setOptions([])\n setIsLoadingOptions(false)\n return\n }\n\n const nextItems = Array.isArray(call.result?.items) ? call.result.items : []\n setOptions(nextItems)\n setSelectedIntegrationId((current) => {\n if (current && nextItems.some((item) => item.integrationId === current)) return current\n return nextItems[0]?.integrationId ?? ''\n })\n setIsLoadingOptions(false)\n }\n }\n\n void loadOptions()\n return () => { cancelled = true }\n }, [scopeVersion, t])\n\n const selectedIntegration = React.useMemo(\n () => options.find((item) => item.integrationId === selectedIntegrationId) ?? null,\n [options, selectedIntegrationId],\n )\n\n const entityOptions = React.useMemo(\n () => selectedIntegration?.supportedEntities ?? [],\n [selectedIntegration],\n )\n\n React.useEffect(() => {\n if (!selectedIntegration) {\n setSelectedEntityType('')\n return\n }\n setSelectedEntityType((current) => (\n current && selectedIntegration.supportedEntities.includes(current)\n ? current\n : (selectedIntegration.supportedEntities[0] ?? '')\n ))\n setSelectedDirection(selectedIntegration.direction === 'export' ? 'export' : 'import')\n }, [selectedIntegration])\n\n React.useEffect(() => {\n if (!selectedIntegration || !selectedEntityType) {\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n return\n }\n\n const currentIntegration = selectedIntegration\n let cancelled = false\n async function loadSchedule() {\n setIsLoadingSchedule(true)\n const integrationId = currentIntegration.integrationId\n const params = new URLSearchParams({\n integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n page: '1',\n pageSize: '1',\n })\n const fallback: SyncSchedulesResponse = { items: [] }\n const call = await apiCall<SyncSchedulesResponse>(`/api/data_sync/schedules?${params.toString()}`, undefined, { fallback })\n\n if (cancelled) return\n\n if (!call.ok) {\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n setIsLoadingSchedule(false)\n return\n }\n\n const record = Array.isArray(call.result?.items) ? call.result?.items[0] : undefined\n if (!record) {\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n setIsLoadingSchedule(false)\n return\n }\n\n setScheduleEditor({\n id: record.id,\n scheduleType: record.scheduleType,\n scheduleValue: record.scheduleValue,\n timezone: record.timezone,\n fullSync: record.fullSync,\n isEnabled: record.isEnabled,\n lastRunAt: record.lastRunAt,\n updatedAt: record.updatedAt ?? null,\n })\n setIsLoadingSchedule(false)\n }\n\n void loadSchedule()\n return () => { cancelled = true }\n }, [selectedDirection, selectedEntityType, selectedIntegration, scopeVersion])\n\n const updateScheduleEditor = React.useCallback((changes: Partial<SyncScheduleEditorState>) => {\n setScheduleEditor((current) => ({ ...current, ...changes }))\n }, [])\n\n const handleCancel = React.useCallback(async (row: SyncRunRow) => {\n // optimistic-lock-exempt: run lifecycle action endpoint (cancel), not a concurrent record edit\n const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/cancel`, {\n method: 'POST',\n }, { fallback: null })\n if (call.ok) {\n flash(t('data_sync.runs.detail.cancelSuccess'), 'success')\n setReloadToken((token) => token + 1)\n } else {\n flash(t('data_sync.runs.detail.cancelError'), 'error')\n }\n }, [t])\n\n const handleRetry = React.useCallback(async (row: SyncRunRow) => {\n // optimistic-lock-exempt: run lifecycle action endpoint (retry), not a concurrent record edit\n const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/retry`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ fromBeginning: false }),\n }, { fallback: null })\n if (call.ok) {\n flash(t('data_sync.runs.detail.retrySuccess'), 'success')\n setReloadToken((token) => token + 1)\n } else {\n flash(t('data_sync.runs.detail.retryError'), 'error')\n }\n }, [t])\n\n const handleFiltersApply = React.useCallback((values: FilterValues) => {\n const next: FilterValues = {}\n Object.entries(values).forEach(([key, value]) => {\n if (value !== undefined && value !== '') next[key] = value\n })\n setFilterValues(next)\n setPage(1)\n }, [])\n\n const handleFiltersClear = React.useCallback(() => {\n setFilterValues({})\n setPage(1)\n }, [])\n\n const handleStartSync = React.useCallback(async () => {\n if (!selectedIntegration || !selectedEntityType) return\n\n const parsedBatchSize = Number.parseInt(batchSize, 10)\n if (!Number.isFinite(parsedBatchSize) || parsedBatchSize < 1 || parsedBatchSize > 1000) {\n flash(t('data_sync.dashboard.start.invalidBatchSize', 'Batch size must be between 1 and 1000.'), 'error')\n return\n }\n\n try {\n const call = await runMutation({\n // optimistic-lock-exempt: starts a new sync run (create), not a concurrent record edit\n operation: () => apiCall<{ id: string }>('/api/data_sync/run', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n batchSize: parsedBatchSize,\n fullSync,\n }),\n }, { fallback: null }),\n mutationPayload: {\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n batchSize: parsedBatchSize,\n fullSync,\n },\n context: {\n operation: 'create',\n actionId: 'start-sync-run',\n integrationId: selectedIntegration.integrationId,\n },\n })\n\n if (!call.ok || !call.result?.id) {\n flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.start.error', 'Failed to start sync run'), 'error')\n return\n }\n\n flash(t('data_sync.dashboard.start.success', 'Sync run started'), 'success')\n setReloadToken((token) => token + 1)\n router.push(`/backend/data-sync/runs/${encodeURIComponent(call.result.id)}`)\n } catch (error) {\n const message = error instanceof Error ? error.message : t('data_sync.dashboard.start.error', 'Failed to start sync run')\n flash(message, 'error')\n }\n }, [batchSize, fullSync, router, runMutation, selectedDirection, selectedEntityType, selectedIntegration, t])\n\n const handleSaveSchedule = React.useCallback(async () => {\n if (!selectedIntegration || !selectedEntityType) return\n if (scheduleEditor.scheduleValue.trim().length === 0) {\n flash(t('data_sync.dashboard.schedule.invalidValue', 'Provide a schedule value before saving.'), 'error')\n return\n }\n\n setIsSavingSchedule(true)\n try {\n const call = await runMutation({\n // Keyed upsert (POST). When the editor holds an existing schedule's\n // `updatedAt`, the lock header version-checks the resolved row on the\n // server; a brand-new schedule has a null `updatedAt`, so the header is\n // empty and the create path stays unaffected.\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(scheduleEditor.updatedAt),\n () => apiCall<SyncScheduleRecord>('/api/data_sync/schedules', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n scheduleType: scheduleEditor.scheduleType,\n scheduleValue: scheduleEditor.scheduleValue.trim(),\n timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,\n fullSync: scheduleEditor.fullSync,\n isEnabled: scheduleEditor.isEnabled,\n }),\n }, { fallback: null }),\n ),\n mutationPayload: {\n integrationId: selectedIntegration.integrationId,\n entityType: selectedEntityType,\n direction: selectedDirection,\n scheduleType: scheduleEditor.scheduleType,\n scheduleValue: scheduleEditor.scheduleValue.trim(),\n timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,\n fullSync: scheduleEditor.fullSync,\n isEnabled: scheduleEditor.isEnabled,\n },\n context: {\n operation: 'update',\n actionId: 'save-sync-schedule',\n integrationId: selectedIntegration.integrationId,\n },\n })\n\n if (!call.ok || !call.result) {\n const conflictError = Object.assign(\n new Error((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule')),\n {\n status: call.status,\n ...(call.result && typeof call.result === 'object' ? call.result : {}),\n },\n )\n if (surfaceRecordConflict(conflictError, t)) {\n return\n }\n flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule'), 'error')\n return\n }\n\n setScheduleEditor({\n id: call.result.id,\n scheduleType: call.result.scheduleType,\n scheduleValue: call.result.scheduleValue,\n timezone: call.result.timezone,\n fullSync: call.result.fullSync,\n isEnabled: call.result.isEnabled,\n lastRunAt: call.result.lastRunAt,\n updatedAt: call.result.updatedAt ?? null,\n })\n flash(t('data_sync.dashboard.schedule.success', 'Recurring schedule saved'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule')\n flash(message, 'error')\n } finally {\n setIsSavingSchedule(false)\n }\n }, [runMutation, scheduleEditor, selectedDirection, selectedEntityType, selectedIntegration, t])\n\n const handleDeleteSchedule = React.useCallback(async () => {\n if (!scheduleEditor.id) return\n\n setIsDeletingSchedule(true)\n try {\n const call = await runMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(scheduleEditor.updatedAt),\n () => apiCall(`/api/data_sync/schedules/${encodeURIComponent(scheduleEditor.id as string)}`, {\n method: 'DELETE',\n }, { fallback: null }),\n ),\n mutationPayload: {\n scheduleId: scheduleEditor.id,\n },\n context: {\n operation: 'delete',\n actionId: 'delete-sync-schedule',\n },\n })\n\n if (!call.ok) {\n flash((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.deleteError', 'Failed to remove recurring schedule'), 'error')\n return\n }\n\n setScheduleEditor(buildDefaultScheduleState(selectedEntityType))\n flash(t('data_sync.dashboard.schedule.deleteSuccess', 'Recurring schedule removed'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('data_sync.dashboard.schedule.deleteError', 'Failed to remove recurring schedule')\n flash(message, 'error')\n } finally {\n setIsDeletingSchedule(false)\n }\n }, [runMutation, scheduleEditor.id, scheduleEditor.updatedAt, selectedEntityType, t])\n\n const filters: FilterDef[] = [\n {\n id: 'status',\n type: 'select',\n label: t('data_sync.dashboard.filters.status'),\n options: [\n { label: t('data_sync.dashboard.filters.allStatuses'), value: '' },\n { label: t('data_sync.dashboard.status.pending'), value: 'pending' },\n { label: t('data_sync.dashboard.status.running'), value: 'running' },\n { label: t('data_sync.dashboard.status.completed'), value: 'completed' },\n { label: t('data_sync.dashboard.status.failed'), value: 'failed' },\n { label: t('data_sync.dashboard.status.cancelled'), value: 'cancelled' },\n ],\n },\n {\n id: 'direction',\n type: 'select',\n label: t('data_sync.dashboard.columns.direction'),\n options: [\n { label: t('data_sync.dashboard.filters.allDirections'), value: '' },\n { label: t('data_sync.dashboard.direction.import'), value: 'import' },\n { label: t('data_sync.dashboard.direction.export'), value: 'export' },\n ],\n },\n ]\n\n const columns = React.useMemo<ColumnDef<SyncRunRow>[]>(() => [\n {\n accessorKey: 'integrationId',\n header: t('data_sync.dashboard.columns.integration'),\n cell: ({ row }) => <span className=\"font-medium text-sm\">{row.original.integrationId}</span>,\n },\n {\n accessorKey: 'entityType',\n header: t('data_sync.dashboard.columns.entityType'),\n },\n {\n accessorKey: 'direction',\n header: t('data_sync.dashboard.columns.direction'),\n cell: ({ row }) => (\n <Badge variant=\"outline\">\n {t(`data_sync.dashboard.direction.${row.original.direction}`)}\n </Badge>\n ),\n },\n {\n accessorKey: 'status',\n header: t('data_sync.dashboard.columns.status'),\n cell: ({ row }) => (\n <StatusBadge variant={getSyncRunStatusVariant(row.original.status)}>\n {t(`data_sync.dashboard.status.${row.original.status}`)}\n </StatusBadge>\n ),\n },\n {\n accessorKey: 'createdCount',\n header: t('data_sync.dashboard.columns.created'),\n },\n {\n accessorKey: 'updatedCount',\n header: t('data_sync.dashboard.columns.updated'),\n },\n {\n accessorKey: 'failedCount',\n header: t('data_sync.dashboard.columns.failed'),\n },\n {\n accessorKey: 'createdAt',\n header: t('data_sync.dashboard.columns.createdAt'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ], [t])\n\n const canStartSelectedIntegration = Boolean(\n selectedIntegration\n && selectedEntityType\n && selectedIntegration.isEnabled\n && selectedIntegration.canStartRun !== false\n && selectedIntegration.hasCredentials,\n )\n const hasSavedSchedule = Boolean(scheduleEditor.id)\n const selectedEntityLabel = selectedEntityType ? formatEntityTypeLabel(selectedEntityType) : t('data_sync.dashboard.columns.entityType')\n const integrationStateVariant = getSyncSummaryVariant(selectedIntegration?.isEnabled ? 'enabled' : 'disabled')\n const credentialsVariant = getSyncSummaryVariant(selectedIntegration?.hasCredentials ? 'ready' : 'missing')\n const scheduleVariant = getSyncSummaryVariant(\n hasSavedSchedule\n ? (scheduleEditor.isEnabled ? 'scheduled' : 'paused')\n : 'none',\n )\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <Card>\n <CardHeader className=\"space-y-4\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-xs font-medium uppercase tracking-widest text-muted-foreground\">\n <Repeat className=\"size-4\" />\n <span>{t('data_sync.dashboard.start.eyebrow', 'Run once or keep it recurring')}</span>\n </div>\n <div className=\"space-y-1\">\n <CardTitle>{t('data_sync.dashboard.start.title', 'Start or schedule a sync')}</CardTitle>\n <p className=\"max-w-3xl text-sm text-muted-foreground\">\n {t('data_sync.dashboard.start.description', 'Pick a sync target, launch an ad-hoc run, or save a recurring schedule for the same entity and direction from this page.')}\n </p>\n </div>\n </div>\n {selectedIntegration ? (\n <Button asChild variant=\"outline\">\n <Link href={selectedIntegration.settingsPath}>\n <Settings2 className=\"mr-2 size-4\" />\n {t('integrations.marketplace.configure')}\n </Link>\n </Button>\n ) : null}\n </div>\n\n {selectedIntegration ? (\n <div className=\"flex flex-wrap gap-2\">\n <Badge variant=\"outline\" className=\"gap-1.5\">\n <PlugZap className=\"size-3.5\" />\n {selectedIntegration.title}\n </Badge>\n <Badge variant=\"outline\" className=\"gap-1.5\">\n <ArrowRightLeft className=\"size-3.5\" />\n {t(`data_sync.dashboard.direction.${selectedDirection}`)}\n </Badge>\n <Badge variant={integrationStateVariant} className=\"gap-1.5\">\n <ShieldCheck className=\"size-3.5\" />\n {selectedIntegration.isEnabled\n ? t('data_sync.dashboard.start.status.enabled', 'Integration enabled')\n : t('data_sync.dashboard.start.status.disabled', 'Integration disabled')}\n </Badge>\n <Badge variant={credentialsVariant} className=\"gap-1.5\">\n <PlugZap className=\"size-3.5\" />\n {selectedIntegration.hasCredentials\n ? t('data_sync.dashboard.start.status.credentialsReady', 'Credentials ready')\n : t('data_sync.dashboard.start.status.credentialsMissing', 'Credentials missing')}\n </Badge>\n <Badge variant={scheduleVariant} className=\"gap-1.5\">\n <CalendarClock className=\"size-3.5\" />\n {hasSavedSchedule\n ? (scheduleEditor.isEnabled\n ? t('data_sync.dashboard.schedule.status.enabled', 'Recurring schedule active')\n : t('data_sync.dashboard.schedule.status.disabled', 'Recurring schedule paused'))\n : t('data_sync.dashboard.schedule.status.none', 'No recurring schedule')}\n </Badge>\n </div>\n ) : null}\n </CardHeader>\n <CardContent className=\"space-y-6\">\n <div className=\"grid gap-4 xl:grid-cols-3\">\n <div className=\"space-y-2 xl:col-span-1\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <PlugZap className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.columns.integration')}</span>\n </Label>\n <Select\n value={selectedIntegrationId || undefined}\n onValueChange={(value) => setSelectedIntegrationId(value ?? '')}\n disabled={isLoadingOptions || options.length === 0}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue\n placeholder={\n options.length === 0\n ? t('integrations.marketplace.noResults', 'No integrations found')\n : undefined\n }\n />\n </SelectTrigger>\n <SelectContent>\n {options.map((item) => (\n <SelectItem key={item.integrationId} value={item.integrationId}>\n {item.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Boxes className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.columns.entityType')}</span>\n </Label>\n <Select\n value={selectedEntityType || undefined}\n onValueChange={(value) => setSelectedEntityType(value ?? '')}\n disabled={entityOptions.length === 0}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {entityOptions.map((entityType) => (\n <SelectItem key={entityType} value={entityType}>\n {formatEntityTypeLabel(entityType)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <ArrowRightLeft className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.columns.direction')}</span>\n </Label>\n <Select\n value={selectedDirection}\n onValueChange={(value) => setSelectedDirection(value === 'export' ? 'export' : 'import')}\n disabled={selectedIntegration?.direction !== 'bidirectional'}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"import\">{t('data_sync.dashboard.direction.import')}</SelectItem>\n {(selectedIntegration?.direction === 'bidirectional' || selectedIntegration?.direction === 'export') ? (\n <SelectItem value=\"export\">{t('data_sync.dashboard.direction.export')}</SelectItem>\n ) : null}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n {selectedIntegration?.description ? (\n <p className=\"text-sm text-muted-foreground\">{selectedIntegration.description}</p>\n ) : null}\n\n <div className=\"grid gap-4 xl:grid-cols-2\">\n <div className=\"rounded-xl border bg-muted/30 p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <Play className=\"size-4 text-primary\" />\n <h3 className=\"text-sm font-semibold\">{t('data_sync.dashboard.start.runNowTitle', 'Run once now')}</h3>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t('data_sync.dashboard.start.runNowDescription', 'Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.')}\n </p>\n </div>\n <Badge variant=\"outline\">{selectedEntityLabel}</Badge>\n </div>\n\n <Separator className=\"my-4\" />\n\n <div className=\"grid gap-4 sm:grid-cols-[minmax(0,180px)_1fr]\">\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Gauge className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.start.batchSize', 'Batch size')}</span>\n </Label>\n <Input\n value={batchSize}\n onChange={(event) => setBatchSize(event.target.value)}\n inputMode=\"numeric\"\n />\n </div>\n <div className=\"rounded-lg border bg-background p-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"space-y-1\">\n <Label className=\"text-sm font-medium\">{t('data_sync.dashboard.start.fullSync', 'Run as full sync')}</Label>\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.start.fullSyncHelp', 'Ignore the saved cursor and process the entire source again for this run.')}\n </p>\n </div>\n <Switch checked={fullSync} onCheckedChange={setFullSync} />\n </div>\n </div>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3\">\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.start.runNowFootnote', 'Manual runs show progress immediately and land on the run detail page after launch.')}\n </p>\n <Button\n type=\"button\"\n onClick={() => void handleStartSync()}\n disabled={!canStartSelectedIntegration}\n >\n <Play className=\"mr-2 size-4\" />\n {t('data_sync.dashboard.start.submit', 'Start sync')}\n </Button>\n </div>\n </div>\n\n <div className=\"rounded-xl border bg-muted/30 p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <CalendarClock className=\"size-4 text-primary\" />\n <h3 className=\"text-sm font-semibold\">{t('data_sync.dashboard.schedule.title', 'Recurring schedule')}</h3>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {t('data_sync.dashboard.schedule.description', 'Save a repeating schedule for the selected integration, entity, and direction without leaving this dashboard.')}\n </p>\n </div>\n <Badge variant=\"outline\">\n {hasSavedSchedule\n ? (scheduleEditor.isEnabled\n ? t('data_sync.dashboard.schedule.status.shortEnabled', 'Scheduled')\n : t('data_sync.dashboard.schedule.status.shortDisabled', 'Paused'))\n : t('data_sync.dashboard.schedule.status.shortNone', 'One-time only')}\n </Badge>\n </div>\n\n <Separator className=\"my-4\" />\n\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Clock3 className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.schedule.type', 'Schedule type')}</span>\n </Label>\n <Select\n value={scheduleEditor.scheduleType}\n onValueChange={(value) => updateScheduleEditor({\n scheduleType: value === 'cron' ? 'cron' : 'interval',\n })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n >\n <SelectTrigger size=\"lg\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"interval\">{t('data_sync.dashboard.schedule.interval', 'Interval')}</SelectItem>\n <SelectItem value=\"cron\">{t('data_sync.dashboard.schedule.cron', 'Cron')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <CalendarClock className=\"size-4 text-muted-foreground\" />\n <span>\n {scheduleEditor.scheduleType === 'cron'\n ? t('data_sync.dashboard.schedule.cronValue', 'Cron expression')\n : t('data_sync.dashboard.schedule.intervalValue', 'Interval')}\n </span>\n </Label>\n <Input\n value={scheduleEditor.scheduleValue}\n onChange={(event) => updateScheduleEditor({ scheduleValue: event.target.value })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n placeholder={scheduleEditor.scheduleType === 'cron' ? '0 * * * *' : '1h'}\n />\n <p className=\"text-xs text-muted-foreground\">\n {scheduleEditor.scheduleType === 'cron'\n ? t('data_sync.dashboard.schedule.cronHelp', 'Example: `0 * * * *` runs at the start of every hour.')\n : t('data_sync.dashboard.schedule.intervalHelp', 'Example: `1h`, `6h`, or `24h` for repeating intervals.')}\n </p>\n </div>\n <div className=\"space-y-2 sm:col-span-2\">\n <Label className=\"flex items-center gap-2 text-sm font-medium\">\n <Clock3 className=\"size-4 text-muted-foreground\" />\n <span>{t('data_sync.dashboard.schedule.timezone', 'Timezone')}</span>\n </Label>\n <Input\n value={scheduleEditor.timezone}\n onChange={(event) => updateScheduleEditor({ timezone: event.target.value })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n />\n </div>\n </div>\n\n <div className=\"mt-4 grid gap-3\">\n <div className=\"rounded-lg border bg-background p-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"space-y-1\">\n <Label className=\"text-sm font-medium\">{t('data_sync.dashboard.schedule.fullSync', 'Run scheduled jobs as full sync')}</Label>\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.schedule.fullSyncHelp', 'When enabled, every recurring run starts from the beginning instead of the saved cursor.')}\n </p>\n </div>\n <Switch\n checked={scheduleEditor.fullSync}\n onCheckedChange={(checked) => updateScheduleEditor({ fullSync: checked })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n />\n </div>\n </div>\n <div className=\"rounded-lg border bg-background p-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"space-y-1\">\n <Label className=\"text-sm font-medium\">{t('data_sync.dashboard.schedule.enabled', 'Schedule enabled')}</Label>\n <p className=\"text-xs text-muted-foreground\">\n {t('data_sync.dashboard.schedule.enabledHelp', 'Pause the recurring job without deleting the schedule definition.')}\n </p>\n </div>\n <Switch\n checked={scheduleEditor.isEnabled}\n onCheckedChange={(checked) => updateScheduleEditor({ isEnabled: checked })}\n disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}\n />\n </div>\n </div>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3\">\n <div className=\"space-y-1 text-xs text-muted-foreground\">\n <div>\n {hasSavedSchedule\n ? (scheduleEditor.lastRunAt\n ? t('data_sync.dashboard.schedule.lastRun', 'Last scheduled run: {value}', {\n value: new Date(scheduleEditor.lastRunAt).toLocaleString(),\n })\n : t('data_sync.dashboard.schedule.neverRun', 'Saved, but no scheduled execution has completed yet.'))\n : t('data_sync.dashboard.schedule.none', 'No recurring schedule saved for this target yet.')}\n </div>\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => void handleDeleteSchedule()}\n disabled={!hasSavedSchedule || isDeletingSchedule}\n >\n {isDeletingSchedule\n ? t('data_sync.dashboard.schedule.deleting', 'Removing...')\n : t('data_sync.dashboard.schedule.delete', 'Remove schedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => void handleSaveSchedule()}\n disabled={isSavingSchedule || !selectedIntegration || !selectedEntityType}\n >\n <CalendarClock className=\"mr-2 size-4\" />\n {isSavingSchedule\n ? t('data_sync.dashboard.schedule.saving', 'Saving...')\n : t('data_sync.dashboard.schedule.save', 'Save recurring schedule')}\n </Button>\n </div>\n </div>\n </div>\n </div>\n\n {selectedIntegration && !selectedIntegration.isEnabled ? (\n <Alert status=\"warning\">\n <AlertDescription>\n {t('integrations.detail.state.disabled', 'This integration is disabled. Enable it on the integration settings page before starting a sync.')}\n </AlertDescription>\n </Alert>\n ) : null}\n {selectedIntegration && !selectedIntegration.hasCredentials ? (\n <Alert status=\"warning\">\n <AlertDescription>\n {t('integrations.detail.credentials.notConfigured', 'Credentials are not configured yet. Save the integration credentials before starting a sync.')}\n </AlertDescription>\n </Alert>\n ) : null}\n {selectedIntegration && selectedIntegration.canStartRun === false ? (\n <Alert status=\"information\">\n <AlertDescription>\n {t('data_sync.dashboard.start.providerManaged', 'This integration starts sync runs from its own setup flow. Open the integration settings page to continue.')}\n </AlertDescription>\n </Alert>\n ) : null}\n </CardContent>\n </Card>\n\n <DataTable\n stickyActionsColumn\n title={t('data_sync.dashboard.title')}\n columns={columns}\n data={rows}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={handleFiltersApply}\n onFiltersClear={handleFiltersClear}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('data_sync.dashboard.searchPlaceholder')}\n perspective={{ tableId: extensionPoints.hosts.runsTable.tableId }}\n onRowClick={(row) => {\n router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`)\n }}\n rowActions={(row) => (\n <RowActions items={[\n {\n id: 'view',\n label: t('data_sync.dashboard.actions.view'),\n onSelect: () => { router.push(`/backend/data-sync/runs/${encodeURIComponent(row.id)}`) },\n },\n ...(row.status === 'running' ? [{\n id: 'cancel',\n label: t('data_sync.runs.detail.cancel'),\n destructive: true,\n onSelect: () => { void handleCancel(row) },\n }] : []),\n ...(row.status === 'failed' ? [{\n id: 'retry',\n label: t('data_sync.runs.detail.retry'),\n onSelect: () => { void handleRetry(row) },\n }] : []),\n ]} />\n )}\n pagination={{ page, pageSize: 20, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/dictionaries/components/DictionaryTable.tsx"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport type { ColumnDef
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport type { SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { renderDictionaryColor, renderDictionaryIcon } from './dictionaryAppearance'\n\nexport type DictionaryTableEntry = {\n id: string\n value: string\n label: string\n color: string | null\n icon: string | null\n usageCount?: number\n organizationId?: string | null\n tenantId?: string | null\n isInherited?: boolean\n createdAt?: string | null\n updatedAt?: string | null\n}\n\nexport type DictionaryTableTranslations = {\n title: string\n valueColumn: string\n labelColumn: string\n appearanceColumn: string\n addLabel: string\n editLabel: string\n deleteLabel: string\n refreshLabel: string\n inheritedLabel: string\n inheritedTooltip: string\n emptyLabel: string\n usageCountLabel?: string\n searchPlaceholder?: string\n}\n\ntype DictionaryTableProps = {\n entries: DictionaryTableEntry[]\n loading?: boolean\n canManage?: boolean\n onCreate?: () => void\n onEdit?: (entry: DictionaryTableEntry) => void\n onDelete?: (entry: DictionaryTableEntry) => void\n onRefresh?: () => void\n translations: DictionaryTableTranslations\n}\n\nexport function DictionaryTable({\n entries,\n loading = false,\n canManage = false,\n onCreate,\n onEdit,\n onDelete,\n onRefresh,\n translations,\n}: DictionaryTableProps) {\n const [search, setSearch] = React.useState('')\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'value', desc: false }])\n const [page, setPage] = React.useState(1)\n const pageSize = 50\n\n const filtered = React.useMemo(() => {\n if (!search.trim()) return entries\n const term = search.trim().toLowerCase()\n return entries.filter((entry) => {\n return (\n entry.value.toLowerCase().includes(term) ||\n (entry.label ?? '').toLowerCase().includes(term)\n )\n })\n }, [entries, search])\n\n const paginated = React.useMemo(() => {\n const start = (page - 1) * pageSize\n return filtered.slice(start, start + pageSize)\n }, [filtered, page])\n\n const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize))\n\n const columns = React.useMemo<ColumnDef<DictionaryTableEntry>[]>(() => [\n {\n accessorKey: 'value',\n header: translations.valueColumn,\n meta: { priority: 1 },\n cell: ({ getValue, row }) => (\n <div className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{String(getValue())}</span>\n {row.original.isInherited ? (\n <span className=\"rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground\" title={translations.inheritedTooltip}>\n {translations.inheritedLabel}\n </span>\n ) : null}\n </div>\n {typeof row.original.usageCount === 'number' && row.original.usageCount > 0 && translations.usageCountLabel ? (\n <div className=\"text-xs text-muted-foreground\">\n {translations.usageCountLabel.replace('{{count}}', String(row.original.usageCount))}\n </div>\n ) : null}\n </div>\n ),\n },\n {\n accessorKey: 'label',\n header: translations.labelColumn,\n meta: { priority: 2 },\n cell: ({ getValue }) => <span>{String(getValue() ?? '')}</span>,\n },\n {\n id: 'appearance',\n header: translations.appearanceColumn,\n meta: { priority: 3 },\n cell: ({ row }) => {\n const { color, icon } = row.original\n if (!color && !icon) return <span className=\"text-muted-foreground\">\u2014</span>\n return (\n <div className=\"flex items-center gap-2\">\n {color ? renderDictionaryColor(color, 'h-4 w-4 rounded-full border border-border') : null}\n {icon ? renderDictionaryIcon(icon, 'h-4 w-4') : null}\n </div>\n )\n },\n },\n ], [translations.appearanceColumn, translations.inheritedLabel, translations.inheritedTooltip, translations.labelColumn, translations.valueColumn])\n\n const actions = React.useMemo(() => {\n if (!canManage || !onCreate) return null\n return (\n <Button size=\"sm\" onClick={onCreate}>\n {translations.addLabel}\n </Button>\n )\n }, [canManage, onCreate, translations.addLabel])\n\n const handleRowClick = canManage && onEdit\n ? (entry: DictionaryTableEntry) => {\n if (entry.isInherited) return\n onEdit(entry)\n }\n : undefined\n\n return (\n <DataTable<DictionaryTableEntry>\n title={translations.title}\n actions={actions}\n columns={columns}\n data={paginated}\n embedded\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n searchValue={search}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n searchPlaceholder={translations.searchPlaceholder}\n isLoading={loading}\n emptyState={<p className=\"py-10 text-center text-sm text-muted-foreground\">{translations.emptyLabel}</p>}\n pagination={{\n page,\n pageSize,\n total: filtered.length,\n totalPages,\n onPageChange: setPage,\n }}\n refreshButton={onRefresh ? {\n label: translations.refreshLabel,\n onRefresh,\n isRefreshing: loading,\n } : undefined}\n onRowClick={handleRowClick}\n rowActions={\n canManage\n ? (entry) => {\n if (!entry) return null\n if (entry.isInherited) return null\n const items: RowActionItem[] = []\n if (onEdit) {\n items.push({\n id: 'edit',\n label: translations.editLabel,\n onSelect: () => onEdit(entry),\n })\n }\n if (onDelete) {\n items.push({\n id: 'delete',\n label: translations.deleteLabel,\n onSelect: () => onDelete(entry),\n destructive: true,\n })\n }\n return items.length ? <RowActions items={items} /> : null\n }\n : undefined\n }\n />\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/directory/backend/directory/organizations/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/directory/extension-points'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype OrganizationRow = {\n id: string\n name: string\n tenantId: string\n tenantName?: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n rootId: string\n treePath: string\n pathLabel: string\n ancestorIds: string[]\n childIds: string[]\n descendantIds: string[]\n childrenCount: number\n descendantsCount: number\n isActive: boolean\n updatedAt?: string | null\n}\n\ntype OrganizationsResponse = {\n items: OrganizationRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n isSuperAdmin?: boolean\n}\n\nconst TREE_BASE_INDENT = 18\nconst TREE_STEP_INDENT = 14\n\nfunction formatTreeLabel(name: string, depth: number): string {\n if (depth <= 0) return name\n return `${'\\u00A0'.repeat(Math.max(0, (depth - 1) * 2))}\u21B3 ${name}`\n}\n\nfunction computeIndent(depth: number): number {\n if (depth <= 0) return 0\n return TREE_BASE_INDENT + (depth - 1) * TREE_STEP_INDENT\n}\n\nexport default function DirectoryOrganizationsPage() {\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [page, setPage] = React.useState(1)\n const [status, setStatus] = React.useState<string>('all')\n const [search, setSearch] = React.useState('')\n const [canManage, setCanManage] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const call = await apiCall<{ granted?: string[]; ok?: boolean }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['directory.organizations.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result?.granted : []\n setCanManage(call.result?.ok === true || granted.includes('directory.organizations.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('view', 'manage')\n params.set('page', String(page))\n params.set('pageSize', '50')\n params.set('status', status)\n if (status !== 'active') params.set('includeInactive', 'true')\n if (search) params.set('search', search)\n return params.toString()\n }, [page, status, search])\n\n const { data, isLoading } = useQuery<OrganizationsResponse>({\n queryKey: ['directory-organizations', queryParams, scopeVersion],\n queryFn: async () => {\n return readApiResultOrThrow<OrganizationsResponse>(\n `/api/directory/organizations?${queryParams}`,\n undefined,\n { errorMessage: t('directory.organizations.list.error.load', 'Failed to load organizations') },\n )\n },\n })\n\n const rows = data?.items ?? []\n const isSuperAdmin = data?.isSuperAdmin ?? false\n const columns = React.useMemo<ColumnDef<OrganizationRow>[]>(() => {\n const base: ColumnDef<OrganizationRow>[] = [\n {\n accessorKey: 'name',\n header: t('directory.organizations.list.columns.organization', 'Organization'),\n cell: ({ row }) => {\n const depth = row.original.depth ?? 0\n return (\n <div className=\"flex items-center text-sm font-medium leading-none text-foreground\">\n <span\n style={{ marginLeft: computeIndent(depth), whiteSpace: 'pre' }}\n >\n {formatTreeLabel(row.original.name, depth)}\n </span>\n </div>\n )\n },\n meta: { priority: 1 },\n },\n {\n accessorKey: 'pathLabel',\n header: t('directory.organizations.list.columns.path', 'Path'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const value = getValue<string>()\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n },\n {\n accessorKey: 'parentName',\n header: t('directory.organizations.list.columns.parent', 'Parent'),\n meta: { priority: 4 },\n cell: ({ getValue }) => getValue<string>() || t('directory.organizations.common.none', '\u2014'),\n },\n {\n accessorKey: 'childrenCount',\n header: t('directory.organizations.list.columns.children', 'Children'),\n meta: { priority: 5 },\n },\n {\n accessorKey: 'isActive',\n header: t('directory.organizations.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />, \n },\n ]\n if (isSuperAdmin) {\n base.splice(1, 0, {\n accessorKey: 'tenantName',\n header: t('directory.organizations.list.columns.tenant', 'Tenant'),\n meta: { priority: 2 },\n cell: ({ row }) => {\n const value = row.original.tenantName ?? row.original.tenantId\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n })\n }\n return base\n }, [isSuperAdmin, t])\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 0\n\n const deleteMutationContextId = 'directory-organizations-list:single-delete'\n const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: deleteMutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const handleDelete = React.useCallback(async (org: OrganizationRow) => {\n const confirmLabel = t('directory.organizations.list.confirmDelete', 'Archive organization \"{{name}}\"?', { name: org.name })\n const confirmed = await confirm({\n title: t('directory.organizations.list.actions.delete', 'Delete'),\n text: confirmLabel,\n variant: 'destructive',\n })\n if (!confirmed) return\n\n try {\n await runDeleteMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(org.updatedAt),\n () => apiCallOrThrow(\n `/api/directory/organizations?id=${encodeURIComponent(org.id)}`,\n { method: 'DELETE' },\n { errorMessage: t('directory.organizations.list.error.delete', 'Failed to delete organization') },\n ),\n ),\n context: {\n formId: deleteMutationContextId,\n resourceKind: 'directory.organization',\n resourceId: org.id,\n retryLastMutation: retryDeleteMutation,\n },\n })\n await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })\n flash(t('directory.organizations.flash.deleted', 'Organization deleted'), 'success')\n } catch (err: unknown) {\n // A stale delete surfaces the unified conflict bar (via the guarded\n // mutation) \u2014 skip the generic error flash to avoid a double message.\n if (surfaceRecordConflict(err, t)) {\n await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })\n return\n }\n const fallback = t('directory.organizations.list.error.delete', 'Failed to delete organization')\n const message = err instanceof Error ? err.message : fallback\n flash(message, 'error')\n }\n }, [confirm, deleteMutationContextId, queryClient, retryDeleteMutation, runDeleteMutation, t])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('directory.organizations.list.title', 'Organizations')}\n actions={canManage ? (\n <Button asChild>\n <Link href=\"/backend/directory/organizations/create\">\n {t('directory.organizations.list.actions.create', 'Create')}\n </Link>\n </Button>\n ) : undefined}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('directory.organizations.list.searchPlaceholder', 'Search organizations')}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n filters={[\n {\n id: 'status',\n label: t('directory.organizations.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'all', label: t('directory.organizations.list.filters.all', 'All') },\n { value: 'active', label: t('directory.organizations.list.filters.active', 'Active') },\n { value: 'inactive', label: t('directory.organizations.list.filters.inactive', 'Inactive') },\n ],\n },\n ]}\n filterValues={status === 'all' ? {} : { status }}\n onFiltersApply={(vals: FilterValues) => {\n const nextStatus = (vals.status as string) || 'all'\n setStatus(nextStatus)\n setPage(1)\n }}\n onFiltersClear={() => {\n setStatus('all')\n setPage(1)\n }}\n sortable={false}\n perspective={{ tableId: extensionPoints.hosts.organizationsTable.tableId }}\n rowActions={(row) => (\n canManage ? (\n <RowActions\n items={[\n { id: 'edit', label: t('directory.organizations.list.actions.edit', 'Edit'), href: `/backend/directory/organizations/${row.id}/edit` },\n { id: 'delete', label: t('directory.organizations.list.actions.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ]}\n />\n ) : null\n )}\n emptyState={(\n <ListEmptyState\n entityName={t('directory.organizations.list.title', 'Organizations')}\n createHref=\"/backend/directory/organizations/create\"\n createLabel={t('directory.organizations.list.actions.create', 'Create')}\n />\n )}\n pagination={{ page, pageSize: 50, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/directory/extension-points'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype OrganizationRow = {\n id: string\n name: string\n tenantId: string\n tenantName?: string | null\n parentId: string | null\n parentName: string | null\n depth: number\n rootId: string\n treePath: string\n pathLabel: string\n ancestorIds: string[]\n childIds: string[]\n descendantIds: string[]\n childrenCount: number\n descendantsCount: number\n isActive: boolean\n updatedAt?: string | null\n}\n\ntype OrganizationsResponse = {\n items: OrganizationRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n isSuperAdmin?: boolean\n}\n\nconst TREE_BASE_INDENT = 18\nconst TREE_STEP_INDENT = 14\n\nfunction formatTreeLabel(name: string, depth: number): string {\n if (depth <= 0) return name\n return `${'\\u00A0'.repeat(Math.max(0, (depth - 1) * 2))}\u21B3 ${name}`\n}\n\nfunction computeIndent(depth: number): number {\n if (depth <= 0) return 0\n return TREE_BASE_INDENT + (depth - 1) * TREE_STEP_INDENT\n}\n\nexport default function DirectoryOrganizationsPage() {\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [page, setPage] = React.useState(1)\n const [status, setStatus] = React.useState<string>('all')\n const [search, setSearch] = React.useState('')\n const [canManage, setCanManage] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n try {\n const call = await apiCall<{ granted?: string[]; ok?: boolean }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['directory.organizations.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result?.granted : []\n setCanManage(call.result?.ok === true || granted.includes('directory.organizations.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('view', 'manage')\n params.set('page', String(page))\n params.set('pageSize', '50')\n params.set('status', status)\n if (status !== 'active') params.set('includeInactive', 'true')\n if (search) params.set('search', search)\n return params.toString()\n }, [page, status, search])\n\n const { data, isLoading } = useQuery<OrganizationsResponse>({\n queryKey: ['directory-organizations', queryParams, scopeVersion],\n queryFn: async () => {\n return readApiResultOrThrow<OrganizationsResponse>(\n `/api/directory/organizations?${queryParams}`,\n undefined,\n { errorMessage: t('directory.organizations.list.error.load', 'Failed to load organizations') },\n )\n },\n })\n\n const rows = data?.items ?? []\n const isSuperAdmin = data?.isSuperAdmin ?? false\n const columns = React.useMemo<ColumnDef<OrganizationRow>[]>(() => {\n const base: ColumnDef<OrganizationRow>[] = [\n {\n accessorKey: 'name',\n header: t('directory.organizations.list.columns.organization', 'Organization'),\n cell: ({ row }) => {\n const depth = row.original.depth ?? 0\n return (\n <div className=\"flex items-center text-sm font-medium leading-none text-foreground\">\n <span\n style={{ marginLeft: computeIndent(depth), whiteSpace: 'pre' }}\n >\n {formatTreeLabel(row.original.name, depth)}\n </span>\n </div>\n )\n },\n meta: { priority: 1 },\n },\n {\n accessorKey: 'pathLabel',\n header: t('directory.organizations.list.columns.path', 'Path'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const value = getValue<string>()\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n },\n {\n accessorKey: 'parentName',\n header: t('directory.organizations.list.columns.parent', 'Parent'),\n meta: { priority: 4 },\n cell: ({ getValue }) => getValue<string>() || t('directory.organizations.common.none', '\u2014'),\n },\n {\n accessorKey: 'childrenCount',\n header: t('directory.organizations.list.columns.children', 'Children'),\n meta: { priority: 5 },\n },\n {\n accessorKey: 'isActive',\n header: t('directory.organizations.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />, \n },\n ]\n if (isSuperAdmin) {\n base.splice(1, 0, {\n accessorKey: 'tenantName',\n header: t('directory.organizations.list.columns.tenant', 'Tenant'),\n meta: { priority: 2 },\n cell: ({ row }) => {\n const value = row.original.tenantName ?? row.original.tenantId\n return <span className=\"text-xs text-muted-foreground\">{value}</span>\n },\n })\n }\n return base\n }, [isSuperAdmin, t])\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 0\n\n const deleteMutationContextId = 'directory-organizations-list:single-delete'\n const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: deleteMutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const handleDelete = React.useCallback(async (org: OrganizationRow) => {\n const confirmLabel = t('directory.organizations.list.confirmDelete', 'Archive organization \"{{name}}\"?', { name: org.name })\n const confirmed = await confirm({\n title: t('directory.organizations.list.actions.delete', 'Delete'),\n text: confirmLabel,\n variant: 'destructive',\n })\n if (!confirmed) return\n\n try {\n await runDeleteMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(org.updatedAt),\n () => apiCallOrThrow(\n `/api/directory/organizations?id=${encodeURIComponent(org.id)}`,\n { method: 'DELETE' },\n { errorMessage: t('directory.organizations.list.error.delete', 'Failed to delete organization') },\n ),\n ),\n context: {\n formId: deleteMutationContextId,\n resourceKind: 'directory.organization',\n resourceId: org.id,\n retryLastMutation: retryDeleteMutation,\n },\n })\n await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })\n flash(t('directory.organizations.flash.deleted', 'Organization deleted'), 'success')\n } catch (err: unknown) {\n // A stale delete surfaces the unified conflict bar (via the guarded\n // mutation) \u2014 skip the generic error flash to avoid a double message.\n if (surfaceRecordConflict(err, t)) {\n await queryClient.invalidateQueries({ queryKey: ['directory-organizations'] })\n return\n }\n const fallback = t('directory.organizations.list.error.delete', 'Failed to delete organization')\n const message = err instanceof Error ? err.message : fallback\n flash(message, 'error')\n }\n }, [confirm, deleteMutationContextId, queryClient, retryDeleteMutation, runDeleteMutation, t])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('directory.organizations.list.title', 'Organizations')}\n actions={canManage ? (\n <Button asChild>\n <Link href=\"/backend/directory/organizations/create\">\n {t('directory.organizations.list.actions.create', 'Create')}\n </Link>\n </Button>\n ) : undefined}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('directory.organizations.list.searchPlaceholder', 'Search organizations')}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n filters={[\n {\n id: 'status',\n label: t('directory.organizations.list.filters.status', 'Status'),\n type: 'select',\n options: [\n { value: 'all', label: t('directory.organizations.list.filters.all', 'All') },\n { value: 'active', label: t('directory.organizations.list.filters.active', 'Active') },\n { value: 'inactive', label: t('directory.organizations.list.filters.inactive', 'Inactive') },\n ],\n },\n ]}\n filterValues={status === 'all' ? {} : { status }}\n onFiltersApply={(vals: FilterValues) => {\n const nextStatus = (vals.status as string) || 'all'\n setStatus(nextStatus)\n setPage(1)\n }}\n onFiltersClear={() => {\n setStatus('all')\n setPage(1)\n }}\n sortable={false}\n perspective={{ tableId: extensionPoints.hosts.organizationsTable.tableId }}\n rowActions={(row) => (\n canManage ? (\n <RowActions\n items={[\n { id: 'edit', label: t('directory.organizations.list.actions.edit', 'Edit'), href: `/backend/directory/organizations/${row.id}/edit` },\n { id: 'delete', label: t('directory.organizations.list.actions.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ]}\n />\n ) : null\n )}\n emptyState={(\n <ListEmptyState\n entityName={t('directory.organizations.list.title', 'Organizations')}\n createHref=\"/backend/directory/organizations/create\"\n createLabel={t('directory.organizations.list.actions.create', 'Create')}\n />\n )}\n pagination={{ page, pageSize: 50, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/directory/backend/directory/tenants/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/directory/extension-points'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { extensionPoints } from '@open-mercato/core/modules/directory/extension-points'\nimport Link from 'next/link'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\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 } from '@open-mercato/ui/backend/DataTable'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport type { FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCall, apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype TenantRow = {\n id: string\n name: string\n isActive: boolean\n createdAt: string | null\n updatedAt: string | null\n}\n\ntype TenantsResponse = {\n items: TenantRow[]\n total: number\n page: number\n pageSize: number\n totalPages: number\n}\n\n\nexport default function DirectoryTenantsPage() {\n const queryClient = useQueryClient()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'name', desc: false }])\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [filters, setFilters] = React.useState<FilterValues>({})\n const [canManage, setCanManage] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const columns = React.useMemo<ColumnDef<TenantRow>[]>(() => [\n { accessorKey: 'name', header: t('directory.tenants.list.columns.tenant', 'Tenant'), meta: { priority: 1 } },\n {\n accessorKey: 'isActive',\n header: t('directory.tenants.list.columns.active', 'Active'),\n enableSorting: false,\n meta: { priority: 2 },\n cell: ({ getValue }) => <BooleanIcon value={Boolean(getValue())} />,\n },\n {\n accessorKey: 'createdAt',\n header: t('directory.tenants.list.columns.created', 'Created'),\n meta: { priority: 3 },\n cell: ({ getValue }) => {\n const timestamp = getValue() as string | null\n if (!timestamp) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n const date = new Date(timestamp)\n if (Number.isNaN(date.getTime())) return <span className=\"text-xs text-muted-foreground\">\u2014</span>\n return <span>{date.toLocaleString()}</span>\n },\n },\n ], [t])\n\n React.useEffect(() => {\n let cancelled = false\n async function loadFeature() {\n try {\n const call = await apiCall<{ ok?: boolean; granted?: string[] }>('/api/auth/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features: ['directory.tenants.manage'] }),\n })\n if (!cancelled) {\n const granted = Array.isArray(call.result?.granted) ? call.result!.granted! : []\n setCanManage(call.result?.ok === true || granted.includes('directory.tenants.manage'))\n }\n } catch {\n if (!cancelled) setCanManage(false)\n }\n }\n loadFeature()\n return () => { cancelled = true }\n }, [])\n\n const queryParams = React.useMemo(() => {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '20')\n if (sorting.length > 0) {\n params.set('sortField', sorting[0]?.id || 'name')\n params.set('sortDir', sorting[0]?.desc ? 'desc' : 'asc')\n }\n if (search) params.set('search', search)\n if (filters.active !== undefined && filters.active !== '') params.set('isActive', String(filters.active))\n return params.toString()\n }, [page, sorting, search, filters])\n\n const { data, isLoading } = useQuery({\n queryKey: ['directory-tenants', queryParams, scopeVersion],\n queryFn: async (): Promise<TenantsResponse> => {\n return readApiResultOrThrow<TenantsResponse>(\n `/api/directory/tenants?${queryParams}`,\n undefined,\n { errorMessage: t('directory.tenants.list.error.load', 'Failed to load tenants') },\n )\n },\n })\n\n const rows = data?.items ?? []\n const total = data?.total ?? 0\n const totalPages = data?.totalPages ?? 0\n\n const deleteMutationContextId = 'directory-tenants-list:single-delete'\n const { runMutation: runDeleteMutation, retryLastMutation: retryDeleteMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: deleteMutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const handleDelete = React.useCallback(async (tenant: TenantRow) => {\n const confirmMessage = t('directory.tenants.list.confirmDelete', 'Delete tenant \"{{name}}\"? This will archive it.').replace('{{name}}', tenant.name)\n const confirmed = await confirm({\n title: t('common.delete', 'Delete'),\n text: confirmMessage,\n variant: 'destructive',\n })\n if (!confirmed) return\n\n try {\n await runDeleteMutation({\n operation: () => withScopedApiRequestHeaders(\n buildOptimisticLockHeader(tenant.updatedAt),\n () => apiCallOrThrow(\n `/api/directory/tenants?id=${encodeURIComponent(tenant.id)}`,\n { method: 'DELETE' },\n { errorMessage: t('directory.tenants.list.error.delete', 'Failed to delete tenant') },\n ),\n ),\n context: {\n formId: deleteMutationContextId,\n resourceKind: 'directory.tenant',\n resourceId: tenant.id,\n retryLastMutation: retryDeleteMutation,\n },\n })\n await queryClient.invalidateQueries({ queryKey: ['directory-tenants'] })\n flash(t('directory.tenants.list.success.delete', 'Tenant deleted'), 'success')\n } catch (err: unknown) {\n // A stale delete surfaces the unified conflict bar (via the guarded\n // mutation) \u2014 skip the generic error flash to avoid a double message.\n if (surfaceRecordConflict(err, t)) {\n await queryClient.invalidateQueries({ queryKey: ['directory-tenants'] })\n return\n }\n const message = err instanceof Error ? err.message : t('directory.tenants.list.error.delete', 'Failed to delete tenant')\n flash(message, 'error')\n }\n }, [confirm, deleteMutationContextId, queryClient, retryDeleteMutation, runDeleteMutation, t])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('directory.tenants.list.title', 'Tenants')}\n actions={canManage ? (\n <Button asChild>\n <Link href=\"/backend/directory/tenants/create\">{t('directory.tenants.list.actions.create', 'Create')}</Link>\n </Button>\n ) : undefined}\n columns={columns}\n data={rows}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n filters={[{ id: 'active', label: t('directory.tenants.list.filters.status', 'Status'), type: 'select', options: [\n { value: 'true', label: t('directory.tenants.list.filters.active', 'Active') },\n { value: 'false', label: t('directory.tenants.list.filters.inactive', 'Inactive') },\n ] }]}\n filterValues={filters}\n onFiltersApply={(vals) => { setFilters(vals); setPage(1) }}\n onFiltersClear={() => { setFilters({}); setPage(1) }}\n sortable\n sorting={sorting}\n onSortingChange={(state) => { setSorting(state); setPage(1) }}\n perspective={{ tableId: extensionPoints.hosts.tenantsTable.tableId }}\n rowActions={(row) => (\n canManage ? (\n <RowActions\n items={[\n { id: 'edit', label: t('common.edit', 'Edit'), href: `/backend/directory/tenants/${row.id}/edit` },\n { id: 'delete', label: t('common.delete', 'Delete'), destructive: true, onSelect: () => handleDelete(row) },\n ]}\n />\n ) : null\n )}\n emptyState={(\n <ListEmptyState\n entityName={t('directory.tenants.list.title', 'Tenants')}\n createHref=\"/backend/directory/tenants/create\"\n createLabel={t('directory.tenants.list.actions.create', 'Create')}\n />\n )}\n pagination={{ page, pageSize: 20, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|