@propeller-commerce/propeller-v2-react-ui 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -888,6 +888,25 @@ function useCart(options) {
888
888
  }, [graphqlClient, language, configuration, user]);
889
889
  return { cart, cartId, loading, error, checkoutAllowed, resolveCart, addItem, updateItemQuantity, updateItemNotes, deleteItem, addActionCode, removeActionCode, requestAuthorization, processCart, getCrossupsells, getMinQuantity, getStep };
890
890
  }
891
+ function buildAddressInput(type, addr) {
892
+ return {
893
+ type,
894
+ firstName: addr.firstName || "",
895
+ lastName: addr.lastName || "",
896
+ street: addr.street || "",
897
+ postalCode: addr.postalCode || "",
898
+ city: addr.city || "",
899
+ ...addr.company && { company: addr.company },
900
+ ...addr.gender && { gender: addr.gender },
901
+ ...addr.middleName && { middleName: addr.middleName },
902
+ ...addr.number && { number: addr.number },
903
+ ...addr.numberExtension && { numberExtension: addr.numberExtension },
904
+ ...addr.country && { country: addr.country },
905
+ ...addr.email && { email: addr.email },
906
+ ...addr.mobile && { mobile: addr.mobile },
907
+ ...addr.phone && { phone: addr.phone }
908
+ };
909
+ }
891
910
  function useCheckout(options) {
892
911
  const { graphqlClient, user, companyId } = options;
893
912
  const language = options.language ?? "NL";
@@ -928,26 +947,9 @@ function useCheckout(options) {
928
947
  if (!hasInvoice) {
929
948
  const defaultInvoice = getUserDefaultAddress("invoice");
930
949
  if (defaultInvoice) {
931
- const input = {
932
- type: propellerSdkV2.CartAddressType.INVOICE,
933
- firstName: defaultInvoice.firstName || "",
934
- lastName: defaultInvoice.lastName || "",
935
- street: defaultInvoice.street || "",
936
- postalCode: defaultInvoice.postalCode || "",
937
- city: defaultInvoice.city || "",
938
- company: defaultInvoice.company,
939
- gender: defaultInvoice.gender,
940
- middleName: defaultInvoice.middleName,
941
- number: defaultInvoice.number,
942
- numberExtension: defaultInvoice.numberExtension,
943
- country: defaultInvoice.country,
944
- email: defaultInvoice.email,
945
- mobile: defaultInvoice.mobile,
946
- phone: defaultInvoice.phone
947
- };
948
950
  updatedCart = await cartService.updateCartAddress({
949
951
  id: updatedCart.cartId,
950
- input,
952
+ input: buildAddressInput(propellerSdkV2.CartAddressType.INVOICE, defaultInvoice),
951
953
  imageVariantFilters,
952
954
  imageSearchFilters,
953
955
  language
@@ -957,26 +959,9 @@ function useCheckout(options) {
957
959
  if (!hasDelivery) {
958
960
  const defaultDelivery = getUserDefaultAddress("delivery");
959
961
  if (defaultDelivery) {
960
- const input = {
961
- type: propellerSdkV2.CartAddressType.DELIVERY,
962
- firstName: defaultDelivery.firstName || "",
963
- lastName: defaultDelivery.lastName || "",
964
- street: defaultDelivery.street || "",
965
- postalCode: defaultDelivery.postalCode || "",
966
- city: defaultDelivery.city || "",
967
- company: defaultDelivery.company,
968
- gender: defaultDelivery.gender,
969
- middleName: defaultDelivery.middleName,
970
- number: defaultDelivery.number,
971
- numberExtension: defaultDelivery.numberExtension,
972
- country: defaultDelivery.country,
973
- email: defaultDelivery.email,
974
- mobile: defaultDelivery.mobile,
975
- phone: defaultDelivery.phone
976
- };
977
962
  updatedCart = await cartService.updateCartAddress({
978
963
  id: updatedCart.cartId,
979
- input,
964
+ input: buildAddressInput(propellerSdkV2.CartAddressType.DELIVERY, defaultDelivery),
980
965
  imageVariantFilters,
981
966
  imageSearchFilters,
982
967
  language
@@ -2498,6 +2483,140 @@ function useProductSearch(options) {
2498
2483
  goToPage: pagination.goToPage
2499
2484
  };
2500
2485
  }
2486
+ function toMatch(item, language) {
2487
+ const isCluster = "clusterId" in item;
2488
+ const displayItem = isCluster ? item.defaultProduct : item;
2489
+ const productId = displayItem?.productId ?? item.productId;
2490
+ const clusterId = isCluster ? item.clusterId : void 0;
2491
+ const name = language && item.names?.find((n) => n.language === language)?.value || item.names?.[0]?.value || "Product";
2492
+ const netPrice = displayItem?.price?.net ?? 0;
2493
+ const grossPrice = displayItem?.price?.gross ?? 0;
2494
+ const minQuantity = Math.max(1, displayItem?.minimumQuantity ?? 1);
2495
+ const imageUrl = isCluster ? propellerV2CoreUi.getClusterImageUrl(item) : propellerV2CoreUi.getProductImageUrl(item);
2496
+ return {
2497
+ productId,
2498
+ clusterId,
2499
+ name,
2500
+ sku: item.sku || displayItem?.sku || "",
2501
+ netPrice,
2502
+ grossPrice,
2503
+ minQuantity,
2504
+ imageUrl
2505
+ };
2506
+ }
2507
+ function useQuickOrder(options) {
2508
+ const {
2509
+ graphqlClient,
2510
+ user,
2511
+ companyId,
2512
+ configuration = {},
2513
+ onCartCreated,
2514
+ afterAddToCart
2515
+ } = options;
2516
+ const language = options.language || "NL";
2517
+ const searchLimit = options.searchLimit ?? 8;
2518
+ const [submitting, setSubmitting] = React6.useState(false);
2519
+ const [error, setError] = React6.useState(null);
2520
+ const searchProducts = React6.useCallback(
2521
+ async (term) => {
2522
+ const trimmed = term.trim();
2523
+ if (!trimmed || !graphqlClient) return [];
2524
+ try {
2525
+ const service = propellerV2CoreUi.createServices(graphqlClient).product;
2526
+ const input = {
2527
+ term: trimmed,
2528
+ language,
2529
+ page: 1,
2530
+ offset: searchLimit,
2531
+ statuses: [propellerSdkV2.ProductStatus.A, propellerSdkV2.ProductStatus.P, propellerSdkV2.ProductStatus.T, propellerSdkV2.ProductStatus.S],
2532
+ sortInputs: [{ field: propellerSdkV2.ProductSortField.RELEVANCE, order: propellerSdkV2.SortOrder.DESC }],
2533
+ searchFields: [
2534
+ {
2535
+ fieldNames: [
2536
+ propellerSdkV2.ProductSearchableField.SKU,
2537
+ propellerSdkV2.ProductSearchableField.NAME,
2538
+ propellerSdkV2.ProductSearchableField.KEYWORDS,
2539
+ propellerSdkV2.ProductSearchableField.CUSTOM_KEYWORDS
2540
+ ],
2541
+ boost: 5
2542
+ },
2543
+ {
2544
+ fieldNames: [
2545
+ propellerSdkV2.ProductSearchableField.MANUFACTURER_CODE,
2546
+ propellerSdkV2.ProductSearchableField.EAN_CODE,
2547
+ propellerSdkV2.ProductSearchableField.BAR_CODE,
2548
+ propellerSdkV2.ProductSearchableField.SUPPLIER_CODE,
2549
+ propellerSdkV2.ProductSearchableField.PRODUCT_ID
2550
+ ],
2551
+ boost: 1
2552
+ }
2553
+ ]
2554
+ };
2555
+ const variables = {
2556
+ input,
2557
+ language,
2558
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2559
+ };
2560
+ const result = await service.getProducts(variables);
2561
+ const items = result?.items ?? [];
2562
+ return items.map((it) => toMatch(it, language));
2563
+ } catch {
2564
+ return [];
2565
+ }
2566
+ },
2567
+ [graphqlClient, language, searchLimit, configuration]
2568
+ );
2569
+ const submit = React6.useCallback(
2570
+ async (lines) => {
2571
+ const valid = lines.filter((l) => l.productId && (l.quantity ?? 0) > 0);
2572
+ if (!valid.length) return { success: false, error: "No items to add" };
2573
+ if (!graphqlClient) return { success: false, error: "No GraphQL client" };
2574
+ setSubmitting(true);
2575
+ setError(null);
2576
+ try {
2577
+ const services = propellerV2CoreUi.createServices(graphqlClient);
2578
+ const cart = await initCart({
2579
+ services,
2580
+ user: user ?? null,
2581
+ companyId,
2582
+ language,
2583
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2584
+ imageVariantFilters: configuration.imageVariantFiltersSmall,
2585
+ onCartCreated
2586
+ });
2587
+ const items = valid.map((l) => ({
2588
+ productId: l.productId,
2589
+ quantity: l.quantity,
2590
+ ...l.clusterId ? { clusterId: l.clusterId } : {}
2591
+ }));
2592
+ const bulk = await services.cart.bulkUpdateCartItems({
2593
+ input: { cartId: cart.cartId, items }
2594
+ });
2595
+ const updated = await services.cart.getCart({
2596
+ cartId: cart.cartId,
2597
+ language,
2598
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2599
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2600
+ });
2601
+ const finalCart = updated ?? cart;
2602
+ afterAddToCart?.(finalCart);
2603
+ return {
2604
+ success: true,
2605
+ cart: finalCart,
2606
+ added: (bulk?.created ?? 0) + (bulk?.updated ?? 0)
2607
+ };
2608
+ } catch (e) {
2609
+ const msg = e instanceof Error ? e.message : "Failed to add items to cart";
2610
+ setError(msg);
2611
+ return { success: false, error: msg };
2612
+ } finally {
2613
+ setSubmitting(false);
2614
+ }
2615
+ },
2616
+ [graphqlClient, user, companyId, language, configuration, onCartCreated, afterAddToCart]
2617
+ );
2618
+ return { submitting, error, searchProducts, submit };
2619
+ }
2501
2620
  var inflightFetches3 = /* @__PURE__ */ new Set();
2502
2621
  var STOREFRONT_STATUSES = [
2503
2622
  propellerSdkV2.ProductStatus.A,
@@ -4931,6 +5050,7 @@ function AddToFavorite(rawProps) {
4931
5050
  ] }) }) : null });
4932
5051
  }
4933
5052
  var AddToFavorite_default = AddToFavorite;
5053
+ var EMAIL_PATTERN = "[^@\\s]+@[^@\\s.]+(?:\\.[^@\\s.]+)*\\.[A-Za-z]{2,}";
4934
5054
  var AddressCardContext = React6__namespace.createContext(null);
4935
5055
  function useAddressCardContext() {
4936
5056
  const ctx = React6__namespace.useContext(AddressCardContext);
@@ -5376,10 +5496,7 @@ function AddressCard(rawProps) {
5376
5496
  ] }),
5377
5497
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
5378
5498
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5379
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block text-sm font-medium mb-1", children: [
5380
- propellerV2CoreUi.getLabel(props.labels, "email", "Email"),
5381
- " *"
5382
- ] }),
5499
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium mb-1", children: propellerV2CoreUi.getLabel(props.labels, "email", "Email") }),
5383
5500
  /* @__PURE__ */ jsxRuntime.jsx(
5384
5501
  "input",
5385
5502
  {
@@ -5389,7 +5506,7 @@ function AddressCard(rawProps) {
5389
5506
  onChange: (e) => {
5390
5507
  setEditEmail(e.target.value);
5391
5508
  },
5392
- required: true
5509
+ pattern: EMAIL_PATTERN
5393
5510
  }
5394
5511
  )
5395
5512
  ] }),
