@metron-labs/quote-react 0.1.41 → 0.1.43

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.
@@ -17,7 +17,7 @@ const SAVED_QUOTE_KEY = "metron-quick-quote";
17
17
  export default function QuickQuote(props) {
18
18
  return (_jsx(CartProvider, { children: _jsx(QuickQuoteInner, { ...props }) }));
19
19
  }
20
- function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = false, }) {
20
+ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = false }) {
21
21
  const [stage, setStage] = useState("entry");
22
22
  const [entryMode, setEntryMode] = useState(null);
23
23
  const [error, setError] = useState("");
@@ -52,7 +52,27 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
52
52
  void fetch(apiUrl("/api/catalogue/resolve-conversions"), {
53
53
  method: "POST",
54
54
  headers: { "Content-Type": "application/json", ...clientHeaders() },
55
- body: JSON.stringify({ lines: unresolved.map((item) => ({ line_id: item.lineId, product_id: item.productId, requested_description: item.requestedDescription ?? item.description, requested_qty: item.qty, requested_uom: item.unit || null, selected_description: item.description, selected_uoi: item.uoi, selected_uoi_count: item.uoiCount, linked_context: item.linkedContext ?? [], selected_conversion: { sales_uom_normalised: item.salesUomNormalised, conversion_type: item.conversionType, sales_unit_size: item.salesUnitSize, sales_unit_size_uom: item.salesUnitSizeUom, coverage_qty: item.coverageQty, coverage_uom: item.coverageUom } })) }),
55
+ body: JSON.stringify({
56
+ lines: unresolved.map((item) => ({
57
+ line_id: item.lineId,
58
+ product_id: item.productId,
59
+ requested_description: item.requestedDescription ?? item.description,
60
+ requested_qty: item.qty,
61
+ requested_uom: item.unit || null,
62
+ selected_description: item.description,
63
+ selected_uoi: item.uoi,
64
+ selected_uoi_count: item.uoiCount,
65
+ linked_context: item.linkedContext ?? [],
66
+ selected_conversion: {
67
+ sales_uom_normalised: item.salesUomNormalised,
68
+ conversion_type: item.conversionType,
69
+ sales_unit_size: item.salesUnitSize,
70
+ sales_unit_size_uom: item.salesUnitSizeUom,
71
+ coverage_qty: item.coverageQty,
72
+ coverage_uom: item.coverageUom,
73
+ },
74
+ })),
75
+ }),
56
76
  })
57
77
  .then((response) => (response.ok ? readJson(response) : null))
58
78
  .then((payload) => {
@@ -267,5 +287,10 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
267
287
  setEntryMode("schedule");
268
288
  await runScheduleMatch(file);
269
289
  }, [runScheduleMatch]);
270
- return (_jsx(TrackContext.Provider, { value: track, children: _jsx("div", { ref: rootRef, className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}${!fullPage && stage === "processing" ? " app-wrapper--loading" : ""}`, style: getQuoteConfig().theme, children: _jsxs("main", { className: "main-content", children: [error && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", error] })), stage === "entry" && (_jsx(EntryStage, { onPasteSchedule: handlePasteSchedule, onUploadSchedule: handleUploadSchedule, onEntryModeChange: setEntryMode })), stage === "results" && entryMode === "schedule" && scheduleResult && (_jsx(ScheduleResultsStage, { scheduleResult: scheduleResult, onStartOver: resetToEntry, cart: cart, onAddToCart: addToCart, onRemoveFromCart: removeFromCart, onGoToBasket: goToBasket, cartSeeded: cartSeeded, onCartSeeded: () => setCartSeeded(true), reviewGateComplete: scheduleReviewGateComplete, onReviewGateComplete: () => setScheduleReviewGateComplete(true), internal: internal })), stage === "basket" && (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onRemove: removeFromCart, onBack: backToResults, onStartNewQuote: resetToEntry, internal: internal }))] }) }) }));
290
+ return (_jsx(TrackContext.Provider, { value: track, children: _jsx("div", { ref: rootRef, className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}${!fullPage && stage === "processing" ? " app-wrapper--loading" : ""}`, style: getQuoteConfig().theme, children: _jsxs("main", { className: "main-content", children: [error && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", error] })), stage === "entry" && _jsx(EntryStage, { onPasteSchedule: handlePasteSchedule, onUploadSchedule: handleUploadSchedule, onEntryModeChange: setEntryMode }), stage === "results" && entryMode === "schedule" && scheduleResult && (_jsx(ScheduleResultsStage, { scheduleResult: scheduleResult, onStartOver: resetToEntry, cart: cart, onAddToCart: addToCart, onRemoveFromCart: removeFromCart, onGoToBasket: goToBasket, cartSeeded: cartSeeded, onCartSeeded: () => setCartSeeded(true), reviewGateComplete: scheduleReviewGateComplete, onReviewGateComplete: () => setScheduleReviewGateComplete(true), internal: internal })), stage === "basket" && (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onRemove: removeFromCart, onBack: backToResults, onStartNewQuote: resetToEntry, onQuoteRequested: () => {
291
+ setScheduleResult(null);
292
+ setScheduleReviewGateComplete(false);
293
+ setCartSeeded(false);
294
+ clearCart();
295
+ }, internal: internal }))] }) }) }));
271
296
  }
