@lodashventure/medusa-campaign 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +741 -306
- package/.medusa/server/src/admin/index.mjs +742 -307
- package/.medusa/server/src/api/admin/buy-x-get-y/[id]/route.js +2 -6
- package/.medusa/server/src/api/admin/coupons/[id]/route.js +76 -0
- package/.medusa/server/src/api/admin/coupons/route.js +88 -0
- package/.medusa/server/src/api/middlewares.js +32 -1
- package/.medusa/server/src/api/store/campaigns/route.js +78 -7
- package/.medusa/server/src/api/store/coupons/public/route.js +110 -0
- package/.medusa/server/src/api/store/customers/me/coupons/route.js +148 -0
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251024000000.js +2 -2
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251025000000.js +2 -2
- package/.medusa/server/src/modules/custom-campaigns/migrations/Migration20251101000000.js +16 -0
- package/.medusa/server/src/modules/custom-campaigns/types/campaign-type.enum.js +2 -1
- package/.medusa/server/src/workflows/custom-campaign/createCouponCampaignWorkflow.js +105 -0
- package/.medusa/server/src/workflows/custom-campaign/updateCouponCampaignWorkflow.js +59 -0
- package/.medusa/server/src/workflows/index.js +6 -2
- package/package.json +15 -30
- package/src/admin/components/BuyXGetYForm.tsx +24 -13
- package/src/admin/components/CouponForm.tsx +352 -0
- package/src/admin/components/CouponPage.tsx +104 -0
- package/src/admin/components/ProductSelector.tsx +22 -11
- package/src/admin/hooks/useCouponById.ts +36 -0
- package/src/admin/hooks/useCoupons.ts +46 -0
- package/src/admin/hooks/useFlashSaleById.ts +36 -10
- package/src/admin/hooks/useFlashSales.ts +36 -10
- package/src/admin/routes/coupons/[id]/page.tsx +147 -0
- package/src/admin/routes/coupons/create/page.tsx +49 -0
- package/src/admin/routes/coupons/page.tsx +15 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +2 -11
- package/src/admin/routes/flash-sales/create/page.tsx +0 -6
- package/src/admin/widgets/campaign-detail-widget.tsx +33 -26
- package/src/api/admin/buy-x-get-y/[id]/route.ts +11 -15
- package/src/api/admin/coupons/[id]/route.ts +98 -0
- package/src/api/admin/coupons/route.ts +109 -0
- package/src/api/middlewares.ts +34 -0
- package/src/api/store/campaigns/route.ts +107 -24
- package/src/api/store/coupons/public/route.ts +165 -0
- package/src/api/store/customers/me/coupons/route.ts +244 -0
- package/src/modules/custom-campaigns/migrations/Migration20251024000000.ts +1 -1
- package/src/modules/custom-campaigns/migrations/Migration20251025000000.ts +1 -1
- package/src/modules/custom-campaigns/migrations/Migration20251101000000.ts +21 -0
- package/src/modules/custom-campaigns/types/campaign-type.enum.ts +1 -0
- package/src/workflows/custom-campaign/createCouponCampaignWorkflow.ts +176 -0
- package/src/workflows/custom-campaign/updateCouponCampaignWorkflow.ts +105 -0
- package/src/workflows/index.ts +3 -1
- package/src/admin/widgets/campaign-stats-widget.tsx +0 -238
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
3
|
import { Container, Heading, Text, Alert, Badge, Button, createDataTableColumnHelper, useDataTable, DataTable, Input, Label, Textarea, Table, Select, FocusModal, toast, Tabs, clx } from "@medusajs/ui";
|
|
4
|
-
import { Sparkles, PhotoSolid, PencilSquare, Eye,
|
|
4
|
+
import { Sparkles, PhotoSolid, PencilSquare, Eye, Tag, Trash, InformationCircle, CommandLine, X, CloudArrowUp } from "@medusajs/icons";
|
|
5
5
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
|
6
6
|
import { useNavigate, useParams } from "react-router-dom";
|
|
7
7
|
import dayjs from "dayjs";
|
|
8
|
-
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
9
8
|
import axios from "axios";
|
|
10
9
|
import z from "zod";
|
|
11
10
|
import { debounce } from "lodash";
|
|
12
11
|
import Medusa from "@medusajs/js-sdk";
|
|
13
|
-
const CampaignDetailWidget = ({
|
|
12
|
+
const CampaignDetailWidget = ({
|
|
13
|
+
data: promotion
|
|
14
|
+
}) => {
|
|
14
15
|
const navigate = useNavigate();
|
|
15
16
|
const [campaign, setCampaign] = useState(null);
|
|
16
|
-
const [campaignDetail, setCampaignDetail] = useState(
|
|
17
|
+
const [campaignDetail, setCampaignDetail] = useState(
|
|
18
|
+
null
|
|
19
|
+
);
|
|
17
20
|
const [loading, setLoading] = useState(true);
|
|
18
21
|
const [error, setError] = useState(null);
|
|
19
22
|
useEffect(() => {
|
|
@@ -110,11 +113,21 @@ const CampaignDetailWidget = ({ data: promotion }) => {
|
|
|
110
113
|
] }),
|
|
111
114
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2 mt-3 pt-3 border-t", children: [
|
|
112
115
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
113
|
-
/* @__PURE__ */ jsx(
|
|
116
|
+
/* @__PURE__ */ jsx(
|
|
117
|
+
PhotoSolid,
|
|
118
|
+
{
|
|
119
|
+
className: `h-4 w-4 ${hasImages ? "text-green-500" : "text-ui-fg-muted"}`
|
|
120
|
+
}
|
|
121
|
+
),
|
|
114
122
|
/* @__PURE__ */ jsx(Text, { className: "text-xs", children: hasImages ? "Images added" : "No images" })
|
|
115
123
|
] }),
|
|
116
124
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
117
|
-
/* @__PURE__ */ jsx(
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
PencilSquare,
|
|
127
|
+
{
|
|
128
|
+
className: `h-4 w-4 ${hasContent ? "text-green-500" : "text-ui-fg-muted"}`
|
|
129
|
+
}
|
|
130
|
+
),
|
|
118
131
|
/* @__PURE__ */ jsx(Text, { className: "text-xs", children: hasContent ? "Content added" : "No content" })
|
|
119
132
|
] })
|
|
120
133
|
] })
|
|
@@ -149,18 +162,10 @@ const CampaignDetailWidget = ({ data: promotion }) => {
|
|
|
149
162
|
] }),
|
|
150
163
|
!hasCampaignDetail && /* @__PURE__ */ jsx(Alert, { variant: "info", className: "mb-4", children: /* @__PURE__ */ jsx(Text, { className: "text-sm", children: "No campaign details added yet. Add images, content, and SEO to enhance this campaign." }) }),
|
|
151
164
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
152
|
-
/* @__PURE__ */ jsxs(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
size: "small",
|
|
157
|
-
onClick: handleEditCampaign,
|
|
158
|
-
children: [
|
|
159
|
-
/* @__PURE__ */ jsx(PencilSquare, { className: "mr-1" }),
|
|
160
|
-
"Edit Campaign"
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
),
|
|
165
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "small", onClick: handleEditCampaign, children: [
|
|
166
|
+
/* @__PURE__ */ jsx(PencilSquare, { className: "mr-1" }),
|
|
167
|
+
"Edit Campaign"
|
|
168
|
+
] }),
|
|
164
169
|
hasCampaignDetail && /* @__PURE__ */ jsxs(
|
|
165
170
|
Button,
|
|
166
171
|
{
|
|
@@ -189,185 +194,35 @@ const CampaignDetailWidget = ({ data: promotion }) => {
|
|
|
189
194
|
defineWidgetConfig({
|
|
190
195
|
zone: "promotion.details.side.after"
|
|
191
196
|
});
|
|
192
|
-
const
|
|
193
|
-
const [
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
complete: 0
|
|
200
|
-
});
|
|
201
|
-
const [loading, setLoading] = useState(true);
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
fetchStats();
|
|
204
|
-
}, []);
|
|
205
|
-
const fetchStats = async () => {
|
|
206
|
-
setLoading(true);
|
|
197
|
+
const useFlashSales = (pagination) => {
|
|
198
|
+
const [data, setData] = useState(null);
|
|
199
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
200
|
+
const [error, setError] = useState(null);
|
|
201
|
+
const fetchFlashSales = async () => {
|
|
202
|
+
setIsLoading(true);
|
|
203
|
+
setError(null);
|
|
207
204
|
try {
|
|
208
|
-
const response = await
|
|
209
|
-
|
|
205
|
+
const response = await axios.get("/admin/flash-sales", {
|
|
206
|
+
params: pagination
|
|
210
207
|
});
|
|
211
|
-
|
|
212
|
-
const data = await response.json();
|
|
213
|
-
const campaigns = data.campaigns || [];
|
|
214
|
-
const now = /* @__PURE__ */ new Date();
|
|
215
|
-
let active = 0;
|
|
216
|
-
let inactive = 0;
|
|
217
|
-
let withImages = 0;
|
|
218
|
-
let withContent = 0;
|
|
219
|
-
let complete = 0;
|
|
220
|
-
campaigns.forEach((campaign) => {
|
|
221
|
-
if (campaign.ends_at && new Date(campaign.ends_at) > now) {
|
|
222
|
-
active++;
|
|
223
|
-
} else {
|
|
224
|
-
inactive++;
|
|
225
|
-
}
|
|
226
|
-
if (campaign.campaign_detail) {
|
|
227
|
-
if (campaign.campaign_detail.image_url || campaign.campaign_detail.thumbnail_url) {
|
|
228
|
-
withImages++;
|
|
229
|
-
}
|
|
230
|
-
if (campaign.campaign_detail.detail_content) {
|
|
231
|
-
withContent++;
|
|
232
|
-
}
|
|
233
|
-
if ((campaign.campaign_detail.image_url || campaign.campaign_detail.thumbnail_url) && campaign.campaign_detail.detail_content) {
|
|
234
|
-
complete++;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
setStats({
|
|
239
|
-
total: campaigns.length,
|
|
240
|
-
active,
|
|
241
|
-
inactive,
|
|
242
|
-
with_images: withImages,
|
|
243
|
-
with_content: withContent,
|
|
244
|
-
complete
|
|
245
|
-
});
|
|
246
|
-
}
|
|
208
|
+
setData(response.data);
|
|
247
209
|
} catch (err) {
|
|
248
|
-
|
|
210
|
+
setError(
|
|
211
|
+
err instanceof Error ? err : new Error("Failed to fetch flash sales")
|
|
212
|
+
);
|
|
249
213
|
} finally {
|
|
250
|
-
|
|
214
|
+
setIsLoading(false);
|
|
251
215
|
}
|
|
252
216
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
const completionRate = stats.total > 0 ? Math.round(stats.complete / stats.total * 100) : 0;
|
|
263
|
-
return /* @__PURE__ */ jsxs(Container, { className: "px-6 py-6", children: [
|
|
264
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-6", children: [
|
|
265
|
-
/* @__PURE__ */ jsx(Sparkles, { className: "text-ui-fg-subtle" }),
|
|
266
|
-
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Campaign Overview" })
|
|
267
|
-
] }),
|
|
268
|
-
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-4 mb-6", children: [
|
|
269
|
-
/* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg border bg-ui-bg-subtle", children: [
|
|
270
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
271
|
-
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle", children: "Total" }),
|
|
272
|
-
/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4 text-ui-fg-subtle" })
|
|
273
|
-
] }),
|
|
274
|
-
/* @__PURE__ */ jsx(Text, { className: "text-2xl font-bold", children: stats.total })
|
|
275
|
-
] }),
|
|
276
|
-
/* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg border bg-green-50", children: [
|
|
277
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
278
|
-
/* @__PURE__ */ jsx(Text, { className: "text-xs text-green-700", children: "Active" }),
|
|
279
|
-
/* @__PURE__ */ jsx(CheckCircleSolid, { className: "h-4 w-4 text-green-600" })
|
|
280
|
-
] }),
|
|
281
|
-
/* @__PURE__ */ jsx(Text, { className: "text-2xl font-bold text-green-700", children: stats.active })
|
|
282
|
-
] }),
|
|
283
|
-
/* @__PURE__ */ jsxs("div", { className: "p-4 rounded-lg border bg-gray-50", children: [
|
|
284
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
285
|
-
/* @__PURE__ */ jsx(Text, { className: "text-xs text-gray-600", children: "Inactive" }),
|
|
286
|
-
/* @__PURE__ */ jsx(ClockSolid, { className: "h-4 w-4 text-gray-500" })
|
|
287
|
-
] }),
|
|
288
|
-
/* @__PURE__ */ jsx(Text, { className: "text-2xl font-bold text-gray-600", children: stats.inactive })
|
|
289
|
-
] })
|
|
290
|
-
] }),
|
|
291
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-3 mb-6", children: [
|
|
292
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3 rounded-lg bg-ui-bg-subtle", children: [
|
|
293
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
294
|
-
/* @__PURE__ */ jsx(PhotoSolid, { className: "h-4 w-4 text-blue-600" }),
|
|
295
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm", children: "With Images" })
|
|
296
|
-
] }),
|
|
297
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
298
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm font-medium", children: stats.with_images }),
|
|
299
|
-
/* @__PURE__ */ jsxs(Badge, { size: "xsmall", color: "blue", children: [
|
|
300
|
-
stats.total > 0 ? Math.round(stats.with_images / stats.total * 100) : 0,
|
|
301
|
-
"%"
|
|
302
|
-
] })
|
|
303
|
-
] })
|
|
304
|
-
] }),
|
|
305
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3 rounded-lg bg-ui-bg-subtle", children: [
|
|
306
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
307
|
-
/* @__PURE__ */ jsx(CheckCircleSolid, { className: "h-4 w-4 text-green-600" }),
|
|
308
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm", children: "With Content" })
|
|
309
|
-
] }),
|
|
310
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
311
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm font-medium", children: stats.with_content }),
|
|
312
|
-
/* @__PURE__ */ jsxs(Badge, { size: "xsmall", color: "green", children: [
|
|
313
|
-
stats.total > 0 ? Math.round(stats.with_content / stats.total * 100) : 0,
|
|
314
|
-
"%"
|
|
315
|
-
] })
|
|
316
|
-
] })
|
|
317
|
-
] }),
|
|
318
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-3 rounded-lg bg-ui-bg-subtle", children: [
|
|
319
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
320
|
-
/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4 text-purple-600" }),
|
|
321
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm", children: "Complete Details" })
|
|
322
|
-
] }),
|
|
323
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
324
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm font-medium", children: stats.complete }),
|
|
325
|
-
/* @__PURE__ */ jsxs(Badge, { size: "xsmall", color: "purple", children: [
|
|
326
|
-
completionRate,
|
|
327
|
-
"%"
|
|
328
|
-
] })
|
|
329
|
-
] })
|
|
330
|
-
] })
|
|
331
|
-
] }),
|
|
332
|
-
/* @__PURE__ */ jsxs("div", { className: "pt-4 border-t", children: [
|
|
333
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
334
|
-
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle", children: "Campaign Completion" }),
|
|
335
|
-
/* @__PURE__ */ jsxs(Text, { className: "text-xs font-medium", children: [
|
|
336
|
-
completionRate,
|
|
337
|
-
"%"
|
|
338
|
-
] })
|
|
339
|
-
] }),
|
|
340
|
-
/* @__PURE__ */ jsx("div", { className: "h-2 bg-ui-bg-subtle rounded-full overflow-hidden", children: /* @__PURE__ */ jsx(
|
|
341
|
-
"div",
|
|
342
|
-
{
|
|
343
|
-
className: "h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-300",
|
|
344
|
-
style: { width: `${completionRate}%` }
|
|
345
|
-
}
|
|
346
|
-
) }),
|
|
347
|
-
/* @__PURE__ */ jsxs(Text, { className: "text-xs text-ui-fg-muted mt-2", children: [
|
|
348
|
-
stats.complete,
|
|
349
|
-
" of ",
|
|
350
|
-
stats.total,
|
|
351
|
-
" campaigns have complete details"
|
|
352
|
-
] })
|
|
353
|
-
] }),
|
|
354
|
-
completionRate < 50 && stats.total > 0 && /* @__PURE__ */ jsx("div", { className: "mt-4 p-3 rounded-lg bg-blue-50 border border-blue-200", children: /* @__PURE__ */ jsx(Text, { className: "text-xs text-blue-800", children: "💡 Tip: Add images and content to campaigns to improve customer engagement!" }) })
|
|
355
|
-
] });
|
|
356
|
-
};
|
|
357
|
-
defineWidgetConfig({
|
|
358
|
-
zone: "campaign.list.before"
|
|
359
|
-
});
|
|
360
|
-
const useFlashSales = (pagination) => {
|
|
361
|
-
const query = useQuery({
|
|
362
|
-
queryKey: ["flash-sales", pagination],
|
|
363
|
-
queryFn: async () => {
|
|
364
|
-
const { data } = await axios.get("/admin/flash-sales", {
|
|
365
|
-
params: pagination
|
|
366
|
-
});
|
|
367
|
-
return data;
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
return query;
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
fetchFlashSales();
|
|
219
|
+
}, [pagination.limit, pagination.offset]);
|
|
220
|
+
return {
|
|
221
|
+
data,
|
|
222
|
+
isLoading,
|
|
223
|
+
error,
|
|
224
|
+
refetch: fetchFlashSales
|
|
225
|
+
};
|
|
371
226
|
};
|
|
372
227
|
const FlashSalePage = () => {
|
|
373
228
|
const navigate = useNavigate();
|
|
@@ -445,10 +300,117 @@ const FlashSalePage = () => {
|
|
|
445
300
|
const FlashSale = () => {
|
|
446
301
|
return /* @__PURE__ */ jsx(FlashSalePage, {});
|
|
447
302
|
};
|
|
448
|
-
const config$
|
|
303
|
+
const config$3 = defineRouteConfig({
|
|
449
304
|
label: "Flash Sale",
|
|
450
305
|
icon: Sparkles
|
|
451
306
|
});
|
|
307
|
+
const useCoupons = ({ limit, offset }) => {
|
|
308
|
+
const [data, setData] = useState(null);
|
|
309
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
310
|
+
const [error, setError] = useState(null);
|
|
311
|
+
const fetchCoupons = async () => {
|
|
312
|
+
setIsLoading(true);
|
|
313
|
+
setError(null);
|
|
314
|
+
try {
|
|
315
|
+
const response = await axios.get("/admin/coupons", {
|
|
316
|
+
params: { limit, offset }
|
|
317
|
+
});
|
|
318
|
+
setData(response.data);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch coupons"));
|
|
321
|
+
} finally {
|
|
322
|
+
setIsLoading(false);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
fetchCoupons();
|
|
327
|
+
}, [limit, offset]);
|
|
328
|
+
return {
|
|
329
|
+
data,
|
|
330
|
+
isLoading,
|
|
331
|
+
error,
|
|
332
|
+
refetch: fetchCoupons
|
|
333
|
+
};
|
|
334
|
+
};
|
|
335
|
+
const columnHelper$1 = createDataTableColumnHelper();
|
|
336
|
+
const useCouponColumns = () => useMemo(
|
|
337
|
+
() => [
|
|
338
|
+
columnHelper$1.accessor("name", {
|
|
339
|
+
header: "Name",
|
|
340
|
+
cell: (info) => info.getValue()
|
|
341
|
+
}),
|
|
342
|
+
columnHelper$1.display({
|
|
343
|
+
id: "code",
|
|
344
|
+
header: "Code",
|
|
345
|
+
cell: (info) => {
|
|
346
|
+
var _a, _b;
|
|
347
|
+
return ((_b = (_a = info.row.original.promotions) == null ? void 0 : _a[0]) == null ? void 0 : _b.code) ?? "-";
|
|
348
|
+
}
|
|
349
|
+
}),
|
|
350
|
+
columnHelper$1.display({
|
|
351
|
+
id: "status",
|
|
352
|
+
header: "Status",
|
|
353
|
+
cell: (info) => {
|
|
354
|
+
const endsAt = info.row.original.ends_at;
|
|
355
|
+
if (!endsAt) {
|
|
356
|
+
return "";
|
|
357
|
+
}
|
|
358
|
+
const isActive = dayjs(endsAt).isAfter(dayjs());
|
|
359
|
+
return /* @__PURE__ */ jsx(Badge, { color: isActive ? "green" : "grey", children: isActive ? "Active" : "Inactive" });
|
|
360
|
+
}
|
|
361
|
+
}),
|
|
362
|
+
columnHelper$1.accessor("starts_at", {
|
|
363
|
+
header: "Start Date",
|
|
364
|
+
cell: (info) => info.getValue() ? dayjs(info.getValue()).format("YYYY-MM-DD HH:mm") : ""
|
|
365
|
+
}),
|
|
366
|
+
columnHelper$1.accessor("ends_at", {
|
|
367
|
+
header: "End Date",
|
|
368
|
+
cell: (info) => info.getValue() ? dayjs(info.getValue()).format("YYYY-MM-DD HH:mm") : ""
|
|
369
|
+
})
|
|
370
|
+
],
|
|
371
|
+
[]
|
|
372
|
+
);
|
|
373
|
+
const CouponPage = () => {
|
|
374
|
+
const navigate = useNavigate();
|
|
375
|
+
const limit = 20;
|
|
376
|
+
const [pagination, setPagination] = useState({
|
|
377
|
+
pageSize: limit,
|
|
378
|
+
pageIndex: 0
|
|
379
|
+
});
|
|
380
|
+
const offset = pagination.pageIndex * limit;
|
|
381
|
+
const { data } = useCoupons({ limit, offset });
|
|
382
|
+
const columns2 = useCouponColumns();
|
|
383
|
+
const table = useDataTable({
|
|
384
|
+
data: (data == null ? void 0 : data.campaigns) ?? [],
|
|
385
|
+
columns: columns2,
|
|
386
|
+
getRowId: (row) => row.id,
|
|
387
|
+
pagination: {
|
|
388
|
+
state: pagination,
|
|
389
|
+
onPaginationChange: setPagination
|
|
390
|
+
},
|
|
391
|
+
rowCount: (data == null ? void 0 : data.count) ?? 0,
|
|
392
|
+
onRowClick: (_, row) => {
|
|
393
|
+
navigate(`/coupons/${row.id}`);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
397
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center", children: [
|
|
398
|
+
/* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold", children: "Coupons" }),
|
|
399
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => navigate("/coupons/create"), children: "Create Coupon" })
|
|
400
|
+
] }),
|
|
401
|
+
/* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
|
|
402
|
+
/* @__PURE__ */ jsx(DataTable.Table, {}),
|
|
403
|
+
/* @__PURE__ */ jsx(DataTable.Pagination, {})
|
|
404
|
+
] })
|
|
405
|
+
] });
|
|
406
|
+
};
|
|
407
|
+
const Coupons = () => {
|
|
408
|
+
return /* @__PURE__ */ jsx(CouponPage, {});
|
|
409
|
+
};
|
|
410
|
+
const config$2 = defineRouteConfig({
|
|
411
|
+
label: "Coupons",
|
|
412
|
+
icon: Tag
|
|
413
|
+
});
|
|
452
414
|
var isCheckBoxInput = (element) => element.type === "checkbox";
|
|
453
415
|
var isDateObject = (value) => value instanceof Date;
|
|
454
416
|
var isNullOrUndefined = (value) => value == null;
|
|
@@ -2356,13 +2318,25 @@ const ProductSelector = ({
|
|
|
2356
2318
|
const debouncedSearchHandler = debounce((value) => {
|
|
2357
2319
|
setSearch(value);
|
|
2358
2320
|
}, 500);
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2321
|
+
const [products, setProducts] = useState(null);
|
|
2322
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
2323
|
+
useEffect(() => {
|
|
2324
|
+
const fetchProducts = async () => {
|
|
2325
|
+
setIsLoading(true);
|
|
2326
|
+
try {
|
|
2327
|
+
const result = await sdk.admin.product.list({
|
|
2328
|
+
q: search,
|
|
2329
|
+
limit: 100
|
|
2330
|
+
});
|
|
2331
|
+
setProducts(result);
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
console.error("Failed to fetch products:", error);
|
|
2334
|
+
} finally {
|
|
2335
|
+
setIsLoading(false);
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
fetchProducts();
|
|
2339
|
+
}, [selectedProductIds, search]);
|
|
2366
2340
|
const selectableProducts = useMemo(() => {
|
|
2367
2341
|
var _a;
|
|
2368
2342
|
return (_a = products == null ? void 0 : products.products) == null ? void 0 : _a.filter(
|
|
@@ -2679,58 +2653,387 @@ const FlashSaleForm = ({
|
|
|
2679
2653
|
] }) })
|
|
2680
2654
|
] });
|
|
2681
2655
|
};
|
|
2682
|
-
const
|
|
2683
|
-
const
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2656
|
+
const FlashSaleCreate = () => {
|
|
2657
|
+
const navigate = useNavigate();
|
|
2658
|
+
async function handleSubmit(campaignData) {
|
|
2659
|
+
var _a;
|
|
2660
|
+
try {
|
|
2661
|
+
await axios.post("/admin/flash-sales", {
|
|
2662
|
+
...campaignData,
|
|
2663
|
+
starts_at: new Date(campaignData.starts_at).toUTCString(),
|
|
2664
|
+
ends_at: new Date(campaignData.ends_at).toUTCString()
|
|
2665
|
+
});
|
|
2666
|
+
toast.success("Flash sale created successfully");
|
|
2667
|
+
navigate("/flash-sales");
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
let message;
|
|
2670
|
+
if (axios.isAxiosError(error)) {
|
|
2671
|
+
console.log(error);
|
|
2672
|
+
message = (_a = error.response) == null ? void 0 : _a.data.message;
|
|
2673
|
+
} else if (error instanceof Error) {
|
|
2674
|
+
message = error.message;
|
|
2675
|
+
} else {
|
|
2676
|
+
message = "Failed to create flash sale";
|
|
2677
|
+
}
|
|
2678
|
+
toast.error("Failed to create flash sale", {
|
|
2679
|
+
description: message
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return /* @__PURE__ */ jsx(
|
|
2684
|
+
FlashSaleForm,
|
|
2685
|
+
{
|
|
2686
|
+
onSubmit: handleSubmit,
|
|
2687
|
+
onCancel: () => navigate("/flash-sales")
|
|
2688
|
+
}
|
|
2689
|
+
);
|
|
2692
2690
|
};
|
|
2693
|
-
const
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2691
|
+
const couponSchema = z.object({
|
|
2692
|
+
name: z.string().min(1, "Name is required"),
|
|
2693
|
+
description: z.string().min(1, "Description is required"),
|
|
2694
|
+
type: z.literal("coupon"),
|
|
2695
|
+
code: z.string().min(1, "Coupon code is required"),
|
|
2696
|
+
discount_type: z.enum(["percentage", "fixed"]),
|
|
2697
|
+
discount_value: z.number().positive("Discount must be positive"),
|
|
2698
|
+
currency_code: z.string().optional(),
|
|
2699
|
+
starts_at: z.string().min(1, "Start date is required"),
|
|
2700
|
+
ends_at: z.string().min(1, "End date is required"),
|
|
2701
|
+
allocation: z.enum(["each", "total"]).optional(),
|
|
2702
|
+
target_type: z.enum(["order", "items"]).optional()
|
|
2703
|
+
}).superRefine((data, ctx) => {
|
|
2704
|
+
if (new Date(data.ends_at) < new Date(data.starts_at)) {
|
|
2705
|
+
ctx.addIssue({
|
|
2706
|
+
code: z.ZodIssueCode.custom,
|
|
2707
|
+
path: ["ends_at"],
|
|
2708
|
+
message: "End date must be after start date"
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
if (data.discount_type === "fixed" && !data.currency_code) {
|
|
2712
|
+
ctx.addIssue({
|
|
2713
|
+
code: z.ZodIssueCode.custom,
|
|
2714
|
+
path: ["currency_code"],
|
|
2715
|
+
message: "Currency is required for fixed discount"
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
const CouponForm = ({
|
|
2720
|
+
initialData,
|
|
2721
|
+
onSubmit,
|
|
2722
|
+
onCancel,
|
|
2723
|
+
disabled = false
|
|
2701
2724
|
}) => {
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
)
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2725
|
+
const {
|
|
2726
|
+
register,
|
|
2727
|
+
control,
|
|
2728
|
+
handleSubmit,
|
|
2729
|
+
watch,
|
|
2730
|
+
setValue,
|
|
2731
|
+
formState: { errors }
|
|
2732
|
+
} = useForm({
|
|
2733
|
+
resolver: t(couponSchema),
|
|
2734
|
+
defaultValues: {
|
|
2735
|
+
name: (initialData == null ? void 0 : initialData.name) ?? "",
|
|
2736
|
+
description: (initialData == null ? void 0 : initialData.description) ?? "",
|
|
2737
|
+
type: "coupon",
|
|
2738
|
+
code: (initialData == null ? void 0 : initialData.code) ?? "",
|
|
2739
|
+
discount_type: (initialData == null ? void 0 : initialData.discount_type) ?? "percentage",
|
|
2740
|
+
discount_value: (initialData == null ? void 0 : initialData.discount_value) ?? 0,
|
|
2741
|
+
currency_code: (initialData == null ? void 0 : initialData.currency_code) ?? "",
|
|
2742
|
+
starts_at: (initialData == null ? void 0 : initialData.starts_at) ?? dayjs().startOf("day").format("YYYY-MM-DDTHH:mm"),
|
|
2743
|
+
ends_at: (initialData == null ? void 0 : initialData.ends_at) ?? dayjs().endOf("day").format("YYYY-MM-DDTHH:mm"),
|
|
2744
|
+
allocation: (initialData == null ? void 0 : initialData.allocation) ?? "total",
|
|
2745
|
+
target_type: (initialData == null ? void 0 : initialData.target_type) ?? "order"
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
const discountType = watch("discount_type");
|
|
2749
|
+
const startsAt = watch("starts_at");
|
|
2750
|
+
const endsAt = watch("ends_at");
|
|
2751
|
+
const handleDateTimeChange = (field, type, value) => {
|
|
2752
|
+
const currentValue = watch(field);
|
|
2753
|
+
if (type === "date") {
|
|
2754
|
+
const time = currentValue ? dayjs(currentValue).format("HH:mm") : field === "starts_at" ? "00:00" : "23:59";
|
|
2755
|
+
setValue(field, `${value}T${time}`);
|
|
2756
|
+
} else {
|
|
2757
|
+
const date = currentValue ? dayjs(currentValue).format("YYYY-MM-DD") : dayjs().format("YYYY-MM-DD");
|
|
2758
|
+
setValue(field, `${date}T${value}`);
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
2761
|
+
const onFormSubmit = (data) => {
|
|
2762
|
+
onSubmit(data);
|
|
2763
|
+
};
|
|
2764
|
+
const onFormError = () => {
|
|
2765
|
+
const errorMessages = Object.values(errors).map((err) => err == null ? void 0 : err.message).filter(Boolean).join(", ");
|
|
2766
|
+
toast.error("Invalid coupon data", {
|
|
2767
|
+
description: errorMessages
|
|
2768
|
+
});
|
|
2769
|
+
};
|
|
2770
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
2771
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
2772
|
+
/* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold", children: "Coupon" }),
|
|
2773
|
+
/* @__PURE__ */ jsx(Button, { variant: "transparent", onClick: onCancel, children: "Cancel" })
|
|
2774
|
+
] }),
|
|
2775
|
+
/* @__PURE__ */ jsxs(
|
|
2776
|
+
"form",
|
|
2777
|
+
{
|
|
2778
|
+
onSubmit: handleSubmit(onFormSubmit, onFormError),
|
|
2779
|
+
className: "space-y-6 my-8",
|
|
2780
|
+
children: [
|
|
2781
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2782
|
+
/* @__PURE__ */ jsx(Label, { children: "Name" }),
|
|
2783
|
+
/* @__PURE__ */ jsx(Input, { ...register("name"), disabled }),
|
|
2784
|
+
errors.name && /* @__PURE__ */ jsx("p", { className: "text-red-500 text-sm mt-1", children: errors.name.message })
|
|
2785
|
+
] }),
|
|
2786
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2787
|
+
/* @__PURE__ */ jsx(Label, { children: "Description" }),
|
|
2788
|
+
/* @__PURE__ */ jsx(Textarea, { ...register("description"), disabled }),
|
|
2789
|
+
errors.description && /* @__PURE__ */ jsx("p", { className: "text-red-500 text-sm mt-1", children: errors.description.message })
|
|
2790
|
+
] }),
|
|
2791
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
2792
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2793
|
+
/* @__PURE__ */ jsx(Label, { children: "Start Date" }),
|
|
2794
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
2795
|
+
/* @__PURE__ */ jsx(
|
|
2796
|
+
Input,
|
|
2797
|
+
{
|
|
2798
|
+
type: "date",
|
|
2799
|
+
value: startsAt ? dayjs(startsAt).format("YYYY-MM-DD") : "",
|
|
2800
|
+
onChange: (e2) => handleDateTimeChange("starts_at", "date", e2.target.value),
|
|
2801
|
+
disabled,
|
|
2802
|
+
className: "flex-1"
|
|
2803
|
+
}
|
|
2804
|
+
),
|
|
2805
|
+
/* @__PURE__ */ jsx(
|
|
2806
|
+
Input,
|
|
2807
|
+
{
|
|
2808
|
+
type: "time",
|
|
2809
|
+
value: startsAt ? dayjs(startsAt).format("HH:mm") : "",
|
|
2810
|
+
onChange: (e2) => handleDateTimeChange("starts_at", "time", e2.target.value),
|
|
2811
|
+
disabled,
|
|
2812
|
+
className: "flex-1"
|
|
2813
|
+
}
|
|
2814
|
+
)
|
|
2815
|
+
] }),
|
|
2816
|
+
errors.starts_at && /* @__PURE__ */ jsx("p", { className: "text-red-500 text-sm mt-1", children: errors.starts_at.message })
|
|
2817
|
+
] }),
|
|
2818
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2819
|
+
/* @__PURE__ */ jsx(Label, { children: "End Date" }),
|
|
2820
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
2821
|
+
/* @__PURE__ */ jsx(
|
|
2822
|
+
Input,
|
|
2823
|
+
{
|
|
2824
|
+
type: "date",
|
|
2825
|
+
value: endsAt ? dayjs(endsAt).format("YYYY-MM-DD") : "",
|
|
2826
|
+
onChange: (e2) => handleDateTimeChange("ends_at", "date", e2.target.value),
|
|
2827
|
+
disabled,
|
|
2828
|
+
className: "flex-1"
|
|
2829
|
+
}
|
|
2830
|
+
),
|
|
2831
|
+
/* @__PURE__ */ jsx(
|
|
2832
|
+
Input,
|
|
2833
|
+
{
|
|
2834
|
+
type: "time",
|
|
2835
|
+
value: endsAt ? dayjs(endsAt).format("HH:mm") : "",
|
|
2836
|
+
onChange: (e2) => handleDateTimeChange("ends_at", "time", e2.target.value),
|
|
2837
|
+
disabled,
|
|
2838
|
+
className: "flex-1"
|
|
2839
|
+
}
|
|
2840
|
+
)
|
|
2841
|
+
] }),
|
|
2842
|
+
errors.ends_at && /* @__PURE__ */ jsx("p", { className: "text-red-500 text-sm mt-1", children: errors.ends_at.message })
|
|
2843
|
+
] })
|
|
2844
|
+
] }),
|
|
2845
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
2846
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2847
|
+
/* @__PURE__ */ jsx(Label, { children: "Coupon Code" }),
|
|
2848
|
+
/* @__PURE__ */ jsx(Input, { ...register("code"), disabled }),
|
|
2849
|
+
errors.code && /* @__PURE__ */ jsx("p", { className: "text-red-500 text-sm mt-1", children: errors.code.message })
|
|
2850
|
+
] }),
|
|
2851
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2852
|
+
/* @__PURE__ */ jsx(Label, { children: "Discount Type" }),
|
|
2853
|
+
/* @__PURE__ */ jsx(
|
|
2854
|
+
Controller,
|
|
2855
|
+
{
|
|
2856
|
+
name: "discount_type",
|
|
2857
|
+
control,
|
|
2858
|
+
render: ({ field }) => /* @__PURE__ */ jsxs(
|
|
2859
|
+
Select,
|
|
2860
|
+
{
|
|
2861
|
+
value: field.value,
|
|
2862
|
+
onValueChange: field.onChange,
|
|
2863
|
+
disabled,
|
|
2864
|
+
children: [
|
|
2865
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Select discount type" }) }),
|
|
2866
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
2867
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "percentage", children: "Percentage" }),
|
|
2868
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "fixed", children: "Fixed Amount" })
|
|
2869
|
+
] })
|
|
2870
|
+
]
|
|
2871
|
+
}
|
|
2872
|
+
)
|
|
2873
|
+
}
|
|
2874
|
+
)
|
|
2875
|
+
] })
|
|
2876
|
+
] }),
|
|
2877
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
2878
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2879
|
+
/* @__PURE__ */ jsx(Label, { children: "Discount Value" }),
|
|
2880
|
+
/* @__PURE__ */ jsx(
|
|
2881
|
+
Controller,
|
|
2882
|
+
{
|
|
2883
|
+
name: "discount_value",
|
|
2884
|
+
control,
|
|
2885
|
+
render: ({ field }) => /* @__PURE__ */ jsx(
|
|
2886
|
+
Input,
|
|
2887
|
+
{
|
|
2888
|
+
type: "number",
|
|
2889
|
+
value: field.value,
|
|
2890
|
+
min: 0,
|
|
2891
|
+
step: field.value % 1 === 0 ? 1 : 0.01,
|
|
2892
|
+
onChange: (e2) => field.onChange(Number(e2.target.value)),
|
|
2893
|
+
disabled
|
|
2894
|
+
}
|
|
2895
|
+
)
|
|
2896
|
+
}
|
|
2897
|
+
),
|
|
2898
|
+
errors.discount_value && /* @__PURE__ */ jsx("p", { className: "text-red-500 text-sm mt-1", children: errors.discount_value.message })
|
|
2899
|
+
] }),
|
|
2900
|
+
discountType === "fixed" && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2901
|
+
/* @__PURE__ */ jsx(Label, { children: "Currency Code" }),
|
|
2902
|
+
/* @__PURE__ */ jsx(Input, { ...register("currency_code"), disabled }),
|
|
2903
|
+
errors.currency_code && /* @__PURE__ */ jsx("p", { className: "text-red-500 text-sm mt-1", children: errors.currency_code.message })
|
|
2904
|
+
] })
|
|
2905
|
+
] }),
|
|
2906
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
2907
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2908
|
+
/* @__PURE__ */ jsx(Label, { children: "Allocation" }),
|
|
2909
|
+
/* @__PURE__ */ jsx(
|
|
2910
|
+
Controller,
|
|
2911
|
+
{
|
|
2912
|
+
name: "allocation",
|
|
2913
|
+
control,
|
|
2914
|
+
render: ({ field }) => /* @__PURE__ */ jsxs(
|
|
2915
|
+
Select,
|
|
2916
|
+
{
|
|
2917
|
+
value: field.value,
|
|
2918
|
+
onValueChange: field.onChange,
|
|
2919
|
+
disabled,
|
|
2920
|
+
children: [
|
|
2921
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Select allocation" }) }),
|
|
2922
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
2923
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "total", children: "Order Total" }),
|
|
2924
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "each", children: "Each Item" })
|
|
2925
|
+
] })
|
|
2926
|
+
]
|
|
2927
|
+
}
|
|
2928
|
+
)
|
|
2929
|
+
}
|
|
2930
|
+
)
|
|
2931
|
+
] }),
|
|
2932
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2933
|
+
/* @__PURE__ */ jsx(Label, { children: "Target Type" }),
|
|
2934
|
+
/* @__PURE__ */ jsx(
|
|
2935
|
+
Controller,
|
|
2936
|
+
{
|
|
2937
|
+
name: "target_type",
|
|
2938
|
+
control,
|
|
2939
|
+
render: ({ field }) => /* @__PURE__ */ jsxs(
|
|
2940
|
+
Select,
|
|
2941
|
+
{
|
|
2942
|
+
value: field.value,
|
|
2943
|
+
onValueChange: field.onChange,
|
|
2944
|
+
disabled,
|
|
2945
|
+
children: [
|
|
2946
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Select target" }) }),
|
|
2947
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
2948
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "order", children: "Order" }),
|
|
2949
|
+
/* @__PURE__ */ jsx(Select.Item, { value: "items", children: "Items" })
|
|
2950
|
+
] })
|
|
2951
|
+
]
|
|
2952
|
+
}
|
|
2953
|
+
)
|
|
2954
|
+
}
|
|
2955
|
+
)
|
|
2956
|
+
] })
|
|
2957
|
+
] }),
|
|
2958
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
2959
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", type: "button", onClick: onCancel, children: "Cancel" }),
|
|
2960
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", disabled, children: "Save Coupon" })
|
|
2961
|
+
] })
|
|
2962
|
+
]
|
|
2963
|
+
}
|
|
2964
|
+
)
|
|
2965
|
+
] });
|
|
2966
|
+
};
|
|
2967
|
+
const useCouponById = (id) => {
|
|
2968
|
+
const [data, setData] = useState(null);
|
|
2969
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
2970
|
+
const [error, setError] = useState(null);
|
|
2971
|
+
const fetchCoupon = async () => {
|
|
2972
|
+
if (!id) {
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
setIsLoading(true);
|
|
2976
|
+
setError(null);
|
|
2977
|
+
try {
|
|
2978
|
+
const response = await axios.get(`/admin/coupons/${id}`);
|
|
2979
|
+
setData(response.data);
|
|
2980
|
+
} catch (err) {
|
|
2981
|
+
setError(err instanceof Error ? err : new Error("Failed to fetch coupon"));
|
|
2982
|
+
} finally {
|
|
2983
|
+
setIsLoading(false);
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
useEffect(() => {
|
|
2987
|
+
fetchCoupon();
|
|
2988
|
+
}, [id]);
|
|
2989
|
+
return {
|
|
2990
|
+
data,
|
|
2991
|
+
isLoading,
|
|
2992
|
+
error,
|
|
2993
|
+
refetch: fetchCoupon
|
|
2994
|
+
};
|
|
2995
|
+
};
|
|
2996
|
+
const MarkdownEditor = ({
|
|
2997
|
+
label,
|
|
2998
|
+
value,
|
|
2999
|
+
onChange,
|
|
3000
|
+
placeholder,
|
|
3001
|
+
helpText,
|
|
3002
|
+
rows = 10,
|
|
3003
|
+
showPreview = true
|
|
3004
|
+
}) => {
|
|
3005
|
+
const [activeTab, setActiveTab] = useState("write");
|
|
3006
|
+
const insertMarkdown = (before, after = "") => {
|
|
3007
|
+
const textarea = document.getElementById(
|
|
3008
|
+
`markdown-${label}`
|
|
3009
|
+
);
|
|
3010
|
+
if (!textarea) return;
|
|
3011
|
+
const start = textarea.selectionStart;
|
|
3012
|
+
const end = textarea.selectionEnd;
|
|
3013
|
+
const selectedText = value.substring(start, end);
|
|
3014
|
+
const newText = value.substring(0, start) + before + selectedText + after + value.substring(end);
|
|
3015
|
+
onChange(newText);
|
|
3016
|
+
setTimeout(() => {
|
|
3017
|
+
textarea.focus();
|
|
3018
|
+
textarea.setSelectionRange(
|
|
3019
|
+
start + before.length,
|
|
3020
|
+
start + before.length + selectedText.length
|
|
3021
|
+
);
|
|
3022
|
+
}, 0);
|
|
3023
|
+
};
|
|
3024
|
+
const renderMarkdownPreview = (markdown) => {
|
|
3025
|
+
let html = markdown;
|
|
3026
|
+
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
|
3027
|
+
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
|
3028
|
+
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
|
3029
|
+
html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
3030
|
+
html = html.replace(/__(.+?)__/g, "<strong>$1</strong>");
|
|
3031
|
+
html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
|
3032
|
+
html = html.replace(/_(.+?)_/g, "<em>$1</em>");
|
|
3033
|
+
html = html.replace(
|
|
3034
|
+
/```(\w+)?\n([\s\S]+?)```/g,
|
|
3035
|
+
'<pre class="bg-ui-bg-subtle p-4 rounded-lg overflow-x-auto"><code class="language-$1">$2</code></pre>'
|
|
3036
|
+
);
|
|
2734
3037
|
html = html.replace(
|
|
2735
3038
|
/`([^`]+)`/g,
|
|
2736
3039
|
'<code class="bg-ui-bg-subtle px-1 py-0.5 rounded text-sm">$1</code>'
|
|
@@ -3532,11 +3835,168 @@ const CampaignDetailForm = ({
|
|
|
3532
3835
|
)
|
|
3533
3836
|
] });
|
|
3534
3837
|
};
|
|
3838
|
+
const CouponDetail = () => {
|
|
3839
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3840
|
+
const { id } = useParams();
|
|
3841
|
+
const navigate = useNavigate();
|
|
3842
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
3843
|
+
const { data, isLoading, refetch } = useCouponById(id ?? "");
|
|
3844
|
+
if (isLoading) {
|
|
3845
|
+
return /* @__PURE__ */ jsx(Container, { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ jsx("div", { className: "animate-spin h-10 w-10 border-4 border-primary rounded-full border-t-transparent" }) });
|
|
3846
|
+
}
|
|
3847
|
+
if (!(data == null ? void 0 : data.campaign)) {
|
|
3848
|
+
return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx("p", { children: "Coupon not found" }) });
|
|
3849
|
+
}
|
|
3850
|
+
const campaign = data.campaign;
|
|
3851
|
+
const promotion = (_a = campaign.promotions) == null ? void 0 : _a[0];
|
|
3852
|
+
const initialValues = {
|
|
3853
|
+
name: campaign.name ?? "",
|
|
3854
|
+
description: campaign.description ?? "",
|
|
3855
|
+
type: "coupon",
|
|
3856
|
+
code: (promotion == null ? void 0 : promotion.code) ?? "",
|
|
3857
|
+
discount_type: ((_b = promotion == null ? void 0 : promotion.application_method) == null ? void 0 : _b.type) ?? "percentage",
|
|
3858
|
+
discount_value: Number(((_c = promotion == null ? void 0 : promotion.application_method) == null ? void 0 : _c.value) ?? 0),
|
|
3859
|
+
currency_code: ((_d = promotion == null ? void 0 : promotion.application_method) == null ? void 0 : _d.currency_code) ?? void 0,
|
|
3860
|
+
starts_at: campaign.starts_at ? dayjs(campaign.starts_at).format("YYYY-MM-DDTHH:mm") : void 0,
|
|
3861
|
+
ends_at: campaign.ends_at ? dayjs(campaign.ends_at).format("YYYY-MM-DDTHH:mm") : void 0,
|
|
3862
|
+
allocation: ((_e = promotion == null ? void 0 : promotion.application_method) == null ? void 0 : _e.allocation) ?? "total",
|
|
3863
|
+
target_type: ((_f = promotion == null ? void 0 : promotion.application_method) == null ? void 0 : _f.target_type) ?? "order"
|
|
3864
|
+
};
|
|
3865
|
+
const handleSubmit = async (formData) => {
|
|
3866
|
+
var _a2, _b2;
|
|
3867
|
+
if (!id) {
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
try {
|
|
3871
|
+
await axios.put(`/admin/coupons/${id}`, {
|
|
3872
|
+
...formData,
|
|
3873
|
+
starts_at: new Date(formData.starts_at).toUTCString(),
|
|
3874
|
+
ends_at: new Date(formData.ends_at).toUTCString(),
|
|
3875
|
+
currency_code: formData.discount_type === "fixed" ? formData.currency_code : void 0
|
|
3876
|
+
});
|
|
3877
|
+
toast.success("Coupon updated successfully");
|
|
3878
|
+
setIsEditing(false);
|
|
3879
|
+
refetch();
|
|
3880
|
+
} catch (error) {
|
|
3881
|
+
let message = "Failed to update coupon";
|
|
3882
|
+
if (axios.isAxiosError(error)) {
|
|
3883
|
+
message = ((_b2 = (_a2 = error.response) == null ? void 0 : _a2.data) == null ? void 0 : _b2.message) ?? message;
|
|
3884
|
+
} else if (error instanceof Error) {
|
|
3885
|
+
message = error.message;
|
|
3886
|
+
}
|
|
3887
|
+
toast.error("Failed to update coupon", {
|
|
3888
|
+
description: message
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
};
|
|
3892
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
3893
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between", children: [
|
|
3894
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
3895
|
+
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold mb-1", children: campaign.name }),
|
|
3896
|
+
/* @__PURE__ */ jsx("p", { className: "text-ui-fg-subtle", children: "Manage coupon configuration and content" })
|
|
3897
|
+
] }),
|
|
3898
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
3899
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => navigate("/coupons"), children: "Back to Coupons" }),
|
|
3900
|
+
/* @__PURE__ */ jsx(Button, { onClick: () => setIsEditing((prev) => !prev), children: isEditing ? "View Mode" : "Edit Mode" })
|
|
3901
|
+
] })
|
|
3902
|
+
] }),
|
|
3903
|
+
/* @__PURE__ */ jsxs(Tabs, { defaultValue: "settings", children: [
|
|
3904
|
+
/* @__PURE__ */ jsxs(Tabs.List, { children: [
|
|
3905
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "settings", children: "Coupon Settings" }),
|
|
3906
|
+
/* @__PURE__ */ jsx(Tabs.Trigger, { value: "details", children: "Coupon Details" })
|
|
3907
|
+
] }),
|
|
3908
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "settings", className: "mt-6", children: /* @__PURE__ */ jsx(
|
|
3909
|
+
CouponForm,
|
|
3910
|
+
{
|
|
3911
|
+
initialData: initialValues,
|
|
3912
|
+
onSubmit: handleSubmit,
|
|
3913
|
+
onCancel: () => navigate("/coupons"),
|
|
3914
|
+
disabled: !isEditing
|
|
3915
|
+
}
|
|
3916
|
+
) }),
|
|
3917
|
+
/* @__PURE__ */ jsx(Tabs.Content, { value: "details", className: "mt-6", children: /* @__PURE__ */ jsx(
|
|
3918
|
+
CampaignDetailForm,
|
|
3919
|
+
{
|
|
3920
|
+
campaignId: campaign.id,
|
|
3921
|
+
campaignName: campaign.name ?? ""
|
|
3922
|
+
}
|
|
3923
|
+
) })
|
|
3924
|
+
] })
|
|
3925
|
+
] });
|
|
3926
|
+
};
|
|
3927
|
+
const config$1 = defineRouteConfig({
|
|
3928
|
+
label: "Coupon Detail",
|
|
3929
|
+
icon: Tag
|
|
3930
|
+
});
|
|
3931
|
+
const CouponCreate = () => {
|
|
3932
|
+
const navigate = useNavigate();
|
|
3933
|
+
const handleSubmit = async (formData) => {
|
|
3934
|
+
var _a, _b;
|
|
3935
|
+
try {
|
|
3936
|
+
await axios.post("/admin/coupons", {
|
|
3937
|
+
...formData,
|
|
3938
|
+
starts_at: new Date(formData.starts_at).toUTCString(),
|
|
3939
|
+
ends_at: new Date(formData.ends_at).toUTCString(),
|
|
3940
|
+
currency_code: formData.discount_type === "fixed" ? formData.currency_code : void 0
|
|
3941
|
+
});
|
|
3942
|
+
toast.success("Coupon created successfully");
|
|
3943
|
+
navigate("/coupons");
|
|
3944
|
+
} catch (error) {
|
|
3945
|
+
let message = "Failed to create coupon";
|
|
3946
|
+
if (axios.isAxiosError(error)) {
|
|
3947
|
+
message = ((_b = (_a = error.response) == null ? void 0 : _a.data) == null ? void 0 : _b.message) ?? message;
|
|
3948
|
+
} else if (error instanceof Error) {
|
|
3949
|
+
message = error.message;
|
|
3950
|
+
}
|
|
3951
|
+
toast.error("Failed to create coupon", {
|
|
3952
|
+
description: message
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
return /* @__PURE__ */ jsx(
|
|
3957
|
+
CouponForm,
|
|
3958
|
+
{
|
|
3959
|
+
onSubmit: handleSubmit,
|
|
3960
|
+
onCancel: () => navigate("/coupons")
|
|
3961
|
+
}
|
|
3962
|
+
);
|
|
3963
|
+
};
|
|
3964
|
+
const useFlashSaleById = (id) => {
|
|
3965
|
+
const [data, setData] = useState(null);
|
|
3966
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
3967
|
+
const [error, setError] = useState(null);
|
|
3968
|
+
const fetchFlashSale = async () => {
|
|
3969
|
+
if (!id) {
|
|
3970
|
+
setIsLoading(false);
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
setIsLoading(true);
|
|
3974
|
+
setError(null);
|
|
3975
|
+
try {
|
|
3976
|
+
const response = await axios.get(`/admin/flash-sales/${id}`);
|
|
3977
|
+
setData(response.data);
|
|
3978
|
+
} catch (err) {
|
|
3979
|
+
setError(
|
|
3980
|
+
err instanceof Error ? err : new Error("Failed to fetch flash sale")
|
|
3981
|
+
);
|
|
3982
|
+
} finally {
|
|
3983
|
+
setIsLoading(false);
|
|
3984
|
+
}
|
|
3985
|
+
};
|
|
3986
|
+
useEffect(() => {
|
|
3987
|
+
fetchFlashSale();
|
|
3988
|
+
}, [id]);
|
|
3989
|
+
return {
|
|
3990
|
+
data,
|
|
3991
|
+
isLoading,
|
|
3992
|
+
error,
|
|
3993
|
+
refetch: fetchFlashSale
|
|
3994
|
+
};
|
|
3995
|
+
};
|
|
3535
3996
|
const FlashSaleDetail = () => {
|
|
3536
3997
|
const { id } = useParams();
|
|
3537
3998
|
const navigate = useNavigate();
|
|
3538
|
-
const
|
|
3539
|
-
const { data, isLoading } = useFlashSaleById(id || "");
|
|
3999
|
+
const { data, isLoading, refetch } = useFlashSaleById(id || "");
|
|
3540
4000
|
const [isEditing, setIsEditing] = useState(false);
|
|
3541
4001
|
async function handleSubmit(campaignData2) {
|
|
3542
4002
|
var _a;
|
|
@@ -3547,14 +4007,7 @@ const FlashSaleDetail = () => {
|
|
|
3547
4007
|
ends_at: new Date(campaignData2.ends_at).toUTCString()
|
|
3548
4008
|
});
|
|
3549
4009
|
toast.success("Flash sale updated successfully");
|
|
3550
|
-
|
|
3551
|
-
exact: false,
|
|
3552
|
-
predicate(query) {
|
|
3553
|
-
return ["flash-sales", "flash-sale"].includes(
|
|
3554
|
-
query.queryKey[0]
|
|
3555
|
-
);
|
|
3556
|
-
}
|
|
3557
|
-
});
|
|
4010
|
+
refetch();
|
|
3558
4011
|
navigate("/flash-sales");
|
|
3559
4012
|
} catch (error) {
|
|
3560
4013
|
let message;
|
|
@@ -3626,54 +4079,10 @@ const config = defineRouteConfig({
|
|
|
3626
4079
|
label: "Flash Sale Detail",
|
|
3627
4080
|
icon: Sparkles
|
|
3628
4081
|
});
|
|
3629
|
-
const FlashSaleCreate = () => {
|
|
3630
|
-
const navigate = useNavigate();
|
|
3631
|
-
const queryClient = useQueryClient();
|
|
3632
|
-
async function handleSubmit(campaignData) {
|
|
3633
|
-
var _a;
|
|
3634
|
-
try {
|
|
3635
|
-
await axios.post("/admin/flash-sales", {
|
|
3636
|
-
...campaignData,
|
|
3637
|
-
starts_at: new Date(campaignData.starts_at).toUTCString(),
|
|
3638
|
-
ends_at: new Date(campaignData.ends_at).toUTCString()
|
|
3639
|
-
});
|
|
3640
|
-
toast.success("Flash sale created successfully");
|
|
3641
|
-
queryClient.invalidateQueries({
|
|
3642
|
-
exact: false,
|
|
3643
|
-
queryKey: ["flash-sales"]
|
|
3644
|
-
});
|
|
3645
|
-
navigate("/flash-sales");
|
|
3646
|
-
} catch (error) {
|
|
3647
|
-
let message;
|
|
3648
|
-
if (axios.isAxiosError(error)) {
|
|
3649
|
-
console.log(error);
|
|
3650
|
-
message = (_a = error.response) == null ? void 0 : _a.data.message;
|
|
3651
|
-
} else if (error instanceof Error) {
|
|
3652
|
-
message = error.message;
|
|
3653
|
-
} else {
|
|
3654
|
-
message = "Failed to create flash sale";
|
|
3655
|
-
}
|
|
3656
|
-
toast.error("Failed to create flash sale", {
|
|
3657
|
-
description: message
|
|
3658
|
-
});
|
|
3659
|
-
}
|
|
3660
|
-
}
|
|
3661
|
-
return /* @__PURE__ */ jsx(
|
|
3662
|
-
FlashSaleForm,
|
|
3663
|
-
{
|
|
3664
|
-
onSubmit: handleSubmit,
|
|
3665
|
-
onCancel: () => navigate("/flash-sales")
|
|
3666
|
-
}
|
|
3667
|
-
);
|
|
3668
|
-
};
|
|
3669
4082
|
const widgetModule = { widgets: [
|
|
3670
4083
|
{
|
|
3671
4084
|
Component: CampaignDetailWidget,
|
|
3672
4085
|
zone: ["promotion.details.side.after"]
|
|
3673
|
-
},
|
|
3674
|
-
{
|
|
3675
|
-
Component: CampaignStatsWidget,
|
|
3676
|
-
zone: ["campaign.list.before"]
|
|
3677
4086
|
}
|
|
3678
4087
|
] };
|
|
3679
4088
|
const routeModule = {
|
|
@@ -3683,21 +4092,45 @@ const routeModule = {
|
|
|
3683
4092
|
path: "/flash-sales"
|
|
3684
4093
|
},
|
|
3685
4094
|
{
|
|
3686
|
-
Component:
|
|
3687
|
-
path: "/
|
|
4095
|
+
Component: Coupons,
|
|
4096
|
+
path: "/coupons"
|
|
3688
4097
|
},
|
|
3689
4098
|
{
|
|
3690
4099
|
Component: FlashSaleCreate,
|
|
3691
4100
|
path: "/flash-sales/create"
|
|
4101
|
+
},
|
|
4102
|
+
{
|
|
4103
|
+
Component: CouponDetail,
|
|
4104
|
+
path: "/coupons/:id"
|
|
4105
|
+
},
|
|
4106
|
+
{
|
|
4107
|
+
Component: CouponCreate,
|
|
4108
|
+
path: "/coupons/create"
|
|
4109
|
+
},
|
|
4110
|
+
{
|
|
4111
|
+
Component: FlashSaleDetail,
|
|
4112
|
+
path: "/flash-sales/:id"
|
|
3692
4113
|
}
|
|
3693
4114
|
]
|
|
3694
4115
|
};
|
|
3695
4116
|
const menuItemModule = {
|
|
3696
4117
|
menuItems: [
|
|
4118
|
+
{
|
|
4119
|
+
label: config$2.label,
|
|
4120
|
+
icon: config$2.icon,
|
|
4121
|
+
path: "/coupons",
|
|
4122
|
+
nested: void 0
|
|
4123
|
+
},
|
|
4124
|
+
{
|
|
4125
|
+
label: config$3.label,
|
|
4126
|
+
icon: config$3.icon,
|
|
4127
|
+
path: "/flash-sales",
|
|
4128
|
+
nested: void 0
|
|
4129
|
+
},
|
|
3697
4130
|
{
|
|
3698
4131
|
label: config$1.label,
|
|
3699
4132
|
icon: config$1.icon,
|
|
3700
|
-
path: "/
|
|
4133
|
+
path: "/coupons/:id",
|
|
3701
4134
|
nested: void 0
|
|
3702
4135
|
},
|
|
3703
4136
|
{
|
|
@@ -3712,12 +4145,14 @@ const formModule = { customFields: {} };
|
|
|
3712
4145
|
const displayModule = {
|
|
3713
4146
|
displays: {}
|
|
3714
4147
|
};
|
|
4148
|
+
const i18nModule = { resources: {} };
|
|
3715
4149
|
const plugin = {
|
|
3716
4150
|
widgetModule,
|
|
3717
4151
|
routeModule,
|
|
3718
4152
|
menuItemModule,
|
|
3719
4153
|
formModule,
|
|
3720
|
-
displayModule
|
|
4154
|
+
displayModule,
|
|
4155
|
+
i18nModule
|
|
3721
4156
|
};
|
|
3722
4157
|
export {
|
|
3723
4158
|
plugin as default
|