@@ -5669,10 +5786,7 @@ function AddressCard(rawProps) {
5669
5786
  ] }),
5670
5787
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
5671
5788
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
5672
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "block text-sm font-medium mb-1", children: [
5673
- propellerV2CoreUi.getLabel(props.labels, "email", "Email"),
5674
- " *"
5675
- ] }),
5789
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium mb-1", children: propellerV2CoreUi.getLabel(props.labels, "email", "Email") }),
5676
5790
  /* @__PURE__ */ jsxRuntime.jsx(
5677
5791
  "input",
5678
5792
  {
@@ -5682,7 +5796,7 @@ function AddressCard(rawProps) {
5682
5796
  onChange: (e) => {
5683
5797
  setEditEmail(e.target.value);
5684
5798
  },
5685
- required: true
5799
+ pattern: EMAIL_PATTERN
5686
5800
  }
5687
5801
  )
5688
5802
  ] }),
@@ -6385,7 +6499,7 @@ function CartItem(rawProps) {
6385
6499
  }
6386
6500
  return "#";
6387
6501
  }
6388
- function getProductImageUrl5() {
6502
+ function getProductImageUrl6() {
6389
6503
  return props.cartItem.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url || "";
6390
6504
  }
6391
6505
  function getProductSku3() {
@@ -6569,7 +6683,7 @@ function CartItem(rawProps) {
6569
6683
  derived: {
6570
6684
  name: getProductName5(),
6571
6685
  sku: getProductSku3(),
6572
- imageUrl: getProductImageUrl5(),
6686
+ imageUrl: getProductImageUrl6(),
6573
6687
  productUrl: getProductUrl2(),
6574
6688
  isBundle: isBundleItem(),
6575
6689
  bundleName: getBundleName(),
@@ -9543,7 +9657,7 @@ function GridToolbar(rawProps) {
9543
9657
  const [currentSortOrder, setCurrentSortOrder] = React6.useState(propellerSdkV2.SortOrder.DESC);
9544
9658
  const [currentOffset, setCurrentOffset] = React6.useState(12);
9545
9659
  const [currentViewMode, setCurrentViewMode] = React6.useState("grid");
9546
- function getLabel54(key) {
9660
+ function getLabel55(key) {
9547
9661
  const labels = props.labels || {};
9548
9662
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS[key] || key;
9549
9663
  }
@@ -9622,7 +9736,7 @@ function GridToolbar(rawProps) {
9622
9736
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "propeller-grid-toolbar__count text-sm text-muted-foreground font-medium", children: props.itemsFound > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
9623
9737
  props.itemsFound,
9624
9738
  "\xA0",
9625
- props.itemsFound === 1 ? getLabel54("productSingular") : getLabel54("productPlural")
9739
+ props.itemsFound === 1 ? getLabel55("productSingular") : getLabel55("productPlural")
9626
9740
  ] }) : null }),
9627
9741
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-toolbar__controls flex flex-wrap items-center gap-3", children: [
9628
9742
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -9633,7 +9747,7 @@ function GridToolbar(rawProps) {
9633
9747
  onChange: (e) => handleOffsetChange(parseInt(e.target.value)),
9634
9748
  children: getOffsetOptions()?.map((n) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: n, children: [
9635
9749
  n,
9636
- getLabel54("perPage")
9750
+ getLabel55("perPage")
9637
9751
  ] }, n))
9638
9752
  }
