@propeller-commerce/propeller-v2-react-ui 0.6.1 → 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
@@ -2498,6 +2498,140 @@ function useProductSearch(options) {
2498
2498
  goToPage: pagination.goToPage
2499
2499
  };
2500
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
+ }
2501
2635
  var inflightFetches3 = /* @__PURE__ */ new Set();
2502
2636
  var STOREFRONT_STATUSES = [
2503
2637
  propellerSdkV2.ProductStatus.A,
@@ -6385,7 +6519,7 @@ function CartItem(rawProps) {
6385
6519
  }
6386
6520
  return "#";
6387
6521
  }
6388
- function getProductImageUrl5() {
6522
+ function getProductImageUrl6() {
6389
6523
  return props.cartItem.product?.media?.images?.items?.[0]?.imageVariants?.[0]?.url || "";
6390
6524
  }
6391
6525
  function getProductSku3() {
@@ -6569,7 +6703,7 @@ function CartItem(rawProps) {
6569
6703
  derived: {
6570
6704
  name: getProductName5(),
6571
6705
  sku: getProductSku3(),
6572
- imageUrl: getProductImageUrl5(),
6706
+ imageUrl: getProductImageUrl6(),
6573
6707
  productUrl: getProductUrl2(),
6574
6708
  isBundle: isBundleItem(),
6575
6709
  bundleName: getBundleName(),
@@ -9543,7 +9677,7 @@ function GridToolbar(rawProps) {
9543
9677
  const [currentSortOrder, setCurrentSortOrder] = React6.useState(propellerSdkV2.SortOrder.DESC);
9544
9678
  const [currentOffset, setCurrentOffset] = React6.useState(12);
9545
9679
  const [currentViewMode, setCurrentViewMode] = React6.useState("grid");
9546
- function getLabel54(key) {
9680
+ function getLabel55(key) {
9547
9681
  const labels = props.labels || {};
9548
9682
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS[key] || key;
9549
9683
  }
@@ -9622,7 +9756,7 @@ function GridToolbar(rawProps) {
9622
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: [
9623
9757
  props.itemsFound,
9624
9758
  "\xA0",
9625
- props.itemsFound === 1 ? getLabel54("productSingular") : getLabel54("productPlural")
9759
+ props.itemsFound === 1 ? getLabel55("productSingular") : getLabel55("productPlural")
9626
9760
  ] }) : null }),
9627
9761
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "propeller-grid-toolbar__controls flex flex-wrap items-center gap-3", children: [
9628
9762
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -9633,7 +9767,7 @@ function GridToolbar(rawProps) {
9633
9767
  onChange: (e) => handleOffsetChange(parseInt(e.target.value)),
9634
9768
  children: getOffsetOptions()?.map((n) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: n, children: [
9635
9769
  n,
9636
- getLabel54("perPage")
9770
+ getLabel55("perPage")
9637
9771
  ] }, n))
9638
9772
  }
9639
9773
  ),
@@ -9649,7 +9783,7 @@ function GridToolbar(rawProps) {
9649
9783
  {
9650
9784
  value: field,
9651
9785
  disabled: field === "PRICE" && isPriceSortDisabled(),
9652
- children: getLabel54(field)
9786
+ children: getLabel55(field)
9653
9787
  },
9654
9788
  field
9655
9789
  ))
@@ -9662,8 +9796,8 @@ function GridToolbar(rawProps) {
9662
9796
  value: currentSortOrder,
9663
9797
  onChange: (e) => handleSortOrderChange(e.target.value),
9664
9798
  children: [
9665
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel54("ASC") }),
9666
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel54("DESC") })
9799
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.ASC, children: getLabel55("ASC") }),
9800
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: propellerSdkV2.SortOrder.DESC, children: getLabel55("DESC") })
9667
9801
  ]
9668
9802
  }
9669
9803
  ),
