@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.
- package/dist/app/QuickQuote.js +28 -3
- package/dist/app/stages/BasketStage.d.ts +3 -1
- package/dist/app/stages/BasketStage.js +2 -1
- package/dist/app/stages/EntryStage.js +3 -0
- package/dist/app/stages/ScheduleResultsStage.js +5 -0
- package/dist/embed/quote.js +31 -33
- package/dist/lib/useEscapeKey.d.ts +2 -0
- package/dist/lib/useEscapeKey.js +20 -0
- package/dist/styles.css +18 -20
- package/package.json +1 -1
package/dist/app/QuickQuote.js
CHANGED
|
@@ -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({
|
|
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" &&
|
|
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)) {
|