9639
9753
  ),
@@ -9649,7 +9763,7 @@ function GridToolbar(rawProps) {
9649
9763
  {
9650
9764
  value: field,
9651
9765
  disabled: field === "PRICE" && isPriceSortDisabled(),
9652
- children: getLabel54(field)
9766
+ children: getLabel55(field)
9653
9767
  },
9654
9768
  field
9655
9769
  ))
@@ -9662,8 +9776,8 @@ function GridToolbar(rawProps) {
9662
9776
  value: currentSortOrder,
9663
9777
  onChange: (e) => handleSortOrderChange(e.target.value),
9664
9778
  children: [
9665
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel54("ASC") }),
9666
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel54("DESC") })
9779
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel55("ASC") }),
9780
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel55("DESC") })
9667
9781
  ]
9668
9782
  }
9669
9783
  ),
@@ -9673,7 +9787,7 @@ function GridToolbar(rawProps) {
9673
9787
  type: "button",
9674
9788
  className: "propeller-grid-toolbar__view-toggle h-9 w-9 flex items-center justify-center rounded-control border border-input bg-transparent hover:bg-accent hover:text-accent-foreground transition-colors",
9675
9789
  onClick: (event) => handleViewChange(),
9676
- title: currentViewMode === "grid" ? getLabel54("switchToList") : getLabel54("switchToGrid"),
9790
+ title: currentViewMode === "grid" ? getLabel55("switchToList") : getLabel55("switchToGrid"),
9677
9791
  children: [
9678
9792
  currentViewMode === "grid" ? /* @__PURE__ */ jsxRuntime.jsxs(
9679
9793
  "svg",
@@ -9731,7 +9845,7 @@ function GridToolbar(rawProps) {
9731
9845
  onClick: (event) => {
9732
9846
  if (props.onClearFilters) props.onClearFilters();
9733
9847
  },
9734
- children: getLabel54("clearAll")
9848
+ children: getLabel55("clearAll")
9735
9849
  }
9736
9850
  ),
9737
9851
  props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
@@ -9742,7 +9856,7 @@ function GridToolbar(rawProps) {
9742
9856
  if (props.onPriceFilterRemove) props.onPriceFilterRemove();
9743
9857
  },
9744
9858
  children: [
9745
- getLabel54("price"),
9859
+ getLabel55("price"),
9746
9860
  ": \u20AC",
9747
9861
  props.priceFilterMin ?? 0,
9748
9862
  " \u2013 \u20AC",
@@ -9779,7 +9893,7 @@ var DEFAULT_LABELS2 = {
9779
9893
  of: "of"
9780
9894
  };
9781
9895
  function GridPagination(props) {
9782
- function getLabel54(key) {
9896
+ function getLabel55(key) {
9783
9897
  const labels = props.labels || {};
9784
9898
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS2[key] || key;
9785
9899
  }
@@ -9852,15 +9966,15 @@ function GridPagination(props) {
9852
9966
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--prev inline-flex items-center rounded-control border border-input bg-card px-4 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
9853
9967
  disabled: currentPage === 1,
9854
9968
  onClick: () => handlePageChange(currentPage - 1),
9855
- children: getLabel54("previous")
9969
+ children: getLabel55("previous")
9856
9970
  }
9857
9971
  ),
9858
9972
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "propeller-grid-pagination__info px-2 text-sm font-medium text-muted-foreground", children: [
9859
- getLabel54("page"),
9973
+ getLabel55("page"),
9860
9974
  "\xA0",
9861
9975
  currentPage,
9862
9976
  "\xA0",
9863
- getLabel54("of"),
9977
+ getLabel55("of"),
9864
9978
  "\xA0",
9865
9979
  totalPages
9866
9980
  ] }),
@@ -9871,7 +9985,7 @@ function GridPagination(props) {
9871
9985
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--next inline-flex items-center rounded-control border border-input bg-card px-4 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
9872
9986
  disabled: currentPage === totalPages,
9873
9987
  onClick: () => handlePageChange(currentPage + 1),
9874
- children: getLabel54("next")
9988
+ children: getLabel55("next")
9875
9989
  }
9876
9990
  )
9877
9991
  ] }) : null,
