@metron-labs/quote-react 0.1.27 → 0.1.29
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/LICENSE +31 -31
- package/README.md +253 -253
- package/dist/app/QuickQuote.js +6 -2
- package/dist/app/stages/BasketStage.js +12 -4
- package/dist/app/stages/ScheduleResultsStage.js +22 -13
- package/dist/embed/quote.js +6777 -6759
- package/dist/lib/boq/toCartItem.d.ts +1 -0
- package/dist/lib/boq/toCartItem.js +1 -0
- package/dist/lib/productImage.js +19 -19
- package/dist/styles.css +6766 -6748
- package/dist/util/pricing.d.ts +1 -0
- package/dist/util/pricing.js +3 -0
- package/dist/util/types.d.ts +8 -2
- package/package.json +53 -53
|
@@ -5,7 +5,7 @@ import { useTrack } from "../../lib/analytics";
|
|
|
5
5
|
import BackArrow from "../../components/BackArrow";
|
|
6
6
|
import { fmtCurrency } from "../../lib/formatters";
|
|
7
7
|
import { productImageSrc } from "../../lib/productImage";
|
|
8
|
-
import { getPricingBreakdown } from "../../util/pricing";
|
|
8
|
+
import { getCartSubtotal, getPricingBreakdown } from "../../util/pricing";
|
|
9
9
|
import { exportBasketExcel } from "../../util/exportScheduleExcel";
|
|
10
10
|
import { renderDisplayUnit, unitDisplayText } from "../components/UnitDisplay";
|
|
11
11
|
import { clientHeaders, getBoqClientId, readJson } from "../../lib/api/boqClient";
|
|
@@ -73,7 +73,8 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
73
73
|
product_id: item.productId,
|
|
74
74
|
requested_description: item.requestedDescription ?? item.description,
|
|
75
75
|
requested_qty: item.qty,
|
|
76
|
-
requested_uom: item.unit,
|
|
76
|
+
requested_uom: item.unit || null,
|
|
77
|
+
linked_context: item.linkedContext ?? [],
|
|
77
78
|
selected_description: item.description,
|
|
78
79
|
selected_uoi: item.uoi,
|
|
79
80
|
selected_uoi_count: item.uoiCount,
|
|
@@ -95,6 +96,7 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
95
96
|
const conversion = resolved?.conversion;
|
|
96
97
|
if (!conversion || typeof resolved?.line_id !== "string")
|
|
97
98
|
continue;
|
|
99
|
+
const current = cart[resolved.line_id];
|
|
98
100
|
updates[resolved.line_id] = {
|
|
99
101
|
conversionType: conversion.conversion_type,
|
|
100
102
|
requestUomsSupported: conversion.request_uoms_supported,
|
|
@@ -106,12 +108,15 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
106
108
|
roundingRule: conversion.rounding_rule,
|
|
107
109
|
conversionConfidence: conversion.conversion_confidence,
|
|
108
110
|
conversionNotes: conversion.conversion_notes,
|
|
111
|
+
...(current?.qty === 0 && Number.isFinite(conversion.suggested_order_qty) && conversion.suggested_order_qty > 0
|
|
112
|
+
? { qty: conversion.suggested_order_qty, unit: conversion.suggested_order_uom || current.unit }
|
|
113
|
+
: {}),
|
|
109
114
|
};
|
|
110
115
|
}
|
|
111
116
|
if (Object.keys(updates).length > 0)
|
|
112
117
|
onResolveConversions(updates);
|
|
113
118
|
})
|
|
114
|
-
.catch(() =>
|
|
119
|
+
.catch((error) => console.error("Catalogue conversion resolution failed", error));
|
|
115
120
|
}, [cart, onResolveConversions]);
|
|
116
121
|
function clearQtyDraft(lineId) {
|
|
117
122
|
setQtyDrafts((prev) => {
|
|
@@ -206,7 +211,10 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
206
211
|
wasteFactorOverride: Math.max(0, Number.parseFloat(wastePercent) || 0) / 100,
|
|
207
212
|
}),
|
|
208
213
|
}));
|
|
209
|
-
const subtotal =
|
|
214
|
+
const subtotal = getCartSubtotal(items, {
|
|
215
|
+
wasteFactorEnabled: wasteEnabled,
|
|
216
|
+
wasteFactorOverride: Math.max(0, Number.parseFloat(wastePercent) || 0) / 100,
|
|
217
|
+
});
|
|
210
218
|
const vat = subtotal * VAT_RATE;
|
|
211
219
|
const total = subtotal + vat;
|
|
212
220
|
// Once per visit to the basket, with the figures it opened with.
|
|
@@ -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 { getCartSubtotal, getPricingBreakdown } from "../../util/pricing";
|
|
15
16
|
const NO_SELECTION = "__none__";
|
|
16
17
|
const PAGE_SIZE = 24;
|
|
17
18
|
const SORT_OPTIONS = [
|
|
@@ -147,7 +148,9 @@ function fmtTextList(values) {
|
|
|
147
148
|
}
|
|
148
149
|
export function displayLineLabel(item, allItems) {
|
|
149
150
|
void allItems;
|
|
150
|
-
|
|
151
|
+
const original = typeof item.description_original === "string" ? item.description_original.trim() : "";
|
|
152
|
+
const label = typeof item.customer_display_label === "string" ? item.customer_display_label.trim() : "";
|
|
153
|
+
return (label && original.includes(label) ? label : original) || "(no description)";
|
|
151
154
|
}
|
|
152
155
|
const LINK_GROUP_BUBBLE_STYLES = [
|
|
153
156
|
{ background: "#fff7ed", borderColor: "#fdba74", color: "#9a3412" },
|
|
@@ -379,16 +382,18 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
379
382
|
band: bandFor(best?.scores?.overall),
|
|
380
383
|
};
|
|
381
384
|
});
|
|
382
|
-
|
|
385
|
+
// Match BasketStage: catalogue waste defaults are opt-in in the basket UI.
|
|
386
|
+
let totalValue = getCartSubtotal(Object.values(cart), { wasteFactorEnabled: false, wasteFactorOverride: 0 });
|
|
383
387
|
let needsPricingCount = 0;
|
|
384
388
|
for (const row of rows) {
|
|
385
389
|
if (row.unmatchable || row.decision?.include_in_quote !== true || !row.best)
|
|
386
390
|
continue;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
else
|
|
391
|
+
const cartItem = cart[row.item.line_id];
|
|
392
|
+
if (!cartItem || getPricingBreakdown(cartItem).lineTotal == null)
|
|
390
393
|
needsPricingCount += 1;
|
|
391
394
|
}
|
|
395
|
+
if (needsPricingCount > 0)
|
|
396
|
+
totalValue = null;
|
|
392
397
|
const hasBestAlternative = (row) => !row.unmatchable && !row.best && row.decision?.technical_classification !== "no_suitable_candidate" && recommendedCandidates(row.result).length > 0;
|
|
393
398
|
const previewCandidate = (row) => row.best ?? recommendedCandidates(row.result)[0];
|
|
394
399
|
const reviewRequiredRows = rows.filter((row) => !row.unmatchable && !row.best);
|
|
@@ -556,6 +561,12 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
556
561
|
description: row.item.description_original,
|
|
557
562
|
unit: row.item.unit,
|
|
558
563
|
qty: row.qty,
|
|
564
|
+
linkedContext: row.item.linked_group_role === "dependent" && row.item.linked_group_id
|
|
565
|
+
? lineItems
|
|
566
|
+
.filter((line) => line.linked_group_id === row.item.linked_group_id && line.linked_group_role === "anchor")
|
|
567
|
+
.slice(0, 1)
|
|
568
|
+
.map((anchor) => ({ description: anchor.description_original, qty: anchor.qty, unit: anchor.unit, role: "anchor" }))
|
|
569
|
+
: undefined,
|
|
559
570
|
});
|
|
560
571
|
}
|
|
561
572
|
// All matched items, including review-required ones, land in the basket automatically
|
|
@@ -571,16 +582,14 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
571
582
|
onCartSeeded();
|
|
572
583
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
573
584
|
}, [cartSeeded]);
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
585
|
+
// The cart is authoritative once a row has been added. Rebuilding an existing row from
|
|
586
|
+
// scheduleResult can overwrite contextual quantities resolved in the basket when the
|
|
587
|
+
// user navigates back here. Only seed rows that are not already present.
|
|
577
588
|
useEffect(() => {
|
|
578
589
|
for (const row of rows) {
|
|
579
590
|
const existing = cart[row.item.line_id];
|
|
580
|
-
if (!existing)
|
|
581
|
-
|
|
582
|
-
const refreshed = toCartItem(row);
|
|
583
|
-
onAddToCart(refreshedCartItem(refreshed, existing));
|
|
591
|
+
if (!existing && !row.unmatchable && row.best)
|
|
592
|
+
onAddToCart(toCartItem(row));
|
|
584
593
|
}
|
|
585
594
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
586
595
|
}, [scheduleResult]);
|
|
@@ -664,7 +673,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
664
673
|
})();
|
|
665
674
|
const activeReviewCartMatches = !!activeReviewRow?.best && cart[activeReviewRow.item.line_id]?.productId === activeReviewRow.best.product_id;
|
|
666
675
|
const activeReviewHasPendingUpdate = !!activeReviewRow?.best && !!cart[activeReviewRow.item.line_id] && !activeReviewCartMatches;
|
|
667
|
-
return (_jsxs("div", { className: "wizard-step entry-stage", children: [_jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsx("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: internal ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: "Your Results" }), _jsxs("span", { className: "summary-chip summary-chip--scope", children: [_jsx("strong", { children: fmtCurrency(totalValue) }), " Est. total"] })] }), _jsx("p", { children: "Each line item from your schedule matched against your catalogue, ranked by confidence." })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("h1", { children: "Review your matched products" }), _jsxs("p", { children: ["Check you're happy with these items, then click ", qzaQuoteRequest ? "View Quote" : "View Basket", " to continue."] })] }), _jsxs("button", { type: "button", className: "results-total-action", onClick: onGoToBasket, "aria-label": qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [_jsx("small", { children: qzaQuoteRequest ? "View Quote" : "View Basket" }), _jsx("strong", { children: fmtCurrency(totalValue) })] }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] })] })) }), _jsxs("div", { className: "quick-order-panel", style: { paddingTop: 16 }, children: [debugEnabled && (_jsx("div", { className: "results-summary", style: { marginBottom: 12 }, children: _jsx("button", { type: "button", className: "bom-toolbar-btn", "aria-pressed": showTimings, onClick: () => setShowTimings((value) => !value), children: showTimings ? "Hide timings" : "Show timings" }) })), debugEnabled && showTimings && _jsx(TimingBreakdown, { ...timings }), !qzaQuoteRequest && (_jsx("div", { className: "results-toolbar-row", children: !activeReviewRow && (_jsxs("div", { className: "results-summary", style: { marginBottom: 0 }, children: [needsPricingCount > 0 && (_jsxs("span", { className: "summary-chip summary-chip--neutral", children: [_jsx("strong", { children: needsPricingCount }), " need pricing"] })), _jsxs("div", { style: {
|
|
676
|
+
return (_jsxs("div", { className: "wizard-step entry-stage", children: [_jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsx("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: internal ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: "Your Results" }), _jsxs("span", { className: "summary-chip summary-chip--scope", children: [_jsx("strong", { children: totalValue == null ? "Calculating…" : fmtCurrency(totalValue) }), " Est. total"] })] }), _jsx("p", { children: "Each line item from your schedule matched against your catalogue, ranked by confidence." })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("h1", { children: "Review your matched products" }), _jsxs("p", { children: ["Check you're happy with these items, then click ", qzaQuoteRequest ? "View Quote" : "View Basket", " to continue."] })] }), _jsxs("button", { type: "button", className: "results-total-action", onClick: onGoToBasket, "aria-label": qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [_jsx("small", { children: qzaQuoteRequest ? "View Quote" : "View Basket" }), _jsx("strong", { children: totalValue == null ? "Calculating…" : fmtCurrency(totalValue) })] }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] })] })) }), _jsxs("div", { className: "quick-order-panel", style: { paddingTop: 16 }, children: [debugEnabled && (_jsx("div", { className: "results-summary", style: { marginBottom: 12 }, children: _jsx("button", { type: "button", className: "bom-toolbar-btn", "aria-pressed": showTimings, onClick: () => setShowTimings((value) => !value), children: showTimings ? "Hide timings" : "Show timings" }) })), debugEnabled && showTimings && _jsx(TimingBreakdown, { ...timings }), !qzaQuoteRequest && (_jsx("div", { className: "results-toolbar-row", children: !activeReviewRow && (_jsxs("div", { className: "results-summary", style: { marginBottom: 0 }, children: [needsPricingCount > 0 && (_jsxs("span", { className: "summary-chip summary-chip--neutral", children: [_jsx("strong", { children: needsPricingCount }), " need pricing"] })), _jsxs("div", { style: {
|
|
668
677
|
display: "flex",
|
|
669
678
|
gap: 8,
|
|
670
679
|
flexWrap: "wrap",
|