@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.js CHANGED
@@ -868,6 +868,25 @@ function useCart(options) {
868
868
  }, [graphqlClient, language, configuration, user]);
869
869
  return { cart, cartId, loading, error, checkoutAllowed, resolveCart, addItem, updateItemQuantity, updateItemNotes, deleteItem, addActionCode, removeActionCode, requestAuthorization, processCart, getCrossupsells, getMinQuantity, getStep };
870
870
  }
871
+ function buildAddressInput(type, addr) {
872
+ return {
873
+ type,
874
+ firstName: addr.firstName || "",
875
+ lastName: addr.lastName || "",
876
+ street: addr.street || "",
877
+ postalCode: addr.postalCode || "",
878
+ city: addr.city || "",
879
+ ...addr.company && { company: addr.company },
880
+ ...addr.gender && { gender: addr.gender },
881
+ ...addr.middleName && { middleName: addr.middleName },
882
+ ...addr.number && { number: addr.number },
883
+ ...addr.numberExtension && { numberExtension: addr.numberExtension },
884
+ ...addr.country && { country: addr.country },
885
+ ...addr.email && { email: addr.email },
886
+ ...addr.mobile && { mobile: addr.mobile },
887
+ ...addr.phone && { phone: addr.phone }
888
+ };
889
+ }
871
890
  function useCheckout(options) {
872
891
  const { graphqlClient, user, companyId } = options;
873
892
  const language = options.language ?? "NL";
@@ -908,26 +927,9 @@ function useCheckout(options) {
908
927
  if (!hasInvoice) {
909
928
  const defaultInvoice = getUserDefaultAddress("invoice");
910
929
  if (defaultInvoice) {
911
- const input = {
912
- type: CartAddressType.INVOICE,
913
- firstName: defaultInvoice.firstName || "",
914
- lastName: defaultInvoice.lastName || "",
915
- street: defaultInvoice.street || "",
916
- postalCode: defaultInvoice.postalCode || "",
917
- city: defaultInvoice.city || "",
918
- company: defaultInvoice.company,
919
- gender: defaultInvoice.gender,
920
- middleName: defaultInvoice.middleName,
921
- number: defaultInvoice.number,
922
- numberExtension: defaultInvoice.numberExtension,
923
- country: defaultInvoice.country,
924
- email: defaultInvoice.email,
925
- mobile: defaultInvoice.mobile,
926
- phone: defaultInvoice.phone
927
- };
928
930
  updatedCart = await cartService.updateCartAddress({
929
931
  id: updatedCart.cartId,
930
- input,
932
+ input: buildAddressInput(CartAddressType.INVOICE, defaultInvoice),
931
933
  imageVariantFilters,
932
934
  imageSearchFilters,
933
935
  language
@@ -937,26 +939,9 @@ function useCheckout(options) {
937
939
  if (!hasDelivery) {
938
940
  const defaultDelivery = getUserDefaultAddress("delivery");
939
941
  if (defaultDelivery) {
940
- const input = {
941
- type: CartAddressType.DELIVERY,
942
- firstName: defaultDelivery.firstName || "",
943
- lastName: defaultDelivery.lastName || "",
944
- street: defaultDelivery.street || "",
945
- postalCode: defaultDelivery.postalCode || "",
946
- city: defaultDelivery.city || "",
947
- company: defaultDelivery.company,
948
- gender: defaultDelivery.gender,
949
- middleName: defaultDelivery.middleName,
950
- number: defaultDelivery.number,
951
- numberExtension: defaultDelivery.numberExtension,
952
- country: defaultDelivery.country,
953
- email: defaultDelivery.email,
954
- mobile: defaultDelivery.mobile,
955
- phone: defaultDelivery.phone
956
- };
957
942
  updatedCart = await cartService.updateCartAddress({
958
943
  id: updatedCart.cartId,
959
- input,
944
+ input: buildAddressInput(CartAddressType.DELIVERY, defaultDelivery),
960
945
  imageVariantFilters,
961
946
  imageSearchFilters,
962
947
  language
@@ -2478,6 +2463,140 @@ function useProductSearch(options) {
2478
2463
  goToPage: pagination.goToPage
2479
2464
  };
2480
2465
  }
2466
+ function toMatch(item, language) {
2467
+ const isCluster = "clusterId" in item;
2468
+ const displayItem = isCluster ? item.defaultProduct : item;
2469
+ const productId = displayItem?.productId ?? item.productId;
2470
+ const clusterId = isCluster ? item.clusterId : void 0;
2471
+ const name = language && item.names?.find((n) => n.language === language)?.value || item.names?.[0]?.value || "Product";
2472
+ const netPrice = displayItem?.price?.net ?? 0;
2473
+ const grossPrice = displayItem?.price?.gross ?? 0;
2474
+ const minQuantity = Math.max(1, displayItem?.minimumQuantity ?? 1);
2475
+ const imageUrl = isCluster ? getClusterImageUrl(item) : getProductImageUrl(item);
2476
+ return {
2477
+ productId,
2478
+ clusterId,
2479
+ name,
2480
+ sku: item.sku || displayItem?.sku || "",
2481
+ netPrice,
2482
+ grossPrice,
2483
+ minQuantity,
2484
+ imageUrl
2485
+ };
2486
+ }
2487
+ function useQuickOrder(options) {
2488
+ const {
2489
+ graphqlClient,
2490
+ user,
2491
+ companyId,
2492
+ configuration = {},
2493
+ onCartCreated,
2494
+ afterAddToCart
2495
+ } = options;
2496
+ const language = options.language || "NL";
2497
+ const searchLimit = options.searchLimit ?? 8;
2498
+ const [submitting, setSubmitting] = useState(false);
2499
+ const [error, setError] = useState(null);
2500
+ const searchProducts = useCallback(
2501
+ async (term) => {
2502
+ const trimmed = term.trim();
2503
+ if (!trimmed || !graphqlClient) return [];
2504
+ try {
2505
+ const service = createServices(graphqlClient).product;
2506
+ const input = {
2507
+ term: trimmed,
2508
+ language,
2509
+ page: 1,
2510
+ offset: searchLimit,
2511
+ statuses: [ProductStatus.A, ProductStatus.P, ProductStatus.T, ProductStatus.S],
2512
+ sortInputs: [{ field: ProductSortField.RELEVANCE, order: SortOrder.DESC }],
2513
+ searchFields: [
2514
+ {
2515
+ fieldNames: [
2516
+ ProductSearchableField.SKU,
2517
+ ProductSearchableField.NAME,
2518
+ ProductSearchableField.KEYWORDS,
2519
+ ProductSearchableField.CUSTOM_KEYWORDS
2520
+ ],
2521
+ boost: 5
2522
+ },
2523
+ {
2524
+ fieldNames: [
2525
+ ProductSearchableField.MANUFACTURER_CODE,
2526
+ ProductSearchableField.EAN_CODE,
2527
+ ProductSearchableField.BAR_CODE,
2528
+ ProductSearchableField.SUPPLIER_CODE,
2529
+ ProductSearchableField.PRODUCT_ID
2530
+ ],
2531
+ boost: 1
2532
+ }
2533
+ ]
2534
+ };
2535
+ const variables = {
2536
+ input,
2537
+ language,
2538
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2539
+ };
2540
+ const result = await service.getProducts(variables);
2541
+ const items = result?.items ?? [];
2542
+ return items.map((it) => toMatch(it, language));
2543
+ } catch {
2544
+ return [];
2545
+ }
2546
+ },
2547
+ [graphqlClient, language, searchLimit, configuration]
2548
+ );
2549
+ const submit = useCallback(
2550
+ async (lines) => {
2551
+ const valid = lines.filter((l) => l.productId && (l.quantity ?? 0) > 0);
2552
+ if (!valid.length) return { success: false, error: "No items to add" };
2553
+ if (!graphqlClient) return { success: false, error: "No GraphQL client" };
2554
+ setSubmitting(true);
2555
+ setError(null);
2556
+ try {
2557
+ const services = createServices(graphqlClient);
2558
+ const cart = await initCart({
2559
+ services,
2560
+ user: user ?? null,
2561
+ companyId,
2562
+ language,
2563
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2564
+ imageVariantFilters: configuration.imageVariantFiltersSmall,
2565
+ onCartCreated
2566
+ });
2567
+ const items = valid.map((l) => ({
2568
+ productId: l.productId,
2569
+ quantity: l.quantity,
2570
+ ...l.clusterId ? { clusterId: l.clusterId } : {}
2571
+ }));
2572
+ const bulk = await services.cart.bulkUpdateCartItems({
2573
+ input: { cartId: cart.cartId, items }
2574
+ });
2575
+ const updated = await services.cart.getCart({
2576
+ cartId: cart.cartId,
2577
+ language,
2578
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2579
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2580
+ });
2581
+ const finalCart = updated ?? cart;
2582
+ afterAddToCart?.(finalCart);
2583
+ return {
2584
+ success: true,
2585
+ cart: finalCart,
2586
+ added: (bulk?.created ?? 0) + (bulk?.updated ?? 0)
2587
+ };
2588
+ } catch (e) {
2589
+ const msg = e instanceof Error ? e.message : "Failed to add items to cart";
2590
+ setError(msg);
2591
+ return { success: false, error: msg };
2592
+ } finally {
2593
+ setSubmitting(false);
2594
+ }
2595
+ },
2596
+ [graphqlClient, user, companyId, language, configuration, onCartCreated, afterAddToCart]
2597
+ );
2598
+ return { submitting, error, searchProducts, submit };
2599
+ }
2481
2600
  var inflightFetches3 = /* @__PURE__ */ new Set();
2482
2601
  var STOREFRONT_STATUSES = [
2483
2602
  ProductStatus.A,
@@ -4911,6 +5030,7 @@ function AddToFavorite(rawProps) {
4911
5030
  ] }) }) : null });
4912
5031
  }
4913
5032
  var AddToFavorite_default = AddToFavorite;
5033
+ var EMAIL_PATTERN = "[^@\\s]+@[^@\\s.]+(?:\\.[^@\\s.]+)*\\.[A-Za-z]{2,}";
4914
5034
  var AddressCardContext = React6.createContext(null);
4915
5035
  function useAddressCardContext() {
4916
5036
  const ctx = React6.useContext(AddressCardContext);
@@ -5356,10 +5476,7 @@ function AddressCard(rawProps) {
5356
5476
  ] }),
5357
5477
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
5358
5478
  /* @__PURE__ */ jsxs("div", { children: [
5359
- /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium mb-1", children: [
5360
- getLabel(props.labels, "email", "Email"),
5361
- " *"
5362
- ] }),
5479
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-1", children: getLabel(props.labels, "email", "Email") }),
5363
5480
  /* @__PURE__ */ jsx(
5364
5481
  "input",
5365
5482
  {
@@ -5369,7 +5486,7 @@ function AddressCard(rawProps) {
5369
5486
  onChange: (e) => {
5370
5487
  setEditEmail(e.target.value);
5371
5488
  },
5372
- required: true
5489
+ pattern: EMAIL_PATTERN
5373
5490
  }
5374
5491
  )
5375
5492
  ] }),
@@ -5649,10 +5766,7 @@ function AddressCard(rawProps) {
5649
5766
  ] }),
