mumz-strapi-plugin-coupon 1.1.1 → 3.0.0

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 (46) hide show
  1. package/README.md +15 -13
  2. package/dist/admin/index.js +677 -0
  3. package/dist/admin/index.mjs +678 -0
  4. package/dist/server/index.js +1201 -0
  5. package/dist/server/index.mjs +1202 -0
  6. package/package.json +44 -20
  7. package/strapi-admin.js +3 -0
  8. package/strapi-server.js +3 -1
  9. package/dist/bootstrap.d.ts +0 -5
  10. package/dist/bootstrap.js +0 -6
  11. package/dist/config/index.d.ts +0 -5
  12. package/dist/config/index.js +0 -6
  13. package/dist/content-types/coupon/index.d.ts +0 -96
  14. package/dist/content-types/coupon/index.js +0 -9
  15. package/dist/content-types/coupon/schema.d.ts +0 -94
  16. package/dist/content-types/coupon/schema.js +0 -117
  17. package/dist/content-types/index.d.ts +0 -161
  18. package/dist/content-types/index.js +0 -11
  19. package/dist/content-types/redemption/index.d.ts +0 -64
  20. package/dist/content-types/redemption/index.js +0 -9
  21. package/dist/content-types/redemption/schema.d.ts +0 -62
  22. package/dist/content-types/redemption/schema.js +0 -74
  23. package/dist/controllers/coupon.d.ts +0 -41
  24. package/dist/controllers/coupon.js +0 -154
  25. package/dist/controllers/index.d.ts +0 -5
  26. package/dist/controllers/index.js +0 -9
  27. package/dist/destroy.d.ts +0 -5
  28. package/dist/destroy.js +0 -6
  29. package/dist/index.d.ts +0 -207
  30. package/dist/index.js +0 -24
  31. package/dist/middlewares/index.d.ts +0 -4
  32. package/dist/middlewares/index.js +0 -9
  33. package/dist/middlewares/rate-limit.d.ts +0 -6
  34. package/dist/middlewares/rate-limit.js +0 -42
  35. package/dist/register.d.ts +0 -5
  36. package/dist/register.js +0 -6
  37. package/dist/routes/content-api/index.d.ts +0 -23
  38. package/dist/routes/content-api/index.js +0 -76
  39. package/dist/routes/index.d.ts +0 -25
  40. package/dist/routes/index.js +0 -9
  41. package/dist/services/coupon.d.ts +0 -64
  42. package/dist/services/coupon.js +0 -432
  43. package/dist/services/index.d.ts +0 -5
  44. package/dist/services/index.js +0 -9
  45. package/dist/utils/validators.d.ts +0 -14
  46. package/dist/utils/validators.js +0 -41
