@open-mercato/core 0.6.2-develop.3330.1.8ad2b3b84c → 0.6.2-develop.3366.1.4692954e44

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.
Files changed (102) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/sync_excel_upload/index.js +37 -0
  3. package/dist/generated/entities/sync_excel_upload/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +4 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +19 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/customers/search.js +3 -2
  9. package/dist/modules/customers/search.js.map +2 -2
  10. package/dist/modules/data_sync/api/runs/[id]/cancel.js +16 -0
  11. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  12. package/dist/modules/data_sync/di.js +2 -1
  13. package/dist/modules/data_sync/di.js.map +2 -2
  14. package/dist/modules/data_sync/lib/sync-engine.js +178 -54
  15. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  16. package/dist/modules/data_sync/lib/sync-run-service.js +23 -0
  17. package/dist/modules/data_sync/lib/sync-run-service.js.map +3 -3
  18. package/dist/modules/integrations/backend/integrations/[id]/page.js +241 -55
  19. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +3 -3
  20. package/dist/modules/sync_excel/acl.js +10 -0
  21. package/dist/modules/sync_excel/acl.js.map +7 -0
  22. package/dist/modules/sync_excel/api/import/route.js +164 -0
  23. package/dist/modules/sync_excel/api/import/route.js.map +7 -0
  24. package/dist/modules/sync_excel/api/preview/route.js +84 -0
  25. package/dist/modules/sync_excel/api/preview/route.js.map +7 -0
  26. package/dist/modules/sync_excel/api/upload/route.js +131 -0
  27. package/dist/modules/sync_excel/api/upload/route.js.map +7 -0
  28. package/dist/modules/sync_excel/data/entities.js +79 -0
  29. package/dist/modules/sync_excel/data/entities.js.map +7 -0
  30. package/dist/modules/sync_excel/data/validators.js +58 -0
  31. package/dist/modules/sync_excel/data/validators.js.map +7 -0
  32. package/dist/modules/sync_excel/di.js +11 -0
  33. package/dist/modules/sync_excel/di.js.map +7 -0
  34. package/dist/modules/sync_excel/index.js +11 -0
  35. package/dist/modules/sync_excel/index.js.map +7 -0
  36. package/dist/modules/sync_excel/integration.js +36 -0
  37. package/dist/modules/sync_excel/integration.js.map +7 -0
  38. package/dist/modules/sync_excel/lib/adapters/customers.js +852 -0
  39. package/dist/modules/sync_excel/lib/adapters/customers.js.map +7 -0
  40. package/dist/modules/sync_excel/lib/column-detector.js +77 -0
  41. package/dist/modules/sync_excel/lib/column-detector.js.map +7 -0
  42. package/dist/modules/sync_excel/lib/mapping.js +16 -0
  43. package/dist/modules/sync_excel/lib/mapping.js.map +7 -0
  44. package/dist/modules/sync_excel/lib/parser.js +125 -0
  45. package/dist/modules/sync_excel/lib/parser.js.map +7 -0
  46. package/dist/modules/sync_excel/lib/scope.js +44 -0
  47. package/dist/modules/sync_excel/lib/scope.js.map +7 -0
  48. package/dist/modules/sync_excel/lib/upload-storage.js +63 -0
  49. package/dist/modules/sync_excel/lib/upload-storage.js.map +7 -0
  50. package/dist/modules/sync_excel/migrations/Migration20260329181120_sync_excel.js +11 -0
  51. package/dist/modules/sync_excel/migrations/Migration20260329181120_sync_excel.js.map +7 -0
  52. package/dist/modules/sync_excel/setup.js +19 -0
  53. package/dist/modules/sync_excel/setup.js.map +7 -0
  54. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js +293 -0
  55. package/dist/modules/sync_excel/widgets/injection/upload-config/target-options.js.map +7 -0
  56. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js +934 -0
  57. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.client.js.map +7 -0
  58. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.js +15 -0
  59. package/dist/modules/sync_excel/widgets/injection/upload-config/widget.js.map +7 -0
  60. package/dist/modules/sync_excel/widgets/injection-table.js +17 -0
  61. package/dist/modules/sync_excel/widgets/injection-table.js.map +7 -0
  62. package/generated/entities/sync_excel_upload/index.ts +17 -0
  63. package/generated/entities.ids.generated.ts +4 -0
  64. package/generated/entity-fields-registry.ts +19 -0
  65. package/package.json +7 -7
  66. package/src/modules/customers/search.ts +3 -2
  67. package/src/modules/data_sync/api/runs/[id]/cancel.ts +18 -0
  68. package/src/modules/data_sync/di.ts +4 -1
  69. package/src/modules/data_sync/lib/adapter.ts +15 -0
  70. package/src/modules/data_sync/lib/sync-engine.ts +205 -52
  71. package/src/modules/data_sync/lib/sync-run-service.ts +24 -0
  72. package/src/modules/integrations/backend/integrations/[id]/page.tsx +220 -15
  73. package/src/modules/integrations/i18n/de.json +9 -0
  74. package/src/modules/integrations/i18n/en.json +9 -0
  75. package/src/modules/integrations/i18n/es.json +9 -0
  76. package/src/modules/integrations/i18n/pl.json +9 -0
  77. package/src/modules/sync_excel/acl.ts +6 -0
  78. package/src/modules/sync_excel/api/import/route.ts +182 -0
  79. package/src/modules/sync_excel/api/preview/route.ts +89 -0
  80. package/src/modules/sync_excel/api/upload/route.ts +144 -0
  81. package/src/modules/sync_excel/data/entities.ts +61 -0
  82. package/src/modules/sync_excel/data/validators.ts +58 -0
  83. package/src/modules/sync_excel/di.ts +9 -0
  84. package/src/modules/sync_excel/i18n/de.json +107 -0
  85. package/src/modules/sync_excel/i18n/en.json +107 -0
  86. package/src/modules/sync_excel/i18n/es.json +107 -0
  87. package/src/modules/sync_excel/i18n/pl.json +107 -0
  88. package/src/modules/sync_excel/index.ts +7 -0
  89. package/src/modules/sync_excel/integration.ts +31 -0
  90. package/src/modules/sync_excel/lib/adapters/customers.ts +1097 -0
  91. package/src/modules/sync_excel/lib/column-detector.ts +91 -0
  92. package/src/modules/sync_excel/lib/mapping.ts +19 -0
  93. package/src/modules/sync_excel/lib/parser.ts +165 -0
  94. package/src/modules/sync_excel/lib/scope.ts +63 -0
  95. package/src/modules/sync_excel/lib/upload-storage.ts +79 -0
  96. package/src/modules/sync_excel/migrations/.snapshot-open-mercato.json +199 -0
  97. package/src/modules/sync_excel/migrations/Migration20260329181120_sync_excel.ts +10 -0
  98. package/src/modules/sync_excel/setup.ts +20 -0
  99. package/src/modules/sync_excel/widgets/injection/upload-config/target-options.ts +351 -0
  100. package/src/modules/sync_excel/widgets/injection/upload-config/widget.client.tsx +1224 -0
  101. package/src/modules/sync_excel/widgets/injection/upload-config/widget.ts +30 -0
  102. package/src/modules/sync_excel/widgets/injection-table.ts +15 -0