5650
5767
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
5651
5768
  /* @__PURE__ */ jsxs("div", { children: [
5652
- /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium mb-1", children: [
5653
- getLabel(props.labels, "email", "Email"),
5654
- " *"
5655
- ] }),
5769
+ /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium mb-1", children: getLabel(props.labels, "email", "Email") }),
5656
5770
  /* @__PURE__ */ jsx(
5657
5771
  "input",
5658
5772
  {
@@ -5662,7 +5776,7 @@ function AddressCard(rawProps) {
5662
5776
  onChange: (e) => {
5663
5777
  setEditEmail(e.target.value);
5664
5778
  },
5665
- required: true
5779
+ pattern: EMAIL_PATTERN
5666
5780
  }
5667
5781
  )
5668
5782
  ] }),
@@ -6365,7 +6479,7 @@ function CartItem(rawProps) {
6365
6479
  }
6366
6480
  return "#";
6367
6481
  }
6368
- function getProductImageUrl5() {
6482
+ function getProductImageUrl6() {
6369
6483
  return props.cartItem.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url || "";
6370
6484
  }
6371
6485
  function getProductSku3() {
@@ -6549,7 +6663,7 @@ function CartItem(rawProps) {
6549
6663
  derived: {
6550
6664
  name: getProductName5(),
6551
6665
  sku: getProductSku3(),
6552
- imageUrl: getProductImageUrl5(),
6666
+ imageUrl: getProductImageUrl6(),
6553
6667
  productUrl: getProductUrl2(),
6554
6668
  isBundle: isBundleItem(),
6555
6669
  bundleName: getBundleName(),
@@ -9523,7 +9637,7 @@ function GridToolbar(rawProps) {
9523
9637
  const [currentSortOrder, setCurrentSortOrder] = useState(SortOrder.DESC);
9524
9638
  const [currentOffset, setCurrentOffset] = useState(12);
9525
9639
  const [currentViewMode, setCurrentViewMode] = useState("grid");
9526
- function getLabel54(key) {
9640
+ function getLabel55(key) {
9527
9641
  const labels = props.labels || {};
9528
9642
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS[key] || key;
9529
9643
  }
@@ -9602,7 +9716,7 @@ function GridToolbar(rawProps) {
9602
9716
  /* @__PURE__ */ jsx("div", { className: "propeller-grid-toolbar__count text-sm text-muted-foreground font-medium", children: props.itemsFound > 0 ? /* @__PURE__ */ jsxs("span", { children: [
9603
9717
  props.itemsFound,
9604
9718
  "\xA0",
9605
- props.itemsFound === 1 ? getLabel54("productSingular") : getLabel54("productPlural")
9719
+ props.itemsFound === 1 ? getLabel55("productSingular") : getLabel55("productPlural")
9606
9720
  ] }) : null }),
9607
9721
  /* @__PURE__ */ jsxs("div", { className: "propeller-grid-toolbar__controls flex flex-wrap items-center gap-3", children: [
9608
9722
  /* @__PURE__ */ jsx(
@@ -9613,7 +9727,7 @@ function GridToolbar(rawProps) {
9613
9727
  onChange: (e) => handleOffsetChange(parseInt(e.target.value)),
9614
9728
  children: getOffsetOptions()?.map((n) => /* @__PURE__ */ jsxs("option", { value: n, children: [
9615
9729
  n,
9616
- getLabel54("perPage")
9730
+ getLabel55("perPage")
9617
9731
  ] }, n))
9618
9732
  }
9619
9733
  ),
@@ -9629,7 +9743,7 @@ function GridToolbar(rawProps) {
9629
9743
  {
9630
9744
  value: field,
9631
9745
  disabled: field === "PRICE" && isPriceSortDisabled(),
9632
- children: getLabel54(field)
9746
+ children: getLabel55(field)
9633
9747
  },
9634
9748
  field
9635
9749
  ))
@@ -9642,8 +9756,8 @@ function GridToolbar(rawProps) {
9642
9756
  value: currentSortOrder,
9643
9757
  onChange: (e) => handleSortOrderChange(e.target.value),
9644
9758
  children: [
9645
- /* @__PURE__ */ jsx("option", { value: SortOrder.ASC, children: getLabel54("ASC") }),
9646
- /* @__PURE__ */ jsx("option", { value: SortOrder.DESC, children: getLabel54("DESC") })
9759
+ /* @__PURE__ */ jsx("option", { value: SortOrder.ASC, children: getLabel55("ASC") }),
9760
+ /* @__PURE__ */ jsx("option", { value: SortOrder.DESC, children: getLabel55("DESC") })
9647
9761
  ]
9648
9762
  }
9649
9763
  ),
@@ -9653,7 +9767,7 @@ function GridToolbar(rawProps) {
9653
9767
  type: "button",
9654
9768
  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",
9655
9769
  onClick: (event) => handleViewChange(),
9656
- title: currentViewMode === "grid" ? getLabel54("switchToList") : getLabel54("switchToGrid"),
9770
+ title: currentViewMode === "grid" ? getLabel55("switchToList") : getLabel55("switchToGrid"),
9657
9771
  children: [
9658
9772
  currentViewMode === "grid" ? /* @__PURE__ */ jsxs(
9659
9773
  "svg",
@@ -9711,7 +9825,7 @@ function GridToolbar(rawProps) {
9711
9825
  onClick: (event) => {
9712
9826
  if (props.onClearFilters) props.onClearFilters();
9713
9827
  },
9714
- children: getLabel54("clearAll")
9828
+ children: getLabel55("clearAll")
9715
9829
  }
9716
9830
  ),
9717
9831
  props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0 ? /* @__PURE__ */ jsxs(
@@ -9722,7 +9836,7 @@ function GridToolbar(rawProps) {
9722
9836
  if (props.onPriceFilterRemove) props.onPriceFilterRemove();
9723
9837
  },
9724
9838
  children: [
9725
- getLabel54("price"),
9839
+ getLabel55("price"),
9726
9840
  ": \u20AC",
9727
9841
  props.priceFilterMin ?? 0,
9728
9842
  " \u2013 \u20AC",
@@ -9759,7 +9873,7 @@ var DEFAULT_LABELS2 = {
9759
9873
  of: "of"
9760
9874
  };
9761
9875
  function GridPagination(props) {
9762
- function getLabel54(key) {
9876
+ function getLabel55(key) {
9763
9877
  const labels = props.labels || {};
9764
9878
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS2[key] || key;
9765
9879
  }
@@ -9832,15 +9946,15 @@ function GridPagination(props) {
9832
9946
  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",
9833
9947
  disabled: currentPage === 1,
9834
9948
  onClick: () => handlePageChange(currentPage - 1),
9835
- children: getLabel54("previous")
9949
+ children: getLabel55("previous")
9836
9950
  }
9837
9951
  ),
9838
9952
  /* @__PURE__ */ jsxs("span", { className: "propeller-grid-pagination__info px-2 text-sm font-medium text-muted-foreground", children: [
9839
- getLabel54("page"),
9953
+ getLabel55("page"),
9840
9954
  "\xA0",
9841
9955
  currentPage,
9842
9956
  "\xA0",
9843
- getLabel54("of"),
9957
+ getLabel55("of"),
9844
9958
  "\xA0",
9845
9959
  totalPages
9846
9960
  ] }),
@@ -9851,7 +9965,7 @@ function GridPagination(props) {
9851
9965
  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",
9852
9966
  disabled: currentPage === totalPages,
9853
9967
  onClick: () => handlePageChange(currentPage + 1),
9854
- children: getLabel54("next")
9968
+ children: getLabel55("next")
9855
9969
  }
9856
9970
  )
9857
9971
  ] }) : null,
@@ -9863,7 +9977,7 @@ function GridPagination(props) {
9863
9977
  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",
9864
9978
  disabled: currentPage === 1,
9865
9979
  onClick: () => handlePageChange(currentPage - 1),
9866
- children: getLabel54("previous")
9980
+ children: getLabel55("previous")
9867
9981
  }
9868
9982
  ),
9869
9983
  getFullPages().map((item, idx) => /* @__PURE__ */ jsx(
@@ -9890,7 +10004,7 @@ function GridPagination(props) {
9890
10004
  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",
9891
10005
  disabled: currentPage === totalPages,
9892
10006
  onClick: () => handlePageChange(currentPage + 1),
9893
- children: getLabel54("next")
10007
+ children: getLabel55("next")
9894
10008
  }
9895
10009
  )
9896
10010
  ] }) : null
@@ -10369,7 +10483,7 @@ function formatPrice9(price, currency) {
10369
10483
  function getProductName2(product) {
10370
10484
  return product.names?.[0]?.value || `Product ${product.productId}`;
10371
10485
  }
10372
- function getProductImageUrl3(product) {
10486
+ function getProductImageUrl4(product) {
10373
10487
  const media = product.media;
10374
10488
  if (media?.images?.items && Array.isArray(media.images.items) && media.images.items.length > 0) {
10375
10489
  const firstImage = media.images.items[0];
@@ -10398,7 +10512,7 @@ function ClusterOptions(props) {
10398
10512
  (p) => p.productId.toString() === selectedProductId
10399
10513
  );
10400
10514
  if (selectedProduct) {
10401
- previewImageUrl = getProductImageUrl3(selectedProduct);
10515
+ previewImageUrl = getProductImageUrl4(selectedProduct);
10402
10516
  previewName = getProductName2(selectedProduct);
10403
10517
  previewPrice = formatPrice9(selectedProduct.price?.gross || 0, props.currency ?? "\u20AC");
10404
10518
  }
@@ -13775,6 +13889,347 @@ function OrderList(rawProps) {
13775
13889
  ] });
13776
13890
  }
13777
13891
  var OrderList_default = OrderList;
13892
+ var ROW_SEQ = 0;
13893
+ function blankRow() {
13894
+ ROW_SEQ += 1;
13895
+ return {
13896
+ key: `qo-${ROW_SEQ}`,
13897
+ code: "",
13898
+ productId: null,
13899
+ name: "",
13900
+ netPrice: 0,
13901
+ quantity: 1,
13902
+ minQuantity: 1,
13903
+ matches: [],
13904
+ searching: false,
13905
+ searched: false
13906
+ };
13907
+ }
13908
+ function QuickOrderInner(props) {
13909
+ const {
13910
+ companyId,
13911
+ language = "NL",
13912
+ currency = "\u20AC",
13913
+ configuration,
13914
+ initialRows = 5,
13915
+ searchThreshold = 3,
13916
+ debounceMs = 300,
13917
+ parseSpreadsheet,
13918
+ templateUrl,
13919
+ maxUploadBytes = 2 * 1024 * 1024,
13920
+ maxUploadRows = 500,
13921
+ onCartCreated,
13922
+ afterAddToCart,
13923
+ onMissingCodes,
13924
+ className,
13925
+ labels
13926
+ } = props;
13927
+ const user = props.user ?? null;
13928
+ const graphqlClient = props.graphqlClient;
13929
+ const displayPrice = (n) => props.formatPrice ? props.formatPrice(n) : formatPrice(n, { symbol: currency });
13930
+ const L = (key, fallback) => getLabel(labels, key, fallback);
13931
+ const { submitting, searchProducts, submit } = useQuickOrder({
13932
+ graphqlClient,
13933
+ user,
13934
+ companyId,
13935
+ language,
13936
+ configuration,
13937
+ onCartCreated,
13938
+ afterAddToCart
13939
+ });
13940
+ const [rows, setRows] = useState(
13941
+ () => Array.from({ length: Math.max(1, initialRows) }, blankRow)
13942
+ );
13943
+ const [missing, setMissing] = useState([]);
13944
+ const [uploadError, setUploadError] = useState(null);
13945
+ const [uploading, setUploading] = useState(false);
13946
+ const [notice, setNotice] = useState(null);
13947
+ const fileInputRef = useRef(null);
13948
+ const searchTimers = useRef({});
13949
+ const patchRow = useCallback((key, patch) => {
13950
+ setRows((prev) => prev.map((r) => r.key === key ? { ...r, ...patch } : r));
13951
+ }, []);
13952
+ const onCodeInput = useCallback(
13953
+ (key, value) => {
13954
+ patchRow(key, { code: value, productId: null, name: "", netPrice: 0, searched: false });
13955
+ setNotice(null);
13956
+ const timers = searchTimers.current;
13957
+ if (timers[key]) clearTimeout(timers[key]);
13958
+ if (value.trim().length < searchThreshold) {
13959
+ patchRow(key, { matches: [], searching: false, searched: false });
13960
+ return;
13961
+ }
13962
+ patchRow(key, { searching: true });
13963
+ timers[key] = setTimeout(async () => {
13964
+ const results = await searchProducts(value);
13965
+ patchRow(key, { matches: results, searching: false, searched: true });
13966
+ }, debounceMs);
13967
+ },
13968
+ [patchRow, searchProducts, searchThreshold, debounceMs]
13969
+ );
13970
+ const selectMatch = useCallback(
13971
+ (key, match) => {
13972
+ const dup = rows.some((r) => r.key !== key && r.productId && r.code === match.sku);
13973
+ if (dup) {
13974
+ setNotice(L("alreadyInList", "Product is already in the list"));
13975
+ patchRow(key, { code: "", matches: [], productId: null, name: "", netPrice: 0, searched: false });
13976
+ return;
13977
+ }
13978
+ patchRow(key, {
13979
+ code: match.sku,
13980
+ productId: match.productId,
13981
+ clusterId: match.clusterId,
13982
+ name: match.name,
13983
+ netPrice: match.netPrice,
13984
+ quantity: match.minQuantity,
13985
+ minQuantity: match.minQuantity,
13986
+ matches: [],
13987
+ searching: false,
13988
+ searched: false
13989
+ });
13990
+ },
13991
+ [rows, patchRow, L]
13992
+ );
13993
+ const setQuantity = useCallback(
13994
+ (key, raw) => {
13995
+ const n = parseInt(raw, 10);
13996
+ setRows(
13997
+ (prev) => prev.map(
13998
+ (r) => r.key === key ? { ...r, quantity: Number.isFinite(n) && n > 0 ? Math.max(r.minQuantity, n) : r.minQuantity } : r
13999
+ )
14000
+ );
14001
+ },
14002
+ []
14003
+ );
14004
+ const addRow = useCallback(() => setRows((prev) => [...prev, blankRow()]), []);
14005
+ const removeRow = useCallback(
14006
+ (key) => setRows((prev) => prev.length > 1 ? prev.filter((r) => r.key !== key) : prev),
14007
+ []
14008
+ );
14009
+ const resolvedCount = useMemo(() => rows.filter((r) => r.productId).length, [rows]);
14010
+ const onFileChosen = useCallback(
14011
+ async (file) => {
14012
+ if (!file || !parseSpreadsheet) return;
14013
+ setUploadError(null);
14014
+ setMissing([]);
14015
+ if (file.size > maxUploadBytes) {
14016
+ setUploadError(`File too large (max ${Math.round(maxUploadBytes / 1024 / 1024)} MB)`);
14017
+ return;
14018
+ }
14019
+ setUploading(true);
14020
+ try {
14021
+ let lines = await parseSpreadsheet(file);
14022
+ 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);
14023
+ if (!lines.length) {
14024
+ setUploadError("No valid rows found in the file");
14025
+ return;
14026
+ }
14027
+ const resolved = [];
14028
+ const notFound = [];
14029
+ for (const line of lines) {
14030
+ const matches = await searchProducts(line.code);
14031
+ const exact = matches.find((m) => m.sku.toLowerCase() === line.code.toLowerCase()) || matches[0];
14032
+ if (!exact) {
14033
+ notFound.push(line.code);
14034
+ continue;
14035
+ }
14036
+ if (resolved.some((r) => r.productId === exact.productId)) continue;
14037
+ const row = blankRow();
14038
+ resolved.push({
14039
+ ...row,
14040
+ code: exact.sku,
14041
+ productId: exact.productId,
14042
+ clusterId: exact.clusterId,
14043
+ name: exact.name,
14044
+ netPrice: exact.netPrice,
14045
+ quantity: Math.max(exact.minQuantity, line.quantity),
14046
+ minQuantity: exact.minQuantity
14047
+ });
14048
+ }
14049
+ if (resolved.length) {
14050
+ setRows([...resolved, blankRow()]);
14051
+ }
14052
+ if (notFound.length) {
14053
+ setMissing(notFound);
14054
+ onMissingCodes?.(notFound);
14055
+ }
14056
+ } catch {
14057
+ setUploadError("Could not read the file");
14058
+ } finally {
14059
+ setUploading(false);
14060
+ if (fileInputRef.current) fileInputRef.current.value = "";
14061
+ }
14062
+ },
14063
+ [parseSpreadsheet, maxUploadBytes, maxUploadRows, searchProducts, onMissingCodes]
14064
+ );
14065
+ const handleSubmit = useCallback(async () => {
14066
+ const lines = rows.filter((r) => r.productId).map((r) => ({ productId: r.productId, quantity: r.quantity, clusterId: r.clusterId, code: r.code }));
14067
+ if (!lines.length) {
14068
+ setNotice(L("noItems", "Add at least one product before submitting"));
14069
+ return;
14070
+ }
14071
+ const res = await submit(lines);
14072
+ if (res.success) {
14073
+ setRows(Array.from({ length: Math.max(1, initialRows) }, blankRow));
14074
+ setMissing([]);
14075
+ setNotice(null);
14076
+ } else {
14077
+ setNotice(res.error ?? "Failed to add items to cart");
14078
+ }
14079
+ }, [rows, submit, initialRows, L]);
14080
+ return /* @__PURE__ */ jsx("div", { className: className ?? "propeller-quick-order", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col lg:flex-row gap-8", children: [
14081
+ parseSpreadsheet ? /* @__PURE__ */ jsxs("div", { className: "w-full lg:w-1/3", children: [
14082
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("uploadTitle", "Upload Excel file") }),
14083
+ templateUrl ? /* @__PURE__ */ jsx(
14084
+ "a",
14085
+ {
14086
+ href: templateUrl,
14087
+ target: "_blank",
14088
+ rel: "noopener nofollow",
14089
+ className: "text-primary hover:underline text-sm inline-block mb-4",
14090
+ children: L("downloadTemplate", "Download XLSX template")
14091
+ }
14092
+ ) : null,
14093
+ /* @__PURE__ */ 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.") }),
14094
+ /* @__PURE__ */ jsx(
14095
+ "input",
14096
+ {
14097
+ ref: fileInputRef,
14098
+ type: "file",
14099
+ accept: ".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel",
14100
+ 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",
14101
+ onChange: (e) => onFileChosen(e.target.files?.[0]),
14102
+ disabled: uploading
14103
+ }
14104
+ ),
14105
+ uploading ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: L("upload", "Uploading\u2026") }) : null,
14106
+ uploadError ? /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mt-2", children: uploadError }) : null
14107
+ ] }) : null,
14108
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
14109
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("title", "Add your products manually") }),
14110
+ /* @__PURE__ */ 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: [
14111
+ /* @__PURE__ */ jsx("div", { className: "col-span-3", children: L("colCode", "Article no. / SKU") }),
14112
+ /* @__PURE__ */ jsx("div", { className: "col-span-3", children: L("colName", "Product name") }),
14113
+ /* @__PURE__ */ jsx("div", { className: "col-span-2", children: L("colPrice", "excl. VAT") }),
14114
+ /* @__PURE__ */ jsx("div", { className: "col-span-1", children: L("colQuantity", "Qty") }),
14115
+ /* @__PURE__ */ jsx("div", { className: "col-span-2 text-right", children: L("colTotal", "Total") }),
14116
+ /* @__PURE__ */ jsx("div", { className: "col-span-1" })
14117
+ ] }),
14118
+ /* @__PURE__ */ jsx("div", { className: "divide-y divide-border", children: rows.map((r) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-12 gap-2 items-center py-2 relative", children: [
14119
+ /* @__PURE__ */ jsxs("div", { className: "col-span-12 md:col-span-3 relative", children: [
14120
+ /* @__PURE__ */ jsx(
14121
+ "input",
14122
+ {
14123
+ type: "text",
14124
+ value: r.code,
14125
+ onChange: (e) => onCodeInput(r.key, e.target.value),
14126
+ readOnly: !!r.productId,
14127
+ placeholder: L("colCode", "Article no. / SKU"),
14128
+ 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"
14129
+ }
14130
+ ),
14131
+ (r.searching || r.matches.length > 0 || r.searched) && !r.productId ? /* @__PURE__ */ 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__ */ jsx("li", { className: "px-3 py-2 text-sm text-muted-foreground", children: "\u2026" }) : r.matches.length ? r.matches.map((m) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
14132
+ "button",
14133
+ {
14134
+ type: "button",
14135
+ onClick: () => selectMatch(r.key, m),
14136
+ className: "flex items-center gap-2 w-full text-left px-3 py-2 hover:bg-muted text-sm",
14137
+ children: [
14138
+ m.imageUrl ? (
14139
+ // eslint-disable-next-line @next/next/no-img-element
14140
+ /* @__PURE__ */ jsx("img", { src: m.imageUrl, alt: "", width: 32, height: 32, className: "rounded object-cover" })
14141
+ ) : null,
14142
+ /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
14143
+ /* @__PURE__ */ jsx("span", { className: "block text-foreground", children: m.name }),
14144
+ m.sku ? /* @__PURE__ */ jsxs("span", { className: "block text-xs text-muted-foreground", children: [
14145
+ "SKU: ",
14146
+ m.sku
14147
+ ] }) : null
14148
+ ] })
14149
+ ]
14150
+ }
14151
+ ) }, `${r.key}-${m.productId}`)) : /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-muted-foreground", children: L("noResults", "No results found") }) }) : null
14152
+ ] }),
14153
+ /* @__PURE__ */ jsx("div", { className: "col-span-6 md:col-span-3", children: /* @__PURE__ */ jsx(
14154
+ "input",
14155
+ {
14156
+ type: "text",
14157
+ value: r.name,
14158
+ disabled: true,
14159
+ className: "w-full rounded border border-input bg-muted/40 px-2 py-1.5 text-sm text-muted-foreground"
14160
+ }
14161
+ ) }),
14162
+ /* @__PURE__ */ jsx("div", { className: "col-span-3 md:col-span-2", children: /* @__PURE__ */ jsx(
14163
+ "input",
14164
+ {
14165
+ type: "text",
14166
+ value: r.productId ? displayPrice(r.netPrice) : "",
14167
+ disabled: true,
14168
+ className: "w-full rounded border border-input bg-muted/40 px-2 py-1.5 text-sm text-muted-foreground"
14169
+ }
14170
+ ) }),
14171
+ /* @__PURE__ */ jsx("div", { className: "col-span-3 md:col-span-1", children: /* @__PURE__ */ jsx(
14172
+ "input",
14173
+ {
14174
+ type: "number",
14175
+ min: r.minQuantity,
14176
+ step: 1,
14177
+ value: r.productId ? r.quantity : "",
14178
+ disabled: !r.productId,
14179
+ onChange: (e) => setQuantity(r.key, e.target.value),
14180
+ onKeyDown: (e) => {
14181
+ if (["e", "E", "+", "-", "."].includes(e.key)) e.preventDefault();
14182
+ },
14183
+ 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"
14184
+ }
14185
+ ) }),
14186
+ /* @__PURE__ */ 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) : "" }),
14187
+ /* @__PURE__ */ jsx("div", { className: "col-span-3 md:col-span-1 flex justify-end", children: /* @__PURE__ */ jsx(
14188
+ "button",
14189
+ {
14190
+ type: "button",
14191
+ onClick: () => removeRow(r.key),
14192
+ "aria-label": L("remove", "Remove"),
14193
+ className: "text-muted-foreground hover:text-destructive p-1",
14194
+ children: "\u2715"
14195
+ }
14196
+ ) })
14197
+ ] }, r.key)) }),
14198
+ /* @__PURE__ */ jsxs(
14199
+ "button",
14200
+ {
14201
+ type: "button",
14202
+ onClick: addRow,
14203
+ className: "mt-3 text-primary hover:underline text-sm font-medium",
14204
+ children: [
14205
+ "+ ",
14206
+ L("addRow", "Add more rows")
14207
+ ]
14208
+ }
14209
+ ),
14210
+ missing.length ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-4", children: [
14211
+ L("missingCodes", "The following products were not added:"),
14212
+ " ",
14213
+ missing.join(", ")
14214
+ ] }) : null,
14215
+ notice ? /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mt-2", children: notice }) : null,
14216
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end mt-6 pt-4 border-t border-border", children: /* @__PURE__ */ jsx(
14217
+ "button",
14218
+ {
14219
+ type: "button",
14220
+ onClick: handleSubmit,
14221
+ disabled: submitting || resolvedCount === 0,
14222
+ 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",
14223
+ children: submitting ? L("adding", "Adding\u2026") : L("addToCart", "Add to cart")
14224
+ }
14225
+ ) })
14226
+ ] })
14227
+ ] }) });
14228
+ }
14229
+ function QuickOrder(rawProps) {
14230
+ const props = useInfraProps(rawProps);
14231
+ return /* @__PURE__ */ jsx(QuickOrderInner, { ...props });
14232
+ }
13778
14233
  function OrderShipments(props) {
13779
14234
  const [activeShipment, setActiveShipment] = useState(null);
13780
14235
  const shipments = props.order?.shipments || [];
@@ -18178,6 +18633,6 @@ function UserDetails(rawProps) {
18178
18633
  }
18179
18634
  var UserDetails_default = UserDetails;
18180
18635
 
18181
- export { AccountIconAndMenu_default as AccountIconAndMenu, ActionCode_default as ActionCode, AddToCart_default as AddToCart, AddToFavorite_default as AddToFavorite, AddressCard_default as AddressCard, AddressSelector_default as AddressSelector, BreadcrumbsWithProvider_default as Breadcrumbs, CartBonusItems_default as CartBonusItems, CartCarriers_default as CartCarriers, CartIconAndSidebar_default as CartIconAndSidebar, CartItem_default as CartItem, CartOverview_default as CartOverview, CartPaymethods_default as CartPaymethods, CartSummary_default as CartSummary, CategoryDescription_default as CategoryDescription, CategoryShortDescriptionWithProvider_default as CategoryShortDescription, ClusterCard_default as ClusterCard, ClusterConfigurator_default as ClusterConfigurator, ClusterInfo_default as ClusterInfo, ClusterOptions_default as ClusterOptions, CompanySwitcher_default as CompanySwitcher, AddToCart_default as DefaultAddToCart, AddToFavorite_default as DefaultAddToFavorite, ItemStock_default as DefaultItemStock, DefaultProductBadges, ProductBulkPrices_default as DefaultProductBulkPrices, ProductBundles_default as DefaultProductBundles, DefaultProductImage, ProductPrice_default as DefaultProductPrice, DefaultProductSurcharges, DeliveryDate_default as DeliveryDate, FavoriteListDetails_default as FavoriteListDetails, FavoriteListItem_default as FavoriteListItem, FavoriteLists_default as FavoriteLists, ForgotPassword_default as ForgotPassword, GridFilters_default as GridFilters, GridFiltersPanel, GridPagination_default as GridPagination, GridTitleWithProvider_default as GridTitle, GridToolbar_default as GridToolbar, ItemStock_default as ItemStock, ItemsOverview_default as ItemsOverview, LoginForm_default as LoginForm, MachineCard, MachineGrid, Menu_default as Menu, OrderActions_default as OrderActions, OrderBonusItems_default as OrderBonusItems, OrderItemCard_default as OrderItemCard, OrderList_default as OrderList, OrderShipments_default as OrderShipments, OrderSummary_default as OrderSummary, OrderTotals_default as OrderTotals, PriceToggle_default as PriceToggle, ProductBulkPricesWithProvider_default as ProductBulkPrices, ProductBundles_default as ProductBundles, ProductCard_default as ProductCard, ProductDescription_default as ProductDescription, ProductDownloads_default as ProductDownloads, ProductGallery_default as ProductGallery, ProductGrid_default as ProductGrid, ProductGridConfigProvider, ProductInfo_default as ProductInfo, ProductPriceWithProvider_default as ProductPrice, ProductShortDescriptionWithProvider_default as ProductShortDescription, ProductSlider_default as ProductSlider, ProductSpecifications_default as ProductSpecifications, ProductTabs_default as ProductTabs, ProductVideos_default as ProductVideos, PropellerDepsProvider, PropellerProvider, PurchaseAuthorizationConfigurator_default as PurchaseAuthorizationConfigurator, PurchaseAuthorizationRequests_default as PurchaseAuthorizationRequests, QuoteActions_default as QuoteActions, RegisterForm_default as RegisterForm, SearchBar_default as SearchBar, UserDetails_default as UserDetails, buildRootMachinesQuery, fetchActiveCart, initCart, mergeAnonymousCart, useAddress, useAuth, useCart, useCheckout, useClusterConfigurator, useCompany, useFavorites, useInfraProps, useMachines, useMenu, useOrders, useProductBundles, useProductGridConfig, useProductInfo, useProductSearch, useProductSlider, useProductSpecs, usePropellerContext, usePropellerDeps, usePropellerScope, usePurchaseAuthorizationConfigurator, usePurchaseAuthorizationRequests, useRequiredPropellerDeps, useResolvedProps, useServices, useSpareParts, useUserMode };
18636
+ export { AccountIconAndMenu_default as AccountIconAndMenu, ActionCode_default as ActionCode, AddToCart_default as AddToCart, AddToFavorite_default as AddToFavorite, AddressCard_default as AddressCard, AddressSelector_default as AddressSelector, BreadcrumbsWithProvider_default as Breadcrumbs, CartBonusItems_default as CartBonusItems, CartCarriers_default as CartCarriers, CartIconAndSidebar_default as CartIconAndSidebar, CartItem_default as CartItem, CartOverview_default as CartOverview, CartPaymethods_default as CartPaymethods, CartSummary_default as CartSummary, CategoryDescription_default as CategoryDescription, CategoryShortDescriptionWithProvider_default as CategoryShortDescription, ClusterCard_default as ClusterCard, ClusterConfigurator_default as ClusterConfigurator, ClusterInfo_default as ClusterInfo, ClusterOptions_default as ClusterOptions, CompanySwitcher_default as CompanySwitcher, AddToCart_default as DefaultAddToCart, AddToFavorite_default as DefaultAddToFavorite, ItemStock_default as DefaultItemStock, DefaultProductBadges, ProductBulkPrices_default as DefaultProductBulkPrices, ProductBundles_default as DefaultProductBundles, DefaultProductImage, ProductPrice_default as DefaultProductPrice, DefaultProductSurcharges, DeliveryDate_default as DeliveryDate, FavoriteListDetails_default as FavoriteListDetails, FavoriteListItem_default as FavoriteListItem, FavoriteLists_default as FavoriteLists, ForgotPassword_default as ForgotPassword, GridFilters_default as GridFilters, GridFiltersPanel, GridPagination_default as GridPagination, GridTitleWithProvider_default as GridTitle, GridToolbar_default as GridToolbar, ItemStock_default as ItemStock, ItemsOverview_default as ItemsOverview, LoginForm_default as LoginForm, MachineCard, MachineGrid, Menu_default as Menu, OrderActions_default as OrderActions, OrderBonusItems_default as OrderBonusItems, OrderItemCard_default as OrderItemCard, OrderList_default as OrderList, OrderShipments_default as OrderShipments, OrderSummary_default as OrderSummary, OrderTotals_default as OrderTotals, PriceToggle_default as PriceToggle, ProductBulkPricesWithProvider_default as ProductBulkPrices, ProductBundles_default as ProductBundles, ProductCard_default as ProductCard, ProductDescription_default as ProductDescription, ProductDownloads_default as ProductDownloads, ProductGallery_default as ProductGallery, ProductGrid_default as ProductGrid, ProductGridConfigProvider, ProductInfo_default as ProductInfo, ProductPriceWithProvider_default as ProductPrice, ProductShortDescriptionWithProvider_default as ProductShortDescription, ProductSlider_default as ProductSlider, ProductSpecifications_default as ProductSpecifications, ProductTabs_default as ProductTabs, ProductVideos_default as ProductVideos, PropellerDepsProvider, PropellerProvider, PurchaseAuthorizationConfigurator_default as PurchaseAuthorizationConfigurator, PurchaseAuthorizationRequests_default as PurchaseAuthorizationRequests, QuickOrder, QuoteActions_default as QuoteActions, RegisterForm_default as RegisterForm, SearchBar_default as SearchBar, UserDetails_default as UserDetails, buildRootMachinesQuery, fetchActiveCart, initCart, mergeAnonymousCart, useAddress, useAuth, useCart, useCheckout, useClusterConfigurator, useCompany, useFavorites, useInfraProps, useMachines, useMenu, useOrders, useProductBundles, useProductGridConfig, useProductInfo, useProductSearch, useProductSlider, useProductSpecs, usePropellerContext, usePropellerDeps, usePropellerScope, usePurchaseAuthorizationConfigurator, usePurchaseAuthorizationRequests, useQuickOrder, useRequiredPropellerDeps, useResolvedProps, useServices, useSpareParts, useUserMode };
18182
18637
  //# sourceMappingURL=index.js.map
18183
18638
  //# sourceMappingURL=index.js.map