medusa-import-product-plugin 0.0.1

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 (40) hide show
  1. package/.medusa/server/src/admin/index.js +590 -0
  2. package/.medusa/server/src/admin/index.mjs +591 -0
  3. package/.medusa/server/src/api/admin/import/[id]/pause/route.js +25 -0
  4. package/.medusa/server/src/api/admin/import/[id]/resume/route.js +51 -0
  5. package/.medusa/server/src/api/admin/import/[id]/route.js +57 -0
  6. package/.medusa/server/src/api/admin/import/route.js +88 -0
  7. package/.medusa/server/src/api/admin/plugin/route.js +7 -0
  8. package/.medusa/server/src/api/middlewares.js +46 -0
  9. package/.medusa/server/src/api/store/plugin/route.js +7 -0
  10. package/.medusa/server/src/modules/import/index.js +13 -0
  11. package/.medusa/server/src/modules/import/migrations/Migration20260603094700.js +21 -0
  12. package/.medusa/server/src/modules/import/migrations/Migration20260605102500.js +14 -0
  13. package/.medusa/server/src/modules/import/models/import-job.js +24 -0
  14. package/.medusa/server/src/modules/import/models/import-row.js +18 -0
  15. package/.medusa/server/src/modules/import/service.js +59 -0
  16. package/.medusa/server/src/modules/import/types/enums.js +19 -0
  17. package/.medusa/server/src/modules/import/types/index.js +9 -0
  18. package/.medusa/server/src/modules/import/types/records.js +3 -0
  19. package/.medusa/server/src/modules/import/types/workflow.js +69 -0
  20. package/.medusa/server/src/subscribers/product-import.js +98 -0
  21. package/.medusa/server/src/workflows/index.js +18 -0
  22. package/.medusa/server/src/workflows/process-import.js +404 -0
  23. package/.medusa/server/src/workflows/steps/create-category-attributes.step.js +29 -0
  24. package/.medusa/server/src/workflows/steps/create-variant-price-sets.step.js +36 -0
  25. package/.medusa/server/src/workflows/steps/download-and-upload-images.step.js +77 -0
  26. package/.medusa/server/src/workflows/steps/find-or-create-category.step.js +40 -0
  27. package/.medusa/server/src/workflows/steps/find-products-by-handle.step.js +13 -0
  28. package/.medusa/server/src/workflows/steps/find-products-by-variant-sku.step.js +19 -0
  29. package/.medusa/server/src/workflows/steps/list-category-attributes-by-labels.step.js +27 -0
  30. package/.medusa/server/src/workflows/steps/list-existing-inventory-levels.step.js +24 -0
  31. package/.medusa/server/src/workflows/steps/list-inventory-items-by-sku.step.js +13 -0
  32. package/.medusa/server/src/workflows/steps/list-product-tags-by-value.step.js +13 -0
  33. package/.medusa/server/src/workflows/steps/parse-csv.step.js +160 -0
  34. package/.medusa/server/src/workflows/steps/refetch-products-by-handle.step.js +16 -0
  35. package/.medusa/server/src/workflows/steps/resolve-default-stock-location.step.js +17 -0
  36. package/.medusa/server/src/workflows/steps/set-product-attributes.step.js +53 -0
  37. package/.medusa/server/src/workflows/steps/update-dealer-prices.step.js +77 -0
  38. package/.medusa/server/src/workflows/steps/update-variant-prices.step.js +74 -0
  39. package/README.md +64 -0
  40. package/package.json +77 -0
