@zaamx/netme-bundle 0.0.4 → 0.0.6

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,514 +13,481 @@ const sdk = new Medusa({
13
13
  type: "session"
14
14
  }
15
15
  });
16
- const limit = 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 VariantPicker = ({
106
+ productId,
107
+ selectedIds,
108
+ onChange
109
+ }) => {
110
+ const [variants, setVariants] = useState([]);
111
+ const [loading, setLoading] = useState(false);
112
+ useEffect(() => {
113
+ if (!productId) {
114
+ setVariants([]);
115
+ return;
116
+ }
117
+ setLoading(true);
118
+ sdk.admin.product.retrieve(productId).then(
119
+ ({ product }) => setVariants(
120
+ (product.variants || []).map((v) => ({ id: v.id, title: v.title ?? v.id, sku: v.sku ?? void 0 }))
121
+ )
122
+ ).catch(() => setVariants([])).finally(() => setLoading(false));
123
+ }, [productId]);
124
+ const toggle = (id) => onChange(
125
+ selectedIds.includes(id) ? selectedIds.filter((s) => s !== id) : [...selectedIds, id]
126
+ );
127
+ if (loading) return /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-400", children: "Loading variants…" });
128
+ if (!loading && variants.length === 0)
129
+ return /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-400", children: "No variants found." });
130
+ return /* @__PURE__ */ jsx("div", { className: "space-y-2 max-h-52 overflow-y-auto pr-1", children: variants.map((v) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer select-none", children: [
131
+ /* @__PURE__ */ jsx(
132
+ "input",
133
+ {
134
+ type: "checkbox",
135
+ checked: selectedIds.includes(v.id),
136
+ onChange: () => toggle(v.id),
137
+ className: "h-4 w-4 rounded border-gray-300"
138
+ }
139
+ ),
140
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: v.title }),
141
+ v.sku && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 font-mono", children: v.sku })
142
+ ] }, v.id)) });
143
+ };
144
+ const emptyBundleItem = () => ({
145
+ product_id: void 0,
146
+ product_title: "",
147
+ variant_mode: "all",
148
+ variant_ids: [],
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
+ const LIMIT = 15;
17
157
  const BundledProductsPage = () => {
18
158
  const [page, setPage] = useState(0);
19
- const [openCreateModal, setOpenCreateModal] = useState(false);
159
+ const [openModal, setOpenModal] = useState(false);
20
160
  const [isEditing, setIsEditing] = useState(false);
21
161
  const [selectedProductId, setSelectedProductId] = useState();
162
+ const [selectedProductTitle, setSelectedProductTitle] = useState("");
22
163
  const [bundleItems, setBundleItems] = useState([]);
23
164
  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
165
  const queryClient = useQueryClient();
33
- const offset = page * limit;
166
+ const offset = page * LIMIT;
167
+ const closeModal = () => {
168
+ setOpenModal(false);
169
+ setIsEditing(false);
170
+ setSelectedProductId(void 0);
171
+ setSelectedProductTitle("");
172
+ setBundleItems([]);
173
+ setBundleMeta([]);
174
+ };
34
175
  const { data, isLoading } = useQuery({
35
- queryKey: ["bundled-products", offset, limit],
176
+ queryKey: ["bundled-products", offset, LIMIT],
36
177
  queryFn: () => sdk.client.fetch("/admin/bundled-products", {
37
178
  method: "GET",
38
- query: {
39
- limit,
40
- offset
41
- }
179
+ query: { limit: LIMIT, offset }
42
180
  })
43
181
  });
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) => {
182
+ const { mutateAsync: saveBundle, isPending: isSaving } = useMutation({
183
+ mutationFn: async () => {
65
184
  if (!selectedProductId) throw new Error("No product selected");
185
+ const validItems = bundleItems.filter((i) => i.product_id);
66
186
  await sdk.client.fetch(`/admin/products/${selectedProductId}/bundle`, {
67
187
  method: "POST",
68
- body: data2
188
+ body: {
189
+ child_product_ids: validItems.map((item) => ({
190
+ id: item.product_id,
191
+ variant_ids: item.variant_mode === "specific" ? item.variant_ids : [],
192
+ min_quantity: item.min_quantity,
193
+ max_quantity: item.max_quantity,
194
+ default_quantity: item.default_quantity,
195
+ optional: item.optional,
196
+ separate_shipping: item.separate_shipping,
197
+ individual_price: item.individual_price
198
+ })),
199
+ bundle_meta: bundleMeta,
200
+ is_bundle: true
201
+ }
69
202
  });
70
203
  }
71
204
  });
72
- const { mutateAsync: deleteBundle, isPending: isDeleting } = useMutation({
73
- mutationFn: async (productId) => {
74
- await sdk.client.fetch(`/admin/products/${productId}/bundle`, {
75
- method: "DELETE"
76
- });
77
- }
205
+ const { mutateAsync: deleteBundle } = useMutation({
206
+ mutationFn: (productId) => sdk.client.fetch(`/admin/products/${productId}/bundle`, { method: "DELETE" })
78
207
  });
79
- const handleDeleteBundle = async (productId) => {
80
- try {
81
- await deleteBundle(productId);
82
- toast.success("Bundle deleted successfully");
83
- queryClient.invalidateQueries({
84
- queryKey: ["bundled-products"]
85
- });
86
- } catch (error) {
87
- console.error("Error deleting bundle:", error);
88
- toast.error("Failed to delete bundle");
89
- }
90
- };
91
208
  const handleEditBundle = (bundle) => {
92
209
  setIsEditing(true);
93
210
  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
- })));
103
- setBundleMeta(bundle.bundle_meta || []);
104
- setOpenCreateModal(true);
105
- };
106
- const handleCreateBundle = async () => {
107
- 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,
211
+ setSelectedProductTitle(bundle.title);
212
+ setBundleItems(
213
+ bundle.items.map((item) => {
214
+ var _a;
215
+ return {
216
+ product_id: item.product.id,
217
+ product_title: item.product.title,
218
+ variant_mode: ((_a = item.variant_ids) == null ? void 0 : _a.length) ? "specific" : "all",
219
+ variant_ids: item.variant_ids ?? [],
120
220
  min_quantity: item.min_quantity,
121
221
  max_quantity: item.max_quantity,
122
222
  default_quantity: item.default_quantity,
123
223
  optional: item.optional,
124
224
  separate_shipping: item.separate_shipping,
125
225
  individual_price: item.individual_price
126
- })),
127
- bundle_meta: bundleMeta,
128
- is_bundle: true
129
- });
130
- setOpenCreateModal(false);
131
- 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");
142
- }
143
- };
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
- 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: "" }]);
226
+ };
227
+ })
228
+ );
229
+ setBundleMeta(bundle.bundle_meta || []);
230
+ setOpenModal(true);
168
231
  };
