@zaamx/netme-bundle 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,565 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { defineRouteConfig } from "@medusajs/admin-sdk";
3
+ import { CubeSolid } from "@medusajs/icons";
4
+ import { Container, Heading, Button, Text, Badge, FocusModal, Label, Select, Input, toast, Switch } from "@medusajs/ui";
5
+ import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query";
6
+ import { useState, useMemo, useRef, useCallback } from "react";
7
+ import Medusa from "@medusajs/js-sdk";
8
+ import { Link } from "react-router-dom";
9
+ const sdk = new Medusa({
10
+ baseUrl: "/",
11
+ debug: false,
12
+ auth: {
13
+ type: "session"
14
+ }
15
+ });
16
+ const limit = 15;
17
+ const BundledProductsPage = () => {
18
+ const [page, setPage] = useState(0);
19
+ const [openCreateModal, setOpenCreateModal] = useState(false);
20
+ const [isEditing, setIsEditing] = useState(false);
21
+ const [selectedProductId, setSelectedProductId] = useState();
22
+ const [bundleItems, setBundleItems] = useState([]);
23
+ 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
+ const queryClient = useQueryClient();
33
+ const offset = page * limit;
34
+ const { data, isLoading } = useQuery({
35
+ queryKey: ["bundled-products", offset, limit],
36
+ queryFn: () => sdk.client.fetch("/admin/bundled-products", {
37
+ method: "GET",
38
+ query: {
39
+ limit,
40
+ offset
41
+ }
42
+ })
43
+ });
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) => {
65
+ if (!selectedProductId) throw new Error("No product selected");
66
+ await sdk.client.fetch(`/admin/products/${selectedProductId}/bundle`, {
67
+ method: "POST",
68
+ body: data2
69
+ });
70
+ }
71
+ });
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
+ }
78
+ });
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
+ const handleEditBundle = (bundle) => {
92
+ setIsEditing(true);
93
+ 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,
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);
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: "" }]);
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
+ ));
176
+ };
177
+ const totalPages = Math.ceil(((data == null ? void 0 : data.count) || 0) / limit);
178
+ 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: [
180
+ /* @__PURE__ */ jsx(Heading, { children: "Bundled Products" }),
181
+ /* @__PURE__ */ jsx(Button, { variant: "primary", onClick: () => setOpenCreateModal(true), children: "Create Bundle" })
182
+ ] }),
183
+ isLoading ? /* @__PURE__ */ jsx("div", { className: "p-6", children: /* @__PURE__ */ jsx(Text, { children: "Loading..." }) }) : /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
184
+ /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse border border-gray-200", children: [
185
+ /* @__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
+ /* @__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" })
191
+ ] }) }),
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,
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)) })
241
+ ] }) }),
242
+ 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
+ ),
252
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center px-4", children: [
253
+ "Page ",
254
+ page + 1,
255
+ " of ",
256
+ totalPages
257
+ ] }),
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
+ )
267
+ ] })
268
+ ] }),
269
+ /* @__PURE__ */ jsx(FocusModal, { open: openCreateModal, onOpenChange: setOpenCreateModal, children: /* @__PURE__ */ jsxs(FocusModal.Content, { children: [
270
+ /* @__PURE__ */ jsx(FocusModal.Header, { children: /* @__PURE__ */ jsx(Heading, { level: "h1", children: isEditing ? "Edit Bundle" : "Create Bundle" }) }),
271
+ /* @__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
+ /* @__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,
277
+ {
278
+ value: selectedProductId,
279
+ onValueChange: setSelectedProductId,
280
+ 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
+ ]
292
+ }
293
+ )
294
+ ] }),
295
+ selectedProductId && /* @__PURE__ */ jsxs(Fragment, { children: [
296
+ /* @__PURE__ */ jsxs("div", { children: [
297
+ /* @__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" }),
299
+ bundleItems.map((item, index) => /* @__PURE__ */ jsx(
300
+ BundleItemForm,
301
+ {
302
+ item,
303
+ index,
304
+ products,
305
+ onUpdate: (field, value) => updateBundleItem(index, field, value),
306
+ onRemove: () => removeBundleItem(index),
307
+ fetchMoreProducts,
308
+ hasNextPage
309
+ },
310
+ index
311
+ )),
312
+ /* @__PURE__ */ jsx(
313
+ Button,
314
+ {
315
+ variant: "secondary",
316
+ onClick: addBundleItem,
317
+ className: "mt-4",
318
+ children: "Add Item"
319
+ }
320
+ )
321
+ ] }),
322
+ /* @__PURE__ */ jsxs("div", { children: [
323
+ /* @__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" }),
325
+ 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
+ )
350
+ ] }, index)),
351
+ /* @__PURE__ */ jsx(
352
+ Button,
353
+ {
354
+ variant: "secondary",
355
+ onClick: addBundleMeta,
356
+ className: "mt-4",
357
+ children: "Add Metadata"
358
+ }
359
+ )
360
+ ] })
361
+ ] })
362
+ ] }) }) }),
363
+ /* @__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
+ )
381
+ ] }) })
382
+ ] }) })
383
+ ] });
384
+ };
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
+ ] })
489
+ ] }),
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
+ ] })
521
+ ] })
522
+ ] });
523
+ };
524
+ const config = defineRouteConfig({
525
+ label: "Bundled Products",
526
+ icon: CubeSolid
527
+ });
528
+ const i18nTranslations0 = {};
529
+ const widgetModule = { widgets: [] };
530
+ const routeModule = {
531
+ routes: [
532
+ {
533
+ Component: BundledProductsPage,
534
+ path: "/bundled-products"
535
+ }
536
+ ]
537
+ };
538
+ const menuItemModule = {
539
+ menuItems: [
540
+ {
541
+ label: config.label,
542
+ icon: config.icon,
543
+ path: "/bundled-products",
544
+ nested: void 0,
545
+ rank: void 0,
546
+ translationNs: void 0
547
+ }
548
+ ]
549
+ };
550
+ const formModule = { customFields: {} };
551
+ const displayModule = {
552
+ displays: {}
553
+ };
554
+ const i18nModule = { resources: i18nTranslations0 };
555
+ const plugin = {
556
+ widgetModule,
557
+ routeModule,
558
+ menuItemModule,
559
+ formModule,
560
+ displayModule,
561
+ i18nModule
562
+ };
563
+ export {
564
+ plugin as default
565
+ };