@@ -9883,7 +9997,7 @@ function GridPagination(props) {
9883
9997
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--prev inline-flex items-center rounded-control border border-input bg-card px-3 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
9884
9998
  disabled: currentPage === 1,
9885
9999
  onClick: () => handlePageChange(currentPage - 1),
9886
- children: getLabel54("previous")
10000
+ children: getLabel55("previous")
9887
10001
  }
9888
10002
  ),
9889
10003
  getFullPages().map((item, idx) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -9910,7 +10024,7 @@ function GridPagination(props) {
9910
10024
  className: "propeller-grid-pagination__btn propeller-grid-pagination__btn--next inline-flex items-center rounded-control border border-input bg-card px-3 py-2 text-sm font-medium text-foreground shadow-sm hover:bg-surface-hover disabled:opacity-40 disabled:cursor-not-allowed",
9911
10025
  disabled: currentPage === totalPages,
9912
10026
  onClick: () => handlePageChange(currentPage + 1),
9913
- children: getLabel54("next")
10027
+ children: getLabel55("next")
9914
10028
  }
9915
10029
  )
9916
10030
  ] }) : null
@@ -10389,7 +10503,7 @@ function formatPrice9(price, currency) {
10389
10503
  function getProductName2(product) {
10390
10504
  return product.names?.[0]?.value || `Product ${product.productId}`;
10391
10505
  }
10392
- function getProductImageUrl3(product) {
10506
+ function getProductImageUrl4(product) {
10393
10507
  const media = product.media;
10394
10508
  if (media?.images?.items && Array.isArray(media.images.items) && media.images.items.length > 0) {
10395
10509
  const firstImage = media.images.items[0];
@@ -10418,7 +10532,7 @@ function ClusterOptions(props) {
10418
10532
  (p) => p.productId.toString() === selectedProductId
10419
10533
  );
10420
10534
  if (selectedProduct) {
10421
- previewImageUrl = getProductImageUrl3(selectedProduct);
10535
+ previewImageUrl = getProductImageUrl4(selectedProduct);
10422
10536
  previewName = getProductName2(selectedProduct);
10423
10537
  previewPrice = formatPrice9(selectedProduct.price?.gross || 0, props.currency ?? "\u20AC");
10424
10538
  }
@@ -13795,6 +13909,347 @@ function OrderList(rawProps) {
13795
13909
  ] });
13796
13910
  }
13797
13911
  var OrderList_default = OrderList;
13912
+ var ROW_SEQ = 0;
13913
+ function blankRow() {
13914
+ ROW_SEQ += 1;
13915
+ return {
13916
+ key: `qo-${ROW_SEQ}`,
13917
+ code: "",
13918
+ productId: null,
13919
+ name: "",
13920
+ netPrice: 0,
13921
+ quantity: 1,
13922
+ minQuantity: 1,
13923
+ matches: [],
13924
+ searching: false,
13925
+ searched: false
13926
+ };
13927
+ }
13928
+ function QuickOrderInner(props) {
13929
+ const {
13930
+ companyId,
13931
+ language = "NL",
13932
+ currency = "\u20AC",
13933
+ configuration,
13934
+ initialRows = 5,
13935
+ searchThreshold = 3,
13936
+ debounceMs = 300,
13937
+ parseSpreadsheet,
13938
+ templateUrl,
13939
+ maxUploadBytes = 2 * 1024 * 1024,
13940
+ maxUploadRows = 500,
13941
+ onCartCreated,
13942
+ afterAddToCart,
13943
+ onMissingCodes,
13944
+ className,
13945
+ labels
13946
+ } = props;
13947
+ const user = props.user ?? null;
13948
+ const graphqlClient = props.graphqlClient;
13949
+ const displayPrice = (n) => props.formatPrice ? props.formatPrice(n) : propellerV2CoreUi.formatPrice(n, { symbol: currency });
13950
+ const L = (key, fallback) => propellerV2CoreUi.getLabel(labels, key, fallback);
13951
+ const { submitting, searchProducts, submit } = useQuickOrder({
13952
+ graphqlClient,
13953
+ user,
13954
+ companyId,
13955
+ language,
13956
+ configuration,
13957
+ onCartCreated,
13958
+ afterAddToCart
13959
+ });
13960
+ const [rows, setRows] = React6.useState(
13961
+ () => Array.from({ length: Math.max(1, initialRows) }, blankRow)
13962
+ );
13963
+ const [missing, setMissing] = React6.useState([]);
13964
+ const [uploadError, setUploadError] = React6.useState(null);
13965
+ const [uploading, setUploading] = React6.useState(false);
13966
+ const [notice, setNotice] = React6.useState(null);
13967
+ const fileInputRef = React6.useRef(null);
13968
+ const searchTimers = React6.useRef({});
13969
+ const patchRow = React6.useCallback((key, patch) => {
13970
+ setRows((prev) => prev.map((r) => r.key === key ? { ...r, ...patch } : r));
13971
+ }, []);
13972
+ const onCodeInput = React6.useCallback(
13973
+ (key, value) => {
13974
+ patchRow(key, { code: value, productId: null, name: "", netPrice: 0, searched: false });
13975
+ setNotice(null);
13976
+ const timers = searchTimers.current;
13977
+ if (timers[key]) clearTimeout(timers[key]);
13978
+ if (value.trim().length < searchThreshold) {
13979
+ patchRow(key, { matches: [], searching: false, searched: false });
13980
+ return;
13981
+ }
13982
+ patchRow(key, { searching: true });
13983
+ timers[key] = setTimeout(async () => {
13984
+ const results = await searchProducts(value);
13985
+ patchRow(key, { matches: results, searching: false, searched: true });
13986
+ }, debounceMs);
13987
+ },
13988
+ [patchRow, searchProducts, searchThreshold, debounceMs]
13989
+ );
13990
+ const selectMatch = React6.useCallback(
13991
+ (key, match) => {
13992
+ const dup = rows.some((r) => r.key !== key && r.productId && r.code === match.sku);
13993
+ if (dup) {
13994
+ setNotice(L("alreadyInList", "Product is already in the list"));
13995
+ patchRow(key, { code: "", matches: [], productId: null, name: "", netPrice: 0, searched: false });
13996
+ return;
13997
+ }
13998
+ patchRow(key, {
13999
+ code: match.sku,
14000
+ productId: match.productId,
14001
+ clusterId: match.clusterId,
14002
+ name: match.name,
14003
+ netPrice: match.netPrice,
14004
+ quantity: match.minQuantity,
14005
+ minQuantity: match.minQuantity,
14006
+ matches: [],
14007
+ searching: false,
14008
+ searched: false
14009
+ });
14010
+ },
14011
+ [rows, patchRow, L]
14012
+ );
14013
+ const setQuantity = React6.useCallback(
14014
+ (key, raw) => {
14015
+ const n = parseInt(raw, 10);
14016
+ setRows(
14017
+ (prev) => prev.map(
14018
+ (r) => r.key === key ? { ...r, quantity: Number.isFinite(n) && n > 0 ? Math.max(r.minQuantity, n) : r.minQuantity } : r
14019
+ )
14020
+ );
14021
+ },
14022
+ []
14023
+ );
14024
+ const addRow = React6.useCallback(() => setRows((prev) => [...prev, blankRow()]), []);
14025
+ const removeRow = React6.useCallback(
14026
+ (key) => setRows((prev) => prev.length > 1 ? prev.filter((r) => r.key !== key) : prev),
14027
+ []
14028
+ );
14029
+ const resolvedCount = React6.useMemo(() => rows.filter((r) => r.productId).length, [rows]);
14030
+ const onFileChosen = React6.useCallback(
14031
+ async (file) => {
14032
+ if (!file || !parseSpreadsheet) return;
14033
+ setUploadError(null);
14034
+ setMissing([]);
14035
+ if (file.size > maxUploadBytes) {
14036
+ setUploadError(`File too large (max ${Math.round(maxUploadBytes / 1024 / 1024)} MB)`);
14037
+ return;
14038
+ }
14039
+ setUploading(true);
14040
+ try {
14041
+ let lines = await parseSpreadsheet(file);
14042
+ lines = lines.slice(0, maxUploadRows).map((l) => ({ code: String(l.code ?? "").trim(), quantity: Math.max(1, parseInt(String(l.quantity), 10) || 1) })).filter((l) => l.code.length > 0);
14043
+ if (!lines.length) {
14044
+ setUploadError("No valid rows found in the file");
14045
+ return;
14046
+ }
14047
+ const resolved = [];
14048
+ const notFound = [];
14049
+ for (const line of lines) {
14050
+ const matches = await searchProducts(line.code);
14051
+ const exact = matches.find((m) => m.sku.toLowerCase() === line.code.toLowerCase()) || matches[0];
14052
+ if (!exact) {
14053
+ notFound.push(line.code);
14054
+ continue;
14055
+ }
14056
+ if (resolved.some((r) => r.productId === exact.productId)) continue;
14057
+ const row = blankRow();
14058
+ resolved.push({
14059
+ ...row,
14060
+ code: exact.sku,
14061
+ productId: exact.productId,
14062
+ clusterId: exact.clusterId,
14063
+ name: exact.name,
14064
+ netPrice: exact.netPrice,
14065
+ quantity: Math.max(exact.minQuantity, line.quantity),
14066
+ minQuantity: exact.minQuantity
14067
+ });
14068
+ }
14069
+ if (resolved.length) {
14070
+ setRows([...resolved, blankRow()]);
14071
+ }
14072
+ if (notFound.length) {
14073
+ setMissing(notFound);
14074
+ onMissingCodes?.(notFound);
14075
+ }
14076
+ } catch {
14077
+ setUploadError("Could not read the file");
14078
+ } finally {
14079
+ setUploading(false);
14080
+ if (fileInputRef.current) fileInputRef.current.value = "";
14081
+ }
14082
+ },
14083
+ [parseSpreadsheet, maxUploadBytes, maxUploadRows, searchProducts, onMissingCodes]
14084
+ );
14085
+ const handleSubmit = React6.useCallback(async () => {
14086
+ const lines = rows.filter((r) => r.productId).map((r) => ({ productId: r.productId, quantity: r.quantity, clusterId: r.clusterId, code: r.code }));
14087
+ if (!lines.length) {
14088
+ setNotice(L("noItems", "Add at least one product before submitting"));
14089
+ return;
14090
+ }
14091
+ const res = await submit(lines);
14092
+ if (res.success) {
14093
+ setRows(Array.from({ length: Math.max(1, initialRows) }, blankRow));
14094
+ setMissing([]);
14095
+ setNotice(null);
14096
+ } else {
14097
+ setNotice(res.error ?? "Failed to add items to cart");
14098
+ }
14099
+ }, [rows, submit, initialRows, L]);
14100
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: className ?? "propeller-quick-order", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col lg:flex-row gap-8", children: [
14101
+ parseSpreadsheet ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full lg:w-1/3", children: [
14102
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("uploadTitle", "Upload Excel file") }),
14103
+ templateUrl ? /* @__PURE__ */ jsxRuntime.jsx(
14104
+ "a",
14105
+ {
14106
+ href: templateUrl,
14107
+ target: "_blank",
14108
+ rel: "noopener nofollow",
14109
+ className: "text-primary hover:underline text-sm inline-block mb-4",
14110
+ children: L("downloadTemplate", "Download XLSX template")
14111
+ }
14112
+ ) : null,
14113
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mb-3", children: L("uploadHint", "Column A: article no. / SKU \u2014 Column B: quantity. First two rows are ignored.") }),
14114
+ /* @__PURE__ */ jsxRuntime.jsx(
14115
+ "input",
14116
+ {
14117
+ ref: fileInputRef,
14118
+ type: "file",
14119
+ accept: ".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel",
14120
+ className: "block w-full text-sm text-muted-foreground file:mr-3 file:py-2 file:px-4 file:rounded file:border-0 file:bg-primary file:text-primary-foreground hover:file:bg-primary/90",
14121
+ onChange: (e) => onFileChosen(e.target.files?.[0]),
14122
+ disabled: uploading
14123
+ }
14124
+ ),
14125
+ uploading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mt-2", children: L("upload", "Uploading\u2026") }) : null,
14126
+ uploadError ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive mt-2", children: uploadError }) : null
14127
+ ] }) : null,
14128
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
14129
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("title", "Add your products manually") }),
14130
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "hidden md:grid grid-cols-12 gap-2 px-2 pb-2 text-xs font-medium text-muted-foreground border-b border-border", children: [
14131
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3", children: L("colCode", "Article no. / SKU") }),
14132
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3", children: L("colName", "Product name") }),
14133
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: L("colPrice", "excl. VAT") }),
14134
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-1", children: L("colQuantity", "Qty") }),
14135
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2 text-right", children: L("colTotal", "Total") }),
14136
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-1" })
14137
+ ] }),
14138
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y divide-border", children: rows.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-12 gap-2 items-center py-2 relative", children: [
14139
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-12 md:col-span-3 relative", children: [
14140
+ /* @__PURE__ */ jsxRuntime.jsx(
14141
+ "input",
14142
+ {
14143
+ type: "text",
14144
+ value: r.code,
14145
+ onChange: (e) => onCodeInput(r.key, e.target.value),
14146
+ readOnly: !!r.productId,
14147
+ placeholder: L("colCode", "Article no. / SKU"),
14148
+ className: "w-full rounded border border-input bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
14149
+ }
14150
+ ),
14151
+ (r.searching || r.matches.length > 0 || r.searched) && !r.productId ? /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "absolute z-20 mt-1 w-[320px] max-w-[90vw] bg-card border border-border rounded shadow-lg max-h-72 overflow-auto", children: r.searching ? /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-3 py-2 text-sm text-muted-foreground", children: "\u2026" }) : r.matches.length ? r.matches.map((m) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
14152
+ "button",
14153
+ {
14154
+ type: "button",
14155
+ onClick: () => selectMatch(r.key, m),
14156
+ className: "flex items-center gap-2 w-full text-left px-3 py-2 hover:bg-muted text-sm",
14157
+ children: [
14158
+ m.imageUrl ? (
14159
+ // eslint-disable-next-line @next/next/no-img-element
14160
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src: m.imageUrl, alt: "", width: 32, height: 32, className: "rounded object-cover" })
14161
+ ) : null,
14162
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
14163
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-foreground", children: m.name }),
14164
+ m.sku ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "block text-xs text-muted-foreground", children: [
14165
+ "SKU: ",
14166
+ m.sku
14167
+ ] }) : null
14168
+ ] })
14169
+ ]
14170
+ }
14171
+ ) }, `${r.key}-${m.productId}`)) : /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-3 py-2 text-sm text-muted-foreground", children: L("noResults", "No results found") }) }) : null
14172
+ ] }),
14173
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-6 md:col-span-3", children: /* @__PURE__ */ jsxRuntime.jsx(
14174
+ "input",
14175
+ {
14176
+ type: "text",
14177
+ value: r.name,
14178
+ disabled: true,
14179
+ className: "w-full rounded border border-input bg-muted/40 px-2 py-1.5 text-sm text-muted-foreground"
14180
+ }
14181
+ ) }),
14182
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3 md:col-span-2", children: /* @__PURE__ */ jsxRuntime.jsx(
14183
+ "input",
14184
+ {
14185
+ type: "text",
14186
+ value: r.productId ? displayPrice(r.netPrice) : "",
14187
+ disabled: true,
14188
+ className: "w-full rounded border border-input bg-muted/40 px-2 py-1.5 text-sm text-muted-foreground"
14189
+ }
14190
+ ) }),
14191
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3 md:col-span-1", children: /* @__PURE__ */ jsxRuntime.jsx(
14192
+ "input",
14193
+ {
14194
+ type: "number",
14195
+ min: r.minQuantity,
14196
+ step: 1,
14197
+ value: r.productId ? r.quantity : "",
14198
+ disabled: !r.productId,
14199
+ onChange: (e) => setQuantity(r.key, e.target.value),
14200
+ onKeyDown: (e) => {
14201
+ if (["e", "E", "+", "-", "."].includes(e.key)) e.preventDefault();
14202
+ },
14203
+ className: "w-full rounded border border-input bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:bg-muted/40"
14204
+ }
14205
+ ) }),
14206
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-9 md:col-span-2 text-right text-sm text-foreground whitespace-nowrap", children: r.productId ? displayPrice(r.netPrice * r.quantity) : "" }),
14207
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3 md:col-span-1 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
14208
+ "button",
14209
+ {
14210
+ type: "button",
14211
+ onClick: () => removeRow(r.key),
14212
+ "aria-label": L("remove", "Remove"),
14213
+ className: "text-muted-foreground hover:text-destructive p-1",
14214
+ children: "\u2715"
14215
+ }
14216
+ ) })
14217
+ ] }, r.key)) }),
14218
+ /* @__PURE__ */ jsxRuntime.jsxs(
14219
+ "button",
14220
+ {
14221
+ type: "button",
14222
+ onClick: addRow,
14223
+ className: "mt-3 text-primary hover:underline text-sm font-medium",
14224
+ children: [
14225
+ "+ ",
14226
+ L("addRow", "Add more rows")
14227
+ ]
14228
+ }
14229
+ ),
14230
+ missing.length ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-destructive mt-4", children: [
14231
+ L("missingCodes", "The following products were not added:"),
14232
+ " ",
14233
+ missing.join(", ")
14234
+ ] }) : null,
14235
+ notice ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive mt-2", children: notice }) : null,
14236
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end mt-6 pt-4 border-t border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
14237
+ "button",
14238
+ {
14239
+ type: "button",
14240
+ onClick: handleSubmit,
14241
+ disabled: submitting || resolvedCount === 0,
14242
+ className: "bg-primary text-primary-foreground px-6 py-2.5 rounded font-medium hover:bg-primary/90 transition disabled:opacity-50 disabled:cursor-not-allowed",
14243
+ children: submitting ? L("adding", "Adding\u2026") : L("addToCart", "Add to cart")
14244
+ }
14245
+ ) })
14246
+ ] })
14247
+ ] }) });
14248
+ }
14249
+ function QuickOrder(rawProps) {
14250
+ const props = useInfraProps(rawProps);
14251
+ return /* @__PURE__ */ jsxRuntime.jsx(QuickOrderInner, { ...props });
14252
+ }
13798
14253
  function OrderShipments(props) {
13799
14254
  const [activeShipment, setActiveShipment] = React6.useState(null);
13800
14255
  const shipments = props.order?.shipments || [];
@@ -18271,6 +18726,7 @@ exports.PropellerDepsProvider = PropellerDepsProvider;
18271
18726
  exports.PropellerProvider = PropellerProvider;
18272
18727
  exports.PurchaseAuthorizationConfigurator = PurchaseAuthorizationConfigurator_default;
18273
18728
  exports.PurchaseAuthorizationRequests = PurchaseAuthorizationRequests_default;
18729
+ exports.QuickOrder = QuickOrder;
18274
18730
  exports.QuoteActions = QuoteActions_default;
18275
18731
  exports.RegisterForm = RegisterForm_default;
18276
18732
  exports.SearchBar = SearchBar_default;
@@ -18301,6 +18757,7 @@ exports.usePropellerDeps = usePropellerDeps;
18301
18757
  exports.usePropellerScope = usePropellerScope;
18302
18758
  exports.usePurchaseAuthorizationConfigurator = usePurchaseAuthorizationConfigurator;
18303
18759
  exports.usePurchaseAuthorizationRequests = usePurchaseAuthorizationRequests;
18760
+ exports.useQuickOrder = useQuickOrder;
18304
18761
  exports.useRequiredPropellerDeps = useRequiredPropellerDeps;
18305
18762
  exports.useResolvedProps = useResolvedProps;
18306
18763
  exports.useServices = useServices;