@@ -0,0 +1,678 @@
1
+ import { useEffect, useState, useRef, useCallback } from "react";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ import { Flex, Box, Typography, Table, Thead, Tr, Th, Tbody, Td, Badge, Button, Field, Loader } from "@strapi/design-system";
4
+ import { Check, Upload, Download, Gift } from "@strapi/icons";
5
+ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
6
+ const PLUGIN_ID = "coupon";
7
+ const Initializer = ({ setPlugin }) => {
8
+ useEffect(() => {
9
+ setPlugin(PLUGIN_ID);
10
+ }, [setPlugin]);
11
+ return null;
12
+ };
13
+ function parseCSV(csvText) {
14
+ const rows = [];
15
+ let currentRow = [];
16
+ let currentCell = "";
17
+ let insideQuotes = false;
18
+ for (let i = 0; i < csvText.length; i++) {
19
+ const char = csvText[i];
20
+ const nextChar = csvText[i + 1];
21
+ if (insideQuotes) {
22
+ if (char === '"') {
23
+ if (nextChar === '"') {
24
+ currentCell += '"';
25
+ i++;
26
+ } else {
27
+ insideQuotes = false;
28
+ }
29
+ } else {
30
+ currentCell += char;
31
+ }
32
+ } else {
33
+ if (char === '"') {
34
+ insideQuotes = true;
35
+ } else if (char === ",") {
36
+ currentRow.push(currentCell.trim());
37
+ currentCell = "";
38
+ } else if (char === "\n" || char === "\r" && nextChar === "\n") {
39
+ currentRow.push(currentCell.trim());
40
+ if (currentRow.length > 0 && currentRow.some((cell) => cell !== "")) {
41
+ rows.push(currentRow);
42
+ }
43
+ currentRow = [];
44
+ currentCell = "";
45
+ if (char === "\r") i++;
46
+ } else if (char !== "\r") {
47
+ currentCell += char;
48
+ }
49
+ }
50
+ }
51
+ if (currentCell || currentRow.length > 0) {
52
+ currentRow.push(currentCell.trim());
53
+ if (currentRow.some((cell) => cell !== "")) {
54
+ rows.push(currentRow);
55
+ }
56
+ }
57
+ return rows;
58
+ }
59
+ async function parseCsvFile(file) {
60
+ const text = await file.text();
61
+ const rows = parseCSV(text);
62
+ if (rows.length < 2) {
63
+ throw new Error("CSV file must have a header row and at least one data row");
64
+ }
65
+ const headers = rows[0].map((h) => h.toLowerCase().trim());
66
+ const coupons = [];
67
+ const errors = [];
68
+ const requiredHeaders = ["code", "discounttype", "discountvalue", "validfrom", "validto"];
69
+ const missingHeaders = requiredHeaders.filter((h) => !headers.includes(h));
70
+ if (missingHeaders.length > 0) {
71
+ throw new Error(`Missing required headers: ${missingHeaders.join(", ")}`);
72
+ }
73
+ const columnIndex = {
74
+ code: headers.indexOf("code"),
75
+ discountType: headers.indexOf("discounttype"),
76
+ discountValue: headers.indexOf("discountvalue"),
77
+ maxUsage: headers.indexOf("maxusage"),
78
+ maxUsagePerUser: headers.indexOf("maxusageperuser"),
79
+ validFrom: headers.indexOf("validfrom"),
80
+ validTo: headers.indexOf("validto"),
81
+ isActive: headers.indexOf("isactive"),
82
+ description: headers.indexOf("description"),
83
+ userRestrictions: headers.indexOf("userrestrictions")
84
+ };
85
+ for (let i = 1; i < rows.length; i++) {
86
+ const row = rows[i];
87
+ const rowNumber = i + 1;
88
+ try {
89
+ const code = (row[columnIndex.code] || "").trim().toUpperCase();
90
+ const discountTypeRaw = (row[columnIndex.discountType] || "").trim().toLowerCase();
91
+ const discountValueRaw = row[columnIndex.discountValue] || "";
92
+ const maxUsageRaw = columnIndex.maxUsage >= 0 ? row[columnIndex.maxUsage] : "";
93
+ const maxUsagePerUserRaw = columnIndex.maxUsagePerUser >= 0 ? row[columnIndex.maxUsagePerUser] : "";
94
+ const validFrom = (row[columnIndex.validFrom] || "").trim();
95
+ const validTo = (row[columnIndex.validTo] || "").trim();
96
+ const description = columnIndex.description >= 0 ? (row[columnIndex.description] || "").trim() : "";
97
+ const userRestrictionsRaw = columnIndex.userRestrictions >= 0 ? (row[columnIndex.userRestrictions] || "").trim() : "";
98
+ if (!code) {
99
+ errors.push(`Row ${rowNumber}: Missing coupon code`);
100
+ continue;
101
+ }
102
+ if (!/^[A-Z0-9]{3,50}$/.test(code)) {
103
+ errors.push(`Row ${rowNumber}: Invalid coupon code format (must be 3-50 uppercase alphanumeric characters)`);
104
+ continue;
105
+ }
106
+ if (!discountTypeRaw || !["percentage", "flat"].includes(discountTypeRaw)) {
107
+ errors.push(`Row ${rowNumber}: Invalid discount type (must be 'percentage' or 'flat')`);
108
+ continue;
109
+ }
110
+ const discountValue = parseFloat(discountValueRaw);
111
+ if (isNaN(discountValue) || discountValue <= 0) {
112
+ errors.push(`Row ${rowNumber}: Invalid discount value (must be a positive number)`);
113
+ continue;
114
+ }
115
+ if (discountTypeRaw === "percentage" && discountValue > 100) {
116
+ errors.push(`Row ${rowNumber}: Percentage discount cannot exceed 100`);
117
+ continue;
118
+ }
119
+ if (!validFrom) {
120
+ errors.push(`Row ${rowNumber}: Missing validFrom date`);
121
+ continue;
122
+ }
123
+ if (!validTo) {
124
+ errors.push(`Row ${rowNumber}: Missing validTo date`);
125
+ continue;
126
+ }
127
+ const fromDate = new Date(validFrom);
128
+ const toDate = new Date(validTo);
129
+ if (isNaN(fromDate.getTime())) {
130
+ errors.push(`Row ${rowNumber}: Invalid validFrom date format`);
131
+ continue;
132
+ }
133
+ if (isNaN(toDate.getTime())) {
134
+ errors.push(`Row ${rowNumber}: Invalid validTo date format`);
135
+ continue;
136
+ }
137
+ if (fromDate >= toDate) {
138
+ errors.push(`Row ${rowNumber}: validFrom must be before validTo`);
139
+ continue;
140
+ }
141
+ let maxUsage;
142
+ if (maxUsageRaw && maxUsageRaw.trim()) {
143
+ const parsed = parseInt(maxUsageRaw.trim(), 10);
144
+ if (isNaN(parsed) || parsed < 0) {
145
+ errors.push(`Row ${rowNumber}: Invalid maxUsage (must be a non-negative integer)`);
146
+ continue;
147
+ }
148
+ maxUsage = parsed === 0 ? void 0 : parsed;
149
+ }
150
+ let maxUsagePerUser;
151
+ if (maxUsagePerUserRaw && maxUsagePerUserRaw.trim()) {
152
+ const parsed = parseInt(maxUsagePerUserRaw.trim(), 10);
153
+ if (isNaN(parsed) || parsed < 0) {
154
+ errors.push(`Row ${rowNumber}: Invalid maxUsagePerUser (must be a non-negative integer)`);
155
+ continue;
156
+ }
157
+ maxUsagePerUser = parsed;
158
+ }
159
+ let userRestrictions;
160
+ if (userRestrictionsRaw) {
161
+ try {
162
+ const parsed = JSON.parse(userRestrictionsRaw);
163
+ if (Array.isArray(parsed)) {
164
+ userRestrictions = parsed.filter((item) => typeof item === "string");
165
+ } else {
166
+ errors.push(`Row ${rowNumber}: userRestrictions must be a JSON array`);
167
+ continue;
168
+ }
169
+ } catch {
170
+ userRestrictions = userRestrictionsRaw.split(",").map((s) => s.trim()).filter(Boolean);
171
+ }
172
+ }
173
+ coupons.push({
174
+ _row: rowNumber,
175
+ code,
176
+ discountType: discountTypeRaw,
177
+ discountValue,
178
+ maxUsage,
179
+ maxUsagePerUser,
180
+ validFrom: fromDate.toISOString(),
181
+ validTo: toDate.toISOString(),
182
+ description: description || void 0,
183
+ userRestrictions
184
+ });
185
+ } catch (err) {
186
+ errors.push(`Row ${rowNumber}: ${err instanceof Error ? err.message : "Unknown error"}`);
187
+ }
188
+ }
189
+ return { coupons, errors };
190
+ }
191
+ const ImportResults = ({ result }) => {
192
+ const { summary, created, failed } = result;
193
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
194
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, children: [
195
+ /* @__PURE__ */ jsxs(
196
+ Box,
197
+ {
198
+ padding: 4,
199
+ background: "neutral100",
200
+ hasRadius: true,
201
+ style: { flex: 1 },
202
+ children: [
203
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: "Total" }),
204
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", children: summary.total })
205
+ ]
206
+ }
207
+ ),
208
+ /* @__PURE__ */ jsxs(
209
+ Box,
210
+ {
211
+ padding: 4,
212
+ background: "success100",
213
+ hasRadius: true,
214
+ style: { flex: 1 },
215
+ children: [
216
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "success600", children: "Created" }),
217
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", textColor: "success600", children: summary.created })
218
+ ]
219
+ }
220
+ ),
221
+ /* @__PURE__ */ jsxs(
222
+ Box,
223
+ {
224
+ padding: 4,
225
+ background: summary.failed > 0 ? "danger100" : "neutral100",
226
+ hasRadius: true,
227
+ style: { flex: 1 },
228
+ children: [
229
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: summary.failed > 0 ? "danger600" : "neutral600", children: "Failed" }),
230
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", textColor: summary.failed > 0 ? "danger600" : "neutral600", children: summary.failed })
231
+ ]
232
+ }
233
+ )
234
+ ] }),
235
+ failed.length > 0 && /* @__PURE__ */ jsxs(Box, { children: [
236
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", marginBottom: 2, children: "Failed Imports" }),
237
+ /* @__PURE__ */ jsxs(
238
+ Box,
239
+ {
240
+ background: "neutral0",
241
+ borderColor: "neutral150",
242
+ borderStyle: "solid",
243
+ borderWidth: "1px",
244
+ hasRadius: true,
245
+ style: { maxHeight: "300px", overflow: "auto" },
246
+ children: [
247
+ /* @__PURE__ */ jsxs(Table, { colCount: 4, rowCount: failed.length, children: [
248
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
249
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Row" }) }),
250
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Code" }) }),
251
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Error" }) }),
252
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Error Code" }) })
253
+ ] }) }),
254
+ /* @__PURE__ */ jsx(Tbody, { children: failed.slice(0, 50).map((item, index2) => /* @__PURE__ */ jsxs(Tr, { children: [
255
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { children: item.row }) }),
256
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", fontWeight: "semiBold", children: item.code || "-" }) }),
257
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "danger600", children: item.error }) }),
258
+ /* @__PURE__ */ jsx(Td, { children: item.errorCode && /* @__PURE__ */ jsx(Badge, { textColor: "danger600", backgroundColor: "danger100", children: item.errorCode }) })
259
+ ] }, index2)) })
260
+ ] }),
261
+ failed.length > 50 && /* @__PURE__ */ jsx(Box, { padding: 2, background: "neutral100", children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
262
+ "Showing 50 of ",
263
+ failed.length,
264
+ " failed items"
265
+ ] }) })
266
+ ]
267
+ }
268
+ )
269
+ ] }),
270
+ summary.created > 0 && summary.failed === 0 && /* @__PURE__ */ jsxs(
271
+ Flex,
272
+ {
273
+ padding: 4,
274
+ background: "success100",
275
+ borderColor: "success200",
276
+ borderWidth: "1px",
277
+ borderStyle: "solid",
278
+ hasRadius: true,
279
+ alignItems: "center",
280
+ gap: 2,
281
+ children: [
282
+ /* @__PURE__ */ jsx(Check, { width: 24, height: 24, fill: "success600" }),
283
+ /* @__PURE__ */ jsxs(Typography, { variant: "delta", textColor: "success600", children: [
284
+ "All ",
285
+ summary.created,
286
+ " coupons imported successfully!"
287
+ ] })
288
+ ]
289
+ }
290
+ ),
291
+ summary.created > 0 && summary.failed > 0 && /* @__PURE__ */ jsx(
292
+ Flex,
293
+ {
294
+ padding: 4,
295
+ background: "warning100",
296
+ borderColor: "warning200",
297
+ borderWidth: "1px",
298
+ borderStyle: "solid",
299
+ hasRadius: true,
300
+ alignItems: "center",
301
+ gap: 2,
302
+ children: /* @__PURE__ */ jsxs(Typography, { variant: "delta", textColor: "warning600", children: [
303
+ "Partially imported: ",
304
+ summary.created,
305
+ " succeeded, ",
306
+ summary.failed,
307
+ " failed"
308
+ ] })
309
+ }
310
+ )
311
+ ] });
312
+ };
313
+ const ImportModal = ({ onClose }) => {
314
+ const [file, setFile] = useState(null);
315
+ const [parsedCoupons, setParsedCoupons] = useState([]);
316
+ const [parseErrors, setParseErrors] = useState([]);
317
+ const [status, setStatus] = useState("idle");
318
+ const [result, setResult] = useState(null);
319
+ const fileInputRef = useRef(null);
320
+ const { post } = useFetchClient();
321
+ const { toggleNotification } = useNotification();
322
+ const handleSelectFile = () => {
323
+ fileInputRef.current?.click();
324
+ };
325
+ const handleFileChange = useCallback(async (e) => {
326
+ const selectedFile = e.target.files?.[0];
327
+ if (!selectedFile) return;
328
+ if (!selectedFile.name.endsWith(".csv")) {
329
+ toggleNotification({
330
+ type: "danger",
331
+ message: "Please select a CSV file"
332
+ });
333
+ return;
334
+ }
335
+ setFile(selectedFile);
336
+ setStatus("parsing");
337
+ setParseErrors([]);
338
+ setParsedCoupons([]);
339
+ try {
340
+ const { coupons, errors } = await parseCsvFile(selectedFile);
341
+ setParsedCoupons(coupons);
342
+ setParseErrors(errors);
343
+ setStatus("idle");
344
+ } catch (error) {
345
+ console.error("Error parsing CSV:", error);
346
+ setParseErrors([error instanceof Error ? error.message : "Failed to parse CSV file"]);
347
+ setStatus("error");
348
+ }
349
+ }, [toggleNotification]);
350
+ const handleImport = async () => {
351
+ if (parsedCoupons.length === 0) {
352
+ toggleNotification({
353
+ type: "danger",
354
+ message: "No valid coupons to import"
355
+ });
356
+ return;
357
+ }
358
+ setStatus("uploading");
359
+ try {
360
+ const response = await post("/api/coupon/bulk-create", {
361
+ coupons: parsedCoupons
362
+ });
363
+ setResult(response.data);
364
+ setStatus("complete");
365
+ const resultData = response.data;
366
+ if (resultData.summary.created > 0) {
367
+ toggleNotification({
368
+ type: "success",
369
+ message: `Successfully imported ${resultData.summary.created} coupons`
370
+ });
371
+ }
372
+ } catch (error) {
373
+ console.error("Error importing coupons:", error);
374
+ setStatus("error");
375
+ toggleNotification({
376
+ type: "danger",
377
+ message: "Failed to import coupons"
378
+ });
379
+ }
380
+ };
381
+ const handleReset = () => {
382
+ setFile(null);
383
+ setParsedCoupons([]);
384
+ setParseErrors([]);
385
+ setStatus("idle");
386
+ setResult(null);
387
+ };
388
+ if (status === "complete" && result) {
389
+ return /* @__PURE__ */ jsxs(Box, { padding: 4, children: [
390
+ /* @__PURE__ */ jsx(ImportResults, { result }),
391
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, marginTop: 4, justifyContent: "flex-end", children: [
392
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: handleReset, children: "Import More" }),
393
+ /* @__PURE__ */ jsx(Button, { onClick: onClose, children: "Done" })
394
+ ] })
395
+ ] });
396
+ }
397
+ return /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
398
+ /* @__PURE__ */ jsx(
399
+ "input",
400
+ {
401
+ ref: fileInputRef,
402
+ type: "file",
403
+ accept: ".csv",
404
+ onChange: handleFileChange,
405
+ style: { display: "none" }
406
+ }
407
+ ),
408
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
409
+ /* @__PURE__ */ jsx(Field.Label, { children: "Select CSV File" }),
410
+ /* @__PURE__ */ jsx(
411
+ Box,
412
+ {
413
+ padding: 4,
414
+ borderColor: "neutral200",
415
+ borderStyle: "dashed",
416
+ borderWidth: "1px",
417
+ hasRadius: true,
418
+ background: "neutral100",
419
+ onClick: handleSelectFile,
420
+ style: { cursor: "pointer" },
421
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 2, children: [
422
+ /* @__PURE__ */ jsx(Upload, { width: 32, height: 32 }),
423
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: file ? file.name : "Click to select a CSV file" })
424
+ ] })
425
+ }
426
+ )
427
+ ] }),
428
+ status === "parsing" && /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { small: true, children: "Parsing CSV file..." }) }),
429
+ parseErrors.length > 0 && /* @__PURE__ */ jsxs(
430
+ Box,
431
+ {
432
+ padding: 4,
433
+ background: "danger100",
434
+ borderColor: "danger200",
435
+ borderWidth: "1px",
436
+ borderStyle: "solid",
437
+ hasRadius: true,
438
+ children: [
439
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", textColor: "danger600", children: "Parse Errors:" }),
440
+ /* @__PURE__ */ jsxs("ul", { style: { margin: 0, paddingLeft: "1.5rem" }, children: [
441
+ parseErrors.slice(0, 10).map((error, index2) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: error }) }, index2)),
442
+ parseErrors.length > 10 && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
443
+ "...and ",
444
+ parseErrors.length - 10,
445
+ " more errors"
446
+ ] }) })
447
+ ] })
448
+ ]
449
+ }
450
+ ),
451
+ parsedCoupons.length > 0 && status === "idle" && /* @__PURE__ */ jsxs(
452
+ Box,
453
+ {
454
+ padding: 4,
455
+ background: "success100",
456
+ borderColor: "success200",
457
+ borderWidth: "1px",
458
+ borderStyle: "solid",
459
+ hasRadius: true,
460
+ children: [
461
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, children: [
462
+ /* @__PURE__ */ jsx(Check, { width: 20, height: 20, fill: "success600" }),
463
+ /* @__PURE__ */ jsxs(Typography, { variant: "delta", textColor: "success600", children: [
464
+ parsedCoupons.length,
465
+ " coupons ready to import"
466
+ ] })
467
+ ] }),
468
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", marginTop: 2, children: [
469
+ "First coupon: ",
470
+ parsedCoupons[0]?.code,
471
+ " (",
472
+ parsedCoupons[0]?.discountType,
473
+ ": ",
474
+ parsedCoupons[0]?.discountValue,
475
+ ")"
476
+ ] })
477
+ ]
478
+ }
479
+ ),
480
+ status === "uploading" && /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsx(Loader, { small: true, children: "Importing coupons..." }) }),
481
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, justifyContent: "flex-end", children: [
482
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: onClose, disabled: status === "uploading", children: "Cancel" }),
483
+ /* @__PURE__ */ jsxs(
484
+ Button,
485
+ {
486
+ onClick: handleImport,
487
+ disabled: parsedCoupons.length === 0 || status !== "idle",
488
+ startIcon: /* @__PURE__ */ jsx(Upload, {}),
489
+ children: [
490
+ "Import ",
491
+ parsedCoupons.length > 0 && `(${parsedCoupons.length})`
492
+ ]
493
+ }
494
+ )
495
+ ] })
496
+ ] }) });
497
+ };
498
+ const CsvPage = () => {
499
+ const [showImportModal, setShowImportModal] = useState(false);
500
+ const { toggleNotification } = useNotification();
501
+ const handleDownloadTemplate = async () => {
502
+ try {
503
+ const response = await fetch("/api/coupon/csv-template");
504
+ if (!response.ok) {
505
+ throw new Error("Failed to download template");
506
+ }
507
+ const blob = await response.blob();
508
+ const url = window.URL.createObjectURL(blob);
509
+ const link = document.createElement("a");
510
+ link.href = url;
511
+ link.download = "coupon-template.csv";
512
+ document.body.appendChild(link);
513
+ link.click();
514
+ document.body.removeChild(link);
515
+ window.URL.revokeObjectURL(url);
516
+ toggleNotification({
517
+ type: "success",
518
+ message: "CSV template downloaded successfully"
519
+ });
520
+ } catch (error) {
521
+ console.error("Error downloading CSV template:", error);
522
+ toggleNotification({
523
+ type: "danger",
524
+ message: "Failed to download CSV template"
525
+ });
526
+ }
527
+ };
528
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, children: [
529
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 6, children: [
530
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", children: "Coupon Bulk Tools" }),
531
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: "Download template or bulk import coupons from CSV" })
532
+ ] }),
533
+ /* @__PURE__ */ jsxs(Box, { padding: 6, background: "neutral0", shadow: "tableShadow", hasRadius: true, children: [
534
+ /* @__PURE__ */ jsxs(Flex, { gap: 6, wrap: "wrap", children: [
535
+ /* @__PURE__ */ jsx(
536
+ Box,
537
+ {
538
+ padding: 6,
539
+ background: "neutral100",
540
+ hasRadius: true,
541
+ style: { flex: "1 1 300px", minWidth: "280px" },
542
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 4, children: [
543
+ /* @__PURE__ */ jsx(Download, { width: 48, height: 48 }),
544
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", textAlign: "center", children: "Download CSV Template" }),
545
+ /* @__PURE__ */ jsx(
546
+ Typography,
547
+ {
548
+ variant: "pi",
549
+ textColor: "neutral600",
550
+ textAlign: "center",
551
+ children: "Get a CSV template with all required columns and example data"
552
+ }
553
+ ),
554
+ /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Download, {}), onClick: handleDownloadTemplate, children: "Download Template" })
555
+ ] })
556
+ }
557
+ ),
558
+ /* @__PURE__ */ jsx(
559
+ Box,
560
+ {
561
+ padding: 6,
562
+ background: "neutral100",
563
+ hasRadius: true,
564
+ style: { flex: "1 1 300px", minWidth: "280px" },
565
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 4, children: [
566
+ /* @__PURE__ */ jsx(Upload, { width: 48, height: 48 }),
567
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", textAlign: "center", children: "Import Coupons from CSV" }),
568
+ /* @__PURE__ */ jsx(
569
+ Typography,
570
+ {
571
+ variant: "pi",
572
+ textColor: "neutral600",
573
+ textAlign: "center",
574
+ children: "Upload a CSV file to bulk create coupons"
575
+ }
576
+ ),
577
+ /* @__PURE__ */ jsx(
578
+ Button,
579
+ {
580
+ startIcon: /* @__PURE__ */ jsx(Upload, {}),
581
+ onClick: () => setShowImportModal(true),
582
+ variant: "secondary",
583
+ children: "Import CSV"
584
+ }
585
+ )
586
+ ] })
587
+ }
588
+ )
589
+ ] }),
590
+ /* @__PURE__ */ jsx(Box, { marginTop: 6, children: /* @__PURE__ */ jsxs(Typography, { variant: "omega", textColor: "neutral600", children: [
591
+ /* @__PURE__ */ jsx("strong", { children: "CSV Format:" }),
592
+ " code, discountType (percentage/flat), discountValue, maxUsage, maxUsagePerUser, validFrom (ISO date), validTo (ISO date), description, userRestrictions (JSON array)"
593
+ ] }) })
594
+ ] }),
595
+ showImportModal && /* @__PURE__ */ jsxs(Fragment, { children: [
596
+ /* @__PURE__ */ jsx(
597
+ Box,
598
+ {
599
+ position: "fixed",
600
+ top: 0,
601
+ left: 0,
602
+ right: 0,
603
+ bottom: 0,
604
+ background: "neutral800",
605
+ style: { opacity: 0.5, zIndex: 100 },
606
+ onClick: () => setShowImportModal(false)
607
+ }
608
+ ),
609
+ /* @__PURE__ */ jsxs(
610
+ Box,
611
+ {
612
+ position: "fixed",
613
+ top: "50%",
614
+ left: "50%",
615
+ style: {
616
+ transform: "translate(-50%, -50%)",
617
+ zIndex: 101,
618
+ width: "600px",
619
+ maxWidth: "90vw",
620
+ maxHeight: "90vh",
621
+ overflow: "auto"
622
+ },
623
+ background: "neutral0",
624
+ shadow: "popupShadow",
625
+ hasRadius: true,
626
+ padding: 6,
627
+ children: [
628
+ /* @__PURE__ */ jsxs(
629
+ Flex,
630
+ {
631
+ justifyContent: "space-between",
632
+ alignItems: "center",
633
+ marginBottom: 4,
634
+ children: [
635
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", children: "Import Coupons from CSV" }),
636
+ /* @__PURE__ */ jsx(
637
+ Button,
638
+ {
639
+ variant: "tertiary",
640
+ onClick: () => setShowImportModal(false),
641
+ children: "Close"
642
+ }
643
+ )
644
+ ]
645
+ }
646
+ ),
647
+ /* @__PURE__ */ jsx(ImportModal, { onClose: () => setShowImportModal(false) })
648
+ ]
649
+ }
650
+ )
651
+ ] })
652
+ ] });
653
+ };
654
+ const index = {
655
+ register(app) {
656
+ app.addMenuLink({
657
+ to: `/plugins/${PLUGIN_ID}`,
658
+ icon: Gift,
659
+ intlLabel: {
660
+ id: `${PLUGIN_ID}.plugin.name`,
661
+ defaultMessage: "Coupon Bulk"
662
+ },
663
+ Component: async () => ({ default: CsvPage }),
664
+ permissions: []
665
+ });
666
+ app.registerPlugin({
667
+ id: PLUGIN_ID,
668
+ initializer: Initializer,
669
+ isReady: false,
670
+ name: PLUGIN_ID
671
+ });
672
+ },
673
+ bootstrap() {
674
+ }
675
+ };
676
+ export {
677
+ index as default
678
+ };