@@ -0,0 +1,590 @@
1
+ "use strict";
2
+ const jsxRuntime = require("react/jsx-runtime");
3
+ const react = require("react");
4
+ const reactRouterDom = require("react-router-dom");
5
+ const ui = require("@medusajs/ui");
6
+ const adminSdk = require("@medusajs/admin-sdk");
7
+ const icons = require("@medusajs/icons");
8
+ const STATUS_COLOR$1 = {
9
+ processing: "blue",
10
+ paused: "orange",
11
+ done: "green",
12
+ failed: "red",
13
+ pending: "grey"
14
+ };
15
+ const STATUS_ICON$1 = {
16
+ processing: /* @__PURE__ */ jsxRuntime.jsx(icons.Loader, { className: "w-3.5 h-3.5" }),
17
+ done: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircle, { className: "w-3.5 h-3.5" }),
18
+ failed: /* @__PURE__ */ jsxRuntime.jsx(icons.XCircle, { className: "w-3.5 h-3.5" }),
19
+ pending: /* @__PURE__ */ jsxRuntime.jsx(icons.Clock, { className: "w-3.5 h-3.5" })
20
+ };
21
+ const PAGE_SIZE$1 = 10;
22
+ function SkeletonRow() {
23
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Row, { children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-full animate-pulse rounded bg-ui-bg-disabled" }) }, i)) });
24
+ }
25
+ function ImportPage() {
26
+ const navigate = reactRouterDom.useNavigate();
27
+ const [file, setFile] = react.useState(null);
28
+ const inputRef = react.useRef(null);
29
+ const [uploading, setUploading] = react.useState(false);
30
+ const [jobs, setJobs] = react.useState([]);
31
+ const [page, setPage] = react.useState(1);
32
+ const [totalPages, setTotalPages] = react.useState(1);
33
+ const [total, setTotal] = react.useState(0);
34
+ const [loading, setLoading] = react.useState(true);
35
+ const [deleting, setDeleting] = react.useState(null);
36
+ const [uploadError, setUploadError] = react.useState(null);
37
+ const [autoRefresh, setAutoRefresh] = react.useState(false);
38
+ const intervalRef = react.useRef(null);
39
+ const fetchJobs = async (p) => {
40
+ try {
41
+ const res = await fetch(`/admin/import?page=${p}&limit=${PAGE_SIZE$1}`);
42
+ if (!res.ok) return;
43
+ const data = await res.json();
44
+ setJobs(data.jobs ?? []);
45
+ setTotalPages(data.totalPages ?? 1);
46
+ setTotal(data.total ?? 0);
47
+ } catch {
48
+ } finally {
49
+ setLoading(false);
50
+ }
51
+ };
52
+ react.useEffect(() => {
53
+ fetchJobs(page);
54
+ }, [page]);
55
+ react.useEffect(() => {
56
+ if (autoRefresh) {
57
+ intervalRef.current = setInterval(() => fetchJobs(page), 5e3);
58
+ }
59
+ return () => {
60
+ if (intervalRef.current) {
61
+ clearInterval(intervalRef.current);
62
+ intervalRef.current = null;
63
+ }
64
+ };
65
+ }, [autoRefresh, page]);
66
+ const handleUpload = async () => {
67
+ if (!file || uploading) return;
68
+ setUploading(true);
69
+ setUploadError(null);
70
+ try {
71
+ const form = new FormData();
72
+ form.append("file", file);
73
+ const res = await fetch("/admin/import", { method: "POST", body: form });
74
+ const data = await res.json();
75
+ if (!res.ok) {
76
+ setUploadError(data.error ?? "Upload failed");
77
+ setUploading(false);
78
+ return;
79
+ }
80
+ setFile(null);
81
+ navigate(`/import/${data.job_id}`);
82
+ } catch (err) {
83
+ setUploadError(err instanceof Error ? err.message : "Upload failed");
84
+ setUploading(false);
85
+ }
86
+ };
87
+ const formatDate = (val) => {
88
+ if (!val) return "—";
89
+ const d = new Date(val);
90
+ if (isNaN(d.getTime())) return "—";
91
+ return d.toLocaleString();
92
+ };
93
+ const handleDelete = async (jobId) => {
94
+ if (deleting) return;
95
+ if (!window.confirm("Delete this import job and all its data?")) return;
96
+ setDeleting(jobId);
97
+ try {
98
+ const res = await fetch(`/admin/import/${jobId}`, { method: "DELETE" });
99
+ if (res.ok) {
100
+ await fetchJobs(page);
101
+ }
102
+ } finally {
103
+ setDeleting(null);
104
+ }
105
+ };
106
+ const getProgress = (job) => {
107
+ if (!job.total) return 0;
108
+ return Math.round(job.processed / job.total * 100);
109
+ };
110
+ const hasActiveJobs = jobs.some((j) => j.status === "processing" || j.status === "paused");
111
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
112
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
113
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
114
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Product Import" }),
115
+ total > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "small", className: "text-ui-fg-subtle", children: total })
116
+ ] }),
117
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: hasActiveJobs && /* @__PURE__ */ jsxRuntime.jsx(
118
+ ui.Button,
119
+ {
120
+ variant: autoRefresh ? "primary" : "secondary",
121
+ size: "small",
122
+ onClick: () => setAutoRefresh(!autoRefresh),
123
+ children: autoRefresh ? "Auto-refresh On" : "Auto-refresh Off"
124
+ }
125
+ ) })
126
+ ] }),
127
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-5", children: [
128
+ /* @__PURE__ */ jsxRuntime.jsxs(
129
+ "div",
130
+ {
131
+ role: "button",
132
+ tabIndex: 0,
133
+ onClick: () => {
134
+ var _a;
135
+ return (_a = inputRef.current) == null ? void 0 : _a.click();
136
+ },
137
+ onKeyDown: (e) => {
138
+ var _a;
139
+ if (e.key === "Enter" || e.key === " ") {
140
+ e.preventDefault();
141
+ (_a = inputRef.current) == null ? void 0 : _a.click();
142
+ }
143
+ },
144
+ onDragOver: (e) => e.preventDefault(),
145
+ onDrop: (e) => {
146
+ var _a;
147
+ e.preventDefault();
148
+ const dropped = e.dataTransfer.files[0];
149
+ if ((_a = dropped == null ? void 0 : dropped.name) == null ? void 0 : _a.endsWith(".csv")) setFile(dropped);
150
+ },
151
+ className: "group flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed border-ui-border-base bg-ui-bg-subtle p-10 cursor-pointer hover:border-ui-border-interactive hover:bg-ui-bg-overlay-hover transition-all",
152
+ children: [
153
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-full bg-ui-bg-base p-3 shadow-elevation-1low group-hover:shadow-elevation-2", children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpTray, { className: "text-ui-fg-muted group-hover:text-ui-fg-interactive" }) }),
154
+ file ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [
155
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", children: file.name }),
156
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: [
157
+ (file.size / 1024).toFixed(1),
158
+ " KB"
159
+ ] })
160
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [
161
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-base", children: "Drop a CSV file here" }),
162
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "or click to browse" })
163
+ ] }),
164
+ /* @__PURE__ */ jsxRuntime.jsx(
165
+ "input",
166
+ {
167
+ ref: inputRef,
168
+ type: "file",
169
+ accept: ".csv",
170
+ className: "hidden",
171
+ onChange: (e) => {
172
+ var _a;
173
+ return setFile(((_a = e.target.files) == null ? void 0 : _a[0]) ?? null);
174
+ }
175
+ }
176
+ )
177
+ ]
178
+ }
179
+ ),
180
+ uploadError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 rounded-lg border border-ui-border-error bg-ui-bg-error p-3", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-error", children: uploadError }) }),
181
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex items-center gap-3", children: [
182
+ /* @__PURE__ */ jsxRuntime.jsx(
183
+ ui.Button,
184
+ {
185
+ onClick: handleUpload,
186
+ disabled: !file || uploading,
187
+ size: "large",
188
+ children: uploading ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [
189
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-4 w-4 animate-spin rounded-full border-2 border-ui-border-base border-t-transparent" }),
190
+ "Importing..."
191
+ ] }) : "Start Import"
192
+ }
193
+ ),
194
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Supports .csv files with one row per variant" })
195
+ ] })
196
+ ] }),
197
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
198
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between mb-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Import History" }) }),
199
+ loading ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
200
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
201
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "File" }),
202
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
203
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Progress" }),
204
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Created" }),
205
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "w-[100px]" })
206
+ ] }) }),
207
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(SkeletonRow, {}, i)) })
208
+ ] }) : jobs.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-16 gap-4", children: [
209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-full bg-ui-bg-subtle p-5", children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpTray, { className: "text-ui-fg-muted w-6 h-6" }) }),
210
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [
211
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-subtle", children: "No imports yet" }),
212
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: "Upload a CSV file above to bulk import products" })
213
+ ] })
214
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
215
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
216
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
217
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "File" }),
218
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
219
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Progress" }),
220
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Created" }),
221
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { className: "w-[100px]" })
222
+ ] }) }),
223
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: jobs.map((job) => {
224
+ const progress = getProgress(job);
225
+ return /* @__PURE__ */ jsxRuntime.jsxs(
226
+ ui.Table.Row,
227
+ {
228
+ className: "cursor-pointer hover:bg-ui-bg-subtle transition-colors",
229
+ onClick: () => navigate(`/import/${job.id}`),
230
+ children: [
231
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "font-mono", children: job.file_name }) }),
232
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: STATUS_COLOR$1[job.status] ?? "grey", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
233
+ STATUS_ICON$1[job.status],
234
+ job.status
235
+ ] }) }) }),
236
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
237
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-24 h-1.5 rounded-full bg-ui-bg-base overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
238
+ "div",
239
+ {
240
+ className: "h-full rounded-full bg-ui-bg-interactive transition-all duration-500",
241
+ style: { width: `${progress}%` }
242
+ }
243
+ ) }),
244
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle tabular-nums", children: [
245
+ job.processed,
246
+ "/",
247
+ job.total
248
+ ] })
249
+ ] }) }),
250
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: formatDate(job.created_at) }) }),
251
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { onClick: (e) => e.stopPropagation(), children: ["pending", "failed", "done"].includes(job.status) && /* @__PURE__ */ jsxRuntime.jsx(
252
+ ui.Button,
253
+ {
254
+ variant: "transparent",
255
+ size: "small",
256
+ disabled: deleting === job.id,
257
+ onClick: () => handleDelete(job.id),
258
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, { className: "w-4 h-4 text-ui-fg-subtle" })
259
+ }
260
+ ) })
261
+ ]
262
+ },
263
+ job.id
264
+ );
265
+ }) })
266
+ ] }),
267
+ totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-1 pt-4", children: [
268
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: [
269
+ "Page ",
270
+ page,
271
+ " of ",
272
+ totalPages
273
+ ] }),
274
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
275
+ /* @__PURE__ */ jsxRuntime.jsx(
276
+ ui.Button,
277
+ {
278
+ variant: "secondary",
279
+ size: "small",
280
+ disabled: page <= 1,
281
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
282
+ children: "Previous"
283
+ }
284
+ ),
285
+ /* @__PURE__ */ jsxRuntime.jsx(
286
+ ui.Button,
287
+ {
288
+ variant: "secondary",
289
+ size: "small",
290
+ disabled: page >= totalPages,
291
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
292
+ children: "Next"
293
+ }
294
+ )
295
+ ] })
296
+ ] })
297
+ ] })
298
+ ] })
299
+ ] });
300
+ }
301
+ const config = adminSdk.defineRouteConfig({
302
+ label: "Import Products",
303
+ icon: icons.ArrowUpTray,
304
+ rank: 1
305
+ });
306
+ const STATUS_COLOR = {
307
+ processing: "blue",
308
+ paused: "orange",
309
+ done: "green",
310
+ failed: "red",
311
+ pending: "grey"
312
+ };
313
+ const ROW_STATUS_COLOR = {
314
+ created: "green",
315
+ updated: "blue",
316
+ failed: "red",
317
+ skipped: "grey"
318
+ };
319
+ const STATUS_ICON = {
320
+ processing: /* @__PURE__ */ jsxRuntime.jsx(icons.Loader, { className: "w-4 h-4" }),
321
+ done: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircle, { className: "w-4 h-4" }),
322
+ failed: /* @__PURE__ */ jsxRuntime.jsx(icons.XCircle, { className: "w-4 h-4" }),
323
+ paused: /* @__PURE__ */ jsxRuntime.jsx(icons.Pause, { className: "w-4 h-4" }),
324
+ pending: /* @__PURE__ */ jsxRuntime.jsx(icons.Clock, { className: "w-4 h-4" })
325
+ };
326
+ const PAGE_SIZE = 20;
327
+ function DetailSkeleton() {
328
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
329
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex items-center gap-3", children: [
330
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-7 w-48 animate-pulse rounded bg-ui-bg-disabled" }),
331
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-20 animate-pulse rounded-full bg-ui-bg-disabled" })
332
+ ] }),
333
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
334
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-full animate-pulse rounded bg-ui-bg-disabled mb-2" }),
335
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-40 animate-pulse rounded bg-ui-bg-disabled" })
336
+ ] }),
337
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
338
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
339
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "SKU" }),
340
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Title" }),
341
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
342
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Error" })
343
+ ] }) }),
344
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Body, { children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Row, { children: Array.from({ length: 4 }).map((_2, j) => /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-full animate-pulse rounded bg-ui-bg-disabled" }) }, j)) }, i)) })
345
+ ] }) })
346
+ ] });
347
+ }
348
+ function ProgressBar({ processed, total, progress }) {
349
+ const pct = Math.min(Math.max(progress ?? 0, 0), 100);
350
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
351
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full h-2.5 rounded-full bg-ui-bg-base overflow-hidden", children: [
352
+ /* @__PURE__ */ jsxRuntime.jsx(
353
+ "div",
354
+ {
355
+ className: "h-full rounded-full bg-ui-bg-interactive transition-all duration-700 ease-out",
356
+ style: { width: `${pct}%` }
357
+ }
358
+ ),
359
+ pct > 0 && pct < 100 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 overflow-hidden rounded-full", children: /* @__PURE__ */ jsxRuntime.jsx(
360
+ "div",
361
+ {
362
+ className: "h-full w-full animate-[shimmer_1.5s_infinite]",
363
+ style: {
364
+ background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.15) 50%, transparent 100%)",
365
+ backgroundSize: "200% 100%"
366
+ }
367
+ }
368
+ ) })
369
+ ] }),
370
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
371
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: [
372
+ processed,
373
+ " / ",
374
+ total,
375
+ " products"
376
+ ] }),
377
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", weight: "plus", className: "text-ui-fg-base tabular-nums", children: [
378
+ pct,
379
+ "%"
380
+ ] })
381
+ ] })
382
+ ] });
383
+ }
384
+ function StatCard({ label, value, color }) {
385
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1 rounded-lg border border-ui-border-base bg-ui-bg-subtle px-4 py-3", children: [
386
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-muted", children: label }),
387
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "large", weight: "plus", className: color ?? "text-ui-fg-base", children: value })
388
+ ] });
389
+ }
390
+ function ImportReportPage() {
391
+ var _a;
392
+ const { id } = reactRouterDom.useParams();
393
+ const navigate = reactRouterDom.useNavigate();
394
+ const [job, setJob] = react.useState(null);
395
+ const [page, setPage] = react.useState(1);
396
+ const intervalRef = react.useRef(null);
397
+ react.useEffect(() => {
398
+ let cancelled = false;
399
+ const tick = async () => {
400
+ try {
401
+ const r = await fetch(`/admin/import/${id}`);
402
+ if (!r.ok) return;
403
+ const data = await r.json();
404
+ if (!cancelled) {
405
+ setJob(data);
406
+ if (data.status === "done" || data.status === "failed") {
407
+ if (intervalRef.current) {
408
+ clearInterval(intervalRef.current);
409
+ intervalRef.current = null;
410
+ }
411
+ }
412
+ }
413
+ } catch {
414
+ }
415
+ };
416
+ tick();
417
+ intervalRef.current = setInterval(tick, 2e3);
418
+ return () => {
419
+ cancelled = true;
420
+ if (intervalRef.current) {
421
+ clearInterval(intervalRef.current);
422
+ intervalRef.current = null;
423
+ }
424
+ };
425
+ }, [id]);
426
+ const pause = async () => {
427
+ await fetch(`/admin/import/${id}/pause`, { method: "POST" });
428
+ };
429
+ const resume = async () => {
430
+ await fetch(`/admin/import/${id}/resume`, { method: "POST" });
431
+ };
432
+ const del = async () => {
433
+ if (!window.confirm("Delete this import job and all its data?")) return;
434
+ await fetch(`/admin/import/${id}`, { method: "DELETE" });
435
+ navigate("/import");
436
+ };
437
+ const formatError = (err) => {
438
+ if (!err || err === "[object Object]") return "An unknown error occurred.";
439
+ return err;
440
+ };
441
+ const rows = (job == null ? void 0 : job.rows) ?? [];
442
+ const totalPages = Math.max(1, Math.ceil(rows.length / PAGE_SIZE));
443
+ const paginatedRows = react.useMemo(() => {
444
+ const start = (page - 1) * PAGE_SIZE;
445
+ return rows.slice(start, start + PAGE_SIZE);
446
+ }, [rows, page]);
447
+ react.useEffect(() => {
448
+ setPage(1);
449
+ }, [(_a = job == null ? void 0 : job.rows) == null ? void 0 : _a.length]);
450
+ if (!job) return /* @__PURE__ */ jsxRuntime.jsx(DetailSkeleton, {});
451
+ const isActive = job.status === "processing" || job.status === "paused";
452
+ const canDelete = ["pending", "done", "failed", "paused"].includes(job.status);
453
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
454
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
455
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
456
+ /* @__PURE__ */ jsxRuntime.jsx(
457
+ ui.Button,
458
+ {
459
+ variant: "transparent",
460
+ size: "small",
461
+ onClick: () => navigate("/import"),
462
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowLeft, { className: "w-4 h-4" })
463
+ }
464
+ ),
465
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
466
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: job.file_name }),
467
+ /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: STATUS_COLOR[job.status] ?? "grey", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1.5", children: [
468
+ STATUS_ICON[job.status],
469
+ job.status
470
+ ] }) })
471
+ ] })
472
+ ] }),
473
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
474
+ isActive && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: pause, children: "Pause" }),
475
+ job.status === "paused" && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", onClick: resume, children: "Resume" }),
476
+ canDelete && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "danger", size: "small", onClick: del, children: "Delete" })
477
+ ] })
478
+ ] }),
479
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ProgressBar, { processed: job.processed, total: job.total, progress: job.progress }) }),
480
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-4 gap-3", children: [
481
+ /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "Created", value: job.created, color: "text-ui-fg-interactive" }),
482
+ /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "Updated", value: job.updated, color: "text-ui-fg-interactive" }),
483
+ /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "Failed", value: job.failed, color: job.failed > 0 ? "text-ui-fg-error" : void 0 }),
484
+ /* @__PURE__ */ jsxRuntime.jsx(StatCard, { label: "Total Rows", value: job.total })
485
+ ] }) }),
486
+ job.error_message && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-ui-border-error bg-ui-bg-error p-4", children: [
487
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", weight: "plus", className: "text-ui-fg-error mb-1", children: job.status === "failed" ? "Import Failed" : "Errors" }),
488
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-subtle whitespace-pre-wrap break-words", children: formatError(job.error_message) })
489
+ ] }) }),
490
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
491
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
492
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
493
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Import Rows" }),
494
+ rows.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { size: "small", className: "text-ui-fg-subtle", children: rows.length })
495
+ ] }),
496
+ isActive && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-ui-fg-muted", children: [
497
+ /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowPath, { className: "w-3.5 h-3.5 animate-spin" }),
498
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", children: "Live updating" })
499
+ ] })
500
+ ] }),
501
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table, { children: [
502
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
503
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "SKU" }),
504
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Title" }),
505
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Status" }),
506
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.HeaderCell, { children: "Error" })
507
+ ] }) }),
508
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Body, { children: [
509
+ paginatedRows.map((row) => /* @__PURE__ */ jsxRuntime.jsxs(ui.Table.Row, { children: [
510
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-mono", children: row.sku }) }),
511
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", children: row.title }) }),
512
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: ROW_STATUS_COLOR[row.status] ?? "grey", children: row.status }) }),
513
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle max-w-[300px] truncate", children: row.error ?? "—" }) })
514
+ ] }, row.id)),
515
+ paginatedRows.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Row, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Table.Cell, { colSpan: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted py-10 block text-center", children: isActive ? "Processing..." : "No rows to display." }) }) })
516
+ ] })
517
+ ] }),
518
+ totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-1 pt-4", children: [
519
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "xsmall", className: "text-ui-fg-subtle", children: [
520
+ "Page ",
521
+ page,
522
+ " of ",
523
+ totalPages
524
+ ] }),
525
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
526
+ /* @__PURE__ */ jsxRuntime.jsx(
527
+ ui.Button,
528
+ {
529
+ variant: "secondary",
530
+ size: "small",
531
+ disabled: page <= 1,
532
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
533
+ children: "Previous"
534
+ }
535
+ ),
536
+ /* @__PURE__ */ jsxRuntime.jsx(
537
+ ui.Button,
538
+ {
539
+ variant: "secondary",
540
+ size: "small",
541
+ disabled: page >= totalPages,
542
+ onClick: () => setPage((p) => Math.min(totalPages, p + 1)),
543
+ children: "Next"
544
+ }
545
+ )
546
+ ] })
547
+ ] })
548
+ ] })
549
+ ] });
550
+ }
551
+ const i18nTranslations0 = {};
552
+ const widgetModule = { widgets: [] };
553
+ const routeModule = {
554
+ routes: [
555
+ {
556
+ Component: ImportPage,
557
+ path: "/import"
558
+ },
559
+ {
560
+ Component: ImportReportPage,
561
+ path: "/import/:id"
562
+ }
563
+ ]
564
+ };
565
+ const menuItemModule = {
566
+ menuItems: [
567
+ {
568
+ label: config.label,
569
+ icon: config.icon,
570
+ path: "/import",
571
+ nested: void 0,
572
+ rank: 1,
573
+ translationNs: void 0
574
+ }
575
+ ]
576
+ };
577
+ const formModule = { customFields: {} };
578
+ const displayModule = {
579
+ displays: {}
580
+ };
581
+ const i18nModule = { resources: i18nTranslations0 };
582
+ const plugin = {
583
+ widgetModule,
584
+ routeModule,
585
+ menuItemModule,
586
+ formModule,
587
+ displayModule,
588
+ i18nModule
589
+ };
590
+ module.exports = plugin;