@propeller-commerce/propeller-v2-react-ui 0.6.0 → 0.7.0

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
@@ -248,6 +248,7 @@ function useAddress(options) {
248
248
  );
249
249
  return { loading, error, createAddress, updateAddress, deleteAddress, setDefaultAddress };
250
250
  }
251
+ var isViewerSearchInput = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
251
252
  function useAuth(options) {
252
253
  const { graphqlClient, language = "NL", onAuthHeaderUpdate, configuration } = options;
253
254
  const [loading, setLoading] = React6.useState(false);
@@ -284,8 +285,14 @@ function useAuth(options) {
284
285
  ...configuration?.companyTrackAttributes?.length && {
285
286
  companyAttributesInput: { attributeDescription: { names: configuration.companyTrackAttributes } }
286
287
  },
287
- ...configuration?.contactPAConfigInput && {
288
+ // Contact-scoped pagination inputs — only when the host supplies a real
289
+ // input object (an empty array `[]` is truthy but 400s the viewer). See
290
+ // isViewerSearchInput.
291
+ ...isViewerSearchInput(configuration?.contactPAConfigInput) && {
288
292
  contactPAConfigInput: configuration.contactPAConfigInput
293
+ },
294
+ ...isViewerSearchInput(configuration?.contactCompaniesSearchInput) && {
295
+ contactCompaniesSearchInput: configuration.contactCompaniesSearchInput
289
296
  }
290
297
  };
291
298
  const viewer = await userService.getViewer(viewerInput);
@@ -2491,6 +2498,140 @@ function useProductSearch(options) {
2491
2498
  goToPage: pagination.goToPage
2492
2499
  };
2493
2500
  }
2501
+ function toMatch(item, language) {
2502
+ const isCluster = "clusterId" in item;
2503
+ const displayItem = isCluster ? item.defaultProduct : item;
2504
+ const productId = displayItem?.productId ?? item.productId;
2505
+ const clusterId = isCluster ? item.clusterId : void 0;
2506
+ const name = language && item.names?.find((n) => n.language === language)?.value || item.names?.[0]?.value || "Product";
2507
+ const netPrice = displayItem?.price?.net ?? 0;
2508
+ const grossPrice = displayItem?.price?.gross ?? 0;
2509
+ const minQuantity = Math.max(1, displayItem?.minimumQuantity ?? 1);
2510
+ const imageUrl = isCluster ? propellerV2CoreUi.getClusterImageUrl(item) : propellerV2CoreUi.getProductImageUrl(item);
2511
+ return {
2512
+ productId,
2513
+ clusterId,
2514
+ name,
2515
+ sku: item.sku || displayItem?.sku || "",
2516
+ netPrice,
2517
+ grossPrice,
2518
+ minQuantity,
2519
+ imageUrl
2520
+ };
2521
+ }
2522
+ function useQuickOrder(options) {
2523
+ const {
2524
+ graphqlClient,
2525
+ user,
2526
+ companyId,
2527
+ configuration = {},
2528
+ onCartCreated,
2529
+ afterAddToCart
2530
+ } = options;
2531
+ const language = options.language || "NL";
2532
+ const searchLimit = options.searchLimit ?? 8;
2533
+ const [submitting, setSubmitting] = React6.useState(false);
2534
+ const [error, setError] = React6.useState(null);
2535
+ const searchProducts = React6.useCallback(
2536
+ async (term) => {
2537
+ const trimmed = term.trim();
2538
+ if (!trimmed || !graphqlClient) return [];
2539
+ try {
2540
+ const service = propellerV2CoreUi.createServices(graphqlClient).product;
2541
+ const input = {
2542
+ term: trimmed,
2543
+ language,
2544
+ page: 1,
2545
+ offset: searchLimit,
2546
+ statuses: [propellerSdkV2.ProductStatus.A, propellerSdkV2.ProductStatus.P, propellerSdkV2.ProductStatus.T, propellerSdkV2.ProductStatus.S],
2547
+ sortInputs: [{ field: propellerSdkV2.ProductSortField.RELEVANCE, order: propellerSdkV2.SortOrder.DESC }],
2548
+ searchFields: [
2549
+ {
2550
+ fieldNames: [
2551
+ propellerSdkV2.ProductSearchableField.SKU,
2552
+ propellerSdkV2.ProductSearchableField.NAME,
2553
+ propellerSdkV2.ProductSearchableField.KEYWORDS,
2554
+ propellerSdkV2.ProductSearchableField.CUSTOM_KEYWORDS
2555
+ ],
2556
+ boost: 5
2557
+ },
2558
+ {
2559
+ fieldNames: [
2560
+ propellerSdkV2.ProductSearchableField.MANUFACTURER_CODE,
2561
+ propellerSdkV2.ProductSearchableField.EAN_CODE,
2562
+ propellerSdkV2.ProductSearchableField.BAR_CODE,
2563
+ propellerSdkV2.ProductSearchableField.SUPPLIER_CODE,
2564
+ propellerSdkV2.ProductSearchableField.PRODUCT_ID
2565
+ ],
2566
+ boost: 1
2567
+ }
2568
+ ]
2569
+ };
2570
+ const variables = {
2571
+ input,
2572
+ language,
2573
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2574
+ };
2575
+ const result = await service.getProducts(variables);
2576
+ const items = result?.items ?? [];
2577
+ return items.map((it) => toMatch(it, language));
2578
+ } catch {
2579
+ return [];
2580
+ }
2581
+ },
2582
+ [graphqlClient, language, searchLimit, configuration]
2583
+ );
2584
+ const submit = React6.useCallback(
2585
+ async (lines) => {
2586
+ const valid = lines.filter((l) => l.productId && (l.quantity ?? 0) > 0);
2587
+ if (!valid.length) return { success: false, error: "No items to add" };
2588
+ if (!graphqlClient) return { success: false, error: "No GraphQL client" };
2589
+ setSubmitting(true);
2590
+ setError(null);
2591
+ try {
2592
+ const services = propellerV2CoreUi.createServices(graphqlClient);
2593
+ const cart = await initCart({
2594
+ services,
2595
+ user: user ?? null,
2596
+ companyId,
2597
+ language,
2598
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2599
+ imageVariantFilters: configuration.imageVariantFiltersSmall,
2600
+ onCartCreated
2601
+ });
2602
+ const items = valid.map((l) => ({
2603
+ productId: l.productId,
2604
+ quantity: l.quantity,
2605
+ ...l.clusterId ? { clusterId: l.clusterId } : {}
2606
+ }));
2607
+ const bulk = await services.cart.bulkUpdateCartItems({
2608
+ input: { cartId: cart.cartId, items }
2609
+ });
2610
+ const updated = await services.cart.getCart({
2611
+ cartId: cart.cartId,
2612
+ language,
2613
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2614
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2615
+ });
2616
+ const finalCart = updated ?? cart;
2617
+ afterAddToCart?.(finalCart);
2618
+ return {
2619
+ success: true,
2620
+ cart: finalCart,
2621
+ added: (bulk?.created ?? 0) + (bulk?.updated ?? 0)
2622
+ };
2623
+ } catch (e) {
2624
+ const msg = e instanceof Error ? e.message : "Failed to add items to cart";
2625
+ setError(msg);
2626
+ return { success: false, error: msg };
2627
+ } finally {
2628
+ setSubmitting(false);
2629
+ }
2630
+ },
2631
+ [graphqlClient, user, companyId, language, configuration, onCartCreated, afterAddToCart]
2632
+ );
2633
+ return { submitting, error, searchProducts, submit };
2634
+ }
2494
2635
  var inflightFetches3 = /* @__PURE__ */ new Set();
2495
2636
  var STOREFRONT_STATUSES = [
2496
2637
  propellerSdkV2.ProductStatus.A,
@@ -3466,7 +3607,13 @@ function LoginForm(rawProps) {
3466
3607
  const [email, setEmail] = React6.useState("");
3467
3608
  const [password, setPassword] = React6.useState("");
3468
3609
  const { loading, error: authError, login } = useAuth({
3469
- graphqlClient: props.graphqlClient
3610
+ graphqlClient: props.graphqlClient,
3611
+ // Resolved from <PropellerDepsProvider> by useInfraProps (the app's full
3612
+ // `config`), so the login viewer requests the configured track attributes +
3613
+ // pagination inputs (companyTrackAttributes → MY_INSTALLATIONS,
3614
+ // contactCompaniesSearchInput, contactPAConfigInput). Without this the header
3615
+ // login returns a viewer with no companies/attributes until a later refresh.
3616
+ configuration: props.configuration
3470
3617
  });
3471
3618
  propellerV2CoreUi.getLabel(props.labels, "email", "Email");
3472
3619
  propellerV2CoreUi.getLabel(props.labels, "password", "Password");
@@ -6372,7 +6519,7 @@ function CartItem(rawProps) {
6372
6519
  }
6373
6520
  return "#";
6374
6521
  }
6375
- function getProductImageUrl5() {
6522
+ function getProductImageUrl6() {
6376
6523
  return props.cartItem.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url || "";
6377
6524
  }
6378
6525
  function getProductSku3() {
@@ -6556,7 +6703,7 @@ function CartItem(rawProps) {
6556
6703
  derived: {
6557
6704
  name: getProductName5(),
6558
6705
  sku: getProductSku3(),
6559
- imageUrl: getProductImageUrl5(),
6706
+ imageUrl: getProductImageUrl6(),
6560
6707
  productUrl: getProductUrl2(),
6561
6708
  isBundle: isBundleItem(),
6562
6709
  bundleName: getBundleName(),
@@ -9530,7 +9677,7 @@ function GridToolbar(rawProps) {
9530
9677
  const [currentSortOrder, setCurrentSortOrder] = React6.useState(propellerSdkV2.SortOrder.DESC);
9531
9678
  const [currentOffset, setCurrentOffset] = React6.useState(12);
9532
9679
  const [currentViewMode, setCurrentViewMode] = React6.useState("grid");
9533
- function getLabel54(key) {
9680
+ function getLabel55(key) {
9534
9681
  const labels = props.labels || {};
9535
9682
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS[key] || key;
9536
9683
  }
@@ -9609,7 +9756,7 @@ function GridToolbar(rawProps) {
9609
9756
  /* @__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: [
9610
9757
  props.itemsFound,
9611
9758
  "\xA0",
9612
- props.itemsFound === 1 ? getLabel54("productSingular") : getLabel54("productPlural")
9759
+ props.itemsFound === 1 ? getLabel55("productSingular") : getLabel55("productPlural")
9613
9760
  ] }) : null }),
9614
9761
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-toolbar__controls flex flex-wrap items-center gap-3", children: [
9615
9762
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -9620,7 +9767,7 @@ function GridToolbar(rawProps) {
9620
9767
  onChange: (e) => handleOffsetChange(parseInt(e.target.value)),
9621
9768
  children: getOffsetOptions()?.map((n) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: n, children: [
9622
9769
  n,
9623
- getLabel54("perPage")
9770
+ getLabel55("perPage")
9624
9771
  ] }, n))
9625
9772
  }
9626
9773
  ),
@@ -9636,7 +9783,7 @@ function GridToolbar(rawProps) {
9636
9783
  {
9637
9784
  value: field,
9638
9785
  disabled: field === "PRICE" && isPriceSortDisabled(),
9639
- children: getLabel54(field)
9786
+ children: getLabel55(field)
9640
9787
  },
9641
9788
  field
9642
9789
  ))
@@ -9649,8 +9796,8 @@ function GridToolbar(rawProps) {
9649
9796
  value: currentSortOrder,
9650
9797
  onChange: (e) => handleSortOrderChange(e.target.value),
9651
9798
  children: [
9652
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel54("ASC") }),
9653
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel54("DESC") })
9799
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel55("ASC") }),
9800
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel55("DESC") })
9654
9801
  ]
9655
9802
  }
9656
9803
  ),
@@ -9660,7 +9807,7 @@ function GridToolbar(rawProps) {
9660
9807
  type: "button",
9661
9808
  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",
9662
9809
  onClick: (event) => handleViewChange(),
9663
- title: currentViewMode === "grid" ? getLabel54("switchToList") : getLabel54("switchToGrid"),
9810
+ title: currentViewMode === "grid" ? getLabel55("switchToList") : getLabel55("switchToGrid"),
9664
9811
  children: [
9665
9812
  currentViewMode === "grid" ? /* @__PURE__ */ jsxRuntime.jsxs(
9666
9813
  "svg",
@@ -9718,7 +9865,7 @@ function GridToolbar(rawProps) {
9718
9865
  onClick: (event) => {
9719
9866
  if (props.onClearFilters) props.onClearFilters();
9720
9867
  },
9721
- children: getLabel54("clearAll")
9868
+ children: getLabel55("clearAll")
9722
9869
  }
9723
9870
  ),
9724
9871
  props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
@@ -9729,7 +9876,7 @@ function GridToolbar(rawProps) {
9729
9876
  if (props.onPriceFilterRemove) props.onPriceFilterRemove();
9730
9877
  },
9731
9878
  children: [
9732
- getLabel54("price"),
9879
+ getLabel55("price"),
9733
9880
  ": \u20AC",
9734
9881
  props.priceFilterMin ?? 0,
9735
9882
  " \u2013 \u20AC",
@@ -9766,7 +9913,7 @@ var DEFAULT_LABELS2 = {
9766
9913
  of: "of"
9767
9914
  };
9768
9915
  function GridPagination(props) {
9769
- function getLabel54(key) {
9916
+ function getLabel55(key) {
9770
9917
  const labels = props.labels || {};
9771
9918
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS2[key] || key;
9772
9919
  }
@@ -9839,15 +9986,15 @@ function GridPagination(props) {
9839
9986
  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",
9840
9987
  disabled: currentPage === 1,
9841
9988
  onClick: () => handlePageChange(currentPage - 1),
9842
- children: getLabel54("previous")
9989
+ children: getLabel55("previous")
9843
9990
  }
9844
9991
  ),
9845
9992
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "propeller-grid-pagination__info px-2 text-sm font-medium text-muted-foreground", children: [
9846
- getLabel54("page"),
9993
+ getLabel55("page"),
9847
9994
  "\xA0",
9848
9995
  currentPage,
9849
9996
  "\xA0",
9850
- getLabel54("of"),
9997
+ getLabel55("of"),
9851
9998
  "\xA0",
9852
9999
  totalPages
9853
10000
  ] }),
@@ -9858,7 +10005,7 @@ function GridPagination(props) {
9858
10005
  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",
9859
10006
  disabled: currentPage === totalPages,
9860
10007
  onClick: () => handlePageChange(currentPage + 1),
9861
- children: getLabel54("next")
10008
+ children: getLabel55("next")
9862
10009
  }
9863
10010
  )
9864
10011
  ] }) : null,
@@ -9870,7 +10017,7 @@ function GridPagination(props) {
9870
10017
  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",
9871
10018
  disabled: currentPage === 1,
9872
10019
  onClick: () => handlePageChange(currentPage - 1),
9873
- children: getLabel54("previous")
10020
+ children: getLabel55("previous")
9874
10021
  }
9875
10022
  ),
9876
10023
  getFullPages().map((item, idx) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -9897,7 +10044,7 @@ function GridPagination(props) {
9897
10044
  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",
9898
10045
  disabled: currentPage === totalPages,
9899
10046
  onClick: () => handlePageChange(currentPage + 1),
9900
- children: getLabel54("next")
10047
+ children: getLabel55("next")
9901
10048
  }
9902
10049
  )
9903
10050
  ] }) : null
@@ -10376,7 +10523,7 @@ function formatPrice9(price, currency) {
10376
10523
  function getProductName2(product) {
10377
10524
  return product.names?.[0]?.value || `Product ${product.productId}`;
10378
10525
  }
10379
- function getProductImageUrl3(product) {
10526
+ function getProductImageUrl4(product) {
10380
10527
  const media = product.media;
10381
10528
  if (media?.images?.items && Array.isArray(media.images.items) && media.images.items.length > 0) {
10382
10529
  const firstImage = media.images.items[0];
@@ -10405,7 +10552,7 @@ function ClusterOptions(props) {
10405
10552
  (p) => p.productId.toString() === selectedProductId
10406
10553
  );
10407
10554
  if (selectedProduct) {
10408
- previewImageUrl = getProductImageUrl3(selectedProduct);
10555
+ previewImageUrl = getProductImageUrl4(selectedProduct);
10409
10556
  previewName = getProductName2(selectedProduct);
10410
10557
  previewPrice = formatPrice9(selectedProduct.price?.gross || 0, props.currency ?? "\u20AC");
10411
10558
  }
@@ -13782,6 +13929,347 @@ function OrderList(rawProps) {
13782
13929
  ] });
13783
13930
  }
13784
13931
  var OrderList_default = OrderList;
13932
+ var ROW_SEQ = 0;
13933
+ function blankRow() {
13934
+ ROW_SEQ += 1;
13935
+ return {
13936
+ key: `qo-${ROW_SEQ}`,
13937
+ code: "",
13938
+ productId: null,
13939
+ name: "",
13940
+ netPrice: 0,
13941
+ quantity: 1,
13942
+ minQuantity: 1,
13943
+ matches: [],
13944
+ searching: false,
13945
+ searched: false
13946
+ };
13947
+ }
13948
+ function QuickOrderInner(props) {
13949
+ const {
13950
+ companyId,
13951
+ language = "NL",
13952
+ currency = "\u20AC",
13953
+ configuration,
13954
+ initialRows = 5,
13955
+ searchThreshold = 3,
13956
+ debounceMs = 300,
13957
+ parseSpreadsheet,
13958
+ templateUrl,
13959
+ maxUploadBytes = 2 * 1024 * 1024,
13960
+ maxUploadRows = 500,
13961
+ onCartCreated,
13962
+ afterAddToCart,
13963
+ onMissingCodes,
13964
+ className,
13965
+ labels
13966
+ } = props;
13967
+ const user = props.user ?? null;
13968
+ const graphqlClient = props.graphqlClient;
13969
+ const displayPrice = (n) => props.formatPrice ? props.formatPrice(n) : propellerV2CoreUi.formatPrice(n, { symbol: currency });
13970
+ const L = (key, fallback) => propellerV2CoreUi.getLabel(labels, key, fallback);
13971
+ const { submitting, searchProducts, submit } = useQuickOrder({
13972
+ graphqlClient,
13973
+ user,
13974
+ companyId,
13975
+ language,
13976
+ configuration,
13977
+ onCartCreated,
13978
+ afterAddToCart
13979
+ });
13980
+ const [rows, setRows] = React6.useState(
13981
+ () => Array.from({ length: Math.max(1, initialRows) }, blankRow)
13982
+ );
13983
+ const [missing, setMissing] = React6.useState([]);
13984
+ const [uploadError, setUploadError] = React6.useState(null);
13985
+ const [uploading, setUploading] = React6.useState(false);
13986
+ const [notice, setNotice] = React6.useState(null);
13987
+ const fileInputRef = React6.useRef(null);
13988
+ const searchTimers = React6.useRef({});
13989
+ const patchRow = React6.useCallback((key, patch) => {
13990
+ setRows((prev) => prev.map((r) => r.key === key ? { ...r, ...patch } : r));
13991
+ }, []);
13992
+ const onCodeInput = React6.useCallback(
13993
+ (key, value) => {
13994
+ patchRow(key, { code: value, productId: null, name: "", netPrice: 0, searched: false });
13995
+ setNotice(null);
13996
+ const timers = searchTimers.current;
13997
+ if (timers[key]) clearTimeout(timers[key]);
13998
+ if (value.trim().length < searchThreshold) {
13999
+ patchRow(key, { matches: [], searching: false, searched: false });
14000
+ return;
14001
+ }
14002
+ patchRow(key, { searching: true });
14003
+ timers[key] = setTimeout(async () => {
14004
+ const results = await searchProducts(value);
14005
+ patchRow(key, { matches: results, searching: false, searched: true });
14006
+ }, debounceMs);
14007
+ },
14008
+ [patchRow, searchProducts, searchThreshold, debounceMs]
14009
+ );
14010
+ const selectMatch = React6.useCallback(
14011
+ (key, match) => {
14012
+ const dup = rows.some((r) => r.key !== key && r.productId && r.code === match.sku);
14013
+ if (dup) {
14014
+ setNotice(L("alreadyInList", "Product is already in the list"));
14015
+ patchRow(key, { code: "", matches: [], productId: null, name: "", netPrice: 0, searched: false });
14016
+ return;
14017
+ }
14018
+ patchRow(key, {
14019
+ code: match.sku,
14020
+ productId: match.productId,
14021
+ clusterId: match.clusterId,
14022
+ name: match.name,
14023
+ netPrice: match.netPrice,
14024
+ quantity: match.minQuantity,
14025
+ minQuantity: match.minQuantity,
14026
+ matches: [],
14027
+ searching: false,
14028
+ searched: false
14029
+ });
14030
+ },
14031
+ [rows, patchRow, L]
14032
+ );
14033
+ const setQuantity = React6.useCallback(
14034
+ (key, raw) => {
14035
+ const n = parseInt(raw, 10);
14036
+ setRows(
14037
+ (prev) => prev.map(
14038
+ (r) => r.key === key ? { ...r, quantity: Number.isFinite(n) && n > 0 ? Math.max(r.minQuantity, n) : r.minQuantity } : r
14039
+ )
14040
+ );
14041
+ },
14042
+ []
14043
+ );
14044
+ const addRow = React6.useCallback(() => setRows((prev) => [...prev, blankRow()]), []);
14045
+ const removeRow = React6.useCallback(
14046
+ (key) => setRows((prev) => prev.length > 1 ? prev.filter((r) => r.key !== key) : prev),
14047
+ []
14048
+ );
14049
+ const resolvedCount = React6.useMemo(() => rows.filter((r) => r.productId).length, [rows]);
14050
+ const onFileChosen = React6.useCallback(
14051
+ async (file) => {
14052
+ if (!file || !parseSpreadsheet) return;
14053
+ setUploadError(null);
14054
+ setMissing([]);
14055
+ if (file.size > maxUploadBytes) {
14056
+ setUploadError(`File too large (max ${Math.round(maxUploadBytes / 1024 / 1024)} MB)`);
14057
+ return;
14058
+ }
14059
+ setUploading(true);
14060
+ try {
14061
+ let lines = await parseSpreadsheet(file);
14062
+ 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);
14063
+ if (!lines.length) {
14064
+ setUploadError("No valid rows found in the file");
14065
+ return;
14066
+ }
14067
+ const resolved = [];
14068
+ const notFound = [];
14069
+ for (const line of lines) {
14070
+ const matches = await searchProducts(line.code);
14071
+ const exact = matches.find((m) => m.sku.toLowerCase() === line.code.toLowerCase()) || matches[0];
14072
+ if (!exact) {
14073
+ notFound.push(line.code);
14074
+ continue;
14075
+ }
14076
+ if (resolved.some((r) => r.productId === exact.productId)) continue;
14077
+ const row = blankRow();
14078
+ resolved.push({
14079
+ ...row,
14080
+ code: exact.sku,
14081
+ productId: exact.productId,
14082
+ clusterId: exact.clusterId,
14083
+ name: exact.name,
14084
+ netPrice: exact.netPrice,
14085
+ quantity: Math.max(exact.minQuantity, line.quantity),
14086
+ minQuantity: exact.minQuantity
14087
+ });
14088
+ }
14089
+ if (resolved.length) {
14090
+ setRows([...resolved, blankRow()]);
14091
+ }
14092
+ if (notFound.length) {
14093
+ setMissing(notFound);
14094
+ onMissingCodes?.(notFound);
14095
+ }
14096
+ } catch {
14097
+ setUploadError("Could not read the file");
14098
+ } finally {
14099
+ setUploading(false);
14100
+ if (fileInputRef.current) fileInputRef.current.value = "";
14101
+ }
14102
+ },
14103
+ [parseSpreadsheet, maxUploadBytes, maxUploadRows, searchProducts, onMissingCodes]
14104
+ );
14105
+ const handleSubmit = React6.useCallback(async () => {
14106
+ const lines = rows.filter((r) => r.productId).map((r) => ({ productId: r.productId, quantity: r.quantity, clusterId: r.clusterId, code: r.code }));
14107
+ if (!lines.length) {
14108
+ setNotice(L("noItems", "Add at least one product before submitting"));
14109
+ return;
14110
+ }
14111
+ const res = await submit(lines);
14112
+ if (res.success) {
14113
+ setRows(Array.from({ length: Math.max(1, initialRows) }, blankRow));
14114
+ setMissing([]);
14115
+ setNotice(null);
14116
+ } else {
14117
+ setNotice(res.error ?? "Failed to add items to cart");
14118
+ }
14119
+ }, [rows, submit, initialRows, L]);
14120
+ 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: [
14121
+ parseSpreadsheet ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full lg:w-1/3", children: [
14122
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("uploadTitle", "Upload Excel file") }),
14123
+ templateUrl ? /* @__PURE__ */ jsxRuntime.jsx(
14124
+ "a",
14125
+ {
14126
+ href: templateUrl,
14127
+ target: "_blank",
14128
+ rel: "noopener nofollow",
14129
+ className: "text-primary hover:underline text-sm inline-block mb-4",
14130
+ children: L("downloadTemplate", "Download XLSX template")
14131
+ }
14132
+ ) : null,
14133
+ /* @__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.") }),
14134
+ /* @__PURE__ */ jsxRuntime.jsx(
14135
+ "input",
14136
+ {
14137
+ ref: fileInputRef,
14138
+ type: "file",
14139
+ accept: ".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel",
14140
+ 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",
14141
+ onChange: (e) => onFileChosen(e.target.files?.[0]),
14142
+ disabled: uploading
14143
+ }
14144
+ ),
14145
+ uploading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mt-2", children: L("upload", "Uploading\u2026") }) : null,
14146
+ uploadError ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive mt-2", children: uploadError }) : null
14147
+ ] }) : null,
14148
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
14149
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("title", "Add your products manually") }),
14150
+ /* @__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: [
14151
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3", children: L("colCode", "Article no. / SKU") }),
14152
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3", children: L("colName", "Product name") }),
14153
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: L("colPrice", "excl. VAT") }),
14154
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-1", children: L("colQuantity", "Qty") }),
14155
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2 text-right", children: L("colTotal", "Total") }),
14156
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-1" })
14157
+ ] }),
14158
+ /* @__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: [
14159
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-12 md:col-span-3 relative", children: [
14160
+ /* @__PURE__ */ jsxRuntime.jsx(
14161
+ "input",
14162
+ {
14163
+ type: "text",
14164
+ value: r.code,
14165
+ onChange: (e) => onCodeInput(r.key, e.target.value),
14166
+ readOnly: !!r.productId,
14167
+ placeholder: L("colCode", "Article no. / SKU"),
14168
+ 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"
14169
+ }
14170
+ ),
14171
+ (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(
14172
+ "button",
14173
+ {
14174
+ type: "button",
14175
+ onClick: () => selectMatch(r.key, m),
14176
+ className: "flex items-center gap-2 w-full text-left px-3 py-2 hover:bg-muted text-sm",
14177
+ children: [
14178
+ m.imageUrl ? (
14179
+ // eslint-disable-next-line @next/next/no-img-element
14180
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src: m.imageUrl, alt: "", width: 32, height: 32, className: "rounded object-cover" })
14181
+ ) : null,
14182
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex-1", children: [
14183
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-foreground", children: m.name }),
14184
+ m.sku ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "block text-xs text-muted-foreground", children: [
14185
+ "SKU: ",
14186
+ m.sku
14187
+ ] }) : null
14188
+ ] })
14189
+ ]
14190
+ }
14191
+ ) }, `${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
14192
+ ] }),
14193
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-6 md:col-span-3", children: /* @__PURE__ */ jsxRuntime.jsx(
14194
+ "input",
14195
+ {
14196
+ type: "text",
14197
+ value: r.name,
14198
+ disabled: true,
14199
+ className: "w-full rounded border border-input bg-muted/40 px-2 py-1.5 text-sm text-muted-foreground"
14200
+ }
14201
+ ) }),
14202
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3 md:col-span-2", children: /* @__PURE__ */ jsxRuntime.jsx(
14203
+ "input",
14204
+ {
14205
+ type: "text",
14206
+ value: r.productId ? displayPrice(r.netPrice) : "",
14207
+ disabled: true,
14208
+ className: "w-full rounded border border-input bg-muted/40 px-2 py-1.5 text-sm text-muted-foreground"
14209
+ }
14210
+ ) }),
14211
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3 md:col-span-1", children: /* @__PURE__ */ jsxRuntime.jsx(
14212
+ "input",
14213
+ {
14214
+ type: "number",
14215
+ min: r.minQuantity,
14216
+ step: 1,
14217
+ value: r.productId ? r.quantity : "",
14218
+ disabled: !r.productId,
14219
+ onChange: (e) => setQuantity(r.key, e.target.value),
14220
+ onKeyDown: (e) => {
14221
+ if (["e", "E", "+", "-", "."].includes(e.key)) e.preventDefault();
14222
+ },
14223
+ 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"
14224
+ }
14225
+ ) }),
14226
+ /* @__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) : "" }),
14227
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-3 md:col-span-1 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
14228
+ "button",
14229
+ {
14230
+ type: "button",
14231
+ onClick: () => removeRow(r.key),
14232
+ "aria-label": L("remove", "Remove"),
14233
+ className: "text-muted-foreground hover:text-destructive p-1",
14234
+ children: "\u2715"
14235
+ }
14236
+ ) })
14237
+ ] }, r.key)) }),
14238
+ /* @__PURE__ */ jsxRuntime.jsxs(
14239
+ "button",
14240
+ {
14241
+ type: "button",
14242
+ onClick: addRow,
14243
+ className: "mt-3 text-primary hover:underline text-sm font-medium",
14244
+ children: [
14245
+ "+ ",
14246
+ L("addRow", "Add more rows")
14247
+ ]
14248
+ }
14249
+ ),
14250
+ missing.length ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-destructive mt-4", children: [
14251
+ L("missingCodes", "The following products were not added:"),
14252
+ " ",
14253
+ missing.join(", ")
14254
+ ] }) : null,
14255
+ notice ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive mt-2", children: notice }) : null,
14256
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end mt-6 pt-4 border-t border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
14257
+ "button",
14258
+ {
14259
+ type: "button",
14260
+ onClick: handleSubmit,
14261
+ disabled: submitting || resolvedCount === 0,
14262
+ 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",
14263
+ children: submitting ? L("adding", "Adding\u2026") : L("addToCart", "Add to cart")
14264
+ }
14265
+ ) })
14266
+ ] })
14267
+ ] }) });
14268
+ }
14269
+ function QuickOrder(rawProps) {
14270
+ const props = useInfraProps(rawProps);
14271
+ return /* @__PURE__ */ jsxRuntime.jsx(QuickOrderInner, { ...props });
14272
+ }
13785
14273
  function OrderShipments(props) {
13786
14274
  const [activeShipment, setActiveShipment] = React6.useState(null);
13787
14275
  const shipments = props.order?.shipments || [];
@@ -18258,6 +18746,7 @@ exports.PropellerDepsProvider = PropellerDepsProvider;
18258
18746
  exports.PropellerProvider = PropellerProvider;
18259
18747
  exports.PurchaseAuthorizationConfigurator = PurchaseAuthorizationConfigurator_default;
18260
18748
  exports.PurchaseAuthorizationRequests = PurchaseAuthorizationRequests_default;
18749
+ exports.QuickOrder = QuickOrder;
18261
18750
  exports.QuoteActions = QuoteActions_default;
18262
18751
  exports.RegisterForm = RegisterForm_default;
18263
18752
  exports.SearchBar = SearchBar_default;
@@ -18288,6 +18777,7 @@ exports.usePropellerDeps = usePropellerDeps;
18288
18777
  exports.usePropellerScope = usePropellerScope;
18289
18778
  exports.usePurchaseAuthorizationConfigurator = usePurchaseAuthorizationConfigurator;
18290
18779
  exports.usePurchaseAuthorizationRequests = usePurchaseAuthorizationRequests;
18780
+ exports.useQuickOrder = useQuickOrder;
18291
18781
  exports.useRequiredPropellerDeps = useRequiredPropellerDeps;
18292
18782
  exports.useResolvedProps = useResolvedProps;
18293
18783
  exports.useServices = useServices;