@lodashventure/medusa-campaign 1.1.0 → 1.1.2

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 (45) hide show
  1. package/.medusa/server/src/admin/index.js +1 -67
  2. package/.medusa/server/src/admin/index.mjs +1 -67
  3. package/.medusa/server/src/workflows/index.js +10 -0
  4. package/package.json +4 -4
  5. package/src/admin/README.md +31 -0
  6. package/src/admin/components/FlashSaleForm.tsx +379 -0
  7. package/src/admin/components/FlashSalePage.tsx +113 -0
  8. package/src/admin/components/ProductSelector.tsx +88 -0
  9. package/src/admin/hooks/useFlashSaleById.ts +21 -0
  10. package/src/admin/hooks/useFlashSales.ts +25 -0
  11. package/src/admin/lib/sdk.ts +10 -0
  12. package/src/admin/routes/flash-sales/[id]/page.tsx +105 -0
  13. package/src/admin/routes/flash-sales/create/page.tsx +51 -0
  14. package/src/admin/routes/flash-sales/page.tsx +15 -0
  15. package/src/admin/tsconfig.json +24 -0
  16. package/src/admin/types/campaign.ts +25 -0
  17. package/src/admin/vite-env.d.ts +1 -0
  18. package/src/api/README.md +133 -0
  19. package/src/api/admin/flash-sales/[id]/route.ts +164 -0
  20. package/src/api/admin/flash-sales/route.ts +87 -0
  21. package/src/api/middlewares.ts +32 -0
  22. package/src/api/store/campaigns/[id]/route.ts +133 -0
  23. package/src/api/store/campaigns/route.ts +36 -0
  24. package/src/jobs/README.md +36 -0
  25. package/src/links/README.md +26 -0
  26. package/src/links/campaign-type.ts +8 -0
  27. package/src/modules/README.md +116 -0
  28. package/src/modules/custom-campaigns/index.ts +8 -0
  29. package/src/modules/custom-campaigns/migrations/.snapshot-medusa-custom-campaign.json +235 -0
  30. package/src/modules/custom-campaigns/migrations/Migration20250524150901.ts +23 -0
  31. package/src/modules/custom-campaigns/migrations/Migration20250526010310.ts +20 -0
  32. package/src/modules/custom-campaigns/migrations/Migration20250529011904.ts +13 -0
  33. package/src/modules/custom-campaigns/models/custom-campaign-type.ts +10 -0
  34. package/src/modules/custom-campaigns/models/promotion-usage-limit.ts +14 -0
  35. package/src/modules/custom-campaigns/service.ts +10 -0
  36. package/src/modules/custom-campaigns/types/campaign-type.enum.ts +3 -0
  37. package/src/providers/README.md +30 -0
  38. package/src/subscribers/README.md +59 -0
  39. package/src/subscribers/order-placed.ts +17 -0
  40. package/src/workflows/README.md +79 -0
  41. package/src/workflows/custom-campaign/createCustomCampaignWorkflow.ts +181 -0
  42. package/src/workflows/custom-campaign/updateCustomFlashSaleWorkflow.ts +185 -0
  43. package/src/workflows/custom-campaign/updatePromotionUsageWorkflow.ts +70 -0
  44. package/src/workflows/hooks/deletePromotionOnCampaignDelete.ts +49 -0
  45. 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: (campaign2) => campaign2.id,
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: (campaign2) => campaign2.id,
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.0",
3
+ "version": "1.1.2",
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
+ };