169
- const removeBundleMeta = (index) => {
170
- setBundleMeta(bundleMeta.filter((_, i) => i !== index));
232
+ const handleDeleteBundle = async (productId) => {
233
+ try {
234
+ await deleteBundle(productId);
235
+ toast.success("Bundle deleted");
236
+ queryClient.invalidateQueries({ queryKey: ["bundled-products"] });
237
+ } catch {
238
+ toast.error("Failed to delete bundle");
239
+ }
171
240
  };
172
- const updateBundleMeta = (index, field, value) => {
173
- setBundleMeta(bundleMeta.map(
174
- (item, i) => i === index ? { ...item, [field]: value } : item
175
- ));
241
+ const handleSave = async () => {
242
+ if (!selectedProductId) {
243
+ toast.error("Please select a product");
244
+ return;
245
+ }
246
+ if (!bundleItems.some((i) => i.product_id)) {
247
+ toast.error("Please add at least one product to the bundle");
248
+ return;
249
+ }
250
+ try {
251
+ await saveBundle();
252
+ toast.success(isEditing ? "Bundle updated" : "Bundle created");
253
+ queryClient.invalidateQueries({ queryKey: ["bundled-products"] });
254
+ closeModal();
255
+ } catch {
256
+ toast.error(isEditing ? "Failed to update bundle" : "Failed to create bundle");
257
+ }
176
258
  };
177
- const totalPages = Math.ceil(((data == null ? void 0 : data.count) || 0) / limit);
259
+ const updateItem = (index, field, value) => setBundleItems((prev) => prev.map((item, i) => i === index ? { ...item, [field]: value } : item));
260
+ const totalPages = Math.ceil(((data == null ? void 0 : data.count) || 0) / LIMIT);
178
261
  return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
179
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2 md:flex-row md:items-center p-6", children: [
262
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-6", children: [
180
263
  /* @__PURE__ */ jsx(Heading, { children: "Bundled Products" }),
181
- /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: () => setOpenCreateModal(true), children: "Create Bundle" })
264
+ /* @__PURE__ */ jsx(
265
+ Button,
266
+ {
267
+ variant: "primary",
268
+ onClick: () => {
269
+ closeModal();
270
+ setOpenModal(true);
271
+ },
272
+ children: "Create Bundle"
273
+ }
274
+ )
182
275
  ] }),
