@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.js CHANGED
@@ -228,6 +228,7 @@ function useAddress(options) {
228
228
  );
229
229
  return { loading, error, createAddress, updateAddress, deleteAddress, setDefaultAddress };
230
230
  }
231
+ var isViewerSearchInput = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
231
232
  function useAuth(options) {
232
233
  const { graphqlClient, language = "NL", onAuthHeaderUpdate, configuration } = options;
233
234
  const [loading, setLoading] = useState(false);
@@ -264,8 +265,14 @@ function useAuth(options) {
264
265
  ...configuration?.companyTrackAttributes?.length && {
265
266
  companyAttributesInput: { attributeDescription: { names: configuration.companyTrackAttributes } }
266
267
  },
267
- ...configuration?.contactPAConfigInput && {
268
+ // Contact-scoped pagination inputs — only when the host supplies a real
269
+ // input object (an empty array `[]` is truthy but 400s the viewer). See
270
+ // isViewerSearchInput.
271
+ ...isViewerSearchInput(configuration?.contactPAConfigInput) && {
268
272
  contactPAConfigInput: configuration.contactPAConfigInput
273
+ },
274
+ ...isViewerSearchInput(configuration?.contactCompaniesSearchInput) && {
275
+ contactCompaniesSearchInput: configuration.contactCompaniesSearchInput
269
276
  }
270
277
  };
271
278
  const viewer = await userService.getViewer(viewerInput);
@@ -2471,6 +2478,140 @@ function useProductSearch(options) {
2471
2478
  goToPage: pagination.goToPage
2472
2479
  };
2473
2480
  }
2481
+ function toMatch(item, language) {
2482
+ const isCluster = "clusterId" in item;
2483
+ const displayItem = isCluster ? item.defaultProduct : item;
2484
+ const productId = displayItem?.productId ?? item.productId;
2485
+ const clusterId = isCluster ? item.clusterId : void 0;
2486
+ const name = language && item.names?.find((n) => n.language === language)?.value || item.names?.[0]?.value || "Product";
2487
+ const netPrice = displayItem?.price?.net ?? 0;
2488
+ const grossPrice = displayItem?.price?.gross ?? 0;
2489
+ const minQuantity = Math.max(1, displayItem?.minimumQuantity ?? 1);
2490
+ const imageUrl = isCluster ? getClusterImageUrl(item) : getProductImageUrl(item);
2491
+ return {
2492
+ productId,
2493
+ clusterId,
2494
+ name,
2495
+ sku: item.sku || displayItem?.sku || "",
2496
+ netPrice,
2497
+ grossPrice,
2498
+ minQuantity,
2499
+ imageUrl
2500
+ };
2501
+ }
2502
+ function useQuickOrder(options) {
2503
+ const {
2504
+ graphqlClient,
2505
+ user,
2506
+ companyId,
2507
+ configuration = {},
2508
+ onCartCreated,
2509
+ afterAddToCart
2510
+ } = options;
2511
+ const language = options.language || "NL";
2512
+ const searchLimit = options.searchLimit ?? 8;
2513
+ const [submitting, setSubmitting] = useState(false);
2514
+ const [error, setError] = useState(null);
2515
+ const searchProducts = useCallback(
2516
+ async (term) => {
2517
+ const trimmed = term.trim();
2518
+ if (!trimmed || !graphqlClient) return [];
2519
+ try {
2520
+ const service = createServices(graphqlClient).product;
2521
+ const input = {
2522
+ term: trimmed,
2523
+ language,
2524
+ page: 1,
2525
+ offset: searchLimit,
2526
+ statuses: [ProductStatus.A, ProductStatus.P, ProductStatus.T, ProductStatus.S],
2527
+ sortInputs: [{ field: ProductSortField.RELEVANCE, order: SortOrder.DESC }],
2528
+ searchFields: [
2529
+ {
2530
+ fieldNames: [
2531
+ ProductSearchableField.SKU,
2532
+ ProductSearchableField.NAME,
2533
+ ProductSearchableField.KEYWORDS,
2534
+ ProductSearchableField.CUSTOM_KEYWORDS
2535
+ ],
2536
+ boost: 5
2537
+ },
2538
+ {
2539
+ fieldNames: [
2540
+ ProductSearchableField.MANUFACTURER_CODE,
2541
+ ProductSearchableField.EAN_CODE,
2542
+ ProductSearchableField.BAR_CODE,
2543
+ ProductSearchableField.SUPPLIER_CODE,
2544
+ ProductSearchableField.PRODUCT_ID
2545
+ ],
2546
+ boost: 1
2547
+ }
2548
+ ]
2549
+ };
2550
+ const variables = {
2551
+ input,
2552
+ language,
2553
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2554
+ };
2555
+ const result = await service.getProducts(variables);
2556
+ const items = result?.items ?? [];
2557
+ return items.map((it) => toMatch(it, language));
2558
+ } catch {
2559
+ return [];
2560
+ }
2561
+ },
2562
+ [graphqlClient, language, searchLimit, configuration]
2563
+ );
2564
+ const submit = useCallback(
2565
+ async (lines) => {
2566
+ const valid = lines.filter((l) => l.productId && (l.quantity ?? 0) > 0);
2567
+ if (!valid.length) return { success: false, error: "No items to add" };
2568
+ if (!graphqlClient) return { success: false, error: "No GraphQL client" };
2569
+ setSubmitting(true);
2570
+ setError(null);
2571
+ try {
2572
+ const services = createServices(graphqlClient);
2573
+ const cart = await initCart({
2574
+ services,
2575
+ user: user ?? null,
2576
+ companyId,
2577
+ language,
2578
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2579
+ imageVariantFilters: configuration.imageVariantFiltersSmall,
2580
+ onCartCreated
2581
+ });
2582
+ const items = valid.map((l) => ({
2583
+ productId: l.productId,
2584
+ quantity: l.quantity,
2585
+ ...l.clusterId ? { clusterId: l.clusterId } : {}
2586
+ }));
2587
+ const bulk = await services.cart.bulkUpdateCartItems({
2588
+ input: { cartId: cart.cartId, items }
2589
+ });
2590
+ const updated = await services.cart.getCart({
2591
+ cartId: cart.cartId,
2592
+ language,
2593
+ imageSearchFilters: configuration.imageSearchFiltersGrid,
2594
+ imageVariantFilters: configuration.imageVariantFiltersSmall
2595
+ });
2596
+ const finalCart = updated ?? cart;
2597
+ afterAddToCart?.(finalCart);
2598
+ return {
2599
+ success: true,
2600
+ cart: finalCart,
2601
+ added: (bulk?.created ?? 0) + (bulk?.updated ?? 0)
2602
+ };
2603
+ } catch (e) {
2604
+ const msg = e instanceof Error ? e.message : "Failed to add items to cart";
2605
+ setError(msg);
2606
+ return { success: false, error: msg };
2607
+ } finally {
2608
+ setSubmitting(false);
2609
+ }
2610
+ },
2611
+ [graphqlClient, user, companyId, language, configuration, onCartCreated, afterAddToCart]
2612
+ );
2613
+ return { submitting, error, searchProducts, submit };
2614
+ }
2474
2615
  var inflightFetches3 = /* @__PURE__ */ new Set();
2475
2616
  var STOREFRONT_STATUSES = [
2476
2617
  ProductStatus.A,
@@ -3446,7 +3587,13 @@ function LoginForm(rawProps) {
3446
3587
  const [email, setEmail] = useState("");
3447
3588
  const [password, setPassword] = useState("");
3448
3589
  const { loading, error: authError, login } = useAuth({
3449
- graphqlClient: props.graphqlClient
3590
+ graphqlClient: props.graphqlClient,
3591
+ // Resolved from <PropellerDepsProvider> by useInfraProps (the app's full
3592
+ // `config`), so the login viewer requests the configured track attributes +
3593
+ // pagination inputs (companyTrackAttributes → MY_INSTALLATIONS,
3594
+ // contactCompaniesSearchInput, contactPAConfigInput). Without this the header
3595
+ // login returns a viewer with no companies/attributes until a later refresh.
3596
+ configuration: props.configuration
3450
3597
  });
3451
3598
  getLabel(props.labels, "email", "Email");
3452
3599
  getLabel(props.labels, "password", "Password");
@@ -6352,7 +6499,7 @@ function CartItem(rawProps) {
6352
6499
  }
6353
6500
  return "#";
6354
6501
  }
6355
- function getProductImageUrl5() {
6502
+ function getProductImageUrl6() {
6356
6503
  return props.cartItem.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url || "";
6357
6504
  }
6358
6505
  function getProductSku3() {
@@ -6536,7 +6683,7 @@ function CartItem(rawProps) {
6536
6683
  derived: {
6537
6684
  name: getProductName5(),
6538
6685
  sku: getProductSku3(),
6539
- imageUrl: getProductImageUrl5(),
6686
+ imageUrl: getProductImageUrl6(),
6540
6687
  productUrl: getProductUrl2(),
6541
6688
  isBundle: isBundleItem(),
6542
6689
  bundleName: getBundleName(),
@@ -9510,7 +9657,7 @@ function GridToolbar(rawProps) {
9510
9657
  const [currentSortOrder, setCurrentSortOrder] = useState(SortOrder.DESC);
9511
9658
  const [currentOffset, setCurrentOffset] = useState(12);
9512
9659
  const [currentViewMode, setCurrentViewMode] = useState("grid");
9513
- function getLabel54(key) {
9660
+ function getLabel55(key) {
9514
9661
  const labels = props.labels || {};
9515
9662
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS[key] || key;
9516
9663
  }
@@ -9589,7 +9736,7 @@ function GridToolbar(rawProps) {
9589
9736
  /* @__PURE__ */ jsx("div", { className: "propeller-grid-toolbar__count text-sm text-muted-foreground font-medium", children: props.itemsFound > 0 ? /* @__PURE__ */ jsxs("span", { children: [
9590
9737
  props.itemsFound,
9591
9738
  "\xA0",
9592
- props.itemsFound === 1 ? getLabel54("productSingular") : getLabel54("productPlural")
9739
+ props.itemsFound === 1 ? getLabel55("productSingular") : getLabel55("productPlural")
9593
9740
  ] }) : null }),
9594
9741
  /* @__PURE__ */ jsxs("div", { className: "propeller-grid-toolbar__controls flex flex-wrap items-center gap-3", children: [
9595
9742
  /* @__PURE__ */ jsx(
@@ -9600,7 +9747,7 @@ function GridToolbar(rawProps) {
9600
9747
  onChange: (e) => handleOffsetChange(parseInt(e.target.value)),
9601
9748
  children: getOffsetOptions()?.map((n) => /* @__PURE__ */ jsxs("option", { value: n, children: [
9602
9749
  n,
9603
- getLabel54("perPage")
9750
+ getLabel55("perPage")
9604
9751
  ] }, n))
9605
9752
  }
9606
9753
  ),
@@ -9616,7 +9763,7 @@ function GridToolbar(rawProps) {
9616
9763
  {
9617
9764
  value: field,
9618
9765
  disabled: field === "PRICE" && isPriceSortDisabled(),
9619
- children: getLabel54(field)
9766
+ children: getLabel55(field)
9620
9767
  },
9621
9768
  field
9622
9769
  ))
@@ -9629,8 +9776,8 @@ function GridToolbar(rawProps) {
9629
9776
  value: currentSortOrder,
9630
9777
  onChange: (e) => handleSortOrderChange(e.target.value),
9631
9778
  children: [
9632
- /* @__PURE__ */ jsx("option", { value: SortOrder.ASC, children: getLabel54("ASC") }),
9633
- /* @__PURE__ */ jsx("option", { value: SortOrder.DESC, children: getLabel54("DESC") })
9779
+ /* @__PURE__ */ jsx("option", { value: SortOrder.ASC, children: getLabel55("ASC") }),
9780
+ /* @__PURE__ */ jsx("option", { value: SortOrder.DESC, children: getLabel55("DESC") })
9634
9781
  ]
9635
9782
  }
9636
9783
  ),
@@ -9640,7 +9787,7 @@ function GridToolbar(rawProps) {
9640
9787
  type: "button",
9641
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",
9642
9789
  onClick: (event) => handleViewChange(),
9643
- title: currentViewMode === "grid" ? getLabel54("switchToList") : getLabel54("switchToGrid"),
9790
+ title: currentViewMode === "grid" ? getLabel55("switchToList") : getLabel55("switchToGrid"),
9644
9791
  children: [
9645
9792
  currentViewMode === "grid" ? /* @__PURE__ */ jsxs(
9646
9793
  "svg",
@@ -9698,7 +9845,7 @@ function GridToolbar(rawProps) {
9698
9845
  onClick: (event) => {
9699
9846
  if (props.onClearFilters) props.onClearFilters();
9700
9847
  },
9701
- children: getLabel54("clearAll")
9848
+ children: getLabel55("clearAll")
9702
9849
  }
9703
9850
  ),
9704
9851
  props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0 ? /* @__PURE__ */ jsxs(
@@ -9709,7 +9856,7 @@ function GridToolbar(rawProps) {
9709
9856
  if (props.onPriceFilterRemove) props.onPriceFilterRemove();
9710
9857
  },
9711
9858
  children: [
9712
- getLabel54("price"),
9859
+ getLabel55("price"),
9713
9860
  ": \u20AC",
9714
9861
  props.priceFilterMin ?? 0,
9715
9862
  " \u2013 \u20AC",
@@ -9746,7 +9893,7 @@ var DEFAULT_LABELS2 = {
9746
9893
  of: "of"
9747
9894
  };
9748
9895
  function GridPagination(props) {
9749
- function getLabel54(key) {
9896
+ function getLabel55(key) {
9750
9897
  const labels = props.labels || {};
9751
9898
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS2[key] || key;
9752
9899
  }
@@ -9819,15 +9966,15 @@ function GridPagination(props) {
9819
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",
9820
9967
  disabled: currentPage === 1,
9821
9968
  onClick: () => handlePageChange(currentPage - 1),
9822
- children: getLabel54("previous")
9969
+ children: getLabel55("previous")
9823
9970
  }
9824
9971
  ),
9825
9972
  /* @__PURE__ */ jsxs("span", { className: "propeller-grid-pagination__info px-2 text-sm font-medium text-muted-foreground", children: [
9826
- getLabel54("page"),
9973
+ getLabel55("page"),
9827
9974
  "\xA0",
9828
9975
  currentPage,
9829
9976
  "\xA0",
9830
- getLabel54("of"),
9977
+ getLabel55("of"),
9831
9978
  "\xA0",
9832
9979
  totalPages
9833
9980
  ] }),
@@ -9838,7 +9985,7 @@ function GridPagination(props) {
9838
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",
9839
9986
  disabled: currentPage === totalPages,
9840
9987
  onClick: () => handlePageChange(currentPage + 1),
9841
- children: getLabel54("next")
9988
+ children: getLabel55("next")
9842
9989
  }
9843
9990
  )
9844
9991
  ] }) : null,
@@ -9850,7 +9997,7 @@ function GridPagination(props) {
9850
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",
9851
9998
  disabled: currentPage === 1,
9852
9999
  onClick: () => handlePageChange(currentPage - 1),
9853
- children: getLabel54("previous")
10000
+ children: getLabel55("previous")
9854
10001
  }
9855
10002
  ),
9856
10003
  getFullPages().map((item, idx) => /* @__PURE__ */ jsx(
@@ -9877,7 +10024,7 @@ function GridPagination(props) {
9877
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",
9878
10025
  disabled: currentPage === totalPages,
9879
10026
  onClick: () => handlePageChange(currentPage + 1),
9880
- children: getLabel54("next")
10027
+ children: getLabel55("next")
9881
10028
  }
9882
10029
  )
9883
10030
  ] }) : null
@@ -10356,7 +10503,7 @@ function formatPrice9(price, currency) {
10356
10503
  function getProductName2(product) {
10357
10504
  return product.names?.[0]?.value || `Product ${product.productId}`;
10358
10505
  }
10359
- function getProductImageUrl3(product) {
10506
+ function getProductImageUrl4(product) {
10360
10507
  const media = product.media;
10361
10508
  if (media?.images?.items && Array.isArray(media.images.items) && media.images.items.length > 0) {
10362
10509
  const firstImage = media.images.items[0];
@@ -10385,7 +10532,7 @@ function ClusterOptions(props) {
10385
10532
  (p) => p.productId.toString() === selectedProductId
10386
10533
  );
10387
10534
  if (selectedProduct) {
10388
- previewImageUrl = getProductImageUrl3(selectedProduct);
10535
+ previewImageUrl = getProductImageUrl4(selectedProduct);
10389
10536
  previewName = getProductName2(selectedProduct);
10390
10537
  previewPrice = formatPrice9(selectedProduct.price?.gross || 0, props.currency ?? "\u20AC");
10391
10538
  }
@@ -13762,6 +13909,347 @@ function OrderList(rawProps) {
13762
13909
  ] });
13763
13910
  }
13764
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) : formatPrice(n, { symbol: currency });
13950
+ const L = (key, fallback) => 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] = useState(
13961
+ () => Array.from({ length: Math.max(1, initialRows) }, blankRow)
13962
+ );
13963
+ const [missing, setMissing] = useState([]);
13964
+ const [uploadError, setUploadError] = useState(null);
13965
+ const [uploading, setUploading] = useState(false);
13966
+ const [notice, setNotice] = useState(null);
13967
+ const fileInputRef = useRef(null);
13968
+ const searchTimers = useRef({});
13969
+ const patchRow = useCallback((key, patch) => {
13970
+ setRows((prev) => prev.map((r) => r.key === key ? { ...r, ...patch } : r));
13971
+ }, []);
13972
+ const onCodeInput = 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 = 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 = 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 = useCallback(() => setRows((prev) => [...prev, blankRow()]), []);
14025
+ const removeRow = useCallback(
14026
+ (key) => setRows((prev) => prev.length > 1 ? prev.filter((r) => r.key !== key) : prev),
14027
+ []
14028
+ );
14029
+ const resolvedCount = useMemo(() => rows.filter((r) => r.productId).length, [rows]);
14030
+ const onFileChosen = 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 = 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__ */ jsx("div", { className: className ?? "propeller-quick-order", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col lg:flex-row gap-8", children: [
14101
+ parseSpreadsheet ? /* @__PURE__ */ jsxs("div", { className: "w-full lg:w-1/3", children: [
14102
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("uploadTitle", "Upload Excel file") }),
14103
+ templateUrl ? /* @__PURE__ */ 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__ */ 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__ */ 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__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: L("upload", "Uploading\u2026") }) : null,
14126
+ uploadError ? /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mt-2", children: uploadError }) : null
14127
+ ] }) : null,
14128
+ /* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
14129
+ /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold mb-3 text-foreground", children: L("title", "Add your products manually") }),
14130
+ /* @__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: [
14131
+ /* @__PURE__ */ jsx("div", { className: "col-span-3", children: L("colCode", "Article no. / SKU") }),
14132
+ /* @__PURE__ */ jsx("div", { className: "col-span-3", children: L("colName", "Product name") }),
14133
+ /* @__PURE__ */ jsx("div", { className: "col-span-2", children: L("colPrice", "excl. VAT") }),
14134
+ /* @__PURE__ */ jsx("div", { className: "col-span-1", children: L("colQuantity", "Qty") }),
14135
+ /* @__PURE__ */ jsx("div", { className: "col-span-2 text-right", children: L("colTotal", "Total") }),
14136
+ /* @__PURE__ */ jsx("div", { className: "col-span-1" })
14137
+ ] }),
14138
+ /* @__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: [
14139
+ /* @__PURE__ */ jsxs("div", { className: "col-span-12 md:col-span-3 relative", children: [
14140
+ /* @__PURE__ */ 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__ */ 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(
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__ */ jsx("img", { src: m.imageUrl, alt: "", width: 32, height: 32, className: "rounded object-cover" })
14161
+ ) : null,
14162
+ /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
14163
+ /* @__PURE__ */ jsx("span", { className: "block text-foreground", children: m.name }),
14164
+ m.sku ? /* @__PURE__ */ 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__ */ jsx("li", { className: "px-3 py-2 text-sm text-muted-foreground", children: L("noResults", "No results found") }) }) : null
14172
+ ] }),
14173
+ /* @__PURE__ */ jsx("div", { className: "col-span-6 md:col-span-3", children: /* @__PURE__ */ 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__ */ jsx("div", { className: "col-span-3 md:col-span-2", children: /* @__PURE__ */ 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__ */ jsx("div", { className: "col-span-3 md:col-span-1", children: /* @__PURE__ */ 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__ */ 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__ */ jsx("div", { className: "col-span-3 md:col-span-1 flex justify-end", children: /* @__PURE__ */ 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__ */ 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__ */ 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__ */ jsx("p", { className: "text-sm text-destructive mt-2", children: notice }) : null,
14236
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end mt-6 pt-4 border-t border-border", children: /* @__PURE__ */ 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__ */ jsx(QuickOrderInner, { ...props });
14252
+ }
13765
14253
  function OrderShipments(props) {
13766
14254
  const [activeShipment, setActiveShipment] = useState(null);
13767
14255
  const shipments = props.order?.shipments || [];
@@ -18165,6 +18653,6 @@ function UserDetails(rawProps) {
18165
18653
  }
18166
18654
  var UserDetails_default = UserDetails;
18167
18655
 
18168
- 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 };
18656
+ 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 };
18169
18657
  //# sourceMappingURL=index.js.map
18170
18658
  //# sourceMappingURL=index.js.map