@zaamx/netme-bundle 0.0.5 → 0.0.8
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 +185 -244
- package/.medusa/server/src/admin/index.mjs +185 -244
- package/.medusa/server/src/api/admin/bundled-products/route.js +3 -2
- package/.medusa/server/src/api/admin/products/[id]/bundle/route.js +1 -1
- package/.medusa/server/src/api/store/special-packs/constants.js +8 -0
- package/.medusa/server/src/api/store/special-packs/route.js +40 -0
- package/.medusa/server/src/modules/bundles/service.js +188 -2
- package/.medusa/server/src/subscribers/redeem-special-pack.js +46 -0
- package/.medusa/server/src/workflows/hooks/validate-special-pack.js +42 -0
- package/.medusa/server/src/workflows/update-product-bundle.js +3 -1
- package/package.json +15 -15
|
@@ -105,9 +105,50 @@ const ProductSearchSelect = ({
|
|
|
105
105
|
] })
|
|
106
106
|
] });
|
|
107
107
|
};
|
|
108
|
+
const VariantPicker = ({
|
|
109
|
+
productId,
|
|
110
|
+
selectedIds,
|
|
111
|
+
onChange
|
|
112
|
+
}) => {
|
|
113
|
+
const [variants, setVariants] = react.useState([]);
|
|
114
|
+
const [loading, setLoading] = react.useState(false);
|
|
115
|
+
react.useEffect(() => {
|
|
116
|
+
if (!productId) {
|
|
117
|
+
setVariants([]);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
setLoading(true);
|
|
121
|
+
sdk.admin.product.retrieve(productId).then(
|
|
122
|
+
({ product }) => setVariants(
|
|
123
|
+
(product.variants || []).map((v) => ({ id: v.id, title: v.title ?? v.id, sku: v.sku ?? void 0 }))
|
|
124
|
+
)
|
|
125
|
+
).catch(() => setVariants([])).finally(() => setLoading(false));
|
|
126
|
+
}, [productId]);
|
|
127
|
+
const toggle = (id) => onChange(
|
|
128
|
+
selectedIds.includes(id) ? selectedIds.filter((s) => s !== id) : [...selectedIds, id]
|
|
129
|
+
);
|
|
130
|
+
if (loading) return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-gray-400", children: "Loading variants…" });
|
|
131
|
+
if (!loading && variants.length === 0)
|
|
132
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-gray-400", children: "No variants found." });
|
|
133
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 max-h-52 overflow-y-auto pr-1", children: variants.map((v) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer select-none", children: [
|
|
134
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
135
|
+
"input",
|
|
136
|
+
{
|
|
137
|
+
type: "checkbox",
|
|
138
|
+
checked: selectedIds.includes(v.id),
|
|
139
|
+
onChange: () => toggle(v.id),
|
|
140
|
+
className: "h-4 w-4 rounded border-gray-300"
|
|
141
|
+
}
|
|
142
|
+
),
|
|
143
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm", children: v.title }),
|
|
144
|
+
v.sku && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-gray-400 font-mono", children: v.sku })
|
|
145
|
+
] }, v.id)) });
|
|
146
|
+
};
|
|
108
147
|
const emptyBundleItem = () => ({
|
|
109
148
|
product_id: void 0,
|
|
110
149
|
product_title: "",
|
|
150
|
+
variant_mode: "all",
|
|
151
|
+
variant_ids: [],
|
|
111
152
|
min_quantity: 1,
|
|
112
153
|
max_quantity: 1,
|
|
113
154
|
default_quantity: 1,
|
|
@@ -115,47 +156,42 @@ const emptyBundleItem = () => ({
|
|
|
115
156
|
separate_shipping: false,
|
|
116
157
|
individual_price: false
|
|
117
158
|
});
|
|
118
|
-
const
|
|
119
|
-
isEditing: false,
|
|
120
|
-
selectedProductId: void 0,
|
|
121
|
-
selectedProductTitle: "",
|
|
122
|
-
bundleItems: [],
|
|
123
|
-
bundleMeta: []
|
|
124
|
-
};
|
|
125
|
-
const limit = 15;
|
|
159
|
+
const LIMIT = 15;
|
|
126
160
|
const BundledProductsPage = () => {
|
|
127
161
|
const [page, setPage] = react.useState(0);
|
|
128
|
-
const [
|
|
162
|
+
const [openModal, setOpenModal] = react.useState(false);
|
|
129
163
|
const [isEditing, setIsEditing] = react.useState(false);
|
|
130
164
|
const [selectedProductId, setSelectedProductId] = react.useState();
|
|
131
165
|
const [selectedProductTitle, setSelectedProductTitle] = react.useState("");
|
|
132
166
|
const [bundleItems, setBundleItems] = react.useState([]);
|
|
133
167
|
const [bundleMeta, setBundleMeta] = react.useState([]);
|
|
134
168
|
const queryClient = reactQuery.useQueryClient();
|
|
135
|
-
const offset = page *
|
|
169
|
+
const offset = page * LIMIT;
|
|
136
170
|
const closeModal = () => {
|
|
137
|
-
|
|
138
|
-
setIsEditing(
|
|
139
|
-
setSelectedProductId(
|
|
140
|
-
setSelectedProductTitle(
|
|
141
|
-
setBundleItems(
|
|
142
|
-
setBundleMeta(
|
|
171
|
+
setOpenModal(false);
|
|
172
|
+
setIsEditing(false);
|
|
173
|
+
setSelectedProductId(void 0);
|
|
174
|
+
setSelectedProductTitle("");
|
|
175
|
+
setBundleItems([]);
|
|
176
|
+
setBundleMeta([]);
|
|
143
177
|
};
|
|
144
178
|
const { data, isLoading } = reactQuery.useQuery({
|
|
145
|
-
queryKey: ["bundled-products", offset,
|
|
179
|
+
queryKey: ["bundled-products", offset, LIMIT],
|
|
146
180
|
queryFn: () => sdk.client.fetch("/admin/bundled-products", {
|
|
147
181
|
method: "GET",
|
|
148
|
-
query: { limit, offset }
|
|
182
|
+
query: { limit: LIMIT, offset }
|
|
149
183
|
})
|
|
150
184
|
});
|
|
151
185
|
const { mutateAsync: saveBundle, isPending: isSaving } = reactQuery.useMutation({
|
|
152
|
-
mutationFn: async (
|
|
186
|
+
mutationFn: async () => {
|
|
153
187
|
if (!selectedProductId) throw new Error("No product selected");
|
|
188
|
+
const validItems = bundleItems.filter((i) => i.product_id);
|
|
154
189
|
await sdk.client.fetch(`/admin/products/${selectedProductId}/bundle`, {
|
|
155
190
|
method: "POST",
|
|
156
191
|
body: {
|
|
157
|
-
child_product_ids:
|
|
192
|
+
child_product_ids: validItems.map((item) => ({
|
|
158
193
|
id: item.product_id,
|
|
194
|
+
variant_ids: item.variant_mode === "specific" ? item.variant_ids : [],
|
|
159
195
|
min_quantity: item.min_quantity,
|
|
160
196
|
max_quantity: item.max_quantity,
|
|
161
197
|
default_quantity: item.default_quantity,
|
|
@@ -163,72 +199,70 @@ const BundledProductsPage = () => {
|
|
|
163
199
|
separate_shipping: item.separate_shipping,
|
|
164
200
|
individual_price: item.individual_price
|
|
165
201
|
})),
|
|
166
|
-
bundle_meta:
|
|
167
|
-
is_bundle:
|
|
202
|
+
bundle_meta: bundleMeta,
|
|
203
|
+
is_bundle: true
|
|
168
204
|
}
|
|
169
205
|
});
|
|
170
206
|
}
|
|
171
207
|
});
|
|
172
208
|
const { mutateAsync: deleteBundle } = reactQuery.useMutation({
|
|
173
|
-
mutationFn:
|
|
174
|
-
await sdk.client.fetch(`/admin/products/${productId}/bundle`, { method: "DELETE" });
|
|
175
|
-
}
|
|
209
|
+
mutationFn: (productId) => sdk.client.fetch(`/admin/products/${productId}/bundle`, { method: "DELETE" })
|
|
176
210
|
});
|
|
177
|
-
const handleDeleteBundle = async (productId) => {
|
|
178
|
-
try {
|
|
179
|
-
await deleteBundle(productId);
|
|
180
|
-
ui.toast.success("Bundle deleted successfully");
|
|
181
|
-
queryClient.invalidateQueries({ queryKey: ["bundled-products"] });
|
|
182
|
-
} catch {
|
|
183
|
-
ui.toast.error("Failed to delete bundle");
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
211
|
const handleEditBundle = (bundle) => {
|
|
187
212
|
setIsEditing(true);
|
|
188
213
|
setSelectedProductId(bundle.product.id);
|
|
189
214
|
setSelectedProductTitle(bundle.title);
|
|
190
215
|
setBundleItems(
|
|
191
|
-
bundle.items.map((item) =>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
bundle.items.map((item) => {
|
|
217
|
+
var _a;
|
|
218
|
+
return {
|
|
219
|
+
product_id: item.product.id,
|
|
220
|
+
product_title: item.product.title,
|
|
221
|
+
variant_mode: ((_a = item.variant_ids) == null ? void 0 : _a.length) ? "specific" : "all",
|
|
222
|
+
variant_ids: item.variant_ids ?? [],
|
|
223
|
+
min_quantity: item.min_quantity,
|
|
224
|
+
max_quantity: item.max_quantity,
|
|
225
|
+
default_quantity: item.default_quantity,
|
|
226
|
+
optional: item.optional,
|
|
227
|
+
separate_shipping: item.separate_shipping,
|
|
228
|
+
individual_price: item.individual_price
|
|
229
|
+
};
|
|
230
|
+
})
|
|
201
231
|
);
|
|
202
232
|
setBundleMeta(bundle.bundle_meta || []);
|
|
203
|
-
|
|
233
|
+
setOpenModal(true);
|
|
204
234
|
};
|
|
205
|
-
const
|
|
235
|
+
const handleDeleteBundle = async (productId) => {
|
|
236
|
+
try {
|
|
237
|
+
await deleteBundle(productId);
|
|
238
|
+
ui.toast.success("Bundle deleted");
|
|
239
|
+
queryClient.invalidateQueries({ queryKey: ["bundled-products"] });
|
|
240
|
+
} catch {
|
|
241
|
+
ui.toast.error("Failed to delete bundle");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const handleSave = async () => {
|
|
206
245
|
if (!selectedProductId) {
|
|
207
246
|
ui.toast.error("Please select a product");
|
|
208
247
|
return;
|
|
209
248
|
}
|
|
210
|
-
|
|
211
|
-
if (validItems.length === 0) {
|
|
249
|
+
if (!bundleItems.some((i) => i.product_id)) {
|
|
212
250
|
ui.toast.error("Please add at least one product to the bundle");
|
|
213
251
|
return;
|
|
214
252
|
}
|
|
215
253
|
try {
|
|
216
|
-
await saveBundle(
|
|
217
|
-
ui.toast.success(isEditing ? "Bundle updated
|
|
254
|
+
await saveBundle();
|
|
255
|
+
ui.toast.success(isEditing ? "Bundle updated" : "Bundle created");
|
|
218
256
|
queryClient.invalidateQueries({ queryKey: ["bundled-products"] });
|
|
219
257
|
closeModal();
|
|
220
258
|
} catch {
|
|
221
259
|
ui.toast.error(isEditing ? "Failed to update bundle" : "Failed to create bundle");
|
|
222
260
|
}
|
|
223
261
|
};
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
(prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item)
|
|
227
|
-
);
|
|
228
|
-
};
|
|
229
|
-
const totalPages = Math.ceil(((data == null ? void 0 : data.count) || 0) / limit);
|
|
262
|
+
const updateItem = (index, field, value) => setBundleItems((prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item));
|
|
263
|
+
const totalPages = Math.ceil(((data == null ? void 0 : data.count) || 0) / LIMIT);
|
|
230
264
|
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
231
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-
|
|
265
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between p-6", children: [
|
|
232
266
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Bundled Products" }),
|
|
233
267
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
234
268
|
ui.Button,
|
|
@@ -236,7 +270,7 @@ const BundledProductsPage = () => {
|
|
|
236
270
|
variant: "primary",
|
|
237
271
|
onClick: () => {
|
|
238
272
|
closeModal();
|
|
239
|
-
|
|
273
|
+
setOpenModal(true);
|
|
240
274
|
},
|
|
241
275
|
children: "Create Bundle"
|
|
242
276
|
}
|
|
@@ -251,110 +285,60 @@ const BundledProductsPage = () => {
|
|
|
251
285
|
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Actions" })
|
|
252
286
|
] }) }),
|
|
253
287
|
/* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
|
|
254
|
-
((data == null ? void 0 : data.bundled_products.length) ?? 0) === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
255
|
-
"td",
|
|
256
|
-
{
|
|
257
|
-
colSpan: 4,
|
|
258
|
-
className: "border border-gray-200 px-4 py-6 text-center text-gray-500",
|
|
259
|
-
children: "No bundled products found."
|
|
260
|
-
}
|
|
261
|
-
) }),
|
|
288
|
+
((data == null ? void 0 : data.bundled_products.length) ?? 0) === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: 4, className: "border border-gray-200 px-4 py-6 text-center text-gray-400", children: "No bundled products found." }) }),
|
|
262
289
|
data == null ? void 0 : data.bundled_products.map((bundle) => /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "hover:bg-gray-50", children: [
|
|
263
290
|
/* @__PURE__ */ jsxRuntime.jsxs("td", { className: "border border-gray-200 px-4 py-3", children: [
|
|
264
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
265
|
-
reactRouterDom.Link,
|
|
266
|
-
{
|
|
267
|
-
to: `/products/${bundle.product.id}`,
|
|
268
|
-
className: "font-medium text-blue-600 hover:underline",
|
|
269
|
-
children: bundle.title
|
|
270
|
-
}
|
|
271
|
-
),
|
|
291
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: `/products/${bundle.product.id}`, className: "font-medium text-blue-600 hover:underline", children: bundle.title }),
|
|
272
292
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-gray-400 mt-0.5", children: bundle.id })
|
|
273
293
|
] }),
|
|
274
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
275
|
-
|
|
276
|
-
{
|
|
277
|
-
className: bundle.status === "published" ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600",
|
|
278
|
-
children: bundle.status ?? "—"
|
|
279
|
-
}
|
|
280
|
-
) }),
|
|
281
|
-
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-0.5", children: [
|
|
294
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { className: bundle.status === "published" ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600", children: bundle.status ?? "—" }) }),
|
|
295
|
+
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
|
|
282
296
|
bundle.items.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-400", children: "No items" }),
|
|
283
|
-
bundle.items.map((item) =>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
to: `/products/${item.product.id}`,
|
|
288
|
-
className: "text-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
item.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
bundle.items.map((item) => {
|
|
298
|
+
var _a;
|
|
299
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm", children: [
|
|
300
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
301
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: `/products/${item.product.id}`, className: "text-blue-600 hover:underline", children: item.product.title }),
|
|
302
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-gray-400", children: [
|
|
303
|
+
"× ",
|
|
304
|
+
item.default_quantity
|
|
305
|
+
] }),
|
|
306
|
+
item.optional && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { className: "bg-gray-100 text-gray-500 text-xs", children: "Optional" })
|
|
307
|
+
] }),
|
|
308
|
+
((_a = item.variant_ids) == null ? void 0 : _a.length) > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-gray-400 ml-1", children: [
|
|
309
|
+
item.variant_ids.length,
|
|
310
|
+
" specific variant",
|
|
311
|
+
item.variant_ids.length > 1 ? "s" : ""
|
|
312
|
+
] })
|
|
313
|
+
] }, item.id);
|
|
314
|
+
})
|
|
298
315
|
] }) }),
|
|
299
316
|
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
300
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
301
|
-
|
|
302
|
-
{
|
|
303
|
-
variant: "secondary",
|
|
304
|
-
size: "small",
|
|
305
|
-
onClick: () => handleEditBundle(bundle),
|
|
306
|
-
children: "Edit"
|
|
307
|
-
}
|
|
308
|
-
),
|
|
309
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
310
|
-
ui.Button,
|
|
311
|
-
{
|
|
312
|
-
variant: "secondary",
|
|
313
|
-
size: "small",
|
|
314
|
-
onClick: () => handleDeleteBundle(bundle.id),
|
|
315
|
-
className: "text-red-600 hover:text-red-700",
|
|
316
|
-
children: "Delete"
|
|
317
|
-
}
|
|
318
|
-
)
|
|
317
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => handleEditBundle(bundle), children: "Edit" }),
|
|
318
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", size: "small", onClick: () => handleDeleteBundle(bundle.id), className: "text-red-600 hover:text-red-700", children: "Delete" })
|
|
319
319
|
] }) })
|
|
320
320
|
] }, bundle.id))
|
|
321
321
|
] })
|
|
322
322
|
] }) }),
|
|
323
323
|
totalPages > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-center gap-2 mt-6", children: [
|
|
324
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
325
|
-
ui.Button,
|
|
326
|
-
{
|
|
327
|
-
variant: "secondary",
|
|
328
|
-
onClick: () => setPage(Math.max(0, page - 1)),
|
|
329
|
-
disabled: page === 0,
|
|
330
|
-
children: "Previous"
|
|
331
|
-
}
|
|
332
|
-
),
|
|
324
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => setPage(Math.max(0, page - 1)), disabled: page === 0, children: "Previous" }),
|
|
333
325
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center px-4", children: [
|
|
334
326
|
"Page ",
|
|
335
327
|
page + 1,
|
|
336
328
|
" of ",
|
|
337
329
|
totalPages
|
|
338
330
|
] }),
|
|
339
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
340
|
-
ui.Button,
|
|
341
|
-
{
|
|
342
|
-
variant: "secondary",
|
|
343
|
-
onClick: () => setPage(Math.min(totalPages - 1, page + 1)),
|
|
344
|
-
disabled: page === totalPages - 1,
|
|
345
|
-
children: "Next"
|
|
346
|
-
}
|
|
347
|
-
)
|
|
331
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => setPage(Math.min(totalPages - 1, page + 1)), disabled: page === totalPages - 1, children: "Next" })
|
|
348
332
|
] })
|
|
349
333
|
] }),
|
|
350
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal, { open:
|
|
334
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal, { open: openModal, onOpenChange: (open) => {
|
|
351
335
|
if (!open) closeModal();
|
|
352
336
|
}, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal.Content, { children: [
|
|
353
337
|
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: isEditing ? "Edit Bundle" : "Create Bundle" }) }),
|
|
354
338
|
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Body, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-1 flex-col items-center h-[80vh] overflow-y-auto px-2 py-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-3xl flex flex-col gap-y-8", children: [
|
|
355
339
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
356
340
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Bundle Product" }),
|
|
357
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-gray-500 mb-2", children: isEditing ? "The product this bundle is attached to" : "Choose the product that
|
|
341
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-gray-500 mb-2", children: isEditing ? "The product this bundle is attached to" : "Choose the product that becomes the bundle" }),
|
|
358
342
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
359
343
|
ProductSearchSelect,
|
|
360
344
|
{
|
|
@@ -378,80 +362,28 @@ const BundledProductsPage = () => {
|
|
|
378
362
|
{
|
|
379
363
|
item,
|
|
380
364
|
index,
|
|
381
|
-
onUpdate: (field, value) =>
|
|
365
|
+
onUpdate: (field, value) => updateItem(index, field, value),
|
|
382
366
|
onRemove: () => setBundleItems((prev) => prev.filter((_, i) => i !== index))
|
|
383
367
|
},
|
|
384
368
|
index
|
|
385
369
|
)),
|
|
386
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
387
|
-
ui.Button,
|
|
388
|
-
{
|
|
389
|
-
variant: "secondary",
|
|
390
|
-
onClick: () => setBundleItems((prev) => [...prev, emptyBundleItem()]),
|
|
391
|
-
className: "mt-4",
|
|
392
|
-
children: "Add Item"
|
|
393
|
-
}
|
|
394
|
-
)
|
|
370
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => setBundleItems((prev) => [...prev, emptyBundleItem()]), className: "mt-2", children: "Add Item" })
|
|
395
371
|
] }),
|
|
396
372
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
397
373
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Bundle Metadata" }),
|
|
398
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-gray-500 mb-4", children: "Custom key-value pairs
|
|
374
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-gray-500 mb-4", children: "Custom key-value pairs" }),
|
|
399
375
|
bundleMeta.map((meta, index) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 mb-2", children: [
|
|
400
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
placeholder: "Key",
|
|
404
|
-
value: meta.key,
|
|
405
|
-
onChange: (e) => setBundleMeta(
|
|
406
|
-
(prev) => prev.map((m, i) => i === index ? { ...m, key: e.target.value } : m)
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
),
|
|
410
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
411
|
-
ui.Input,
|
|
412
|
-
{
|
|
413
|
-
placeholder: "Value",
|
|
414
|
-
value: meta.value,
|
|
415
|
-
onChange: (e) => setBundleMeta(
|
|
416
|
-
(prev) => prev.map(
|
|
417
|
-
(m, i) => i === index ? { ...m, value: e.target.value } : m
|
|
418
|
-
)
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
),
|
|
422
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
423
|
-
ui.Button,
|
|
424
|
-
{
|
|
425
|
-
variant: "secondary",
|
|
426
|
-
onClick: () => setBundleMeta((prev) => prev.filter((_, i) => i !== index)),
|
|
427
|
-
children: "Remove"
|
|
428
|
-
}
|
|
429
|
-
)
|
|
376
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { placeholder: "Key", value: meta.key, onChange: (e) => setBundleMeta((prev) => prev.map((m, i) => i === index ? { ...m, key: e.target.value } : m)) }),
|
|
377
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { placeholder: "Value", value: meta.value, onChange: (e) => setBundleMeta((prev) => prev.map((m, i) => i === index ? { ...m, value: e.target.value } : m)) }),
|
|
378
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => setBundleMeta((prev) => prev.filter((_, i) => i !== index)), children: "Remove" })
|
|
430
379
|
] }, index)),
|
|
431
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
432
|
-
ui.Button,
|
|
433
|
-
{
|
|
434
|
-
variant: "secondary",
|
|
435
|
-
onClick: () => setBundleMeta((prev) => [...prev, { key: "", value: "" }]),
|
|
436
|
-
className: "mt-4",
|
|
437
|
-
children: "Add Metadata"
|
|
438
|
-
}
|
|
439
|
-
)
|
|
380
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: () => setBundleMeta((prev) => [...prev, { key: "", value: "" }]), className: "mt-2", children: "Add Metadata" })
|
|
440
381
|
] })
|
|
441
382
|
] })
|
|
442
383
|
] }) }) }),
|
|
443
384
|
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Footer, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-x-2", children: [
|
|
444
385
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: closeModal, children: "Cancel" }),
|
|
445
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
446
|
-
ui.Button,
|
|
447
|
-
{
|
|
448
|
-
variant: "primary",
|
|
449
|
-
onClick: handleSaveBundle,
|
|
450
|
-
isLoading: isSaving,
|
|
451
|
-
disabled: !selectedProductId,
|
|
452
|
-
children: isEditing ? "Update Bundle" : "Create Bundle"
|
|
453
|
-
}
|
|
454
|
-
)
|
|
386
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "primary", onClick: handleSave, isLoading: isSaving, disabled: !selectedProductId, children: isEditing ? "Update Bundle" : "Create Bundle" })
|
|
455
387
|
] }) })
|
|
456
388
|
] }) })
|
|
457
389
|
] });
|
|
@@ -474,78 +406,87 @@ const BundleItemForm = ({ item, index, onUpdate, onRemove }) => /* @__PURE__ */
|
|
|
474
406
|
onChange: (id, title) => {
|
|
475
407
|
onUpdate("product_id", id || void 0);
|
|
476
408
|
onUpdate("product_title", title);
|
|
409
|
+
onUpdate("variant_mode", "all");
|
|
410
|
+
onUpdate("variant_ids", []);
|
|
477
411
|
},
|
|
478
412
|
placeholder: "Search for a product…"
|
|
479
413
|
}
|
|
480
414
|
)
|
|
481
415
|
] }),
|
|
482
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "
|
|
483
|
-
/* @__PURE__ */ jsxRuntime.
|
|
484
|
-
|
|
416
|
+
item.product_id && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
417
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Applies to" }),
|
|
418
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-6", children: [
|
|
419
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 text-sm cursor-pointer select-none", children: [
|
|
420
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
421
|
+
"input",
|
|
422
|
+
{
|
|
423
|
+
type: "radio",
|
|
424
|
+
name: `variant-mode-${index}`,
|
|
425
|
+
checked: item.variant_mode === "all",
|
|
426
|
+
onChange: () => {
|
|
427
|
+
onUpdate("variant_mode", "all");
|
|
428
|
+
onUpdate("variant_ids", []);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
),
|
|
432
|
+
"All variations"
|
|
433
|
+
] }),
|
|
434
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 text-sm cursor-pointer select-none", children: [
|
|
435
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
436
|
+
"input",
|
|
437
|
+
{
|
|
438
|
+
type: "radio",
|
|
439
|
+
name: `variant-mode-${index}`,
|
|
440
|
+
checked: item.variant_mode === "specific",
|
|
441
|
+
onChange: () => onUpdate("variant_mode", "specific")
|
|
442
|
+
}
|
|
443
|
+
),
|
|
444
|
+
"Specific variations"
|
|
445
|
+
] })
|
|
446
|
+
] }),
|
|
447
|
+
item.variant_mode === "specific" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border border-gray-200 rounded-md p-3 bg-gray-50", children: [
|
|
448
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-xs text-gray-500 mb-3", children: 'Check each variation that qualifies for this bundle item. Switching back to "All variations" will clear this selection.' }),
|
|
485
449
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
486
|
-
|
|
450
|
+
VariantPicker,
|
|
487
451
|
{
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
onChange: (e) => onUpdate("min_quantity", parseInt(e.target.value) || 0)
|
|
452
|
+
productId: item.product_id,
|
|
453
|
+
selectedIds: item.variant_ids,
|
|
454
|
+
onChange: (ids) => onUpdate("variant_ids", ids)
|
|
492
455
|
}
|
|
493
|
-
)
|
|
456
|
+
),
|
|
457
|
+
item.variant_ids.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { className: "text-xs text-gray-400 mt-2", children: [
|
|
458
|
+
item.variant_ids.length,
|
|
459
|
+
" variation",
|
|
460
|
+
item.variant_ids.length > 1 ? "s" : "",
|
|
461
|
+
" selected"
|
|
462
|
+
] })
|
|
463
|
+
] })
|
|
464
|
+
] }),
|
|
465
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
466
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
467
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Min Quantity" }),
|
|
468
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", min: 0, value: item.min_quantity, onChange: (e) => onUpdate("min_quantity", parseInt(e.target.value) || 0) })
|
|
494
469
|
] }),
|
|
495
470
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
496
471
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Max Quantity" }),
|
|
497
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
498
|
-
ui.Input,
|
|
499
|
-
{
|
|
500
|
-
type: "number",
|
|
501
|
-
min: 0,
|
|
502
|
-
value: item.max_quantity,
|
|
503
|
-
onChange: (e) => onUpdate("max_quantity", parseInt(e.target.value) || 0)
|
|
504
|
-
}
|
|
505
|
-
)
|
|
472
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", min: 0, value: item.max_quantity, onChange: (e) => onUpdate("max_quantity", parseInt(e.target.value) || 0) })
|
|
506
473
|
] }),
|
|
507
474
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
508
475
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Default Quantity" }),
|
|
509
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
510
|
-
ui.Input,
|
|
511
|
-
{
|
|
512
|
-
type: "number",
|
|
513
|
-
min: 0,
|
|
514
|
-
value: item.default_quantity,
|
|
515
|
-
onChange: (e) => onUpdate("default_quantity", parseInt(e.target.value) || 0)
|
|
516
|
-
}
|
|
517
|
-
)
|
|
476
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Input, { type: "number", min: 0, value: item.default_quantity, onChange: (e) => onUpdate("default_quantity", parseInt(e.target.value) || 0) })
|
|
518
477
|
] })
|
|
519
478
|
] }),
|
|
520
479
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
|
|
521
480
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
522
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
523
|
-
ui.Switch,
|
|
524
|
-
{
|
|
525
|
-
checked: item.optional,
|
|
526
|
-
onCheckedChange: (checked) => onUpdate("optional", checked)
|
|
527
|
-
}
|
|
528
|
-
),
|
|
481
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { checked: item.optional, onCheckedChange: (v) => onUpdate("optional", v) }),
|
|
529
482
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Optional" })
|
|
530
483
|
] }),
|
|
531
484
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
532
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
533
|
-
ui.Switch,
|
|
534
|
-
{
|
|
535
|
-
checked: item.separate_shipping,
|
|
536
|
-
onCheckedChange: (checked) => onUpdate("separate_shipping", checked)
|
|
537
|
-
}
|
|
538
|
-
),
|
|
485
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { checked: item.separate_shipping, onCheckedChange: (v) => onUpdate("separate_shipping", v) }),
|
|
539
486
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Separate Shipping" })
|
|
540
487
|
] }),
|
|
541
488
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
542
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
543
|
-
ui.Switch,
|
|
544
|
-
{
|
|
545
|
-
checked: item.individual_price,
|
|
546
|
-
onCheckedChange: (checked) => onUpdate("individual_price", checked)
|
|
547
|
-
}
|
|
548
|
-
),
|
|
489
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Switch, { checked: item.individual_price, onCheckedChange: (v) => onUpdate("individual_price", v) }),
|
|
549
490
|
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Individual Price" })
|
|
550
491
|
] })
|
|
551
492
|
] })
|