183
- isLoading ? /* @__PURE__ */ jsx("div", { className: "p-6", children: /* @__PURE__ */ jsx(Text, { children: "Loading..." }) }) : /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
276
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "p-6", children: /* @__PURE__ */ jsx(Text, { children: "Loading" }) }) : /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
184
277
  /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse border border-gray-200", children: [
185
278
  /* @__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
279
  /* @__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" })
280
+ /* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Status" }),
281
+ /* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Items" }),
282
+ /* @__PURE__ */ jsx("th", { className: "border border-gray-200 px-4 py-2 text-left", children: "Actions" })
191
283
  ] }) }),
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
284
+ /* @__PURE__ */ jsxs("tbody", { children: [
285
+ ((data == null ? void 0 : data.bundled_products.length) ?? 0) === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: 4, className: "border border-gray-200 px-4 py-6 text-center text-gray-400", children: "No bundled products found." }) }),
286
+ data == null ? void 0 : data.bundled_products.map((bundle) => /* @__PURE__ */ jsxs("tr", { className: "hover:bg-gray-50", children: [
287
+ /* @__PURE__ */ jsxs("td", { className: "border border-gray-200 px-4 py-3", children: [
288
+ /* @__PURE__ */ jsx(Link, { to: `/products/${bundle.product.id}`, className: "font-medium text-blue-600 hover:underline", children: bundle.title }),
289
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-400 mt-0.5", children: bundle.id })
207
290
  ] }),
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,
212
- {
213
- to: `/products/${bundle.product.id}`,
214
- className: "text-blue-600 hover:underline",
215
- children: "View Product"
216
- }
217
- ) }),
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,
231
- {
232
- variant: "secondary",
233
- size: "small",
234
- onClick: () => handleDeleteBundle(bundle.id),
235
- className: "text-red-600 hover:text-red-700",
236
- children: "Delete"
237
- }
238
- )
239
- ] }) })
240
- ] }, bundle.id)) })
291
+ /* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsx(Badge, { className: bundle.status === "published" ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600", children: bundle.status ?? "" }) }),
292
+ /* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
293
+ bundle.items.length === 0 && /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-400", children: "No items" }),
294
+ bundle.items.map((item) => {
295
+ var _a;
296
+ return /* @__PURE__ */ jsxs("div", { className: "text-sm", children: [
297
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
298
+ /* @__PURE__ */ jsx(Link, { to: `/products/${item.product.id}`, className: "text-blue-600 hover:underline", children: item.product.title }),
299
+ /* @__PURE__ */ jsxs("span", { className: "text-gray-400", children: [
300
+ ",
301
+ item.default_quantity
302
+ ] }),
303
+ item.optional && /* @__PURE__ */ jsx(Badge, { className: "bg-gray-100 text-gray-500 text-xs", children: "Optional" })
304
+ ] }),
305
+ ((_a = item.variant_ids) == null ? void 0 : _a.length) > 0 && /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-400 ml-1", children: [
306
+ item.variant_ids.length,
307
+ " specific variant",
308
+ item.variant_ids.length > 1 ? "s" : ""
309
+ ] })
310
+ ] }, item.id);
311
+ })
312
+ ] }) }),
313
+ /* @__PURE__ */ jsx("td", { className: "border border-gray-200 px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
314
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => handleEditBundle(bundle), children: "Edit" }),
315
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onClick: () => handleDeleteBundle(bundle.id), className: "text-red-600 hover:text-red-700", children: "Delete" })
316
+ ] }) })
317
+ ] }, bundle.id))
318
+ ] })
241
319
  ] }) }),