@@ -0,0 +1,934 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { cn } from "@open-mercato/shared/lib/utils";
7
+ import { flash } from "@open-mercato/ui/backend/FlashMessages";
8
+ import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
9
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
10
+ import { useCustomFieldDefs } from "@open-mercato/ui/backend/utils/customFieldDefs";
11
+ import { Alert, AlertDescription, AlertTitle } from "@open-mercato/ui/primitives/alert";
12
+ import { Badge } from "@open-mercato/ui/primitives/badge";
13
+ import { Button } from "@open-mercato/ui/primitives/button";
14
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@open-mercato/ui/primitives/card";
15
+ import { Input } from "@open-mercato/ui/primitives/input";
16
+ import { Label } from "@open-mercato/ui/primitives/label";
17
+ import { Progress } from "@open-mercato/ui/primitives/progress";
18
+ import { Spinner } from "@open-mercato/ui/primitives/spinner";
19
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@open-mercato/ui/primitives/table";
20
+ import { AlertTriangle, CheckCircle2, Play, RefreshCw, Upload, XCircle } from "lucide-react";
21
+ import {
22
+ buildPeopleSuggestedMapping,
23
+ buildPeopleTargetOptions,
24
+ buildSuggestedMappingSignature,
25
+ findMappingTargetOption,
26
+ SYNC_EXCEL_PEOPLE_CUSTOM_FIELD_ENTITY_IDS
27
+ } from "./target-options.js";
28
+ const ENTITY_TYPE = "customers.person";
29
+ const SESSION_STORAGE_PREFIX = "om:sync_excel:session";
30
+ const RUN_POLL_INTERVAL_MS = 4e3;
31
+ const SELECT_CLASS_NAME = "flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50";
32
+ function buildMappingRows(headers, suggestedMapping) {
33
+ const mappedFields = new Map(suggestedMapping.fields.map((field) => [field.externalField, field.localField]));
34
+ return headers.map((header) => ({
35
+ sourceColumn: header,
36
+ targetField: mappedFields.get(header) ?? ""
37
+ }));
38
+ }
39
+ function buildFieldMapping(sourceColumn, targetField, targetOptions) {
40
+ const matchedOption = findMappingTargetOption(targetOptions, targetField);
41
+ const mappingKind = matchedOption?.mappingKind ?? (targetField.startsWith("cf:") ? "custom_field" : "core");
42
+ const dedupeRole = matchedOption?.dedupeRole;
43
+ return {
44
+ externalField: sourceColumn,
45
+ localField: targetField,
46
+ mappingKind,
47
+ ...dedupeRole ? { dedupeRole } : {}
48
+ };
49
+ }
50
+ function inferMatchStrategy(rows) {
51
+ if (rows.some((row) => row.targetField === "person.externalId")) return "externalId";
52
+ if (rows.some((row) => row.targetField === "person.primaryEmail")) return "email";
53
+ return "custom";
54
+ }
55
+ function buildMapping(rows, matchStrategy, targetOptions) {
56
+ const mappedRows = rows.filter((row) => row.targetField.trim().length > 0);
57
+ const fields = mappedRows.map((row) => buildFieldMapping(row.sourceColumn, row.targetField, targetOptions));
58
+ const resolvedMatchStrategy = matchStrategy === "custom" ? inferMatchStrategy(rows) : matchStrategy;
59
+ const matchField = resolvedMatchStrategy === "externalId" ? "person.externalId" : resolvedMatchStrategy === "email" ? "person.primaryEmail" : void 0;
60
+ return {
61
+ entityType: ENTITY_TYPE,
62
+ matchStrategy: resolvedMatchStrategy,
63
+ ...matchField ? { matchField } : {},
64
+ fields,
65
+ unmappedColumns: rows.filter((row) => row.targetField.trim().length === 0).map((row) => row.sourceColumn)
66
+ };
67
+ }
68
+ function buildDiagnostics(rows) {
69
+ const duplicateTargets = Array.from(
70
+ rows.filter((row) => row.targetField.trim().length > 0).reduce((accumulator, row) => {
71
+ const nextCount = (accumulator.get(row.targetField) ?? 0) + 1;
72
+ accumulator.set(row.targetField, nextCount);
73
+ return accumulator;
74
+ }, /* @__PURE__ */ new Map()).entries()
75
+ ).filter(([, count]) => count > 1).map(([target]) => target);
76
+ const hasNameTarget = rows.some((row) => row.targetField === "person.lastName" || row.targetField === "person.displayName");
77
+ const hasIdentityTarget = rows.some((row) => row.targetField === "person.externalId" || row.targetField === "person.primaryEmail");
78
+ const mappedCount = rows.filter((row) => row.targetField.trim().length > 0).length;
79
+ return {
80
+ duplicateTargets,
81
+ hasNameTarget,
82
+ hasIdentityTarget,
83
+ mappedCount,
84
+ unmappedCount: rows.length - mappedCount
85
+ };
86
+ }
87
+ function formatTargetLabel(targetField, t, targetOptions) {
88
+ const option = findMappingTargetOption(targetOptions, targetField);
89
+ if (!option) return targetField;
90
+ return option.labelKey ? t(option.labelKey, option.fallback) : option.fallback;
91
+ }
92
+ function getMatchStrategyLabel(matchStrategy, t) {
93
+ if (matchStrategy === "externalId") {
94
+ return t("sync_excel.widget.matchStrategy.externalId", "Source record ID (Recommended)");
95
+ }
96
+ if (matchStrategy === "email") {
97
+ return t("sync_excel.widget.matchStrategy.email", "Email address");
98
+ }
99
+ return t("sync_excel.widget.matchStrategy.custom", "Custom matching");
100
+ }
101
+ function formatFileSize(bytes) {
102
+ if (bytes < 1024) return `${bytes} B`;
103
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
104
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
105
+ }
106
+ function readApiErrorMessage(result, fallback) {
107
+ const value = result?.error;
108
+ if (typeof value === "string" && value.trim().length > 0) return value;
109
+ return fallback;
110
+ }
111
+ function normalizeStatus(status) {
112
+ if (status === "pending" || status === "running" || status === "completed" || status === "failed" || status === "cancelled") {
113
+ return status;
114
+ }
115
+ return "idle";
116
+ }
117
+ function getSessionStorageKey(integrationId) {
118
+ return `${SESSION_STORAGE_PREFIX}:${integrationId ?? "sync_excel"}`;
119
+ }
120
+ function readPersistedSessionSnapshot(integrationId) {
121
+ if (typeof window === "undefined") return null;
122
+ try {
123
+ const raw = window.sessionStorage.getItem(getSessionStorageKey(integrationId));
124
+ if (!raw) return null;
125
+ const parsed = JSON.parse(raw);
126
+ if (!parsed || typeof parsed !== "object") return null;
127
+ if (typeof parsed.uploadId !== "string" || parsed.uploadId.trim().length === 0) return null;
128
+ if (typeof parsed.filename !== "string") return null;
129
+ if (!Array.isArray(parsed.mappingRows)) return null;
130
+ if (parsed.matchStrategy !== "externalId" && parsed.matchStrategy !== "email" && parsed.matchStrategy !== "custom") {
131
+ return null;
132
+ }
133
+ const preview = parsed.preview && typeof parsed.preview === "object" && typeof parsed.preview.uploadId === "string" && parsed.preview.uploadId.trim().length > 0 && typeof parsed.preview.filename === "string" && Array.isArray(parsed.preview.headers) && Array.isArray(parsed.preview.sampleRows) && typeof parsed.preview.totalRows === "number" && typeof parsed.preview.fileSize === "number" && parsed.preview.entityType === ENTITY_TYPE && parsed.preview.suggestedMapping ? parsed.preview : null;
134
+ return {
135
+ uploadId: parsed.uploadId,
136
+ filename: parsed.filename,
137
+ preview,
138
+ mappingRows: parsed.mappingRows.filter((row) => Boolean(row) && typeof row.sourceColumn === "string" && typeof row.targetField === "string"),
139
+ matchStrategy: parsed.matchStrategy,
140
+ runId: typeof parsed.runId === "string" && parsed.runId.trim().length > 0 ? parsed.runId : null,
141
+ progressJobId: typeof parsed.progressJobId === "string" && parsed.progressJobId.trim().length > 0 ? parsed.progressJobId : null
142
+ };
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ function writePersistedSessionSnapshot(integrationId, snapshot) {
148
+ if (typeof window === "undefined") return;
149
+ try {
150
+ const key = getSessionStorageKey(integrationId);
151
+ if (!snapshot) {
152
+ window.sessionStorage.removeItem(key);
153
+ return;
154
+ }
155
+ window.sessionStorage.setItem(key, JSON.stringify(snapshot));
156
+ } catch {
157
+ }
158
+ }
159
+ function SyncExcelUploadConfigWidget({
160
+ context,
161
+ data
162
+ }) {
163
+ const pathname = usePathname();
164
+ const router = useRouter();
165
+ const searchParams = useSearchParams();
166
+ const t = useT();
167
+ const { data: customFieldDefs = [] } = useCustomFieldDefs([...SYNC_EXCEL_PEOPLE_CUSTOM_FIELD_ENTITY_IDS]);
168
+ const [selectedFile, setSelectedFile] = React.useState(null);
169
+ const [preview, setPreview] = React.useState(null);
170
+ const [mappingRows, setMappingRows] = React.useState([]);
171
+ const [matchStrategy, setMatchStrategy] = React.useState("custom");
172
+ const [isMappingDirty, setIsMappingDirty] = React.useState(false);
173
+ const [runDetail, setRunDetail] = React.useState(null);
174
+ const [runId, setRunId] = React.useState(null);
175
+ const [progressJobId, setProgressJobId] = React.useState(null);
176
+ const [isUploading, setIsUploading] = React.useState(false);
177
+ const [isRefreshingPreview, setIsRefreshingPreview] = React.useState(false);
178
+ const [isStartingImport, setIsStartingImport] = React.useState(false);
179
+ const [isCancelling, setIsCancelling] = React.useState(false);
180
+ const [isRestoringSession, setIsRestoringSession] = React.useState(true);
181
+ const mutationContextId = `${context.formId ?? "sync-excel"}:sync-excel-import`;
182
+ const lastAppliedSuggestionSignatureRef = React.useRef(null);
183
+ const restoringSnapshotRef = React.useRef(null);
184
+ const lastRestoreRequestKeyRef = React.useRef(null);
185
+ const { runMutation } = useGuardedMutation({
186
+ contextId: mutationContextId,
187
+ spotId: context.integrationDetailWidgetSpotId
188
+ });
189
+ const targetOptions = React.useMemo(
190
+ () => buildPeopleTargetOptions(customFieldDefs),
191
+ [customFieldDefs]
192
+ );
193
+ const previewSuggestion = React.useMemo(
194
+ () => preview ? buildPeopleSuggestedMapping(preview.headers, preview.suggestedMapping, customFieldDefs) : null,
195
+ [customFieldDefs, preview]
196
+ );
197
+ const resolvedIntegrationEnabled = data?.state?.isEnabled ?? context.state?.isEnabled ?? true;
198
+ const diagnostics = React.useMemo(() => buildDiagnostics(mappingRows), [mappingRows]);
199
+ const canStartImport = Boolean(preview) && diagnostics.duplicateTargets.length === 0 && diagnostics.hasNameTarget && diagnostics.hasIdentityTarget && mappingRows.some((row) => row.targetField.trim().length > 0);
200
+ const importStatus = normalizeStatus(runDetail?.status ?? (runId ? "pending" : null));
201
+ const progressValue = runDetail?.progressJob?.progressPercent ?? 0;
202
+ const isRunActive = importStatus === "pending" || importStatus === "running";
203
+ const searchParamsKey = searchParams?.toString() ?? "";
204
+ const uploadIdFromUrl = searchParams?.get("uploadId") ?? null;
205
+ const runIdFromUrl = searchParams?.get("runId") ?? null;
206
+ const shouldHideInteractiveContent = isRestoringSession && Boolean(uploadIdFromUrl);
207
+ const hasExistingRunForCurrentUpload = Boolean(preview?.uploadId && runId);
208
+ const hasSafeDedupeStrategy = matchStrategy === "externalId" || matchStrategy === "email";
209
+ const replaceQueryParams = React.useCallback((updates) => {
210
+ const params = new URLSearchParams(searchParamsKey);
211
+ for (const [key, value] of Object.entries(updates)) {
212
+ if (typeof value === "string" && value.trim().length > 0) {
213
+ params.set(key, value);
214
+ } else {
215
+ params.delete(key);
216
+ }
217
+ }
218
+ const query = params.toString();
219
+ if (query === searchParamsKey) return;
220
+ router.replace(query ? `${pathname}?${query}` : pathname);
221
+ }, [pathname, router, searchParamsKey]);
222
+ const clearPersistedSession = React.useCallback(() => {
223
+ writePersistedSessionSnapshot(context.integrationId, null);
224
+ replaceQueryParams({ uploadId: null, runId: null });
225
+ }, [context.integrationId, replaceQueryParams]);
226
+ const persistCurrentSession = React.useCallback((next) => {
227
+ const persistedPreview = next?.preview ?? preview ?? null;
228
+ const uploadId = next?.uploadId ?? persistedPreview?.uploadId ?? null;
229
+ const filename = next?.filename ?? persistedPreview?.filename ?? null;
230
+ if (!uploadId || !filename) {
231
+ writePersistedSessionSnapshot(context.integrationId, null);
232
+ return;
233
+ }
234
+ writePersistedSessionSnapshot(context.integrationId, {
235
+ uploadId,
236
+ filename,
237
+ preview: persistedPreview,
238
+ mappingRows: next?.mappingRows ?? mappingRows,
239
+ matchStrategy: next?.matchStrategy ?? matchStrategy,
240
+ runId: next?.runId ?? runId,
241
+ progressJobId: next?.progressJobId ?? progressJobId
242
+ });
243
+ }, [context.integrationId, mappingRows, matchStrategy, preview, progressJobId, runId]);
244
+ const syncPreviewState = React.useCallback((nextPreview, options) => {
245
+ const nextSuggestedMapping = buildPeopleSuggestedMapping(nextPreview.headers, nextPreview.suggestedMapping, customFieldDefs);
246
+ setPreview(nextPreview);
247
+ const restoredSnapshot = options?.restoredSnapshot;
248
+ const shouldRestoreSnapshot = Boolean(
249
+ restoredSnapshot && restoredSnapshot.uploadId === nextPreview.uploadId && restoredSnapshot.mappingRows.length > 0
250
+ );
251
+ if (shouldRestoreSnapshot) {
252
+ setMappingRows(restoredSnapshot.mappingRows);
253
+ setMatchStrategy(restoredSnapshot.matchStrategy);
254
+ setRunId(restoredSnapshot.runId);
255
+ setProgressJobId(restoredSnapshot.progressJobId);
256
+ setIsMappingDirty(true);
257
+ lastAppliedSuggestionSignatureRef.current = null;
258
+ persistCurrentSession({
259
+ uploadId: nextPreview.uploadId,
260
+ filename: nextPreview.filename,
261
+ preview: nextPreview,
262
+ mappingRows: restoredSnapshot.mappingRows,
263
+ matchStrategy: restoredSnapshot.matchStrategy,
264
+ runId: restoredSnapshot.runId,
265
+ progressJobId: restoredSnapshot.progressJobId
266
+ });
267
+ replaceQueryParams({
268
+ uploadId: nextPreview.uploadId,
269
+ runId: restoredSnapshot.runId
270
+ });
271
+ return;
272
+ }
273
+ if (options?.preserveManualState && preview?.uploadId === nextPreview.uploadId) {
274
+ persistCurrentSession({
275
+ uploadId: nextPreview.uploadId,
276
+ filename: nextPreview.filename,
277
+ preview: nextPreview
278
+ });
279
+ replaceQueryParams({ uploadId: nextPreview.uploadId });
280
+ return;
281
+ }
282
+ const nextRows = buildMappingRows(nextPreview.headers, nextSuggestedMapping);
283
+ setMappingRows(nextRows);
284
+ setMatchStrategy(nextSuggestedMapping.matchStrategy);
285
+ setIsMappingDirty(false);
286
+ lastAppliedSuggestionSignatureRef.current = buildSuggestedMappingSignature(nextPreview.headers, nextSuggestedMapping);
287
+ persistCurrentSession({
288
+ uploadId: nextPreview.uploadId,
289
+ filename: nextPreview.filename,
290
+ preview: nextPreview,
291
+ mappingRows: nextRows,
292
+ matchStrategy: nextSuggestedMapping.matchStrategy,
293
+ runId: null,
294
+ progressJobId: null
295
+ });
296
+ replaceQueryParams({
297
+ uploadId: nextPreview.uploadId,
298
+ runId: null
299
+ });
300
+ }, [customFieldDefs, persistCurrentSession, preview?.uploadId, replaceQueryParams]);
301
+ React.useEffect(() => {
302
+ if (!preview || !previewSuggestion) return;
303
+ if (isMappingDirty) return;
304
+ const nextSignature = buildSuggestedMappingSignature(preview.headers, previewSuggestion);
305
+ if (lastAppliedSuggestionSignatureRef.current === nextSignature) return;
306
+ setMappingRows(buildMappingRows(preview.headers, previewSuggestion));
307
+ setMatchStrategy(previewSuggestion.matchStrategy);
308
+ lastAppliedSuggestionSignatureRef.current = nextSignature;
309
+ }, [isMappingDirty, preview, previewSuggestion]);
310
+ const refreshRunDetail = React.useCallback(async (currentRunId) => {
311
+ const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(currentRunId)}`, void 0, { fallback: null });
312
+ if (call.ok && call.result) {
313
+ setRunDetail(call.result);
314
+ setProgressJobId(call.result.progressJobId ?? null);
315
+ persistCurrentSession({
316
+ runId: currentRunId,
317
+ progressJobId: call.result.progressJobId ?? null
318
+ });
319
+ return call.result;
320
+ }
321
+ return null;
322
+ }, [persistCurrentSession]);
323
+ React.useEffect(() => {
324
+ if (!preview?.uploadId) return;
325
+ persistCurrentSession();
326
+ }, [mappingRows, matchStrategy, persistCurrentSession, preview?.uploadId, progressJobId, runId]);
327
+ React.useEffect(() => {
328
+ const persistedSnapshot = readPersistedSessionSnapshot(context.integrationId);
329
+ const resolvedUploadId = uploadIdFromUrl ?? persistedSnapshot?.uploadId ?? null;
330
+ const resolvedRunId = runIdFromUrl ?? persistedSnapshot?.runId ?? null;
331
+ if (!resolvedUploadId) {
332
+ restoringSnapshotRef.current = null;
333
+ lastRestoreRequestKeyRef.current = null;
334
+ setIsRestoringSession(false);
335
+ return;
336
+ }
337
+ if (!uploadIdFromUrl && persistedSnapshot?.uploadId === resolvedUploadId) {
338
+ replaceQueryParams({
339
+ uploadId: resolvedUploadId,
340
+ runId: resolvedRunId
341
+ });
342
+ }
343
+ if (preview?.uploadId === resolvedUploadId && mappingRows.length > 0) {
344
+ if (resolvedRunId && runId !== resolvedRunId) {
345
+ setRunId(resolvedRunId);
346
+ }
347
+ setIsRestoringSession(false);
348
+ return;
349
+ }
350
+ const restoreRequestKey = `${resolvedUploadId}:${resolvedRunId ?? ""}`;
351
+ if (lastRestoreRequestKeyRef.current === restoreRequestKey) {
352
+ setIsRestoringSession(false);
353
+ return;
354
+ }
355
+ lastRestoreRequestKeyRef.current = restoreRequestKey;
356
+ let cancelled = false;
357
+ restoringSnapshotRef.current = persistedSnapshot;
358
+ const persistedPreview = restoringSnapshotRef.current?.preview?.uploadId === resolvedUploadId ? restoringSnapshotRef.current.preview : null;
359
+ if (persistedPreview) {
360
+ syncPreviewState(persistedPreview, {
361
+ restoredSnapshot: restoringSnapshotRef.current
362
+ });
363
+ setIsRestoringSession(false);
364
+ } else {
365
+ setIsRestoringSession(true);
366
+ }
367
+ const restoreSession = async () => {
368
+ const call = await apiCall(
369
+ `/api/sync_excel/preview?uploadId=${encodeURIComponent(resolvedUploadId)}&entityType=${encodeURIComponent(ENTITY_TYPE)}`,
370
+ void 0,
371
+ { fallback: null }
372
+ );
373
+ if (cancelled) return;
374
+ if (!call.ok || !call.result) {
375
+ setPreview(null);
376
+ setMappingRows([]);
377
+ setMatchStrategy("custom");
378
+ setRunId(null);
379
+ setRunDetail(null);
380
+ setProgressJobId(null);
381
+ clearPersistedSession();
382
+ lastRestoreRequestKeyRef.current = null;
383
+ setIsRestoringSession(false);
384
+ return;
385
+ }
386
+ syncPreviewState(call.result, {
387
+ restoredSnapshot: restoringSnapshotRef.current
388
+ });
389
+ if (resolvedRunId && !restoringSnapshotRef.current?.runId) {
390
+ setRunId(resolvedRunId);
391
+ }
392
+ setIsRestoringSession(false);
393
+ };
394
+ void restoreSession();
395
+ return () => {
396
+ cancelled = true;
397
+ };
398
+ }, [clearPersistedSession, context.integrationId, mappingRows.length, preview?.uploadId, replaceQueryParams, runId, runIdFromUrl, syncPreviewState, uploadIdFromUrl]);
399
+ React.useEffect(() => {
400
+ if (!runId) return;
401
+ let cancelled = false;
402
+ let timeoutId = null;
403
+ const poll = async () => {
404
+ const detail = await refreshRunDetail(runId);
405
+ if (cancelled) return;
406
+ if (context.activeTab === "logs") {
407
+ await context.refreshLogs?.();
408
+ }
409
+ if (context.activeTab === "health" || normalizeStatus(detail?.status) !== "idle") {
410
+ await context.refreshHealthSnapshot?.();
411
+ }
412
+ const status = normalizeStatus(detail?.status);
413
+ if (status === "pending" || status === "running") {
414
+ timeoutId = setTimeout(() => {
415
+ void poll();
416
+ }, RUN_POLL_INTERVAL_MS);
417
+ }
418
+ };
419
+ void poll();
420
+ return () => {
421
+ cancelled = true;
422
+ if (timeoutId) clearTimeout(timeoutId);
423
+ };
424
+ }, [context.activeTab, context.refreshHealthSnapshot, context.refreshLogs, refreshRunDetail, runId]);
425
+ const handleUpload = React.useCallback(async () => {
426
+ if (!selectedFile) {
427
+ flash(t("sync_excel.widget.messages.fileRequired", "Select a CSV file first."), "error");
428
+ return;
429
+ }
430
+ setIsUploading(true);
431
+ try {
432
+ const formData = new FormData();
433
+ formData.set("file", selectedFile);
434
+ formData.set("entityType", ENTITY_TYPE);
435
+ const call = await runMutation({
436
+ operation: () => apiCall("/api/sync_excel/upload", {
437
+ method: "POST",
438
+ body: formData
439
+ }, { fallback: null }),
440
+ mutationPayload: {
441
+ integrationId: context.integrationId ?? "sync_excel",
442
+ entityType: ENTITY_TYPE,
443
+ fileName: selectedFile.name
444
+ },
445
+ context: {
446
+ ...context,
447
+ operation: "create",
448
+ actionId: "sync-excel-upload"
449
+ }
450
+ });
451
+ if (!call.ok || !call.result) {
452
+ flash(
453
+ readApiErrorMessage(call.result, t("sync_excel.widget.messages.uploadError", "Failed to upload CSV file.")),
454
+ "error"
455
+ );
456
+ return;
457
+ }
458
+ syncPreviewState(call.result);
459
+ setSelectedFile(null);
460
+ setRunId(null);
461
+ setRunDetail(null);
462
+ setProgressJobId(null);
463
+ flash(t("sync_excel.widget.messages.uploadSuccess", "CSV preview is ready."), "success");
464
+ } catch {
465
+ flash(t("sync_excel.widget.messages.uploadError", "Failed to upload CSV file."), "error");
466
+ } finally {
467
+ setIsUploading(false);
468
+ }
469
+ }, [context, runMutation, selectedFile, syncPreviewState, t]);
470
+ const handleRefreshPreview = React.useCallback(async () => {
471
+ if (!preview) return;
472
+ setIsRefreshingPreview(true);
473
+ try {
474
+ const call = await apiCall(
475
+ `/api/sync_excel/preview?uploadId=${encodeURIComponent(preview.uploadId)}&entityType=${encodeURIComponent(ENTITY_TYPE)}`,
476
+ void 0,
477
+ { fallback: null }
478
+ );
479
+ if (!call.ok || !call.result) {
480
+ flash(t("sync_excel.widget.messages.previewError", "Failed to refresh preview."), "error");
481
+ return;
482
+ }
483
+ syncPreviewState(call.result, { preserveManualState: true });
484
+ flash(t("sync_excel.widget.messages.previewRefreshed", "Preview refreshed."), "success");
485
+ } catch {
486
+ flash(t("sync_excel.widget.messages.previewError", "Failed to refresh preview."), "error");
487
+ } finally {
488
+ setIsRefreshingPreview(false);
489
+ }
490
+ }, [preview, syncPreviewState, t]);
491
+ const handleTargetFieldChange = React.useCallback((sourceColumn, targetField) => {
492
+ setIsMappingDirty(true);
493
+ setMappingRows((current) => current.map((row) => row.sourceColumn === sourceColumn ? { ...row, targetField } : row));
494
+ }, []);
495
+ const handleResetToSuggested = React.useCallback(() => {
496
+ if (!preview || !previewSuggestion) return;
497
+ setMappingRows(buildMappingRows(preview.headers, previewSuggestion));
498
+ setMatchStrategy(previewSuggestion.matchStrategy);
499
+ setIsMappingDirty(false);
500
+ lastAppliedSuggestionSignatureRef.current = buildSuggestedMappingSignature(preview.headers, previewSuggestion);
501
+ flash(t("sync_excel.widget.messages.mappingReset", "Suggested mapping restored."), "success");
502
+ }, [preview, previewSuggestion, t]);
503
+ const handleStartImport = React.useCallback(async () => {
504
+ if (!preview) return;
505
+ const mapping = buildMapping(mappingRows, matchStrategy, targetOptions);
506
+ setIsStartingImport(true);
507
+ try {
508
+ const call = await runMutation({
509
+ operation: () => apiCall("/api/sync_excel/import", {
510
+ method: "POST",
511
+ headers: { "Content-Type": "application/json" },
512
+ body: JSON.stringify({
513
+ uploadId: preview.uploadId,
514
+ entityType: ENTITY_TYPE,
515
+ mapping
516
+ })
517
+ }, { fallback: null }),
518
+ mutationPayload: {
519
+ integrationId: context.integrationId ?? "sync_excel",
520
+ uploadId: preview.uploadId,
521
+ entityType: ENTITY_TYPE,
522
+ mapping
523
+ },
524
+ context: {
525
+ ...context,
526
+ operation: "create",
527
+ actionId: "sync-excel-start-import"
528
+ }
529
+ });
530
+ if (!call.ok || !call.result) {
531
+ flash(
532
+ readApiErrorMessage(call.result, t("sync_excel.widget.messages.importError", "Failed to start import run.")),
533
+ "error"
534
+ );
535
+ return;
536
+ }
537
+ setRunId(call.result.runId);
538
+ setProgressJobId(call.result.progressJobId);
539
+ replaceQueryParams({
540
+ uploadId: preview.uploadId,
541
+ runId: call.result.runId
542
+ });
543
+ persistCurrentSession({
544
+ uploadId: preview.uploadId,
545
+ filename: preview.filename,
546
+ runId: call.result.runId,
547
+ progressJobId: call.result.progressJobId
548
+ });
549
+ flash(t("sync_excel.widget.messages.importStarted", "Import run started."), "success");
550
+ await refreshRunDetail(call.result.runId);
551
+ await context.refreshLogs?.();
552
+ await context.refreshHealthSnapshot?.();
553
+ } catch {
554
+ flash(t("sync_excel.widget.messages.importError", "Failed to start import run."), "error");
555
+ } finally {
556
+ setIsStartingImport(false);
557
+ }
558
+ }, [context, mappingRows, matchStrategy, persistCurrentSession, preview, refreshRunDetail, replaceQueryParams, runMutation, t, targetOptions]);
559
+ const handleCancelRun = React.useCallback(async () => {
560
+ if (!runId) return;
561
+ setIsCancelling(true);
562
+ try {
563
+ const call = await runMutation({
564
+ operation: () => apiCall(`/api/data_sync/runs/${encodeURIComponent(runId)}/cancel`, {
565
+ method: "POST"
566
+ }, { fallback: null }),
567
+ mutationPayload: {
568
+ integrationId: context.integrationId ?? "sync_excel",
569
+ runId
570
+ },
571
+ context: {
572
+ ...context,
573
+ operation: "update",
574
+ actionId: "sync-excel-cancel-import"
575
+ }
576
+ });
577
+ if (!call.ok) {
578
+ flash(t("sync_excel.widget.messages.cancelError", "Failed to cancel import run."), "error");
579
+ return;
580
+ }
581
+ flash(t("sync_excel.widget.messages.cancelSuccess", "Import run cancelled."), "success");
582
+ await refreshRunDetail(runId);
583
+ await context.refreshLogs?.();
584
+ await context.refreshHealthSnapshot?.();
585
+ } catch {
586
+ flash(t("sync_excel.widget.messages.cancelError", "Failed to cancel import run."), "error");
587
+ } finally {
588
+ setIsCancelling(false);
589
+ }
590
+ }, [context, refreshRunDetail, runId, runMutation, t]);
591
+ const handleRefreshAll = React.useCallback(async () => {
592
+ if (!runId) return;
593
+ await refreshRunDetail(runId);
594
+ await context.refreshLogs?.();
595
+ await context.refreshHealthSnapshot?.();
596
+ }, [context, refreshRunDetail, runId]);
597
+ const firstSampleRow = preview?.sampleRows[0] ?? null;
598
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
599
+ isRestoringSession ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex items-center gap-3 py-6", children: [
600
+ /* @__PURE__ */ jsx(Spinner, { className: "size-4" }),
601
+ /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: t("sync_excel.widget.messages.restoringSession", "Restoring the last CSV session...") })
602
+ ] }) }) : null,
603
+ !resolvedIntegrationEnabled ? /* @__PURE__ */ jsx(
604
+ StatusAlert,
605
+ {
606
+ variant: "warning",
607
+ title: t("sync_excel.widget.disabled.title", "Integration disabled"),
608
+ message: t("sync_excel.widget.disabled.message", "Enable this integration to upload CSV files and start imports.")
609
+ }
610
+ ) : null,
611
+ !shouldHideInteractiveContent ? /* @__PURE__ */ jsxs(Card, { children: [
612
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
613
+ /* @__PURE__ */ jsx(CardTitle, { children: t("sync_excel.widget.upload.title", "Upload CSV") }),
614
+ /* @__PURE__ */ jsx(CardDescription, { children: t("sync_excel.widget.upload.description", "Upload a CSV file, review the suggested people mapping, and start a background import run.") })
615
+ ] }),
616
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
617
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
618
+ /* @__PURE__ */ jsx(Label, { htmlFor: "sync-excel-upload-file", children: t("sync_excel.widget.upload.fileLabel", "CSV file") }),
619
+ /* @__PURE__ */ jsx(
620
+ Input,
621
+ {
622
+ id: "sync-excel-upload-file",
623
+ type: "file",
624
+ accept: ".csv,text/csv",
625
+ onChange: (event) => setSelectedFile(event.target.files?.[0] ?? null),
626
+ disabled: isUploading || isRunActive || isRestoringSession
627
+ }
628
+ )
629
+ ] }),
630
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
631
+ /* @__PURE__ */ jsxs(Button, { type: "button", onClick: () => void handleUpload(), disabled: !selectedFile || isUploading || isRunActive || isRestoringSession, children: [
632
+ isUploading ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }) : /* @__PURE__ */ jsx(Upload, { className: "mr-2 size-4" }),
633
+ isUploading ? t("sync_excel.widget.actions.uploadPending", "Uploading...") : t("sync_excel.widget.actions.upload", "Upload and preview")
634
+ ] }),
635
+ preview ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: () => void handleRefreshPreview(), disabled: isRefreshingPreview || isUploading || isRunActive || isRestoringSession, children: [
636
+ isRefreshingPreview ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }) : /* @__PURE__ */ jsx(RefreshCw, { className: "mr-2 size-4" }),
637
+ isRefreshingPreview ? t("sync_excel.widget.actions.refreshPending", "Refreshing...") : t("sync_excel.widget.actions.refresh", "Refresh preview")
638
+ ] }) : null
639
+ ] }),
640
+ selectedFile ? /* @__PURE__ */ jsxs("div", { className: "text-sm text-muted-foreground", children: [
641
+ t("sync_excel.widget.upload.selectedFile", "Selected file"),
642
+ ": ",
643
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: selectedFile.name })
644
+ ] }) : null
645
+ ] })
646
+ ] }) : null,
647
+ preview && !shouldHideInteractiveContent ? /* @__PURE__ */ jsxs(Card, { children: [
648
+ /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
649
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
650
+ /* @__PURE__ */ jsx(CardTitle, { children: t("sync_excel.widget.preview.title", "Preview and mapping") }),
651
+ /* @__PURE__ */ jsx(CardDescription, { children: t("sync_excel.widget.preview.description", "Every source column stays visible here. Leave a target empty to keep the column unmapped for now.") })
652
+ ] }),
653
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
654
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: preview.filename }),
655
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: t("sync_excel.widget.preview.rows", "{count} rows", { count: preview.totalRows }) }),
656
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: formatFileSize(preview.fileSize) })
657
+ ] })
658
+ ] }) }),
659
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
660
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4", children: [
661
+ /* @__PURE__ */ jsx(
662
+ MetricCard,
663
+ {
664
+ label: t("sync_excel.widget.metrics.mapped", "Mapped columns"),
665
+ value: String(diagnostics.mappedCount)
666
+ }
667
+ ),
668
+ /* @__PURE__ */ jsx(
669
+ MetricCard,
670
+ {
671
+ label: t("sync_excel.widget.metrics.unmapped", "Unmapped columns"),
672
+ value: String(diagnostics.unmappedCount)
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsx(
676
+ MetricCard,
677
+ {
678
+ label: t("sync_excel.widget.metrics.identity", "Identity strategy"),
679
+ value: getMatchStrategyLabel(matchStrategy, t)
680
+ }
681
+ ),
682
+ /* @__PURE__ */ jsx(
683
+ MetricCard,
684
+ {
685
+ label: t("sync_excel.widget.metrics.entity", "Target entity"),
686
+ value: ENTITY_TYPE
687
+ }
688
+ )
689
+ ] }),
690
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]", children: [
691
+ /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border", children: /* @__PURE__ */ jsxs(Table, { children: [
692
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
693
+ /* @__PURE__ */ jsx(TableHead, { children: t("sync_excel.widget.mapping.sourceColumn", "Source column") }),
694
+ /* @__PURE__ */ jsx(TableHead, { children: t("sync_excel.widget.mapping.sampleValue", "Sample value") }),
695
+ /* @__PURE__ */ jsx(TableHead, { children: t("sync_excel.widget.mapping.targetField", "Target field") })
696
+ ] }) }),
697
+ /* @__PURE__ */ jsx(TableBody, { children: mappingRows.map((row) => /* @__PURE__ */ jsxs(TableRow, { children: [
698
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: row.sourceColumn }),
699
+ /* @__PURE__ */ jsx(TableCell, { className: "max-w-[260px] truncate text-muted-foreground", children: firstSampleRow?.[row.sourceColumn] ?? "\u2014" }),
700
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs(
701
+ "select",
702
+ {
703
+ className: SELECT_CLASS_NAME,
704
+ value: row.targetField,
705
+ onChange: (event) => handleTargetFieldChange(row.sourceColumn, event.target.value),
706
+ disabled: isStartingImport || isRunActive,
707
+ children: [
708
+ /* @__PURE__ */ jsx("option", { value: "", children: t("sync_excel.mapping.targets.ignore", "Leave unmapped") }),
709
+ targetOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.labelKey ? t(option.labelKey, option.fallback) : option.fallback }, option.value))
710
+ ]
711
+ }
712
+ ) })
713
+ ] }, row.sourceColumn)) })
714
+ ] }) }),
715
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
716
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
717
+ /* @__PURE__ */ jsx(Label, { htmlFor: "sync-excel-match-strategy", children: t("sync_excel.widget.matchStrategy.label", "How to match existing people") }),
718
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t(
719
+ "sync_excel.widget.matchStrategy.help",
720
+ "This decides whether imported rows update existing people or create new ones."
721
+ ) }),
722
+ /* @__PURE__ */ jsxs(
723
+ "select",
724
+ {
725
+ id: "sync-excel-match-strategy",
726
+ className: SELECT_CLASS_NAME,
727
+ value: matchStrategy,
728
+ onChange: (event) => {
729
+ setIsMappingDirty(true);
730
+ setMatchStrategy(event.target.value);
731
+ },
732
+ disabled: isStartingImport || isRunActive,
733
+ children: [
734
+ /* @__PURE__ */ jsx("option", { value: "externalId", children: t("sync_excel.widget.matchStrategy.externalId", "Source record ID (Recommended)") }),
735
+ /* @__PURE__ */ jsx("option", { value: "email", children: t("sync_excel.widget.matchStrategy.email", "Email address") }),
736
+ /* @__PURE__ */ jsx("option", { value: "custom", children: t("sync_excel.widget.matchStrategy.custom", "Custom matching") })
737
+ ]
738
+ }
739
+ ),
740
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: matchStrategy === "externalId" ? t(
741
+ "sync_excel.widget.matchStrategy.externalIdDescription",
742
+ "Best for repeated imports from the same source. Future imports update the same people instead of creating duplicates."
743
+ ) : matchStrategy === "email" ? t(
744
+ "sync_excel.widget.matchStrategy.emailDescription",
745
+ "Matches people by email. Works best when each person has one reliable, unique email address."
746
+ ) : t(
747
+ "sync_excel.widget.matchStrategy.customDescription",
748
+ "Advanced option. Use only if you know how existing records should be identified in this import."
749
+ ) })
750
+ ] }),
751
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
752
+ /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: handleResetToSuggested, disabled: isStartingImport || isRunActive, children: [
753
+ /* @__PURE__ */ jsx(RefreshCw, { className: "mr-2 size-4" }),
754
+ t("sync_excel.widget.actions.reset", "Reset to suggestion")
755
+ ] }),
756
+ /* @__PURE__ */ jsxs(Button, { type: "button", onClick: () => void handleStartImport(), disabled: !canStartImport || isStartingImport || isRunActive, children: [
757
+ isStartingImport ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }) : /* @__PURE__ */ jsx(Play, { className: "mr-2 size-4" }),
758
+ isStartingImport ? t("sync_excel.widget.actions.startPending", "Starting...") : t("sync_excel.widget.actions.start", "Start import")
759
+ ] })
760
+ ] }),
761
+ matchStrategy === "custom" ? /* @__PURE__ */ jsx(
762
+ StatusAlert,
763
+ {
764
+ variant: "warning",
765
+ title: t("sync_excel.widget.matchStrategy.customWarningTitle", "Use custom matching with care"),
766
+ message: t(
767
+ "sync_excel.widget.matchStrategy.customWarningMessage",
768
+ "Custom matching may create duplicates if the mapped fields do not uniquely identify a person."
769
+ )
770
+ }
771
+ ) : null,
772
+ hasExistingRunForCurrentUpload ? /* @__PURE__ */ jsx(
773
+ StatusAlert,
774
+ {
775
+ variant: "warning",
776
+ title: t("sync_excel.widget.validation.reimportTitle", "This upload already has a run"),
777
+ message: t(
778
+ "sync_excel.widget.validation.reimportMessage",
779
+ "Starting another import from the same uploaded CSV may create duplicates unless your matching strategy reliably identifies existing people."
780
+ )
781
+ }
782
+ ) : null,
783
+ !hasSafeDedupeStrategy ? /* @__PURE__ */ jsx(
784
+ StatusAlert,
785
+ {
786
+ variant: "warning",
787
+ title: t("sync_excel.widget.validation.duplicateRiskTitle", "Duplicate risk"),
788
+ message: t(
789
+ "sync_excel.widget.validation.duplicateRiskMessage",
790
+ "This mapping does not use the recommended External ID or Email matching strategy. Re-importing the same dataset may create duplicate people."
791
+ )
792
+ }
793
+ ) : null,
794
+ diagnostics.duplicateTargets.length > 0 ? /* @__PURE__ */ jsx(
795
+ StatusAlert,
796
+ {
797
+ variant: "error",
798
+ title: t("sync_excel.widget.validation.duplicateTargetsTitle", "Duplicate target fields"),
799
+ message: t(
800
+ "sync_excel.widget.validation.duplicateTargetsMessage",
801
+ "Each target field can only be mapped once in this foundation slice: {targets}",
802
+ { targets: diagnostics.duplicateTargets.map((target) => formatTargetLabel(target, t, targetOptions)).join(", ") }
803
+ )
804
+ }
805
+ ) : null,
806
+ !diagnostics.hasIdentityTarget ? /* @__PURE__ */ jsx(
807
+ StatusAlert,
808
+ {
809
+ variant: "warning",
810
+ title: t("sync_excel.widget.validation.identityTitle", "Map an identity field"),
811
+ message: t("sync_excel.widget.validation.identityMessage", "Map either External ID or Primary email so reimports can match existing people.")
812
+ }
813
+ ) : null,
814
+ !diagnostics.hasNameTarget ? /* @__PURE__ */ jsx(
815
+ StatusAlert,
816
+ {
817
+ variant: "warning",
818
+ title: t("sync_excel.widget.validation.nameTitle", "Map a name field"),
819
+ message: t("sync_excel.widget.validation.nameMessage", "Map Last name or Display name so the importer can build person records.")
820
+ }
821
+ ) : null,
822
+ diagnostics.hasIdentityTarget && diagnostics.hasNameTarget && diagnostics.duplicateTargets.length === 0 ? /* @__PURE__ */ jsx(
823
+ StatusAlert,
824
+ {
825
+ variant: "info",
826
+ title: t("sync_excel.widget.validation.readyTitle", "Ready to import"),
827
+ message: t("sync_excel.widget.validation.readyMessage", "The current mapping is valid for the first customers.person slice.")
828
+ }
829
+ ) : null
830
+ ] })
831
+ ] })
832
+ ] })
833
+ ] }) : null,
834
+ runId && !shouldHideInteractiveContent ? /* @__PURE__ */ jsxs(Card, { children: [
835
+ /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
836
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
837
+ /* @__PURE__ */ jsx(CardTitle, { children: t("sync_excel.widget.run.title", "Import run status") }),
838
+ /* @__PURE__ */ jsx(CardDescription, { children: t("sync_excel.widget.run.description", "The import runs in the background through Data Sync and updates this view as progress changes.") })
839
+ ] }),
840
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
841
+ /* @__PURE__ */ jsx(RunStatusBadge, { status: importStatus, t }),
842
+ progressJobId ? /* @__PURE__ */ jsx(Badge, { variant: "outline", children: progressJobId }) : null
843
+ ] })
844
+ ] }) }),
845
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
846
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
847
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-sm", children: [
848
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("sync_excel.widget.run.progressLabel", "Progress") }),
849
+ /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
850
+ progressValue,
851
+ "%"
852
+ ] })
853
+ ] }),
854
+ /* @__PURE__ */ jsx(Progress, { value: progressValue, className: "h-2.5" })
855
+ ] }),
856
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-5", children: [
857
+ /* @__PURE__ */ jsx(MetricCard, { label: t("sync_excel.widget.run.metrics.created", "Created"), value: String(runDetail?.createdCount ?? 0) }),
858
+ /* @__PURE__ */ jsx(MetricCard, { label: t("sync_excel.widget.run.metrics.updated", "Updated"), value: String(runDetail?.updatedCount ?? 0) }),
859
+ /* @__PURE__ */ jsx(MetricCard, { label: t("sync_excel.widget.run.metrics.skipped", "Skipped"), value: String(runDetail?.skippedCount ?? 0) }),
860
+ /* @__PURE__ */ jsx(MetricCard, { label: t("sync_excel.widget.run.metrics.failed", "Failed"), value: String(runDetail?.failedCount ?? 0) }),
861
+ /* @__PURE__ */ jsx(MetricCard, { label: t("sync_excel.widget.run.metrics.processed", "Processed"), value: String(runDetail?.progressJob?.processedCount ?? 0) })
862
+ ] }),
863
+ runDetail?.lastError ? /* @__PURE__ */ jsx(
864
+ StatusAlert,
865
+ {
866
+ variant: "error",
867
+ title: t("sync_excel.widget.run.errorTitle", "Run error"),
868
+ message: runDetail.lastError
869
+ }
870
+ ) : null,
871
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
872
+ /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: () => void handleRefreshAll(), disabled: isRefreshingPreview || isRestoringSession, children: [
873
+ /* @__PURE__ */ jsx(RefreshCw, { className: "mr-2 size-4" }),
874
+ t("sync_excel.widget.actions.refreshRun", "Refresh run status")
875
+ ] }),
876
+ isRunActive ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "destructive", onClick: () => void handleCancelRun(), disabled: isCancelling, children: [
877
+ isCancelling ? /* @__PURE__ */ jsx(Spinner, { className: "mr-2 size-4" }) : /* @__PURE__ */ jsx(XCircle, { className: "mr-2 size-4" }),
878
+ isCancelling ? t("sync_excel.widget.actions.cancelPending", "Cancelling...") : t("sync_excel.widget.actions.cancel", "Cancel run")
879
+ ] }) : null
880
+ ] })
881
+ ] })
882
+ ] }) : null
883
+ ] });
884
+ }
885
+ function StatusAlert({
886
+ variant,
887
+ title,
888
+ message
889
+ }) {
890
+ return /* @__PURE__ */ jsxs(Alert, { variant: variant === "error" ? "destructive" : variant, children: [
891
+ /* @__PURE__ */ jsx(AlertTitle, { children: title }),
892
+ /* @__PURE__ */ jsx(AlertDescription, { children: message })
893
+ ] });
894
+ }
895
+ function MetricCard({ label, value }) {
896
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-muted/30 p-4", children: [
897
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: label }),
898
+ /* @__PURE__ */ jsx("div", { className: "mt-2 text-lg font-semibold text-foreground", children: value })
899
+ ] });
900
+ }
901
+ function RunStatusBadge({
902
+ status,
903
+ t
904
+ }) {
905
+ if (status === "completed") {
906
+ return /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "gap-1.5 border-emerald-500/30 bg-emerald-500/10 text-emerald-300", children: [
907
+ /* @__PURE__ */ jsx(CheckCircle2, { className: "size-3.5" }),
908
+ t("sync_excel.widget.run.status.completed", "Completed")
909
+ ] });
910
+ }
911
+ if (status === "failed") {
912
+ return /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "gap-1.5 border-red-500/30 bg-red-500/10 text-red-300", children: [
913
+ /* @__PURE__ */ jsx(AlertTriangle, { className: "size-3.5" }),
914
+ t("sync_excel.widget.run.status.failed", "Failed")
915
+ ] });
916
+ }
917
+ if (status === "cancelled") {
918
+ return /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "gap-1.5 border-zinc-500/30 bg-zinc-500/10 text-zinc-300", children: [
919
+ /* @__PURE__ */ jsx(XCircle, { className: "size-3.5" }),
920
+ t("sync_excel.widget.run.status.cancelled", "Cancelled")
921
+ ] });
922
+ }
923
+ if (status === "pending" || status === "running") {
924
+ return /* @__PURE__ */ jsxs(Badge, { variant: "outline", className: "gap-1.5 border-blue-500/30 bg-blue-500/10 text-blue-300", children: [
925
+ /* @__PURE__ */ jsx(RefreshCw, { className: cn("size-3.5", status === "running" ? "animate-spin" : "") }),
926
+ status === "pending" ? t("sync_excel.widget.run.status.pending", "Pending") : t("sync_excel.widget.run.status.running", "Running")
927
+ ] });
928
+ }
929
+ return /* @__PURE__ */ jsx(Badge, { variant: "outline", className: "gap-1.5 border-zinc-500/30 bg-zinc-500/10 text-zinc-300", children: t("sync_excel.widget.run.status.idle", "Idle") });
930
+ }
931
+ export {
932
+ SyncExcelUploadConfigWidget as default
933
+ };
934
+ //# sourceMappingURL=widget.client.js.map