@@ -8,7 +8,9 @@ interface BasketStageProps {
8
8
  onBack: () => void;
9
9
  /** Clears the basket and returns to the start, from the quote-request confirmation page. */
10
10
  onStartNewQuote: () => void;
11
+ /** Called once the quote request is sent; the confirmation stays up whatever it clears. */
12
+ onQuoteRequested?: () => void;
11
13
  internal?: boolean;
12
14
  }
13
- export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, internal }: BasketStageProps): import("react").JSX.Element;
15
+ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, onQuoteRequested, internal }: BasketStageProps): import("react").JSX.Element;
14
16
  export {};
@@ -36,7 +36,7 @@ function TrashIcon() {
36
36
  function ChevronIcon() {
37
37
  return (_jsx("svg", { width: "8", height: "5", viewBox: "0 0 8 5", fill: "none", children: _jsx("path", { d: "M1 4L4 1L7 4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }));
38
38
  }
39
- export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, internal = false }) {
39
+ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, onQuoteRequested, internal = false }) {
40
40
  // Both widgets can render a basket on the same page, so element ids are made unique.
41
41
  const uid = useId();
42
42
  const qzaBasketLayout = !internal && getBoqClientId() === "QZA6-MRA4";
@@ -195,6 +195,7 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
195
195
  }
196
196
  track("quote_requested", { items: pricedItems.length, total: Math.round(total * 100) / 100 });
197
197
  setQuoteRequestSent(true);
198
+ onQuoteRequested?.();
198
199
  window.scrollTo(0, 0);
199
200
  }
200
201
  catch {
@@ -8,6 +8,7 @@ import CloseIcon from "../../components/CloseIcon";
8
8
  import { clientHeaders, readJson } from "../../lib/api/boqClient";
9
9
  import { FILE_EXTENSIONS, isPhotoFile, shrinkPhoto, validateLines, validateUploadFile } from "../../lib/boq/inputLimits";
10
10
  import { ProcessingContent } from "./ProcessingStage";
11
+ import { useEscapeKey } from "../../lib/useEscapeKey";
11
12
  const UPLOAD_ACCEPT = [...FILE_EXTENSIONS.schedule, ...FILE_EXTENSIONS.photo].join(",");
12
13
  const STEPS = {
13
14
  paste: [
@@ -53,6 +54,8 @@ export default function EntryStage({ onPasteSchedule, onUploadSchedule, onEntryM
53
54
  setActiveTab(null);
54
55
  onEntryModeChange(null);
55
56
  };
57
+ // Like the close button, but not mid-match, where the modal is showing progress.
58
+ useEscapeKey(Boolean(activeTab) && !matching, closeWorkflow);
56
59
  const applyUploadFile = (files) => {
57
60
  if (!files?.length)
58
61
  return;
@@ -12,6 +12,7 @@ import { productImageSrc } from "../../lib/productImage";
12
12
  import { candidateToCartItem } from "../../lib/boq/toCartItem";
13
13
  import { apiHeaders, getBoqClientId, readJson } from "../../lib/api/boqClient";
14
14
  import { exportScheduleExcel } from "../../util/exportScheduleExcel";
15
+ import { useEscapeKey } from "../../lib/useEscapeKey";
15
16
  import { getCartSubtotal, getPricingBreakdown } from "../../util/pricing";
16
17
  const NO_SELECTION = "__none__";
17
18
  const PAGE_SIZE = 24;
@@ -486,6 +487,10 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
486
487
  setSelectedReviewId(null);
487
488
  setReviewFlowActive(false);
488
489
  };
490
+ // Escape does what each dialog's own close or cancel does. The review gate has neither - it's
491
+ // a required step - so Escape leaves it open.
492
+ useEscapeKey(confirmStartOver, () => setConfirmStartOver(false));
493
+ useEscapeKey(Boolean(activeReviewRow) && !confirmStartOver, () => closeReview());
489
494
  const completeReviewAndReturnToGate = (lineId) => {
490
495
  setReviewedReviewIds((ids) => new Set(ids).add(lineId));
491
496
  if (pendingReviewRows.some((row) => row.item.line_id !== lineId)) {