@lodashventure/medusa-campaign 1.1.0 → 1.1.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.
- package/.medusa/server/src/admin/index.js +1 -67
- package/.medusa/server/src/admin/index.mjs +1 -67
- package/.medusa/server/src/workflows/index.js +10 -0
- package/package.json +4 -4
- package/src/admin/README.md +31 -0
- package/src/admin/components/FlashSaleForm.tsx +379 -0
- package/src/admin/components/FlashSalePage.tsx +113 -0
- package/src/admin/components/ProductSelector.tsx +88 -0
- package/src/admin/hooks/useFlashSaleById.ts +21 -0
- package/src/admin/hooks/useFlashSales.ts +25 -0
- package/src/admin/lib/sdk.ts +10 -0
- package/src/admin/routes/flash-sales/[id]/page.tsx +105 -0
- package/src/admin/routes/flash-sales/create/page.tsx +51 -0
- package/src/admin/routes/flash-sales/page.tsx +15 -0
- package/src/admin/tsconfig.json +24 -0
- package/src/admin/types/campaign.ts +25 -0
- package/src/admin/vite-env.d.ts +1 -0
- package/src/api/README.md +133 -0
- package/src/api/admin/flash-sales/[id]/route.ts +164 -0
- package/src/api/admin/flash-sales/route.ts +87 -0
- package/src/api/middlewares.ts +32 -0
- package/src/api/store/campaigns/[id]/route.ts +133 -0
- package/src/api/store/campaigns/route.ts +36 -0
- package/src/jobs/README.md +36 -0
- package/src/links/README.md +26 -0
- package/src/links/campaign-type.ts +8 -0
- package/src/modules/README.md +116 -0
- package/src/modules/custom-campaigns/index.ts +8 -0
- package/src/modules/custom-campaigns/migrations/.snapshot-medusa-custom-campaign.json +235 -0
- package/src/modules/custom-campaigns/migrations/Migration20250524150901.ts +23 -0
- package/src/modules/custom-campaigns/migrations/Migration20250526010310.ts +20 -0
- package/src/modules/custom-campaigns/migrations/Migration20250529011904.ts +13 -0
- package/src/modules/custom-campaigns/models/custom-campaign-type.ts +10 -0
- package/src/modules/custom-campaigns/models/promotion-usage-limit.ts +14 -0
- package/src/modules/custom-campaigns/service.ts +10 -0
- package/src/modules/custom-campaigns/types/campaign-type.enum.ts +3 -0
- package/src/providers/README.md +30 -0
- package/src/subscribers/README.md +59 -0
- package/src/subscribers/order-placed.ts +17 -0
- package/src/workflows/README.md +79 -0
- package/src/workflows/custom-campaign/createCustomCampaignWorkflow.ts +181 -0
- package/src/workflows/custom-campaign/updateCustomFlashSaleWorkflow.ts +185 -0
- package/src/workflows/custom-campaign/updatePromotionUsageWorkflow.ts +70 -0
- package/src/workflows/hooks/deletePromotionOnCampaignDelete.ts +49 -0
- package/src/workflows/index.ts +3 -0
|
@@ -1230,7 +1230,7 @@ const FlashSalePage = () => {
|
|
|
1230
1230
|
const table = ui.useDataTable({
|
|
1231
1231
|
data: (data == null ? void 0 : data.campaigns) || [],
|
|
1232
1232
|
columns: columns2,
|
|
1233
|
-
getRowId: (
|
|
1233
|
+
getRowId: (campaign) => campaign.id,
|
|
1234
1234
|
pagination: {
|
|
1235
1235
|
state: pagination,
|
|
1236
1236
|
onPaginationChange: setPagination
|
|
@@ -7340,42 +7340,6 @@ const FlashSaleForm = ({
|
|
|
7340
7340
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
7341
7341
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
7342
7342
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Start Date" }),
|
|
7343
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
7344
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7345
|
-
ui.Input,
|
|
7346
|
-
{
|
|
7347
|
-
type: "date",
|
|
7348
|
-
name: "start_date",
|
|
7349
|
-
value: campaign.starts_at ? dayjs__default.default(campaign.starts_at).format("YYYY-MM-DD") : "",
|
|
7350
|
-
onChange: (e2) => {
|
|
7351
|
-
const time = campaign.starts_at ? dayjs__default.default(campaign.starts_at).format("HH:mm") : "00:00";
|
|
7352
|
-
handleCampaignChange(
|
|
7353
|
-
"starts_at",
|
|
7354
|
-
`${e2.target.value}T${time}`
|
|
7355
|
-
);
|
|
7356
|
-
},
|
|
7357
|
-
disabled,
|
|
7358
|
-
className: "flex-1"
|
|
7359
|
-
}
|
|
7360
|
-
),
|
|
7361
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7362
|
-
ui.Input,
|
|
7363
|
-
{
|
|
7364
|
-
type: "time",
|
|
7365
|
-
name: "start_time",
|
|
7366
|
-
value: campaign.starts_at ? dayjs__default.default(campaign.starts_at).format("HH:mm") : "",
|
|
7367
|
-
onChange: (e2) => {
|
|
7368
|
-
const date = campaign.starts_at ? dayjs__default.default(campaign.starts_at).format("YYYY-MM-DD") : dayjs__default.default().format("YYYY-MM-DD");
|
|
7369
|
-
handleCampaignChange(
|
|
7370
|
-
"starts_at",
|
|
7371
|
-
`${date}T${e2.target.value}`
|
|
7372
|
-
);
|
|
7373
|
-
},
|
|
7374
|
-
disabled,
|
|
7375
|
-
className: "w-32"
|
|
7376
|
-
}
|
|
7377
|
-
)
|
|
7378
|
-
] }),
|
|
7379
7343
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
7380
7344
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7381
7345
|
ui.Input,
|
|
@@ -7402,36 +7366,6 @@ const FlashSaleForm = ({
|
|
|
7402
7366
|
] }),
|
|
7403
7367
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
7404
7368
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "End Date" }),
|
|
7405
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
7406
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7407
|
-
ui.Input,
|
|
7408
|
-
{
|
|
7409
|
-
type: "date",
|
|
7410
|
-
name: "end_date",
|
|
7411
|
-
value: campaign.ends_at ? dayjs__default.default(campaign.ends_at).format("YYYY-MM-DD") : "",
|
|
7412
|
-
onChange: (e2) => {
|
|
7413
|
-
const time = campaign.ends_at ? dayjs__default.default(campaign.ends_at).format("HH:mm") : "23:59";
|
|
7414
|
-
handleCampaignChange("ends_at", `${e2.target.value}T${time}`);
|
|
7415
|
-
},
|
|
7416
|
-
disabled,
|
|
7417
|
-
className: "flex-1"
|
|
7418
|
-
}
|
|
7419
|
-
),
|
|
7420
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7421
|
-
ui.Input,
|
|
7422
|
-
{
|
|
7423
|
-
type: "time",
|
|
7424
|
-
name: "end_time",
|
|
7425
|
-
value: campaign.ends_at ? dayjs__default.default(campaign.ends_at).format("HH:mm") : "",
|
|
7426
|
-
onChange: (e2) => {
|
|
7427
|
-
const date = campaign.ends_at ? dayjs__default.default(campaign.ends_at).format("YYYY-MM-DD") : dayjs__default.default().format("YYYY-MM-DD");
|
|
7428
|
-
handleCampaignChange("ends_at", `${date}T${e2.target.value}`);
|
|
7429
|
-
},
|
|
7430
|
-
disabled,
|
|
7431
|
-
className: "w-32"
|
|
7432
|
-
}
|
|
7433
|
-
)
|
|
7434
|
-
] }),
|
|
7435
7369
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
7436
7370
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7437
7371
|
ui.Input,
|
|
@@ -1225,7 +1225,7 @@ const FlashSalePage = () => {
|
|
|
1225
1225
|
const table = useDataTable({
|
|
1226
1226
|
data: (data == null ? void 0 : data.campaigns) || [],
|
|
1227
1227
|
columns: columns2,
|
|
1228
|
-
getRowId: (
|
|
1228
|
+
getRowId: (campaign) => campaign.id,
|
|
1229
1229
|
pagination: {
|
|
1230
1230
|
state: pagination,
|
|
1231
1231
|
onPaginationChange: setPagination
|
|
@@ -7335,42 +7335,6 @@ const FlashSaleForm = ({
|
|
|
7335
7335
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
7336
7336
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
7337
7337
|
/* @__PURE__ */ jsx(Label, { children: "Start Date" }),
|
|
7338
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
7339
|
-
/* @__PURE__ */ jsx(
|
|
7340
|
-
Input,
|
|
7341
|
-
{
|
|
7342
|
-
type: "date",
|
|
7343
|
-
name: "start_date",
|
|
7344
|
-
value: campaign.starts_at ? dayjs(campaign.starts_at).format("YYYY-MM-DD") : "",
|
|
7345
|
-
onChange: (e2) => {
|
|
7346
|
-
const time = campaign.starts_at ? dayjs(campaign.starts_at).format("HH:mm") : "00:00";
|
|
7347
|
-
handleCampaignChange(
|
|
7348
|
-
"starts_at",
|
|
7349
|
-
`${e2.target.value}T${time}`
|
|
7350
|
-
);
|
|
7351
|
-
},
|
|
7352
|
-
disabled,
|
|
7353
|
-
className: "flex-1"
|
|
7354
|
-
}
|
|
7355
|
-
),
|
|
7356
|
-
/* @__PURE__ */ jsx(
|
|
7357
|
-
Input,
|
|
7358
|
-
{
|
|
7359
|
-
type: "time",
|
|
7360
|
-
name: "start_time",
|
|
7361
|
-
value: campaign.starts_at ? dayjs(campaign.starts_at).format("HH:mm") : "",
|
|
7362
|
-
onChange: (e2) => {
|
|
7363
|
-
const date = campaign.starts_at ? dayjs(campaign.starts_at).format("YYYY-MM-DD") : dayjs().format("YYYY-MM-DD");
|
|
7364
|
-
handleCampaignChange(
|
|
7365
|
-
"starts_at",
|
|
7366
|
-
`${date}T${e2.target.value}`
|
|
7367
|
-
);
|
|
7368
|
-
},
|
|
7369
|
-
disabled,
|
|
7370
|
-
className: "w-32"
|
|
7371
|
-
}
|
|
7372
|
-
)
|
|
7373
|
-
] }),
|
|
7374
7338
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
7375
7339
|
/* @__PURE__ */ jsx(
|
|
7376
7340
|
Input,
|
|
@@ -7397,36 +7361,6 @@ const FlashSaleForm = ({
|
|
|
7397
7361
|
] }),
|
|
7398
7362
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
7399
7363
|
/* @__PURE__ */ jsx(Label, { children: "End Date" }),
|
|
7400
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
7401
|
-
/* @__PURE__ */ jsx(
|
|
7402
|
-
Input,
|
|
7403
|
-
{
|
|
7404
|
-
type: "date",
|
|
7405
|
-
name: "end_date",
|
|
7406
|
-
value: campaign.ends_at ? dayjs(campaign.ends_at).format("YYYY-MM-DD") : "",
|
|
7407
|
-
onChange: (e2) => {
|
|
7408
|
-
const time = campaign.ends_at ? dayjs(campaign.ends_at).format("HH:mm") : "23:59";
|
|
7409
|
-
handleCampaignChange("ends_at", `${e2.target.value}T${time}`);
|
|
7410
|
-
},
|
|
7411
|
-
disabled,
|
|
7412
|
-
className: "flex-1"
|
|
7413
|
-
}
|
|
7414
|
-
),
|
|
7415
|
-
/* @__PURE__ */ jsx(
|
|
7416
|
-
Input,
|
|
7417
|
-
{
|
|
7418
|
-
type: "time",
|
|
7419
|
-
name: "end_time",
|
|
7420
|
-
value: campaign.ends_at ? dayjs(campaign.ends_at).format("HH:mm") : "",
|
|
7421
|
-
onChange: (e2) => {
|
|
7422
|
-
const date = campaign.ends_at ? dayjs(campaign.ends_at).format("YYYY-MM-DD") : dayjs().format("YYYY-MM-DD");
|
|
7423
|
-
handleCampaignChange("ends_at", `${date}T${e2.target.value}`);
|
|
7424
|
-
},
|
|
7425
|
-
disabled,
|
|
7426
|
-
className: "w-32"
|
|
7427
|
-
}
|
|
7428
|
-
)
|
|
7429
|
-
] }),
|
|
7430
7364
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
7431
7365
|
/* @__PURE__ */ jsx(
|
|
7432
7366
|
Input,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updatePromotionUsageWorkflow = exports.updateCustomFlashSaleWorkflow = exports.createCustomFlashSaleWorkflow = void 0;
|
|
4
|
+
var createCustomCampaignWorkflow_1 = require("./custom-campaign/createCustomCampaignWorkflow");
|
|
5
|
+
Object.defineProperty(exports, "createCustomFlashSaleWorkflow", { enumerable: true, get: function () { return createCustomCampaignWorkflow_1.createCustomFlashSaleWorkflow; } });
|
|
6
|
+
var updateCustomFlashSaleWorkflow_1 = require("./custom-campaign/updateCustomFlashSaleWorkflow");
|
|
7
|
+
Object.defineProperty(exports, "updateCustomFlashSaleWorkflow", { enumerable: true, get: function () { return updateCustomFlashSaleWorkflow_1.updateCustomFlashSaleWorkflow; } });
|
|
8
|
+
var updatePromotionUsageWorkflow_1 = require("./custom-campaign/updatePromotionUsageWorkflow");
|
|
9
|
+
Object.defineProperty(exports, "updatePromotionUsageWorkflow", { enumerable: true, get: function () { return updatePromotionUsageWorkflow_1.updatePromotionUsageWorkflow; } });
|
|
10
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLCtGQUErRjtBQUF0Riw2SUFBQSw2QkFBNkIsT0FBQTtBQUN0QyxpR0FBZ0c7QUFBdkYsOElBQUEsNkJBQTZCLE9BQUE7QUFDdEMsK0ZBQThGO0FBQXJGLDRJQUFBLDRCQUE0QixPQUFBIn0=
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lodashventure/medusa-campaign",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A starter for Medusa plugins.",
|
|
5
5
|
"author": "Medusa (https://medusajs.com)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"files": [
|
|
8
|
-
".medusa/server"
|
|
8
|
+
".medusa/server",
|
|
9
|
+
"src"
|
|
9
10
|
],
|
|
10
11
|
"exports": {
|
|
11
12
|
"./package.json": "./package.json",
|
|
@@ -25,8 +26,7 @@
|
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build": "medusa plugin:build",
|
|
28
|
-
"dev": "medusa plugin:develop"
|
|
29
|
-
"prepublishOnly": "medusa plugin:build"
|
|
29
|
+
"dev": "medusa plugin:develop"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@medusajs/admin-sdk": "2.10.0",
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Admin Customizations
|
|
2
|
+
|
|
3
|
+
You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities.
|
|
4
|
+
|
|
5
|
+
## Example: Create a Widget
|
|
6
|
+
|
|
7
|
+
A widget is a React component that can be injected into an existing page in the admin dashboard.
|
|
8
|
+
|
|
9
|
+
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
|
|
10
|
+
|
|
11
|
+
```tsx title="src/admin/widgets/product-widget.tsx"
|
|
12
|
+
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
|
13
|
+
|
|
14
|
+
// The widget
|
|
15
|
+
const ProductWidget = () => {
|
|
16
|
+
return (
|
|
17
|
+
<div>
|
|
18
|
+
<h2>Product Widget</h2>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The widget's configurations
|
|
24
|
+
export const config = defineWidgetConfig({
|
|
25
|
+
zone: "product.details.after",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export default ProductWidget
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This inserts a widget with the text “Product Widget” at the end of a product’s details page.
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Trash } from "@medusajs/icons";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Container,
|
|
5
|
+
FocusModal,
|
|
6
|
+
Input,
|
|
7
|
+
Label,
|
|
8
|
+
Select,
|
|
9
|
+
Table,
|
|
10
|
+
Textarea,
|
|
11
|
+
toast,
|
|
12
|
+
} from "@medusajs/ui";
|
|
13
|
+
import dayjs from "dayjs";
|
|
14
|
+
import { FC, useState } from "react";
|
|
15
|
+
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
|
16
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
17
|
+
import z from "zod";
|
|
18
|
+
import { ProductSelector } from "./ProductSelector";
|
|
19
|
+
|
|
20
|
+
const customCampaignSchema = z.object({
|
|
21
|
+
name: z.string().min(1, "Name is required"),
|
|
22
|
+
description: z.string().min(1, "Description is required"),
|
|
23
|
+
type: z.literal("flash-sale"),
|
|
24
|
+
starts_at: z.string().min(1, "Start date is required"),
|
|
25
|
+
ends_at: z.string().min(1, "End date is required"),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type CustomCampaign = z.infer<typeof customCampaignSchema>;
|
|
29
|
+
|
|
30
|
+
const campaignProductSchema = z.object({
|
|
31
|
+
product: z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
title: z.string(),
|
|
34
|
+
}),
|
|
35
|
+
discountType: z.enum([
|
|
36
|
+
"percentage",
|
|
37
|
+
// , "fixed"
|
|
38
|
+
]),
|
|
39
|
+
discountValue: z.number().min(1),
|
|
40
|
+
limit: z.number().min(1),
|
|
41
|
+
maxQty: z.number().min(1),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type CampaignProduct = z.infer<typeof campaignProductSchema>;
|
|
45
|
+
|
|
46
|
+
export const campaignDataSchema = customCampaignSchema
|
|
47
|
+
.extend({
|
|
48
|
+
products: z
|
|
49
|
+
.array(campaignProductSchema)
|
|
50
|
+
.min(1, "At least one product is required"),
|
|
51
|
+
})
|
|
52
|
+
.refine(
|
|
53
|
+
(data) => new Date(data.starts_at) < new Date(data.ends_at),
|
|
54
|
+
"End date must be after start date",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export type CampaignData = z.infer<typeof campaignDataSchema>;
|
|
58
|
+
|
|
59
|
+
interface FlashSaleFormProps {
|
|
60
|
+
initialData?: CustomCampaign;
|
|
61
|
+
initialProducts?: Map<string, CampaignProduct>;
|
|
62
|
+
onSubmit: (data: CampaignData) => void;
|
|
63
|
+
onCancel: () => void;
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const FlashSaleForm: FC<FlashSaleFormProps> = ({
|
|
68
|
+
initialData,
|
|
69
|
+
initialProducts,
|
|
70
|
+
onSubmit,
|
|
71
|
+
onCancel,
|
|
72
|
+
disabled = false,
|
|
73
|
+
}) => {
|
|
74
|
+
const {
|
|
75
|
+
control,
|
|
76
|
+
register,
|
|
77
|
+
handleSubmit,
|
|
78
|
+
watch,
|
|
79
|
+
setValue,
|
|
80
|
+
formState: { errors },
|
|
81
|
+
} = useForm<CampaignData>({
|
|
82
|
+
resolver: zodResolver(campaignDataSchema),
|
|
83
|
+
defaultValues: {
|
|
84
|
+
name: initialData?.name || "",
|
|
85
|
+
description: initialData?.description || "",
|
|
86
|
+
type: "flash-sale",
|
|
87
|
+
starts_at: initialData?.starts_at || "",
|
|
88
|
+
ends_at: initialData?.ends_at || "",
|
|
89
|
+
products: initialProducts ? Array.from(initialProducts.values()) : [],
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { fields, append, remove, update } = useFieldArray({
|
|
94
|
+
control,
|
|
95
|
+
name: "products",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const [openProductModal, setOpenProductModal] = useState(false);
|
|
99
|
+
|
|
100
|
+
const startsAt = watch("starts_at");
|
|
101
|
+
const endsAt = watch("ends_at");
|
|
102
|
+
|
|
103
|
+
const handleDateTimeChange = (
|
|
104
|
+
field: "starts_at" | "ends_at",
|
|
105
|
+
type: "date" | "time",
|
|
106
|
+
value: string,
|
|
107
|
+
) => {
|
|
108
|
+
const currentValue = watch(field);
|
|
109
|
+
|
|
110
|
+
if (type === "date") {
|
|
111
|
+
const time = currentValue
|
|
112
|
+
? dayjs(currentValue).format("HH:mm")
|
|
113
|
+
: field === "starts_at"
|
|
114
|
+
? "00:00"
|
|
115
|
+
: "23:59";
|
|
116
|
+
setValue(field, `${value}T${time}`);
|
|
117
|
+
} else {
|
|
118
|
+
const date = currentValue
|
|
119
|
+
? dayjs(currentValue).format("YYYY-MM-DD")
|
|
120
|
+
: dayjs().format("YYYY-MM-DD");
|
|
121
|
+
setValue(field, `${date}T${value}`);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const onFormSubmit = (data: CampaignData) => {
|
|
126
|
+
onSubmit(data);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const onFormError = () => {
|
|
130
|
+
const errorMessages = Object.entries(errors)
|
|
131
|
+
.map(([key, value]) => {
|
|
132
|
+
if (key === "products" && Array.isArray(value)) {
|
|
133
|
+
return value
|
|
134
|
+
.map((item, index) =>
|
|
135
|
+
item
|
|
136
|
+
? `Product ${index + 1}: ${Object.values(item).join(", ")}`
|
|
137
|
+
: "",
|
|
138
|
+
)
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(", ");
|
|
141
|
+
}
|
|
142
|
+
return value?.message || value?.root?.message;
|
|
143
|
+
})
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join(", ");
|
|
146
|
+
|
|
147
|
+
toast.error("Invalid data", {
|
|
148
|
+
description: errorMessages,
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Container>
|
|
154
|
+
<div className="flex items-center justify-between">
|
|
155
|
+
<h1 className="text-xl font-semibold">Flash sale campaign</h1>
|
|
156
|
+
<Button variant="transparent" onClick={onCancel}>
|
|
157
|
+
Cancel
|
|
158
|
+
</Button>
|
|
159
|
+
</div>
|
|
160
|
+
<form
|
|
161
|
+
onSubmit={handleSubmit(onFormSubmit, onFormError)}
|
|
162
|
+
className="space-y-4 my-8"
|
|
163
|
+
>
|
|
164
|
+
<div>
|
|
165
|
+
<Label>Name</Label>
|
|
166
|
+
<Input {...register("name")} disabled={disabled} />
|
|
167
|
+
{errors.name && (
|
|
168
|
+
<p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div>
|
|
173
|
+
<Label>Description</Label>
|
|
174
|
+
<Textarea {...register("description")} disabled={disabled} />
|
|
175
|
+
{errors.description && (
|
|
176
|
+
<p className="text-red-500 text-sm mt-1">
|
|
177
|
+
{errors.description.message}
|
|
178
|
+
</p>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="grid grid-cols-2 gap-4">
|
|
183
|
+
<div>
|
|
184
|
+
<Label>Start Date</Label>
|
|
185
|
+
<div className="flex gap-2">
|
|
186
|
+
<Input
|
|
187
|
+
type="date"
|
|
188
|
+
value={startsAt ? dayjs(startsAt).format("YYYY-MM-DD") : ""}
|
|
189
|
+
onChange={(e) =>
|
|
190
|
+
handleDateTimeChange("starts_at", "date", e.target.value)
|
|
191
|
+
}
|
|
192
|
+
disabled={disabled}
|
|
193
|
+
className="flex-1"
|
|
194
|
+
/>
|
|
195
|
+
<Input
|
|
196
|
+
type="time"
|
|
197
|
+
value={startsAt ? dayjs(startsAt).format("HH:mm") : ""}
|
|
198
|
+
onChange={(e) =>
|
|
199
|
+
handleDateTimeChange("starts_at", "time", e.target.value)
|
|
200
|
+
}
|
|
201
|
+
disabled={disabled}
|
|
202
|
+
className="w-32"
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
{errors.starts_at && (
|
|
206
|
+
<p className="text-red-500 text-sm mt-1">
|
|
207
|
+
{errors.starts_at.message}
|
|
208
|
+
</p>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
<div>
|
|
212
|
+
<Label>End Date</Label>
|
|
213
|
+
<div className="flex gap-2">
|
|
214
|
+
<Input
|
|
215
|
+
type="date"
|
|
216
|
+
value={endsAt ? dayjs(endsAt).format("YYYY-MM-DD") : ""}
|
|
217
|
+
onChange={(e) =>
|
|
218
|
+
handleDateTimeChange("ends_at", "date", e.target.value)
|
|
219
|
+
}
|
|
220
|
+
disabled={disabled}
|
|
221
|
+
className="flex-1"
|
|
222
|
+
/>
|
|
223
|
+
<Input
|
|
224
|
+
type="time"
|
|
225
|
+
value={endsAt ? dayjs(endsAt).format("HH:mm") : ""}
|
|
226
|
+
onChange={(e) =>
|
|
227
|
+
handleDateTimeChange("ends_at", "time", e.target.value)
|
|
228
|
+
}
|
|
229
|
+
disabled={disabled}
|
|
230
|
+
className="w-32"
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
{errors.ends_at && (
|
|
234
|
+
<p className="text-red-500 text-sm mt-1">
|
|
235
|
+
{errors.ends_at.message}
|
|
236
|
+
</p>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div className="flex justify-between items-center">
|
|
242
|
+
<Label>Products</Label>
|
|
243
|
+
<Button
|
|
244
|
+
type="button"
|
|
245
|
+
variant="secondary"
|
|
246
|
+
onClick={() => setOpenProductModal(true)}
|
|
247
|
+
disabled={disabled}
|
|
248
|
+
>
|
|
249
|
+
Add Product
|
|
250
|
+
</Button>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{errors.products?.root && (
|
|
254
|
+
<p className="text-red-500 text-sm">{errors.products.root.message}</p>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
<Table>
|
|
258
|
+
<Table.Header>
|
|
259
|
+
<Table.Row>
|
|
260
|
+
<Table.HeaderCell>Product</Table.HeaderCell>
|
|
261
|
+
<Table.HeaderCell>Discount Type</Table.HeaderCell>
|
|
262
|
+
<Table.HeaderCell>Discount Value</Table.HeaderCell>
|
|
263
|
+
<Table.HeaderCell>Limit</Table.HeaderCell>
|
|
264
|
+
<Table.HeaderCell>Max Qty per Order</Table.HeaderCell>
|
|
265
|
+
<Table.HeaderCell>Actions</Table.HeaderCell>
|
|
266
|
+
</Table.Row>
|
|
267
|
+
</Table.Header>
|
|
268
|
+
<Table.Body>
|
|
269
|
+
{fields.map((field, index) => (
|
|
270
|
+
<Table.Row key={field.id}>
|
|
271
|
+
<Table.Cell>{field.product.title}</Table.Cell>
|
|
272
|
+
<Table.Cell>
|
|
273
|
+
<Controller
|
|
274
|
+
name={`products.${index}.discountType`}
|
|
275
|
+
control={control}
|
|
276
|
+
render={({ field }) => (
|
|
277
|
+
<Select
|
|
278
|
+
value={field.value}
|
|
279
|
+
onValueChange={field.onChange}
|
|
280
|
+
disabled={disabled}
|
|
281
|
+
>
|
|
282
|
+
<Select.Trigger>
|
|
283
|
+
<Select.Value placeholder="Select discount type" />
|
|
284
|
+
</Select.Trigger>
|
|
285
|
+
<Select.Content>
|
|
286
|
+
<Select.Item value="percentage">
|
|
287
|
+
Percentage
|
|
288
|
+
</Select.Item>
|
|
289
|
+
{/* <Select.Item value="fixed">Fixed</Select.Item> */}
|
|
290
|
+
</Select.Content>
|
|
291
|
+
</Select>
|
|
292
|
+
)}
|
|
293
|
+
/>
|
|
294
|
+
</Table.Cell>
|
|
295
|
+
<Table.Cell>
|
|
296
|
+
<Controller
|
|
297
|
+
name={`products.${index}.discountValue`}
|
|
298
|
+
control={control}
|
|
299
|
+
render={({ field }) => (
|
|
300
|
+
<Input
|
|
301
|
+
type="number"
|
|
302
|
+
value={field.value}
|
|
303
|
+
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
304
|
+
disabled={disabled}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
307
|
+
/>
|
|
308
|
+
</Table.Cell>
|
|
309
|
+
<Table.Cell>
|
|
310
|
+
<Controller
|
|
311
|
+
name={`products.${index}.limit`}
|
|
312
|
+
control={control}
|
|
313
|
+
render={({ field }) => (
|
|
314
|
+
<Input
|
|
315
|
+
type="number"
|
|
316
|
+
value={field.value}
|
|
317
|
+
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
318
|
+
disabled={disabled}
|
|
319
|
+
/>
|
|
320
|
+
)}
|
|
321
|
+
/>
|
|
322
|
+
</Table.Cell>
|
|
323
|
+
<Table.Cell>
|
|
324
|
+
<Controller
|
|
325
|
+
name={`products.${index}.maxQty`}
|
|
326
|
+
control={control}
|
|
327
|
+
render={({ field }) => (
|
|
328
|
+
<Input
|
|
329
|
+
type="number"
|
|
330
|
+
value={field.value}
|
|
331
|
+
onChange={(e) => field.onChange(Number(e.target.value))}
|
|
332
|
+
disabled={disabled}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
335
|
+
/>
|
|
336
|
+
</Table.Cell>
|
|
337
|
+
<Table.Cell>
|
|
338
|
+
<Button
|
|
339
|
+
type="button"
|
|
340
|
+
variant="danger"
|
|
341
|
+
onClick={() => remove(index)}
|
|
342
|
+
disabled={disabled}
|
|
343
|
+
>
|
|
344
|
+
<Trash />
|
|
345
|
+
</Button>
|
|
346
|
+
</Table.Cell>
|
|
347
|
+
</Table.Row>
|
|
348
|
+
))}
|
|
349
|
+
</Table.Body>
|
|
350
|
+
</Table>
|
|
351
|
+
|
|
352
|
+
<Button type="submit" disabled={disabled}>
|
|
353
|
+
Save
|
|
354
|
+
</Button>
|
|
355
|
+
</form>
|
|
356
|
+
|
|
357
|
+
<FocusModal open={openProductModal} onOpenChange={setOpenProductModal}>
|
|
358
|
+
<FocusModal.Content>
|
|
359
|
+
<FocusModal.Header title="Add Product" />
|
|
360
|
+
<FocusModal.Body>
|
|
361
|
+
<ProductSelector
|
|
362
|
+
selectedProductIds={fields.map((f) => f.product.id)}
|
|
363
|
+
onSelectProduct={(product) => {
|
|
364
|
+
append({
|
|
365
|
+
product,
|
|
366
|
+
discountType: "percentage",
|
|
367
|
+
discountValue: 10,
|
|
368
|
+
limit: 10,
|
|
369
|
+
maxQty: 1,
|
|
370
|
+
});
|
|
371
|
+
setOpenProductModal(false);
|
|
372
|
+
}}
|
|
373
|
+
/>
|
|
374
|
+
</FocusModal.Body>
|
|
375
|
+
</FocusModal.Content>
|
|
376
|
+
</FocusModal>
|
|
377
|
+
</Container>
|
|
378
|
+
);
|
|
379
|
+
};
|