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