@zaamx/netme-bundle 0.0.2 → 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.
@@ -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, Select, Input, toast, Switch } from "@medusajs/ui";
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, useMemo, useRef, useCallback } from "react";
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
- useQuery({
45
- queryKey: ["products"],
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: data2
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, isPending: isDeleting } = useMutation({
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
- queryKey: ["bundled-products"]
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
- setBundleItems(bundle.items.map((item) => ({
95
- product_id: item.product.id,
96
- min_quantity: item.min_quantity || 1,
97
- max_quantity: item.max_quantity || 1,
98
- default_quantity: item.quantity,
99
- optional: item.optional || false,
100
- separate_shipping: item.separate_shipping || false,
101
- individual_price: item.individual_price || false
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 handleCreateBundle = async () => {
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
- if (!selectedProductId) {
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
- queryKey: ["bundled-products"]
134
- });
135
- setIsEditing(false);
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(bundleItems.map(
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(Button, { variant: "primary", onClick: () => setOpenCreateModal(true), children: "Create Bundle" })
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..." }) }) : /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
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__ */ jsx("tbody", { children: data == null ? void 0 : data.bundled_products.map((bundle) => /* @__PURE__ */ jsxs("tr", { className: "hover:bg-gray-50", children: [
193
- /* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-2", children: /* @__PURE__ */ jsx("code", { className: "text-sm", children: bundle.id }) }),
194
- /* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-2", children: bundle.title }),
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
- to: `/products/${bundle.product.id}`,
214
- className: "text-blue-600 hover:underline",
215
- children: "View Product"
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
- /* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
219
- /* @__PURE__ */ jsx(Badge, { className: bundle.is_bundle ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700", children: bundle.is_bundle ? "Active" : "Inactive" }),
220
- /* @__PURE__ */ jsx(
221
- Button,
222
- {
223
- variant: "secondary",
224
- size: "small",
225
- onClick: () => handleEditBundle(bundle),
226
- children: "Edit"
227
- }
228
- ),
229
- /* @__PURE__ */ jsx(
230
- Button,
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
- variant: "secondary",
233
- size: "small",
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
- ] }, bundle.id)) })
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: setOpenCreateModal, children: /* @__PURE__ */ jsxs(FocusModal.Content, { children: [
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: "Select Product to Bundle" }),
274
- /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-600 mb-4", children: "Choose a product that will become a bundle" }),
275
- /* @__PURE__ */ jsxs(
276
- Select,
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
- onValueChange: setSelectedProductId,
359
+ valueLabel: selectedProductTitle,
360
+ onChange: (id, title) => {
361
+ setSelectedProductId(id || void 0);
362
+ setSelectedProductTitle(title);
363
+ },
280
364
  disabled: isEditing,
281
- children: [
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-600 mb-4", children: "Select products to include in this bundle" }),
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: () => removeBundleItem(index),
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: addBundleItem,
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-600 mb-4", children: "Add custom metadata for this bundle" }),
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) => updateBundleMeta(index, "key", e.target.value)
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) => updateBundleMeta(index, "value", e.target.value)
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: () => removeBundleMeta(index),
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: addBundleMeta,
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: handleCreateBundle,
376
- isLoading: isCreating,
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
- item,
387
- index,
388
- products,
389
- onUpdate,
390
- onRemove,
391
- fetchMoreProducts,
392
- hasNextPage
393
- }) => {
394
- const observer = useRef(
395
- new IntersectionObserver(
396
- (entries) => {
397
- if (!hasNextPage) {
398
- return;
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
- const first = entries[0];
401
- if (first.isIntersecting) {
402
- fetchMoreProducts();
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: "Product" }),
432
- /* @__PURE__ */ jsxs(
433
- Select,
505
+ /* @__PURE__ */ jsx(Label, { children: "Default Quantity" }),
506
+ /* @__PURE__ */ jsx(
507
+ Input,
434
508
  {
435
- value: item.product_id,
436
- onValueChange: (value) => onUpdate("product_id", value),
437
- children: [
438
- /* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Select Product" }) }),
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: "grid grid-cols-3 gap-4", children: [
453
- /* @__PURE__ */ jsxs("div", { children: [
454
- /* @__PURE__ */ jsx(Label, { children: "Min Quantity" }),
455
- /* @__PURE__ */ jsx(
456
- Input,
457
- {
458
- type: "number",
459
- min: 0,
460
- value: item.min_quantity,
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: "space-y-2", children: [
491
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
492
- /* @__PURE__ */ jsx(
493
- Switch,
494
- {
495
- checked: item.optional,
496
- onCheckedChange: (checked) => onUpdate("optional", checked)
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