242
320
  totalPages > 1 && /* @__PURE__ */ jsxs("div", { className: "flex justify-center gap-2 mt-6", children: [
243
- /* @__PURE__ */ jsx(
244
- Button,
245
- {
246
- variant: "secondary",
247
- onClick: () => setPage(Math.max(0, page - 1)),
248
- disabled: page === 0,
249
- children: "Previous"
250
- }
251
- ),
321
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setPage(Math.max(0, page - 1)), disabled: page === 0, children: "Previous" }),
252
322
  /* @__PURE__ */ jsxs("span", { className: "flex items-center px-4", children: [
253
323
  "Page ",
254
324
  page + 1,
255
325
  " of ",
256
326
  totalPages
257
327
  ] }),
258
- /* @__PURE__ */ jsx(
259
- Button,
260
- {
261
- variant: "secondary",
262
- onClick: () => setPage(Math.min(totalPages - 1, page + 1)),
263
- disabled: page === totalPages - 1,
264
- children: "Next"
265
- }
266
- )
328
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setPage(Math.min(totalPages - 1, page + 1)), disabled: page === totalPages - 1, children: "Next" })
267
329
  ] })
268
330
  ] }),
269
- /* @__PURE__ */ jsx(FocusModal, { open: openCreateModal, onOpenChange: setOpenCreateModal, children: /* @__PURE__ */ jsxs(FocusModal.Content, { children: [
331
+ /* @__PURE__ */ jsx(FocusModal, { open: openModal, onOpenChange: (open) => {
332
+ if (!open) closeModal();
333
+ }, children: /* @__PURE__ */ jsxs(FocusModal.Content, { children: [
270
334
  /* @__PURE__ */ jsx(FocusModal.Header, { children: /* @__PURE__ */ jsx(Heading, { level: "h1", children: isEditing ? "Edit Bundle" : "Create Bundle" }) }),
271
335
  /* @__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
336
  /* @__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,
337
+ /* @__PURE__ */ jsx(Label, { children: "Bundle Product" }),
338
+ /* @__PURE__ */ jsx(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" }),
339
+ /* @__PURE__ */ jsx(
340
+ ProductSearchSelect,
277
341
  {
278
342
  value: selectedProductId,
279
- onValueChange: setSelectedProductId,
343
+ valueLabel: selectedProductTitle,
344
+ onChange: (id, title) => {
345
+ setSelectedProductId(id || void 0);
346
+ setSelectedProductTitle(title);
347
+ },
280
348
  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
- ]
349
+ placeholder: "Search for a product to bundle…"
292
350
  }
293
351
  )
294
352
  ] }),
295
353
  selectedProductId && /* @__PURE__ */ jsxs(Fragment, { children: [
296
354
  /* @__PURE__ */ jsxs("div", { children: [
297
355
  /* @__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" }),
356
+ /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500 mb-4", children: "Products included in this bundle" }),
299
357
  bundleItems.map((item, index) => /* @__PURE__ */ jsx(
300
358
  BundleItemForm,
301
359
  {
302
360
  item,
303
361
  index,
304
- products,
305
- onUpdate: (field, value) => updateBundleItem(index, field, value),
306
- onRemove: () => removeBundleItem(index),
307
- fetchMoreProducts,
308
- hasNextPage
362
+ onUpdate: (field, value) => updateItem(index, field, value),
363
+ onRemove: () => setBundleItems((prev) => prev.filter((_, i) => i !== index))
309
364
  },
310
365
  index
311
366
  )),
312
- /* @__PURE__ */ jsx(
313
- Button,
314
- {
315
- variant: "secondary",
316
- onClick: addBundleItem,
317
- className: "mt-4",
318
- children: "Add Item"
319
- }
320
- )
367
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setBundleItems((prev) => [...prev, emptyBundleItem()]), className: "mt-2", children: "Add Item" })
321
368
  ] }),
322
369
  /* @__PURE__ */ jsxs("div", { children: [
323
370
  /* @__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" }),
371
+ /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500 mb-4", children: "Custom key-value pairs" }),
325
372
  bundleMeta.map((meta, index) => /* @__PURE__ */ jsxs("div", { className: "flex gap-2 mb-2", children: [
326
- /* @__PURE__ */ jsx(
327
- Input,
328
- {
329
- placeholder: "Key",
330
- value: meta.key,
331
- onChange: (e) => updateBundleMeta(index, "key", e.target.value)
332
- }
333
- ),
334
- /* @__PURE__ */ jsx(
335
- Input,
336
- {
337
- placeholder: "Value",
338
- value: meta.value,
339
- onChange: (e) => updateBundleMeta(index, "value", e.target.value)
340
- }
341
- ),
342
- /* @__PURE__ */ jsx(
343
- Button,
344
- {
345
- variant: "secondary",
346
- onClick: () => removeBundleMeta(index),
347
- children: "Remove"
348
- }
349
- )
373
+ /* @__PURE__ */ jsx(Input, { placeholder: "Key", value: meta.key, onChange: (e) => setBundleMeta((prev) => prev.map((m, i) => i === index ? { ...m, key: e.target.value } : m)) }),
374
+ /* @__PURE__ */ jsx(Input, { placeholder: "Value", value: meta.value, onChange: (e) => setBundleMeta((prev) => prev.map((m, i) => i === index ? { ...m, value: e.target.value } : m)) }),
375
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setBundleMeta((prev) => prev.filter((_, i) => i !== index)), children: "Remove" })
350
376
  ] }, index)),
351
- /* @__PURE__ */ jsx(
352
- Button,
353
- {
354
- variant: "secondary",
355
- onClick: addBundleMeta,
356
- className: "mt-4",
357
- children: "Add Metadata"
358
- }
359
- )
377
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setBundleMeta((prev) => [...prev, { key: "", value: "" }]), className: "mt-2", children: "Add Metadata" })
360
378
  ] })
361
379
  ] })
