@zaamx/netme-bundle 0.0.4 → 0.0.5
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 +359 -333
- package/.medusa/server/src/admin/index.mjs +361 -335
- package/.medusa/server/src/api/admin/bundled-products/route.js +64 -72
- package/.medusa/server/src/api/admin/products/[id]/bundle/route.js +2 -11
- package/README.md +443 -48
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
3
|
import { CubeSolid } from "@medusajs/icons";
|
|
4
|
-
import { Container, Heading, Button, Text, Badge, FocusModal, Label,
|
|
4
|
+
import { Container, Heading, Button, Text, Badge, FocusModal, Label, Input, toast, Switch } from "@medusajs/ui";
|
|
5
5
|
import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
|
|
6
|
-
import { useState,
|
|
6
|
+
import { useState, useRef, useEffect } from "react";
|
|
7
7
|
import Medusa from "@medusajs/js-sdk";
|
|
8
8
|
import { Link } from "react-router-dom";
|
|
9
9
|
const sdk = new Medusa({
|
|
@@ -13,231 +13,309 @@ const sdk = new Medusa({
|
|
|
13
13
|
type: "session"
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
|
+
const ProductSearchSelect = ({
|
|
17
|
+
value,
|
|
18
|
+
valueLabel,
|
|
19
|
+
onChange,
|
|
20
|
+
disabled,
|
|
21
|
+
placeholder = "Search products…"
|
|
22
|
+
}) => {
|
|
23
|
+
const [search, setSearch] = useState("");
|
|
24
|
+
const [open, setOpen] = useState(false);
|
|
25
|
+
const [results, setResults] = useState([]);
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
27
|
+
const containerRef = useRef(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const handler = (e) => {
|
|
30
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
31
|
+
setOpen(false);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
document.addEventListener("mousedown", handler);
|
|
35
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
36
|
+
}, []);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!open) return;
|
|
39
|
+
const delay = search.trim() ? 300 : 0;
|
|
40
|
+
const timer = setTimeout(async () => {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
const { products } = await sdk.admin.product.list({
|
|
44
|
+
q: search.trim() || void 0,
|
|
45
|
+
limit: 15
|
|
46
|
+
});
|
|
47
|
+
setResults(products.map((p) => ({ id: p.id, title: p.title ?? "" })));
|
|
48
|
+
} catch {
|
|
49
|
+
setResults([]);
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}, delay);
|
|
54
|
+
return () => clearTimeout(timer);
|
|
55
|
+
}, [search, open]);
|
|
56
|
+
if (disabled) {
|
|
57
|
+
return /* @__PURE__ */ jsx("div", { className: "border border-gray-200 rounded-md px-3 py-2 bg-gray-50 text-sm text-gray-700 min-h-[38px]", children: valueLabel || value || /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "—" }) });
|
|
58
|
+
}
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative", children: [
|
|
60
|
+
value && valueLabel && /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center gap-2", children: [
|
|
61
|
+
/* @__PURE__ */ jsx(Badge, { className: "bg-blue-50 text-blue-700 text-xs", children: valueLabel }),
|
|
62
|
+
/* @__PURE__ */ jsx(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
type: "button",
|
|
66
|
+
className: "text-xs text-gray-400 hover:text-gray-600",
|
|
67
|
+
onClick: () => onChange("", ""),
|
|
68
|
+
children: "✕ clear"
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
] }),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
Input,
|
|
74
|
+
{
|
|
75
|
+
placeholder: value ? "Search to change…" : placeholder,
|
|
76
|
+
value: search,
|
|
77
|
+
onChange: (e) => {
|
|
78
|
+
setSearch(e.target.value);
|
|
79
|
+
setOpen(true);
|
|
80
|
+
},
|
|
81
|
+
onFocus: () => setOpen(true)
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
open && /* @__PURE__ */ jsxs("div", { className: "absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg max-h-60 overflow-y-auto", children: [
|
|
85
|
+
loading && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-sm text-gray-400", children: "Searching…" }),
|
|
86
|
+
!loading && results.length === 0 && search.trim() && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-sm text-gray-400", children: "No products found." }),
|
|
87
|
+
results.map((product) => /* @__PURE__ */ jsx(
|
|
88
|
+
"button",
|
|
89
|
+
{
|
|
90
|
+
type: "button",
|
|
91
|
+
className: "w-full text-left px-3 py-2 hover:bg-gray-50 text-sm border-b border-gray-100 last:border-0",
|
|
92
|
+
onMouseDown: (e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
onChange(product.id, product.title);
|
|
95
|
+
setSearch("");
|
|
96
|
+
setOpen(false);
|
|
97
|
+
},
|
|
98
|
+
children: product.title
|
|
99
|
+
},
|
|
100
|
+
product.id
|
|
101
|
+
))
|
|
102
|
+
] })
|
|
103
|
+
] });
|
|
104
|
+
};
|
|
105
|
+
const emptyBundleItem = () => ({
|
|
106
|
+
product_id: void 0,
|
|
107
|
+
product_title: "",
|
|
108
|
+
min_quantity: 1,
|
|
109
|
+
max_quantity: 1,
|
|
110
|
+
default_quantity: 1,
|
|
111
|
+
optional: false,
|
|
112
|
+
separate_shipping: false,
|
|
113
|
+
individual_price: false
|
|
114
|
+
});
|
|
115
|
+
const resetModal = {
|
|
116
|
+
isEditing: false,
|
|
117
|
+
selectedProductId: void 0,
|
|
118
|
+
selectedProductTitle: "",
|
|
119
|
+
bundleItems: [],
|
|
120
|
+
bundleMeta: []
|
|
121
|
+
};
|
|
16
122
|
const limit = 15;
|
|
17
123
|
const BundledProductsPage = () => {
|
|
18
124
|
const [page, setPage] = useState(0);
|
|
19
125
|
const [openCreateModal, setOpenCreateModal] = useState(false);
|
|
20
126
|
const [isEditing, setIsEditing] = useState(false);
|
|
21
127
|
const [selectedProductId, setSelectedProductId] = useState();
|
|
128
|
+
const [selectedProductTitle, setSelectedProductTitle] = useState("");
|
|
22
129
|
const [bundleItems, setBundleItems] = useState([]);
|
|
23
130
|
const [bundleMeta, setBundleMeta] = useState([]);
|
|
24
|
-
const [products, setProducts] = useState([]);
|
|
25
|
-
const productsLimit = 15;
|
|
26
|
-
const [currentProductPage, setCurrentProductPage] = useState(0);
|
|
27
|
-
const [productsCount, setProductsCount] = useState(0);
|
|
28
|
-
const hasNextPage = useMemo(
|
|
29
|
-
() => productsCount ? productsCount > productsLimit : true,
|
|
30
|
-
[productsCount, productsLimit]
|
|
31
|
-
);
|
|
32
131
|
const queryClient = useQueryClient();
|
|
33
132
|
const offset = page * limit;
|
|
133
|
+
const closeModal = () => {
|
|
134
|
+
setOpenCreateModal(false);
|
|
135
|
+
setIsEditing(resetModal.isEditing);
|
|
136
|
+
setSelectedProductId(resetModal.selectedProductId);
|
|
137
|
+
setSelectedProductTitle(resetModal.selectedProductTitle);
|
|
138
|
+
setBundleItems(resetModal.bundleItems);
|
|
139
|
+
setBundleMeta(resetModal.bundleMeta);
|
|
140
|
+
};
|
|
34
141
|
const { data, isLoading } = useQuery({
|
|
35
142
|
queryKey: ["bundled-products", offset, limit],
|
|
36
143
|
queryFn: () => sdk.client.fetch("/admin/bundled-products", {
|
|
37
144
|
method: "GET",
|
|
38
|
-
query: {
|
|
39
|
-
limit,
|
|
40
|
-
offset
|
|
41
|
-
}
|
|
145
|
+
query: { limit, offset }
|
|
42
146
|
})
|
|
43
147
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
queryFn: async () => {
|
|
47
|
-
const { products: products2, count } = await sdk.admin.product.list({
|
|
48
|
-
limit: productsLimit,
|
|
49
|
-
offset: currentProductPage * productsLimit
|
|
50
|
-
});
|
|
51
|
-
setProductsCount(count);
|
|
52
|
-
setProducts((prev) => [...prev, ...products2]);
|
|
53
|
-
return products2;
|
|
54
|
-
},
|
|
55
|
-
enabled: hasNextPage
|
|
56
|
-
});
|
|
57
|
-
const fetchMoreProducts = () => {
|
|
58
|
-
if (!hasNextPage) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
setCurrentProductPage(currentProductPage + 1);
|
|
62
|
-
};
|
|
63
|
-
const { mutateAsync: createBundle, isPending: isCreating } = useMutation({
|
|
64
|
-
mutationFn: async (data2) => {
|
|
148
|
+
const { mutateAsync: saveBundle, isPending: isSaving } = useMutation({
|
|
149
|
+
mutationFn: async (payload) => {
|
|
65
150
|
if (!selectedProductId) throw new Error("No product selected");
|
|
66
151
|
await sdk.client.fetch(`/admin/products/${selectedProductId}/bundle`, {
|
|
67
152
|
method: "POST",
|
|
68
|
-
body:
|
|
153
|
+
body: {
|
|
154
|
+
child_product_ids: payload.child_product_ids.map((item) => ({
|
|
155
|
+
id: item.product_id,
|
|
156
|
+
min_quantity: item.min_quantity,
|
|
157
|
+
max_quantity: item.max_quantity,
|
|
158
|
+
default_quantity: item.default_quantity,
|
|
159
|
+
optional: item.optional,
|
|
160
|
+
separate_shipping: item.separate_shipping,
|
|
161
|
+
individual_price: item.individual_price
|
|
162
|
+
})),
|
|
163
|
+
bundle_meta: payload.bundle_meta,
|
|
164
|
+
is_bundle: payload.is_bundle
|
|
165
|
+
}
|
|
69
166
|
});
|
|
70
167
|
}
|
|
71
168
|
});
|
|
72
|
-
const { mutateAsync: deleteBundle
|
|
169
|
+
const { mutateAsync: deleteBundle } = useMutation({
|
|
73
170
|
mutationFn: async (productId) => {
|
|
74
|
-
await sdk.client.fetch(`/admin/products/${productId}/bundle`, {
|
|
75
|
-
method: "DELETE"
|
|
76
|
-
});
|
|
171
|
+
await sdk.client.fetch(`/admin/products/${productId}/bundle`, { method: "DELETE" });
|
|
77
172
|
}
|
|
78
173
|
});
|
|
79
174
|
const handleDeleteBundle = async (productId) => {
|
|
80
175
|
try {
|
|
81
176
|
await deleteBundle(productId);
|
|
82
177
|
toast.success("Bundle deleted successfully");
|
|
83
|
-
queryClient.invalidateQueries({
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error("Error deleting bundle:", error);
|
|
178
|
+
queryClient.invalidateQueries({ queryKey: ["bundled-products"] });
|
|
179
|
+
} catch {
|
|
88
180
|
toast.error("Failed to delete bundle");
|
|
89
181
|
}
|
|
90
182
|
};
|
|
91
183
|
const handleEditBundle = (bundle) => {
|
|
92
184
|
setIsEditing(true);
|
|
93
185
|
setSelectedProductId(bundle.product.id);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
186
|
+
setSelectedProductTitle(bundle.title);
|
|
187
|
+
setBundleItems(
|
|
188
|
+
bundle.items.map((item) => ({
|
|
189
|
+
product_id: item.product.id,
|
|
190
|
+
product_title: item.product.title,
|
|
191
|
+
min_quantity: item.min_quantity,
|
|
192
|
+
max_quantity: item.max_quantity,
|
|
193
|
+
default_quantity: item.default_quantity,
|
|
194
|
+
optional: item.optional,
|
|
195
|
+
separate_shipping: item.separate_shipping,
|
|
196
|
+
individual_price: item.individual_price
|
|
197
|
+
}))
|
|
198
|
+
);
|
|
103
199
|
setBundleMeta(bundle.bundle_meta || []);
|
|
104
200
|
setOpenCreateModal(true);
|
|
105
201
|
};
|
|
106
|
-
const
|
|
202
|
+
const handleSaveBundle = async () => {
|
|
203
|
+
if (!selectedProductId) {
|
|
204
|
+
toast.error("Please select a product");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const validItems = bundleItems.filter((item) => item.product_id);
|
|
208
|
+
if (validItems.length === 0) {
|
|
209
|
+
toast.error("Please add at least one product to the bundle");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
107
212
|
try {
|
|
108
|
-
|
|
109
|
-
toast.error("Please select a product");
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const validItems = bundleItems.filter((item) => item.product_id);
|
|
113
|
-
if (validItems.length === 0) {
|
|
114
|
-
toast.error("Please add at least one product to the bundle");
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
await createBundle({
|
|
118
|
-
child_product_ids: validItems.map((item) => ({
|
|
119
|
-
id: item.product_id,
|
|
120
|
-
min_quantity: item.min_quantity,
|
|
121
|
-
max_quantity: item.max_quantity,
|
|
122
|
-
default_quantity: item.default_quantity,
|
|
123
|
-
optional: item.optional,
|
|
124
|
-
separate_shipping: item.separate_shipping,
|
|
125
|
-
individual_price: item.individual_price
|
|
126
|
-
})),
|
|
127
|
-
bundle_meta: bundleMeta,
|
|
128
|
-
is_bundle: true
|
|
129
|
-
});
|
|
130
|
-
setOpenCreateModal(false);
|
|
213
|
+
await saveBundle({ child_product_ids: validItems, bundle_meta: bundleMeta, is_bundle: true });
|
|
131
214
|
toast.success(isEditing ? "Bundle updated successfully" : "Bundle created successfully");
|
|
132
|
-
queryClient.invalidateQueries({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
setSelectedProductId(void 0);
|
|
137
|
-
setBundleItems([]);
|
|
138
|
-
setBundleMeta([]);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error("Error creating bundle:", error);
|
|
141
|
-
toast.error("Failed to create bundle");
|
|
215
|
+
queryClient.invalidateQueries({ queryKey: ["bundled-products"] });
|
|
216
|
+
closeModal();
|
|
217
|
+
} catch {
|
|
218
|
+
toast.error(isEditing ? "Failed to update bundle" : "Failed to create bundle");
|
|
142
219
|
}
|
|
143
220
|
};
|
|
144
|
-
const addBundleItem = () => {
|
|
145
|
-
setBundleItems([
|
|
146
|
-
...bundleItems,
|
|
147
|
-
{
|
|
148
|
-
product_id: void 0,
|
|
149
|
-
min_quantity: 1,
|
|
150
|
-
max_quantity: 1,
|
|
151
|
-
default_quantity: 1,
|
|
152
|
-
optional: false,
|
|
153
|
-
separate_shipping: false,
|
|
154
|
-
individual_price: false
|
|
155
|
-
}
|
|
156
|
-
]);
|
|
157
|
-
};
|
|
158
|
-
const removeBundleItem = (index) => {
|
|
159
|
-
setBundleItems(bundleItems.filter((_, i) => i !== index));
|
|
160
|
-
};
|
|
161
221
|
const updateBundleItem = (index, field, value) => {
|
|
162
|
-
setBundleItems(
|
|
163
|
-
(item, i) => i === index ? { ...item, [field]: value } : item
|
|
164
|
-
)
|
|
165
|
-
};
|
|
166
|
-
const addBundleMeta = () => {
|
|
167
|
-
setBundleMeta([...bundleMeta, { key: "", value: "" }]);
|
|
168
|
-
};
|
|
169
|
-
const removeBundleMeta = (index) => {
|
|
170
|
-
setBundleMeta(bundleMeta.filter((_, i) => i !== index));
|
|
171
|
-
};
|
|
172
|
-
const updateBundleMeta = (index, field, value) => {
|
|
173
|
-
setBundleMeta(bundleMeta.map(
|
|
174
|
-
(item, i) => i === index ? { ...item, [field]: value } : item
|
|
175
|
-
));
|
|
222
|
+
setBundleItems(
|
|
223
|
+
(prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item)
|
|
224
|
+
);
|
|
176
225
|
};
|
|
177
226
|
const totalPages = Math.ceil(((data == null ? void 0 : data.count) || 0) / limit);
|
|
178
227
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
179
228
|
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2 md:flex-row md:items-center p-6", children: [
|
|
180
229
|
/* @__PURE__ */ jsx(Heading, { children: "Bundled Products" }),
|
|
181
|
-
/* @__PURE__ */ jsx(
|
|
230
|
+
/* @__PURE__ */ jsx(
|
|
231
|
+
Button,
|
|
232
|
+
{
|
|
233
|
+
variant: "primary",
|
|
234
|
+
onClick: () => {
|
|
235
|
+
closeModal();
|
|
236
|
+
setOpenCreateModal(true);
|
|
237
|
+
},
|
|
238
|
+
children: "Create Bundle"
|
|
239
|
+
}
|
|
240
|
+
)
|
|
182
241
|
] }),
|
|
183
|
-
isLoading ? /* @__PURE__ */ jsx("div", { className: "p-6", children: /* @__PURE__ */ jsx(Text, { children: "Loading
|
|
242
|
+
isLoading ? /* @__PURE__ */ jsx("div", { className: "p-6", children: /* @__PURE__ */ jsx(Text, { children: "Loading…" }) }) : /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
|
|
184
243
|
/* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse border border-gray-200", children: [
|
|
185
244
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-gray-50", children: [
|
|
186
|
-
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "ID" }),
|
|
187
|
-
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Title" }),
|
|
188
|
-
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Items" }),
|
|
189
245
|
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Product" }),
|
|
190
|
-
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Status" })
|
|
246
|
+
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Status" }),
|
|
247
|
+
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Items" }),
|
|
248
|
+
/* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Actions" })
|
|
191
249
|
] }) }),
|
|
192
|
-
/* @__PURE__ */
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
/* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-2", children: /* @__PURE__ */ jsx("div", { className: "space-y-1", children: bundle.items.map((item) => /* @__PURE__ */ jsxs("div", { className: "text-sm", children: [
|
|
196
|
-
/* @__PURE__ */ jsx(
|
|
197
|
-
Link,
|
|
198
|
-
{
|
|
199
|
-
to: `/products/${item.product.id}`,
|
|
200
|
-
className: "text-blue-600 hover:underline",
|
|
201
|
-
children: item.product.title
|
|
202
|
-
}
|
|
203
|
-
),
|
|
204
|
-
/* @__PURE__ */ jsxs("span", { className: "text-gray-500", children: [
|
|
205
|
-
" x ",
|
|
206
|
-
item.quantity
|
|
207
|
-
] }),
|
|
208
|
-
item.optional && /* @__PURE__ */ jsx(Badge, { className: "ml-2 bg-gray-100 text-gray-700", children: "Optional" })
|
|
209
|
-
] }, item.id)) }) }),
|
|
210
|
-
/* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-2", children: /* @__PURE__ */ jsx(
|
|
211
|
-
Link,
|
|
250
|
+
/* @__PURE__ */ jsxs("tbody", { children: [
|
|
251
|
+
((data == null ? void 0 : data.bundled_products.length) ?? 0) === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
|
|
252
|
+
"td",
|
|
212
253
|
{
|
|
213
|
-
|
|
214
|
-
className: "text-
|
|
215
|
-
children: "
|
|
254
|
+
colSpan: 4,
|
|
255
|
+
className: "border border-gray-200 px-4 py-6 text-center text-gray-500",
|
|
256
|
+
children: "No bundled products found."
|
|
216
257
|
}
|
|
217
258
|
) }),
|
|
218
|
-
|
|
219
|
-
/* @__PURE__ */
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
259
|
+
data == null ? void 0 : data.bundled_products.map((bundle) => /* @__PURE__ */ jsxs("tr", { className: "hover:bg-gray-50", children: [
|
|
260
|
+
/* @__PURE__ */ jsxs("td", { className: "border border-gray-200 px-4 py-3", children: [
|
|
261
|
+
/* @__PURE__ */ jsx(
|
|
262
|
+
Link,
|
|
263
|
+
{
|
|
264
|
+
to: `/products/${bundle.product.id}`,
|
|
265
|
+
className: "font-medium text-blue-600 hover:underline",
|
|
266
|
+
children: bundle.title
|
|
267
|
+
}
|
|
268
|
+
),
|
|
269
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs text-gray-400 mt-0.5", children: bundle.id })
|
|
270
|
+
] }),
|
|
271
|
+
/* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsx(
|
|
272
|
+
Badge,
|
|
231
273
|
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
onClick: () => handleDeleteBundle(bundle.id),
|
|
235
|
-
className: "text-red-600 hover:text-red-700",
|
|
236
|
-
children: "Delete"
|
|
274
|
+
className: bundle.status === "published" ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600",
|
|
275
|
+
children: bundle.status ?? "—"
|
|
237
276
|
}
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
277
|
+
) }),
|
|
278
|
+
/* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
|
|
279
|
+
bundle.items.length === 0 && /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-400", children: "No items" }),
|
|
280
|
+
bundle.items.map((item) => /* @__PURE__ */ jsxs("div", { className: "text-sm flex items-center gap-1", children: [
|
|
281
|
+
/* @__PURE__ */ jsx(
|
|
282
|
+
Link,
|
|
283
|
+
{
|
|
284
|
+
to: `/products/${item.product.id}`,
|
|
285
|
+
className: "text-blue-600 hover:underline",
|
|
286
|
+
children: item.product.title
|
|
287
|
+
}
|
|
288
|
+
),
|
|
289
|
+
/* @__PURE__ */ jsxs("span", { className: "text-gray-400", children: [
|
|
290
|
+
"× ",
|
|
291
|
+
item.default_quantity
|
|
292
|
+
] }),
|
|
293
|
+
item.optional && /* @__PURE__ */ jsx(Badge, { className: "bg-gray-100 text-gray-500 text-xs", children: "Optional" })
|
|
294
|
+
] }, item.id))
|
|
295
|
+
] }) }),
|
|
296
|
+
/* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
297
|
+
/* @__PURE__ */ jsx(
|
|
298
|
+
Button,
|
|
299
|
+
{
|
|
300
|
+
variant: "secondary",
|
|
301
|
+
size: "small",
|
|
302
|
+
onClick: () => handleEditBundle(bundle),
|
|
303
|
+
children: "Edit"
|
|
304
|
+
}
|
|
305
|
+
),
|
|
306
|
+
/* @__PURE__ */ jsx(
|
|
307
|
+
Button,
|
|
308
|
+
{
|
|
309
|
+
variant: "secondary",
|
|
310
|
+
size: "small",
|
|
311
|
+
onClick: () => handleDeleteBundle(bundle.id),
|
|
312
|
+
className: "text-red-600 hover:text-red-700",
|
|
313
|
+
children: "Delete"
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
] }) })
|
|
317
|
+
] }, bundle.id))
|
|
318
|
+
] })
|
|
241
319
|
] }) }),
|
|
242
320
|
totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "flex justify-center gap-2 mt-6", children: [
|
|
243
321
|
/* @__PURE__ */ jsx(
|
|
@@ -266,46 +344,39 @@ const BundledProductsPage = () => {
|
|
|
266
344
|
)
|
|
267
345
|
] })
|
|
268
346
|
] }),
|
|
269
|
-
/* @__PURE__ */ jsx(FocusModal, { open: openCreateModal, onOpenChange:
|
|
347
|
+
/* @__PURE__ */ jsx(FocusModal, { open: openCreateModal, onOpenChange: (open) => {
|
|
348
|
+
if (!open) closeModal();
|
|
349
|
+
}, children: /* @__PURE__ */ jsxs(FocusModal.Content, { children: [
|
|
270
350
|
/* @__PURE__ */ jsx(FocusModal.Header, { children: /* @__PURE__ */ jsx(Heading, { level: "h1", children: isEditing ? "Edit Bundle" : "Create Bundle" }) }),
|
|
271
351
|
/* @__PURE__ */ jsx(FocusModal.Body, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col items-center h-[80vh] overflow-y-auto px-2 py-8", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-3xl flex flex-col gap-y-8", children: [
|
|
272
352
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
273
|
-
/* @__PURE__ */ jsx(Label, { children: "
|
|
274
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-
|
|
275
|
-
/* @__PURE__ */
|
|
276
|
-
|
|
353
|
+
/* @__PURE__ */ jsx(Label, { children: "Bundle Product" }),
|
|
354
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500 mb-2", children: isEditing ? "The product this bundle is attached to" : "Choose the product that will become a bundle" }),
|
|
355
|
+
/* @__PURE__ */ jsx(
|
|
356
|
+
ProductSearchSelect,
|
|
277
357
|
{
|
|
278
358
|
value: selectedProductId,
|
|
279
|
-
|
|
359
|
+
valueLabel: selectedProductTitle,
|
|
360
|
+
onChange: (id, title) => {
|
|
361
|
+
setSelectedProductId(id || void 0);
|
|
362
|
+
setSelectedProductTitle(title);
|
|
363
|
+
},
|
|
280
364
|
disabled: isEditing,
|
|
281
|
-
|
|
282
|
-
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Select a product to bundle" }) }),
|
|
283
|
-
/* @__PURE__ */ jsx(Select.Content, { children: products == null ? void 0 : products.map((product) => /* @__PURE__ */ jsx(
|
|
284
|
-
Select.Item,
|
|
285
|
-
{
|
|
286
|
-
value: product.id,
|
|
287
|
-
children: product.title
|
|
288
|
-
},
|
|
289
|
-
product.id
|
|
290
|
-
)) })
|
|
291
|
-
]
|
|
365
|
+
placeholder: "Search for a product to bundle…"
|
|
292
366
|
}
|
|
293
367
|
)
|
|
294
368
|
] }),
|
|
295
369
|
selectedProductId && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
296
370
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
297
371
|
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Bundle Items" }),
|
|
298
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-
|
|
372
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500 mb-4", children: "Products included in this bundle" }),
|
|
299
373
|
bundleItems.map((item, index) => /* @__PURE__ */ jsx(
|
|
300
374
|
BundleItemForm,
|
|
301
375
|
{
|
|
302
376
|
item,
|
|
303
377
|
index,
|
|
304
|
-
products,
|
|
305
378
|
onUpdate: (field, value) => updateBundleItem(index, field, value),
|
|
306
|
-
onRemove: () =>
|
|
307
|
-
fetchMoreProducts,
|
|
308
|
-
hasNextPage
|
|
379
|
+
onRemove: () => setBundleItems((prev) => prev.filter((_, i) => i !== index))
|
|
309
380
|
},
|
|
310
381
|
index
|
|
311
382
|
)),
|
|
@@ -313,7 +384,7 @@ const BundledProductsPage = () => {
|
|
|
313
384
|
Button,
|
|
314
385
|
{
|
|
315
386
|
variant: "secondary",
|
|
316
|
-
onClick:
|
|
387
|
+
onClick: () => setBundleItems((prev) => [...prev, emptyBundleItem()]),
|
|
317
388
|
className: "mt-4",
|
|
318
389
|
children: "Add Item"
|
|
319
390
|
}
|
|
@@ -321,14 +392,16 @@ const BundledProductsPage = () => {
|
|
|
321
392
|
] }),
|
|
322
393
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
323
394
|
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Bundle Metadata" }),
|
|
324
|
-
/* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-
|
|
395
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500 mb-4", children: "Custom key-value pairs for this bundle" }),
|
|
325
396
|
bundleMeta.map((meta, index) => /* @__PURE__ */ jsxs("div", { className: "flex gap-2 mb-2", children: [
|
|
326
397
|
/* @__PURE__ */ jsx(
|
|
327
398
|
Input,
|
|
328
399
|
{
|
|
329
400
|
placeholder: "Key",
|
|
330
401
|
value: meta.key,
|
|
331
|
-
onChange: (e) =>
|
|
402
|
+
onChange: (e) => setBundleMeta(
|
|
403
|
+
(prev) => prev.map((m, i) => i === index ? { ...m, key: e.target.value } : m)
|
|
404
|
+
)
|
|
332
405
|
}
|
|
333
406
|
),
|
|
334
407
|
/* @__PURE__ */ jsx(
|
|
@@ -336,14 +409,18 @@ const BundledProductsPage = () => {
|
|
|
336
409
|
{
|
|
337
410
|
placeholder: "Value",
|
|
338
411
|
value: meta.value,
|
|
339
|
-
onChange: (e) =>
|
|
412
|
+
onChange: (e) => setBundleMeta(
|
|
413
|
+
(prev) => prev.map(
|
|
414
|
+
(m, i) => i === index ? { ...m, value: e.target.value } : m
|
|
415
|
+
)
|
|
416
|
+
)
|
|
340
417
|
}
|
|
341
418
|
),
|
|
342
419
|
/* @__PURE__ */ jsx(
|
|
343
420
|
Button,
|
|
344
421
|
{
|
|
345
422
|
variant: "secondary",
|
|
346
|
-
onClick: () =>
|
|
423
|
+
onClick: () => setBundleMeta((prev) => prev.filter((_, i) => i !== index)),
|
|
347
424
|
children: "Remove"
|
|
348
425
|
}
|
|
349
426
|
)
|
|
@@ -352,7 +429,7 @@ const BundledProductsPage = () => {
|
|
|
352
429
|
Button,
|
|
353
430
|
{
|
|
354
431
|
variant: "secondary",
|
|
355
|
-
onClick:
|
|
432
|
+
onClick: () => setBundleMeta((prev) => [...prev, { key: "", value: "" }]),
|
|
356
433
|
className: "mt-4",
|
|
357
434
|
children: "Add Metadata"
|
|
358
435
|
}
|
|
@@ -361,19 +438,13 @@ const BundledProductsPage = () => {
|
|
|
361
438
|
] })
|
|
362
439
|
] }) }) }),
|
|
363
440
|
/* @__PURE__ */ jsx(FocusModal.Footer, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-x-2", children: [
|
|
364
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick:
|
|
365
|
-
setOpenCreateModal(false);
|
|
366
|
-
setIsEditing(false);
|
|
367
|
-
setSelectedProductId(void 0);
|
|
368
|
-
setBundleItems([]);
|
|
369
|
-
setBundleMeta([]);
|
|
370
|
-
}, children: "Cancel" }),
|
|
441
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: closeModal, children: "Cancel" }),
|
|
371
442
|
/* @__PURE__ */ jsx(
|
|
372
443
|
Button,
|
|
373
444
|
{
|
|
374
445
|
variant: "primary",
|
|
375
|
-
onClick:
|
|
376
|
-
isLoading:
|
|
446
|
+
onClick: handleSaveBundle,
|
|
447
|
+
isLoading: isSaving,
|
|
377
448
|
disabled: !selectedProductId,
|
|
378
449
|
children: isEditing ? "Update Bundle" : "Create Bundle"
|
|
379
450
|
}
|
|
@@ -382,145 +453,100 @@ const BundledProductsPage = () => {
|
|
|
382
453
|
] }) })
|
|
383
454
|
] });
|
|
384
455
|
};
|
|
385
|
-
const BundleItemForm = ({
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
456
|
+
const BundleItemForm = ({ item, index, onUpdate, onRemove }) => /* @__PURE__ */ jsxs("div", { className: "border rounded-lg p-4 space-y-4 mb-4", children: [
|
|
457
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
458
|
+
/* @__PURE__ */ jsxs(Heading, { level: "h3", children: [
|
|
459
|
+
"Item ",
|
|
460
|
+
index + 1
|
|
461
|
+
] }),
|
|
462
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onRemove, children: "Remove" })
|
|
463
|
+
] }),
|
|
464
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
465
|
+
/* @__PURE__ */ jsx(Label, { children: "Product" }),
|
|
466
|
+
/* @__PURE__ */ jsx(
|
|
467
|
+
ProductSearchSelect,
|
|
468
|
+
{
|
|
469
|
+
value: item.product_id,
|
|
470
|
+
valueLabel: item.product_title,
|
|
471
|
+
onChange: (id, title) => {
|
|
472
|
+
onUpdate("product_id", id || void 0);
|
|
473
|
+
onUpdate("product_title", title);
|
|
474
|
+
},
|
|
475
|
+
placeholder: "Search for a product…"
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
] }),
|
|
479
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
|
|
480
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
481
|
+
/* @__PURE__ */ jsx(Label, { children: "Min Quantity" }),
|
|
482
|
+
/* @__PURE__ */ jsx(
|
|
483
|
+
Input,
|
|
484
|
+
{
|
|
485
|
+
type: "number",
|
|
486
|
+
min: 0,
|
|
487
|
+
value: item.min_quantity,
|
|
488
|
+
onChange: (e) => onUpdate("min_quantity", parseInt(e.target.value) || 0)
|
|
399
489
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
490
|
+
)
|
|
491
|
+
] }),
|
|
492
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
493
|
+
/* @__PURE__ */ jsx(Label, { children: "Max Quantity" }),
|
|
494
|
+
/* @__PURE__ */ jsx(
|
|
495
|
+
Input,
|
|
496
|
+
{
|
|
497
|
+
type: "number",
|
|
498
|
+
min: 0,
|
|
499
|
+
value: item.max_quantity,
|
|
500
|
+
onChange: (e) => onUpdate("max_quantity", parseInt(e.target.value) || 0)
|
|
403
501
|
}
|
|
404
|
-
|
|
405
|
-
{ threshold: 1 }
|
|
406
|
-
)
|
|
407
|
-
);
|
|
408
|
-
const lastOptionRef = useCallback(
|
|
409
|
-
(node) => {
|
|
410
|
-
if (!hasNextPage) {
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
if (observer.current) {
|
|
414
|
-
observer.current.disconnect();
|
|
415
|
-
}
|
|
416
|
-
if (node) {
|
|
417
|
-
observer.current.observe(node);
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
[hasNextPage]
|
|
421
|
-
);
|
|
422
|
-
return /* @__PURE__ */ jsxs("div", { className: "border rounded-lg p-4 space-y-4", children: [
|
|
423
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
424
|
-
/* @__PURE__ */ jsxs(Heading, { level: "h3", children: [
|
|
425
|
-
"Item ",
|
|
426
|
-
index + 1
|
|
427
|
-
] }),
|
|
428
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onRemove, children: "Remove" })
|
|
502
|
+
)
|
|
429
503
|
] }),
|
|
430
504
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
431
|
-
/* @__PURE__ */ jsx(Label, { children: "
|
|
432
|
-
/* @__PURE__ */
|
|
433
|
-
|
|
505
|
+
/* @__PURE__ */ jsx(Label, { children: "Default Quantity" }),
|
|
506
|
+
/* @__PURE__ */ jsx(
|
|
507
|
+
Input,
|
|
434
508
|
{
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
/* @__PURE__ */ jsx(Select.Content, { children: products == null ? void 0 : products.map((product, productIndex) => /* @__PURE__ */ jsx(
|
|
440
|
-
Select.Item,
|
|
441
|
-
{
|
|
442
|
-
value: product.id,
|
|
443
|
-
ref: productIndex === products.length - 1 ? lastOptionRef : null,
|
|
444
|
-
children: product.title
|
|
445
|
-
},
|
|
446
|
-
product.id
|
|
447
|
-
)) })
|
|
448
|
-
]
|
|
509
|
+
type: "number",
|
|
510
|
+
min: 0,
|
|
511
|
+
value: item.default_quantity,
|
|
512
|
+
onChange: (e) => onUpdate("default_quantity", parseInt(e.target.value) || 0)
|
|
449
513
|
}
|
|
450
514
|
)
|
|
515
|
+
] })
|
|
516
|
+
] }),
|
|
517
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
518
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
519
|
+
/* @__PURE__ */ jsx(
|
|
520
|
+
Switch,
|
|
521
|
+
{
|
|
522
|
+
checked: item.optional,
|
|
523
|
+
onCheckedChange: (checked) => onUpdate("optional", checked)
|
|
524
|
+
}
|
|
525
|
+
),
|
|
526
|
+
/* @__PURE__ */ jsx(Label, { children: "Optional" })
|
|
451
527
|
] }),
|
|
452
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
453
|
-
/* @__PURE__ */
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
onChange: (e) => onUpdate("min_quantity", parseInt(e.target.value) || 0)
|
|
462
|
-
}
|
|
463
|
-
)
|
|
464
|
-
] }),
|
|
465
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
466
|
-
/* @__PURE__ */ jsx(Label, { children: "Max Quantity" }),
|
|
467
|
-
/* @__PURE__ */ jsx(
|
|
468
|
-
Input,
|
|
469
|
-
{
|
|
470
|
-
type: "number",
|
|
471
|
-
min: 0,
|
|
472
|
-
value: item.max_quantity,
|
|
473
|
-
onChange: (e) => onUpdate("max_quantity", parseInt(e.target.value) || 0)
|
|
474
|
-
}
|
|
475
|
-
)
|
|
476
|
-
] }),
|
|
477
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
478
|
-
/* @__PURE__ */ jsx(Label, { children: "Default Quantity" }),
|
|
479
|
-
/* @__PURE__ */ jsx(
|
|
480
|
-
Input,
|
|
481
|
-
{
|
|
482
|
-
type: "number",
|
|
483
|
-
min: 0,
|
|
484
|
-
value: item.default_quantity,
|
|
485
|
-
onChange: (e) => onUpdate("default_quantity", parseInt(e.target.value) || 0)
|
|
486
|
-
}
|
|
487
|
-
)
|
|
488
|
-
] })
|
|
528
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
529
|
+
/* @__PURE__ */ jsx(
|
|
530
|
+
Switch,
|
|
531
|
+
{
|
|
532
|
+
checked: item.separate_shipping,
|
|
533
|
+
onCheckedChange: (checked) => onUpdate("separate_shipping", checked)
|
|
534
|
+
}
|
|
535
|
+
),
|
|
536
|
+
/* @__PURE__ */ jsx(Label, { children: "Separate Shipping" })
|
|
489
537
|
] }),
|
|
490
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
491
|
-
/* @__PURE__ */
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
/* @__PURE__ */ jsx(Label, { children: "Optional" })
|
|
500
|
-
] }),
|
|
501
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
502
|
-
/* @__PURE__ */ jsx(
|
|
503
|
-
Switch,
|
|
504
|
-
{
|
|
505
|
-
checked: item.separate_shipping,
|
|
506
|
-
onCheckedChange: (checked) => onUpdate("separate_shipping", checked)
|
|
507
|
-
}
|
|
508
|
-
),
|
|
509
|
-
/* @__PURE__ */ jsx(Label, { children: "Separate Shipping" })
|
|
510
|
-
] }),
|
|
511
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
512
|
-
/* @__PURE__ */ jsx(
|
|
513
|
-
Switch,
|
|
514
|
-
{
|
|
515
|
-
checked: item.individual_price,
|
|
516
|
-
onCheckedChange: (checked) => onUpdate("individual_price", checked)
|
|
517
|
-
}
|
|
518
|
-
),
|
|
519
|
-
/* @__PURE__ */ jsx(Label, { children: "Individual Price" })
|
|
520
|
-
] })
|
|
538
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
539
|
+
/* @__PURE__ */ jsx(
|
|
540
|
+
Switch,
|
|
541
|
+
{
|
|
542
|
+
checked: item.individual_price,
|
|
543
|
+
onCheckedChange: (checked) => onUpdate("individual_price", checked)
|
|
544
|
+
}
|
|
545
|
+
),
|
|
546
|
+
/* @__PURE__ */ jsx(Label, { children: "Individual Price" })
|
|
521
547
|
] })
|
|
522
|
-
] })
|
|
523
|
-
};
|
|
548
|
+
] })
|
|
549
|
+
] });
|
|
524
550
|
const config = defineRouteConfig({
|
|
525
551
|
label: "Bundled Products",
|
|
526
552
|
icon: CubeSolid
|