@@ -9673,7 +9807,7 @@ function GridToolbar(rawProps) {
9673
9807
  type: "button",
9674
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",
9675
9809
  onClick: (event) => handleViewChange(),
9676
- title: currentViewMode === "grid" ? getLabel54("switchToList") : getLabel54("switchToGrid"),
9810
+ title: currentViewMode === "grid" ? getLabel55("switchToList") : getLabel55("switchToGrid"),
9677
9811
  children: [
9678
9812
  currentViewMode === "grid" ? /* @__PURE__ */ jsxRuntime.jsxs(
9679
9813
  "svg",
@@ -9731,7 +9865,7 @@ function GridToolbar(rawProps) {
9731
9865
  onClick: (event) => {
9732
9866
  if (props.onClearFilters) props.onClearFilters();
9733
9867
  },
9734
- children: getLabel54("clearAll")
9868
+ children: getLabel55("clearAll")
9735
9869
  }
9736
9870
  ),
9737
9871
  props.priceFilterMin !== void 0 || props.priceFilterMax !== void 0 ? /* @__PURE__ */ jsxRuntime.jsxs(
@@ -9742,7 +9876,7 @@ function GridToolbar(rawProps) {
9742
9876
  if (props.onPriceFilterRemove) props.onPriceFilterRemove();
9743
9877
  },
9744
9878
  children: [
9745
- getLabel54("price"),
9879
+ getLabel55("price"),
9746
9880
  ": \u20AC",
9747
9881
  props.priceFilterMin ?? 0,
9748
9882
  " \u2013 \u20AC",
@@ -9779,7 +9913,7 @@ var DEFAULT_LABELS2 = {
9779
9913
  of: "of"
9780
9914
  };
9781
9915
  function GridPagination(props) {
9782
- function getLabel54(key) {
9916
+ function getLabel55(key) {
9783
9917
  const labels = props.labels || {};
9784
9918
  return labels[key] !== void 0 ? labels[key] : DEFAULT_LABELS2[key] || key;
9785
9919
  }
@@ -9852,15 +9986,15 @@ function GridPagination(props) {
9852
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",
9853
9987
  disabled: currentPage === 1,
9854
9988
  onClick: () => handlePageChange(currentPage - 1),
9855
- children: getLabel54("previous")
9989
+ children: getLabel55("previous")
9856
9990
  }
9857
9991
  ),
9858
9992
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "propeller-grid-pagination__info px-2 text-sm font-medium text-muted-foreground", children: [
9859
- getLabel54("page"),
9993
+ getLabel55("page"),
9860
9994
  "\xA0",
9861
9995
  currentPage,
9862
9996
  "\xA0",
9863
- getLabel54("of"),
9997
+ getLabel55("of"),
9864
9998
  "\xA0",
9865
9999
  totalPages
9866
10000
  ] }),
@@ -9871,7 +10005,7 @@ function GridPagination(props) {
9871
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",
9872
10006
  disabled: currentPage === totalPages,
9873
10007
  onClick: () => handlePageChange(currentPage + 1),
9874
- children: getLabel54("next")
10008
+ children: getLabel55("next")
9875
10009
  }
9876
10010
  )
9877
10011
  ] }) : null,
@@ -9883,7 +10017,7 @@ function GridPagination(props) {
9883
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",
9884
10018
  disabled: currentPage === 1,
9885
10019
  onClick: () => handlePageChange(currentPage - 1),
9886
- children: getLabel54("previous")
10020
+ children: getLabel55("previous")
9887
10021
  }
9888
10022
  ),
9889
10023
  getFullPages().map((item, idx) => /* @__PURE__ */ jsxRuntime.jsx(
@@ -9910,7 +10044,7 @@ function GridPagination(props) {
9910
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",
9911
10045
  disabled: currentPage === totalPages,
9912
10046
  onClick: () => handlePageChange(currentPage + 1),
9913
- children: getLabel54("next")
10047
+ children: getLabel55("next")
9914
10048
  }
9915
10049
  )
9916
10050
  ] }) : null
@@ -10389,7 +10523,7 @@ function formatPrice9(price, currency) {
10389
10523
  function getProductName2(product) {
10390
10524
  return product.names?.[0]?.value || `Product ${product.productId}`;
10391
10525
  }
10392
- function getProductImageUrl3(product) {
10526
+ function getProductImageUrl4(product) {
10393
10527
  const media = product.media;
10394
10528
  if (media?.images?.items && Array.isArray(media.images.items) && media.images.items.length > 0) {
10395
10529
  const firstImage = media.images.items[0];
@@ -10418,7 +10552,7 @@ function ClusterOptions(props) {
10418
10552
  (p) => p.productId.toString() === selectedProductId
10419
10553
  );
10420
10554
  if (selectedProduct) {
10421
- previewImageUrl = getProductImageUrl3(selectedProduct);
10555
+ previewImageUrl = getProductImageUrl4(selectedProduct);
10422
10556
  previewName = getProductName2(selectedProduct);
10423
10557
  previewPrice = formatPrice9(selectedProduct.price?.gross || 0, props.currency ?? "\u20AC");
10424
10558
  }
@@ -13795,6 +13929,347 @@ function OrderList(rawProps) {
13795
13929
  ] });
13796
13930
  }
13797
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
+ }
13798
14273
  function OrderShipments(props) {
13799
14274
  const [activeShipment, setActiveShipment] = React6.useState(null);
13800
14275
  const shipments = props.order?.shipments || [];
@@ -18271,6 +18746,7 @@ exports.PropellerDepsProvider = PropellerDepsProvider;
18271
18746
  exports.PropellerProvider = PropellerProvider;
18272
18747
  exports.PurchaseAuthorizationConfigurator = PurchaseAuthorizationConfigurator_default;
18273
18748
  exports.PurchaseAuthorizationRequests = PurchaseAuthorizationRequests_default;
18749
+ exports.QuickOrder = QuickOrder;
18274
18750
  exports.QuoteActions = QuoteActions_default;
18275
18751
  exports.RegisterForm = RegisterForm_default;
18276
18752
  exports.SearchBar = SearchBar_default;
@@ -18301,6 +18777,7 @@ exports.usePropellerDeps = usePropellerDeps;
18301
18777
  exports.usePropellerScope = usePropellerScope;
18302
18778
  exports.usePurchaseAuthorizationConfigurator = usePurchaseAuthorizationConfigurator;
18303
18779
  exports.usePurchaseAuthorizationRequests = usePurchaseAuthorizationRequests;
18780
+ exports.useQuickOrder = useQuickOrder;
18304
18781
  exports.useRequiredPropellerDeps = useRequiredPropellerDeps;
18305
18782
  exports.useResolvedProps = useResolvedProps;
18306
18783
  exports.useServices = useServices;