362
380
  ] }) }) }),
363
381
  /* @__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" }),
371
- /* @__PURE__ */ jsx(
372
- Button,
373
- {
374
- variant: "primary",
375
- onClick: handleCreateBundle,
376
- isLoading: isCreating,
377
- disabled: !selectedProductId,
378
- children: isEditing ? "Update Bundle" : "Create Bundle"
379
- }
380
- )
382
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: closeModal, children: "Cancel" }),
383
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: handleSave, isLoading: isSaving, disabled: !selectedProductId, children: isEditing ? "Update Bundle" : "Create Bundle" })
381
384
  ] }) })
382
385
  ] }) })
383
386
  ] });
384
387
  };
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;
399
- }
400
- const first = entries[0];
401
- if (first.isIntersecting) {
402
- fetchMoreProducts();
403
- }
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" })
429
- ] }),
430
- /* @__PURE__ */ jsxs("div", { children: [
431
- /* @__PURE__ */ jsx(Label, { children: "Product" }),
432
- /* @__PURE__ */ jsxs(
433
- Select,
434
- {
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
- ]
449
- }
450
- )
451
- ] }),
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
- ] })
388
+ const BundleItemForm = ({ item, index, onUpdate, onRemove }) => /* @__PURE__ */ jsxs("div", { className: "border rounded-lg p-4 space-y-4 mb-4", children: [
389
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
390
+ /* @__PURE__ */ jsxs(Heading, { level: "h3", children: [
391
+ "Item ",
392
+ index + 1
489
393
  ] }),
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: [
394
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onRemove, children: "Remove" })
395
+ ] }),
396
+ /* @__PURE__ */ jsxs("div", { children: [
397
+ /* @__PURE__ */ jsx(Label, { children: "Product" }),
398
+ /* @__PURE__ */ jsx(
399
+ ProductSearchSelect,
400
+ {
401
+ value: item.product_id,
402
+ valueLabel: item.product_title,
403
+ onChange: (id, title) => {
404
+ onUpdate("product_id", id || void 0);
405
+ onUpdate("product_title", title);
406
+ onUpdate("variant_mode", "all");
407
+ onUpdate("variant_ids", []);
408
+ },
409
+ placeholder: "Search for a product…"
410
+ }
411
+ )
412
+ ] }),
413
+ item.product_id && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
414
+ /* @__PURE__ */ jsx(Label, { children: "Applies to" }),
415
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-6", children: [
416
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm cursor-pointer select-none", children: [
502
417
  /* @__PURE__ */ jsx(
503
- Switch,
418
+ "input",
504
419
  {
505
- checked: item.separate_shipping,
506
- onCheckedChange: (checked) => onUpdate("separate_shipping", checked)
420
+ type: "radio",
421
+ name: `variant-mode-${index}`,
422
+ checked: item.variant_mode === "all",
423
+ onChange: () => {
424
+ onUpdate("variant_mode", "all");
425
+ onUpdate("variant_ids", []);
426
+ }
507
427
  }
508
428
  ),
509
- /* @__PURE__ */ jsx(Label, { children: "Separate Shipping" })
429
+ "All variations"
510
430
  ] }),
511
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
431
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm cursor-pointer select-none", children: [
512
432
  /* @__PURE__ */ jsx(
513
- Switch,
433
+ "input",
514
434
  {
515
- checked: item.individual_price,
516
- onCheckedChange: (checked) => onUpdate("individual_price", checked)
435
+ type: "radio",
436
+ name: `variant-mode-${index}`,
437
+ checked: item.variant_mode === "specific",
438
+ onChange: () => onUpdate("variant_mode", "specific")
517
439
  }
518
440
  ),
519
- /* @__PURE__ */ jsx(Label, { children: "Individual Price" })
441
+ "Specific variations"
442
+ ] })
443
+ ] }),
444
+ item.variant_mode === "specific" && /* @__PURE__ */ jsxs("div", { className: "border border-gray-200 rounded-md p-3 bg-gray-50", children: [
445
+ /* @__PURE__ */ jsx(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.' }),
446
+ /* @__PURE__ */ jsx(
447
+ VariantPicker,
448
+ {
449
+ productId: item.product_id,
450
+ selectedIds: item.variant_ids,
451
+ onChange: (ids) => onUpdate("variant_ids", ids)
452
+ }
453
+ ),
454
+ item.variant_ids.length > 0 && /* @__PURE__ */ jsxs(Text, { className: "text-xs text-gray-400 mt-2", children: [
455
+ item.variant_ids.length,
456
+ " variation",
457
+ item.variant_ids.length > 1 ? "s" : "",
458
+ " selected"
520
459
  ] })
521
460
  ] })
522
- ] });
523
- };
461
+ ] }),
462
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-4", children: [
463
+ /* @__PURE__ */ jsxs("div", { children: [
464
+ /* @__PURE__ */ jsx(Label, { children: "Min Quantity" }),
465
+ /* @__PURE__ */ jsx(Input, { type: "number", min: 0, value: item.min_quantity, onChange: (e) => onUpdate("min_quantity", parseInt(e.target.value) || 0) })
466
+ ] }),
467
+ /* @__PURE__ */ jsxs("div", { children: [
468
+ /* @__PURE__ */ jsx(Label, { children: "Max Quantity" }),
469
+ /* @__PURE__ */ jsx(Input, { type: "number", min: 0, value: item.max_quantity, onChange: (e) => onUpdate("max_quantity", parseInt(e.target.value) || 0) })
470
+ ] }),
471
+ /* @__PURE__ */ jsxs("div", { children: [
472
+ /* @__PURE__ */ jsx(Label, { children: "Default Quantity" }),
473
+ /* @__PURE__ */ jsx(Input, { type: "number", min: 0, value: item.default_quantity, onChange: (e) => onUpdate("default_quantity", parseInt(e.target.value) || 0) })
474
+ ] })
475
+ ] }),
476
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
477
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
478
+ /* @__PURE__ */ jsx(Switch, { checked: item.optional, onCheckedChange: (v) => onUpdate("optional", v) }),
479
+ /* @__PURE__ */ jsx(Label, { children: "Optional" })
480
+ ] }),
481
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
482
+ /* @__PURE__ */ jsx(Switch, { checked: item.separate_shipping, onCheckedChange: (v) => onUpdate("separate_shipping", v) }),
483
+ /* @__PURE__ */ jsx(Label, { children: "Separate Shipping" })
484
+ ] }),
485
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
486
+ /* @__PURE__ */ jsx(Switch, { checked: item.individual_price, onCheckedChange: (v) => onUpdate("individual_price", v) }),
487
+ /* @__PURE__ */ jsx(Label, { children: "Individual Price" })
488
+ ] })
489
+ ] })
490
+ ] });
524
491
  const config = defineRouteConfig({
525
492
  label: "Bundled Products",
526
493
  icon: CubeSolid