@metron-labs/quote-react 0.1.14 โ 0.1.16
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 +222 -222
- package/dist/app/stages/BasketStage.js +78 -9
- package/dist/app/stages/ScheduleResultsStage.d.ts +1 -1
- package/dist/app/stages/ScheduleResultsStage.js +87 -30
- package/dist/embed/quote.js +6719 -6719
- package/dist/lib/boq/toCartItem.js +6 -1
- package/dist/lib/cart/CartContext.js +10 -1
- package/dist/lib/productImage.js +19 -19
- package/dist/styles.css +6708 -6708
- package/dist/util/exportScheduleExcel.js +1 -4
- package/dist/util/pricing.js +5 -17
- package/package.json +53 -53
|
@@ -41,6 +41,7 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
41
41
|
const items = Object.values(cart);
|
|
42
42
|
const [reference, setReference] = useState("");
|
|
43
43
|
const [quoteRequestSent, setQuoteRequestSent] = useState(false);
|
|
44
|
+
const [quoteRequestSending, setQuoteRequestSending] = useState(false);
|
|
44
45
|
const [quoteContactError, setQuoteContactError] = useState("");
|
|
45
46
|
const [qtyDrafts, setQtyDrafts] = useState({});
|
|
46
47
|
const [sellUnitsDrafts, setSellUnitsDrafts] = useState({});
|
|
@@ -59,7 +60,26 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
59
60
|
void fetch(apiUrl("/api/catalogue/resolve-conversions"), {
|
|
60
61
|
method: "POST",
|
|
61
62
|
headers: { "Content-Type": "application/json", ...clientHeaders() },
|
|
62
|
-
body: JSON.stringify({
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
lines: unresolved.map((item) => ({
|
|
65
|
+
line_id: item.lineId,
|
|
66
|
+
product_id: item.productId,
|
|
67
|
+
requested_description: item.requestedDescription ?? item.description,
|
|
68
|
+
requested_qty: item.qty,
|
|
69
|
+
requested_uom: item.unit,
|
|
70
|
+
selected_description: item.description,
|
|
71
|
+
selected_uoi: item.uoi,
|
|
72
|
+
selected_uoi_count: item.uoiCount,
|
|
73
|
+
selected_conversion: {
|
|
74
|
+
sales_uom_normalised: item.salesUomNormalised,
|
|
75
|
+
conversion_type: item.conversionType,
|
|
76
|
+
sales_unit_size: item.salesUnitSize,
|
|
77
|
+
sales_unit_size_uom: item.salesUnitSizeUom,
|
|
78
|
+
coverage_qty: item.coverageQty,
|
|
79
|
+
coverage_uom: item.coverageUom,
|
|
80
|
+
},
|
|
81
|
+
})),
|
|
82
|
+
}),
|
|
63
83
|
})
|
|
64
84
|
.then((response) => (response.ok ? readJson(response) : null))
|
|
65
85
|
.then((payload) => {
|
|
@@ -120,13 +140,44 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
120
140
|
onUpdateSellUnits(lineId, Math.max(0, current + delta));
|
|
121
141
|
clearSellUnitsDraft(lineId);
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
// Sends the basket to the merchant as a quote request (POST /v1/quote-requests). Success only
|
|
144
|
+
// shows once the API confirms the email went out; otherwise the customer is asked to retry.
|
|
145
|
+
async function submitQuoteRequest() {
|
|
146
|
+
const email = reference.trim();
|
|
147
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
125
148
|
setQuoteContactError("Enter a valid email address so we can send your better price.");
|
|
126
149
|
return;
|
|
127
150
|
}
|
|
128
151
|
setQuoteContactError("");
|
|
129
|
-
|
|
152
|
+
setQuoteRequestSending(true);
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(apiUrl("/api/quote-requests"), {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json", ...clientHeaders() },
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
email,
|
|
159
|
+
// The quantity the basket shows to order: sale units where it could work them out,
|
|
160
|
+
// otherwise what the customer asked for.
|
|
161
|
+
items: pricedItems.map(({ item, pricing }) => {
|
|
162
|
+
const quantity = pricing.requiredSellUnits ?? pricing.requestedAmount;
|
|
163
|
+
const unit = pricing.requiredSellUnits != null ? pricing.sellUnit : pricing.requestedUnit;
|
|
164
|
+
return { product_id: item.productId, description: item.description, quantity, unit: unitDisplayText(unit, quantity) };
|
|
165
|
+
}),
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
const payload = await readJson(res);
|
|
170
|
+
setQuoteContactError(res.status === 429 ? "You've sent a few requests already. Please try again in a few minutes." : (payload?.message ?? "Your request couldn't be sent. Please try again."));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
setQuoteRequestSent(true);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
setQuoteContactError("Your request couldn't be sent. Check your connection and try again.");
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
setQuoteRequestSending(false);
|
|
180
|
+
}
|
|
130
181
|
}
|
|
131
182
|
const pricedItems = items.map((item) => ({
|
|
132
183
|
item,
|
|
@@ -141,17 +192,35 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
|
|
|
141
192
|
const totalPages = Math.max(1, Math.ceil(pricedItems.length / PAGE_SIZE));
|
|
142
193
|
const currentPage = Math.min(page, totalPages);
|
|
143
194
|
const pagedItems = pricedItems.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
|
|
144
|
-
return (_jsx("div", { className: `wizard-step entry-stage${qzaBasketLayout ? " basket-layout--qza6-mra4" : ""}`, children: _jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsxs("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: [internal && _jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "basket-hero-copy", children: [qzaBasketLayout && _jsxs("button", { type: "button", className: "cta-btn cta-btn-secondary basket-hero-back", onClick: onBack, children: [_jsx(BackArrow, {}), _jsx("span", { children: "Back to results" })] }), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: internal ? "Your Selection" : qzaBasketLayout ? "Request a
|
|
195
|
+
return (_jsx("div", { className: `wizard-step entry-stage${qzaBasketLayout ? " basket-layout--qza6-mra4" : ""}`, children: _jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsxs("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: [internal && _jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "basket-hero-copy", children: [qzaBasketLayout && (_jsxs("button", { type: "button", className: "cta-btn cta-btn-secondary basket-hero-back", onClick: onBack, children: [_jsx(BackArrow, {}), _jsx("span", { children: "Back to results" })] })), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: internal ? "Your Selection" : qzaBasketLayout ? "Request a quote" : "Your Basket" }), internal && (_jsxs("span", { className: "summary-chip summary-chip--scope", children: [_jsx("strong", { children: items.length }), " ", items.length === 1 ? "item" : "items"] }))] }), _jsx("p", { children: internal
|
|
196
|
+
? "Review the items you've added before exporting them for review."
|
|
197
|
+
: qzaBasketLayout
|
|
198
|
+
? "Check your items & quantities below, and we'll do our best to beat list prices."
|
|
199
|
+
: "Review your quantities and order summary before you buy." })] })] }), _jsx("div", { className: "quick-order-panel", style: { paddingTop: 16 }, children: items.length === 0 ? (_jsxs("div", { className: "quick-order-history-empty", children: [_jsx("div", { className: "basket-empty-icon", "aria-hidden": "true", children: internal ? "๐" : "๐งบ" }), _jsx("p", { children: internal ? "Your selection is empty." : qzaBasketLayout ? "Your quote request is empty." : "Your basket is empty." }), _jsx("p", { className: "basket-empty-hint", children: qzaBasketLayout ? "Go back to your results and add items for your quote." : "Go back to your results and add quoted items." }), _jsx("div", { className: "btn-row", style: { marginTop: 20, justifyContent: "center" }, children: _jsx("button", { className: "cta-btn", style: { width: "auto", maxWidth: 280 }, onClick: onBack, "aria-label": "Back to results", children: _jsx("span", { children: "Back to Results" }) }) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "basket-layout", children: [_jsxs("div", { className: "basket-main", children: [_jsx("div", { className: "basket-list", "aria-label": internal ? "Selection items" : qzaBasketLayout ? "Quote request items" : "Basket items", children: pagedItems.map(({ item, pricing }) => {
|
|
145
200
|
const requestedQtyValue = qtyDrafts[item.lineId] ?? String(pricing.requestedAmount);
|
|
146
201
|
const requestedLocked = pricing.requestedUnit === pricing.sellUnit || pricing.costingMode === "review";
|
|
147
202
|
const rawDiffersFromOrder = pricing.requiredSellUnits == null || pricing.requestedAmount !== pricing.requiredSellUnits || pricing.requestedUnit !== pricing.sellUnit;
|
|
148
|
-
return (_jsxs("article", { className: "basket-card", children: [_jsxs("div", { className: "basket-card-top", children: [_jsx("img", { className: "basket-card-thumb", src: productImageSrc(item.description, item.productId, item.pdpPhotoUrl), alt: "", width: 48, height: 48 }), _jsxs("div", { className: "basket-card-info", children: [_jsx("div", { className: "bom-component-name", children: item.description }), rawDiffersFromOrder && pricing.costingMode !== "review" && (_jsxs("button", { type: "button", className: "basket-calc-toggle", onClick: () => setCalcOpen((prev) => ({
|
|
203
|
+
return (_jsxs("article", { className: "basket-card", children: [_jsxs("div", { className: "basket-card-top", children: [_jsx("img", { className: "basket-card-thumb", src: productImageSrc(item.description, item.productId, item.pdpPhotoUrl), alt: "", width: 48, height: 48 }), _jsxs("div", { className: "basket-card-info", children: [_jsx("div", { className: "bom-component-name", children: item.description }), rawDiffersFromOrder && pricing.costingMode !== "review" && (_jsxs("button", { type: "button", className: "basket-calc-toggle", onClick: () => setCalcOpen((prev) => ({
|
|
204
|
+
...prev,
|
|
205
|
+
[item.lineId]: !prev[item.lineId],
|
|
206
|
+
})), "aria-expanded": !!calcOpen[item.lineId], children: ["How we calculated this", _jsx("span", { className: `basket-calc-toggle-chevron${calcOpen[item.lineId] ? " basket-calc-toggle-chevron--open" : ""}`, children: _jsx(ChevronIcon, {}) })] }))] }), _jsx("div", { className: "basket-card-actions", children: _jsx("button", { type: "button", className: "mf-remove-small-btn basket-card-remove", onClick: () => onRemove(item.lineId), "aria-label": `Remove ${item.description}`, children: _jsx(TrashIcon, {}) }) })] }), internal && (_jsxs("div", { className: "results-detail-block basket-internal-detail", children: [_jsxs("dl", { className: "results-detail-grid", children: [_jsxs("div", { children: [_jsx("dt", { children: "Conversion type" }), _jsx("dd", { children: item.conversionType ?? "-" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Conversion confidence" }), _jsx("dd", { children: item.conversionConfidence ?? "-" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Sales unit" }), _jsx("dd", { children: item.salesUnitSize != null ? (_jsxs(_Fragment, { children: [item.salesUnitSize, " ", renderDisplayUnit(item.salesUnitSizeUom, item.salesUnitSize)] })) : ("-") })] }), _jsxs("div", { children: [_jsx("dt", { children: "Coverage" }), _jsx("dd", { children: item.coverageQty != null ? (_jsxs(_Fragment, { children: [item.coverageQty, " ", renderDisplayUnit(item.coverageUom, item.coverageQty)] })) : ("-") })] }), _jsxs("div", { children: [_jsx("dt", { children: "Waste factor" }), _jsx("dd", { children: item.wasteFactorDefault ?? "-" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Rounding rule" }), _jsx("dd", { children: item.roundingRule ?? "-" })] })] }), item.conversionNotes && _jsx("p", { className: "results-rerank-note", children: item.conversionNotes })] })), pricing.costingMode === "review" ? (_jsxs("p", { className: "basket-calc-text basket-calc-text--review", children: [_jsx("strong", { children: "Quantity needs confirmation." }), " ", reviewMessage(pricing)] })) : rawDiffersFromOrder && calcOpen[item.lineId] ? (_jsx("p", { className: "basket-calc-text", children: orderSummary(item, pricing) })) : null, _jsxs("div", { className: "basket-card-stats", children: [_jsxs("div", { className: "basket-stat", children: [_jsx("span", { className: "basket-stat-label", children: "Requested qty" }), _jsxs("div", { className: "basket-stat-value", children: [_jsxs("div", { className: "basket-qty-field", children: [_jsx("input", { className: "bom-qty-edit", type: "number", inputMode: "decimal", min: "0", placeholder: "-", value: requestedQtyValue, disabled: requestedLocked, onChange: (e) => setQtyDrafts((prev) => ({
|
|
207
|
+
...prev,
|
|
208
|
+
[item.lineId]: e.target.value,
|
|
209
|
+
})), onBlur: (e) => commitRequestedQty(item.lineId, e.target.value), onKeyDown: (e) => {
|
|
149
210
|
if (e.key === "Enter")
|
|
150
211
|
e.target.blur();
|
|
151
|
-
}, "aria-label": `${item.description} requested quantity` }), _jsxs("div", { className: "basket-qty-arrows", children: [_jsx("button", { type: "button", className: "basket-qty-arrow", disabled: requestedLocked, onClick: () => stepRequestedQty(item.lineId, parseFloat(requestedQtyValue) || 0, 1), "aria-label": `Increase ${item.description} quantity`, children: _jsx(ChevronIcon, {}) }), _jsx("button", { type: "button", className: "basket-qty-arrow basket-qty-arrow--down", disabled: requestedLocked, onClick: () => stepRequestedQty(item.lineId, parseFloat(requestedQtyValue) || 0, -1), "aria-label": `Decrease ${item.description} quantity`, children: _jsx(ChevronIcon, {}) })] })] }), _jsx("span", { className: "basket-metric-unit", children: renderDisplayUnit(pricing.requestedUnit, parseFloat(requestedQtyValue) || 0) })] })] }), _jsxs("div", { className: "basket-stat basket-stat--right", children: [_jsx("span", { className: "basket-stat-label", children: "Order qty" }), _jsxs("div", { className: "basket-stat-value", children: [_jsxs("div", { className: "basket-qty-field", children: [_jsx("input", { className: "bom-qty-edit", type: "number", inputMode: "decimal", min: "0", placeholder: "-", value: sellUnitsDrafts[item.lineId] ?? String(pricing.requiredSellUnits ?? ""), onChange: (e) => setSellUnitsDrafts((prev) => ({
|
|
212
|
+
}, "aria-label": `${item.description} requested quantity` }), _jsxs("div", { className: "basket-qty-arrows", children: [_jsx("button", { type: "button", className: "basket-qty-arrow", disabled: requestedLocked, onClick: () => stepRequestedQty(item.lineId, parseFloat(requestedQtyValue) || 0, 1), "aria-label": `Increase ${item.description} quantity`, children: _jsx(ChevronIcon, {}) }), _jsx("button", { type: "button", className: "basket-qty-arrow basket-qty-arrow--down", disabled: requestedLocked, onClick: () => stepRequestedQty(item.lineId, parseFloat(requestedQtyValue) || 0, -1), "aria-label": `Decrease ${item.description} quantity`, children: _jsx(ChevronIcon, {}) })] })] }), _jsx("span", { className: "basket-metric-unit", children: renderDisplayUnit(pricing.requestedUnit, parseFloat(requestedQtyValue) || 0) })] })] }), _jsxs("div", { className: "basket-stat basket-stat--right", children: [_jsx("span", { className: "basket-stat-label", children: "Order qty" }), _jsxs("div", { className: "basket-stat-value", children: [_jsxs("div", { className: "basket-qty-field", children: [_jsx("input", { className: "bom-qty-edit", type: "number", inputMode: "decimal", min: "0", placeholder: "-", value: sellUnitsDrafts[item.lineId] ?? String(pricing.requiredSellUnits ?? ""), onChange: (e) => setSellUnitsDrafts((prev) => ({
|
|
213
|
+
...prev,
|
|
214
|
+
[item.lineId]: e.target.value,
|
|
215
|
+
})), onBlur: (e) => commitSellUnits(item.lineId, e.target.value), onKeyDown: (e) => {
|
|
152
216
|
if (e.key === "Enter")
|
|
153
217
|
e.target.blur();
|
|
154
218
|
}, "aria-label": `${item.description} order quantity` }), _jsxs("div", { className: "basket-qty-arrows", children: [_jsx("button", { type: "button", className: "basket-qty-arrow", onClick: () => stepSellUnits(item.lineId, parseFloat(sellUnitsDrafts[item.lineId] ?? String(pricing.requiredSellUnits ?? "0")) || 0, 1), "aria-label": `Increase ${item.description} order quantity`, children: _jsx(ChevronIcon, {}) }), _jsx("button", { type: "button", className: "basket-qty-arrow basket-qty-arrow--down", onClick: () => stepSellUnits(item.lineId, parseFloat(sellUnitsDrafts[item.lineId] ?? String(pricing.requiredSellUnits ?? "0")) || 0, -1), "aria-label": `Decrease ${item.description} order quantity`, children: _jsx(ChevronIcon, {}) })] })] }), _jsx("span", { className: "basket-metric-unit", children: renderDisplayUnit(pricing.sellUnit, parseFloat(sellUnitsDrafts[item.lineId] ?? String(pricing.requiredSellUnits ?? "0")) || 0) })] })] }), _jsxs("div", { className: "basket-stat basket-stat--total", children: [_jsx("span", { className: "basket-stat-label", children: "Total" }), _jsxs("div", { className: "basket-stat-value", children: [_jsx("strong", { children: pricing.lineTotal != null ? fmtCurrency(pricing.lineTotal) : "-" }), pricing.lineTotal != null && _jsx("span", { className: "basket-metric-unit", children: "ex. VAT" })] })] })] })] }, item.lineId));
|
|
155
|
-
}) }), totalPages > 1 && (_jsxs("div", { className: "results-pagination", children: [_jsxs("span", { className: "results-pagination-count", children: ["Showing ", (currentPage - 1) * PAGE_SIZE + 1, "\u2013", Math.min(currentPage * PAGE_SIZE, pricedItems.length), " of ", pricedItems.length] }), _jsxs("div", { className: "results-pagination-controls", children: [_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage - 1), disabled: currentPage === 1, children: "Previous" }), _jsxs("span", { className: "results-pagination-page", children: ["Page ", currentPage, " of ", totalPages] }), _jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage + 1), disabled: currentPage === totalPages, children: "Next" })] })] }))] }), _jsxs("aside", { className: "basket-sidebar", children: [_jsx("div", { className: "basket-sidebar-title", children: qzaBasketLayout ? "Quote request summary" : "Order summary" }), _jsxs("div", { className: "basket-sidebar-row", children: [_jsx("span", { children: qzaBasketLayout ? "
|
|
156
|
-
|
|
219
|
+
}) }), totalPages > 1 && (_jsxs("div", { className: "results-pagination", children: [_jsxs("span", { className: "results-pagination-count", children: ["Showing ", (currentPage - 1) * PAGE_SIZE + 1, "\u2013", Math.min(currentPage * PAGE_SIZE, pricedItems.length), " of ", pricedItems.length] }), _jsxs("div", { className: "results-pagination-controls", children: [_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage - 1), disabled: currentPage === 1, children: "Previous" }), _jsxs("span", { className: "results-pagination-page", children: ["Page ", currentPage, " of ", totalPages] }), _jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage + 1), disabled: currentPage === totalPages, children: "Next" })] })] }))] }), _jsxs("aside", { className: "basket-sidebar", children: [_jsx("div", { className: "basket-sidebar-title", children: qzaBasketLayout ? "Quote request summary" : "Order summary" }), _jsxs("div", { className: "basket-sidebar-row", children: [_jsx("span", { children: qzaBasketLayout ? "List price (ex. VAT)" : "Subtotal (ex. VAT)" }), _jsx("span", { children: fmtCurrency(subtotal) })] }), _jsxs("div", { className: "basket-sidebar-row", children: [_jsx("span", { children: qzaBasketLayout ? "VAT (20%)" : "VAT (20%)" }), _jsx("span", { children: fmtCurrency(vat) })] }), _jsxs("div", { className: "basket-sidebar-row basket-sidebar-row--total", children: [_jsx("span", { children: qzaBasketLayout ? "List price (inc. VAT)" : "Total" }), _jsx("span", { children: fmtCurrency(total) })] }), !qzaBasketLayout && (_jsx("div", { className: "basket-summary-waste", children: _jsxs("div", { className: "basket-waste-control", children: [_jsxs("label", { className: "basket-global-waste", children: [_jsx("input", { type: "checkbox", checked: wasteEnabled, onChange: (e) => setWasteEnabled(e.target.checked) }), _jsx("span", { children: "Add waste factor" })] }), _jsxs("label", { className: "basket-global-waste-percent", children: [_jsx("input", { type: "number", inputMode: "decimal", min: "0", step: "0.1", value: wastePercent, onChange: (e) => setWastePercent(e.target.value), disabled: !wasteEnabled, "aria-label": "Waste factor percent" }), _jsx("span", { children: "%" })] })] }) })), _jsx("label", { htmlFor: `${uid}-basket-reference`, className: "basket-sidebar-label", children: qzaBasketLayout ? "Enter email for your sharper quote" : "Your reference (optional)" }), _jsx("input", { id: `${uid}-basket-reference`, type: qzaBasketLayout ? "email" : "text", className: "basket-sidebar-input", value: reference, onChange: (e) => {
|
|
220
|
+
setReference(e.target.value);
|
|
221
|
+
if (qzaBasketLayout)
|
|
222
|
+
setQuoteContactError("");
|
|
223
|
+
}, placeholder: qzaBasketLayout ? "you@example.com" : "PO number or job reference" }), qzaBasketLayout && quoteContactError && (_jsx("p", { className: "basket-quote-request-error", role: "alert", children: quoteContactError })), qzaBasketLayout ? (quoteRequestSent ? (_jsxs("div", { className: "basket-quote-request-success", role: "status", children: [_jsx("strong", { children: "Better price requested" }), _jsx("span", { children: "We'll use this email address to prepare your better price." })] })) : (_jsx("button", { type: "button", className: "cta-btn", onClick: () => void submitQuoteRequest(), disabled: quoteRequestSending, "aria-busy": quoteRequestSending, children: _jsx("span", { children: quoteRequestSending ? "Sendingโฆ" : "Request our best quote" }) }))) : (_jsx("button", { className: "cta-btn", onClick: () => void exportBasketExcel(items), "aria-label": internal ? "Download selection" : "Download basket", children: _jsx("span", { children: internal ? "Download Quote" : "Buy Now" }) })), _jsxs("p", { className: "basket-sidebar-disclaimer", children: [_jsx("span", { "aria-hidden": "true", children: "\u26A0\uFE0F" }), qzaBasketLayout
|
|
224
|
+
? "This is a quote request, not an order. We'll confirm our best price, availability and delivery before you approve."
|
|
225
|
+
: "Prices and availability are estimates and should be confirmed before procurement."] })] })] }), _jsx("div", { className: "btn-row results-actions", children: _jsxs("button", { className: "cta-btn cta-btn-secondary", style: { flex: "0 1 30%", gap: 10, justifyContent: "center" }, onClick: onBack, "aria-label": "Back to results", children: [_jsx(BackArrow, {}), _jsx("span", { children: "Back to results" })] }) })] })) })] }) }));
|
|
157
226
|
}
|
|
@@ -14,5 +14,5 @@ interface ResultsStageProps {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function candidateFromCart(item: CartItem): MatchCandidate;
|
|
16
16
|
export declare function displayLineLabel(item: LineItem, allItems: LineItem[]): string;
|
|
17
|
-
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal
|
|
17
|
+
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal }: ResultsStageProps): import("react").JSX.Element;
|
|
18
18
|
export {};
|
|
@@ -115,12 +115,8 @@ function fmtTextList(values) {
|
|
|
115
115
|
return items.length > 0 ? items.join(", ") : "-";
|
|
116
116
|
}
|
|
117
117
|
export function displayLineLabel(item, allItems) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (siblings.length <= 1 || !item.procurement_intent || description === item.procurement_intent) {
|
|
121
|
-
return description || "(no description)";
|
|
122
|
-
}
|
|
123
|
-
return `${description} -> ${item.procurement_intent}`;
|
|
118
|
+
void allItems;
|
|
119
|
+
return item.customer_display_label?.trim() || item.description_original || "(no description)";
|
|
124
120
|
}
|
|
125
121
|
const LINK_GROUP_BUBBLE_STYLES = [
|
|
126
122
|
{ background: "#fff7ed", borderColor: "#fdba74", color: "#9a3412" },
|
|
@@ -164,9 +160,7 @@ function curatedCandidates(candidates, result, selectedProductId) {
|
|
|
164
160
|
const recommendedIds = result?.rerank?.recommendedCandidateIds ?? [];
|
|
165
161
|
if (recommendedIds.length === 0) {
|
|
166
162
|
const top5 = candidates.slice(0, 5);
|
|
167
|
-
const pinnedSelected = !top5.some((candidate) => candidate.product_id === selectedProductId)
|
|
168
|
-
? candidates.find((candidate) => candidate.product_id === selectedProductId)
|
|
169
|
-
: undefined;
|
|
163
|
+
const pinnedSelected = !top5.some((candidate) => candidate.product_id === selectedProductId) ? candidates.find((candidate) => candidate.product_id === selectedProductId) : undefined;
|
|
170
164
|
return pinnedSelected ? [...top5, pinnedSelected] : top5;
|
|
171
165
|
}
|
|
172
166
|
const byId = new Map(candidates.map((candidate) => [candidate.product_id, candidate]));
|
|
@@ -258,9 +252,7 @@ function AdjudicationDebugBlock({ result }) {
|
|
|
258
252
|
function DetailedDecision({ item, decision, result, showRetrievalDebug, reviewOnly = false, linkedGroupMeta, }) {
|
|
259
253
|
if (!decision)
|
|
260
254
|
return null;
|
|
261
|
-
const reviewLabel = decision.technical_classification === "no_suitable_candidate"
|
|
262
|
-
? "Exact match not found:"
|
|
263
|
-
: `AI review (${decision.confidence ?? "confidence not stated"}):`;
|
|
255
|
+
const reviewLabel = decision.technical_classification === "no_suitable_candidate" ? "Exact match not found:" : `AI review (${decision.confidence ?? "confidence not stated"}):`;
|
|
264
256
|
if (reviewOnly) {
|
|
265
257
|
return decision.reason ? (_jsxs("div", { className: "results-detail-block", children: [_jsxs("p", { className: `basket-calc-text basket-calc-text--review`, children: [_jsx("strong", { children: reviewLabel }), "\u00A0", decision.reason] }), hasVisibleLinkedGroup(item) ? (_jsxs("p", { className: "results-row-meta", style: { marginTop: 8 }, children: ["Linked context: ", linkedGroupMeta ? `Link ${linkedGroupMeta.token} - ` : "", linkedGroupSummary(item)] })) : null] })) : null;
|
|
266
258
|
}
|
|
@@ -284,7 +276,11 @@ function CandidateBreakdown({ item, candidates, result, decision, unmatchable, s
|
|
|
284
276
|
const shown = showAll || internal || detailMode === "detailed" ? candidates : curatedCandidates(candidates, result, selectedProductId);
|
|
285
277
|
return (_jsxs(_Fragment, { children: [detailBlock, _jsxs("table", { className: "score-breakdown-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Product" }), _jsx("th", { children: "Dims (mm)" }), _jsx("th", { children: "Price" }), showScoreColumns && (_jsxs(_Fragment, { children: [_jsx("th", { children: "dimension" }), _jsx("th", { children: "grade" }), _jsx("th", { children: "brand" }), _jsx("th", { children: "fuzzy" }), _jsx("th", { children: "bm25" }), _jsx("th", { children: "overall" })] })), _jsx("th", { "aria-label": "Actions" })] }) }), _jsx("tbody", { children: shown.map((c, i) => {
|
|
286
278
|
const isCurrent = c.product_id === selectedProductId;
|
|
287
|
-
return (_jsxs("tr", { className: isCurrent ? "score-breakdown-row--best" : "", children: [_jsxs("td", { children: [_jsx("div", { children: c.description }), _jsx("div", { className: "results-row-meta", children: c.product_id })] }), _jsx("td", { children: _jsx("span", { className: "unit-badge", children: fmtMeasurements(c.measurements_mm ?? c.debug?.candidate.measurements_mm) }) }), _jsx("td", { children: fmtPrice(c.price, c.price_basis) }), showScoreColumns && (_jsxs(_Fragment, { children: [_jsx("td", { children: (c.scores?.dimension ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.grade ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.brand ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.fuzzy ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.bm25 ?? 0).toFixed(2) }), _jsx("td", { children: _jsxs("div", { style: {
|
|
279
|
+
return (_jsxs("tr", { className: isCurrent ? "score-breakdown-row--best" : "", children: [_jsxs("td", { children: [_jsx("div", { children: c.description }), _jsx("div", { className: "results-row-meta", children: c.product_id })] }), _jsx("td", { children: _jsx("span", { className: "unit-badge", children: fmtMeasurements(c.measurements_mm ?? c.debug?.candidate.measurements_mm) }) }), _jsx("td", { children: fmtPrice(c.price, c.price_basis) }), showScoreColumns && (_jsxs(_Fragment, { children: [_jsx("td", { children: (c.scores?.dimension ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.grade ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.brand ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.fuzzy ?? 0).toFixed(2) }), _jsx("td", { children: (c.scores?.bm25 ?? 0).toFixed(2) }), _jsx("td", { children: _jsxs("div", { style: {
|
|
280
|
+
display: "inline-flex",
|
|
281
|
+
alignItems: "center",
|
|
282
|
+
gap: 6,
|
|
283
|
+
}, children: [_jsx(ConfChip, { band: bandFor(c.scores?.overall) }), _jsx("span", { className: "results-overall-cell", children: (c.scores?.overall ?? 0).toFixed(2) })] }) })] })), _jsx("td", { children: _jsxs("label", { className: "switch-toggle", title: isCurrent ? "Currently selected" : "Use this candidate", children: [_jsx("input", { type: "checkbox", checked: isCurrent, onChange: () => {
|
|
288
284
|
if (isCurrent)
|
|
289
285
|
onClear?.();
|
|
290
286
|
else
|
|
@@ -292,7 +288,7 @@ function CandidateBreakdown({ item, candidates, result, decision, unmatchable, s
|
|
|
292
288
|
}, "aria-label": isCurrent ? `${c.description} selected` : `Use ${c.description}` }), _jsx("span", { className: "switch-toggle-track", "aria-hidden": "true" })] }) })] }, `${c.product_id}-${i}`));
|
|
293
289
|
}) })] })] }));
|
|
294
290
|
}
|
|
295
|
-
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal = false
|
|
291
|
+
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal = false }) {
|
|
296
292
|
const qzaQuoteRequest = !internal && getBoqClientId() === "QZA6-MRA4";
|
|
297
293
|
const customerCollection = qzaQuoteRequest ? "quote" : "basket";
|
|
298
294
|
const { results, lineItems, parseReport, timings } = scheduleResult;
|
|
@@ -341,7 +337,15 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
341
337
|
const matched = pick === NO_SELECTION ? undefined : (pickOverrideCandidates[item.line_id] ?? (savedCartMatch && savedCartMatch.productId !== automaticMatch?.product_id ? candidateFromCart(savedCartMatch) : automaticMatch));
|
|
342
338
|
const best = unmatchable ? undefined : matched;
|
|
343
339
|
const qty = item.qty ?? 0;
|
|
344
|
-
return {
|
|
340
|
+
return {
|
|
341
|
+
item,
|
|
342
|
+
result,
|
|
343
|
+
decision,
|
|
344
|
+
unmatchable,
|
|
345
|
+
best,
|
|
346
|
+
qty,
|
|
347
|
+
band: bandFor(best?.scores?.overall),
|
|
348
|
+
};
|
|
345
349
|
});
|
|
346
350
|
let totalValue = 0;
|
|
347
351
|
let needsPricingCount = 0;
|
|
@@ -384,7 +388,12 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
384
388
|
const { top, left, width, height } = row.getBoundingClientRect();
|
|
385
389
|
const actionCell = row.querySelector(".bom-toolbar-btn")?.closest("td");
|
|
386
390
|
const actionLeft = actionCell?.getBoundingClientRect().left;
|
|
387
|
-
setHoveredRowBounds({
|
|
391
|
+
setHoveredRowBounds({
|
|
392
|
+
top,
|
|
393
|
+
left,
|
|
394
|
+
width: actionLeft ? actionLeft - left : width,
|
|
395
|
+
height,
|
|
396
|
+
});
|
|
388
397
|
};
|
|
389
398
|
useEffect(() => {
|
|
390
399
|
window.addEventListener("scroll", updateHoveredRowBounds, true);
|
|
@@ -413,8 +422,14 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
413
422
|
return;
|
|
414
423
|
const saved = cart[selectedReviewId];
|
|
415
424
|
if (saved) {
|
|
416
|
-
setPickOverrides((prev) => ({
|
|
417
|
-
|
|
425
|
+
setPickOverrides((prev) => ({
|
|
426
|
+
...prev,
|
|
427
|
+
[selectedReviewId]: saved.productId,
|
|
428
|
+
}));
|
|
429
|
+
setPickOverrideCandidates((prev) => ({
|
|
430
|
+
...prev,
|
|
431
|
+
[selectedReviewId]: candidateFromCart(saved),
|
|
432
|
+
}));
|
|
418
433
|
return;
|
|
419
434
|
}
|
|
420
435
|
setPickOverrides((prev) => {
|
|
@@ -492,7 +507,14 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
492
507
|
}
|
|
493
508
|
};
|
|
494
509
|
function handleExport() {
|
|
495
|
-
void exportScheduleExcel(rows.map(({ item, best, qty, band, unmatchable, decision }) => ({
|
|
510
|
+
void exportScheduleExcel(rows.map(({ item, best, qty, band, unmatchable, decision }) => ({
|
|
511
|
+
item,
|
|
512
|
+
best,
|
|
513
|
+
qty,
|
|
514
|
+
band,
|
|
515
|
+
unmatchable,
|
|
516
|
+
decision,
|
|
517
|
+
})));
|
|
496
518
|
}
|
|
497
519
|
function toCartItem(row) {
|
|
498
520
|
const candidate = row.best ?? recommendedCandidates(row.result)[0];
|
|
@@ -525,7 +547,11 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
525
547
|
if (!existing)
|
|
526
548
|
continue;
|
|
527
549
|
const refreshed = toCartItem(row);
|
|
528
|
-
onAddToCart({
|
|
550
|
+
onAddToCart({
|
|
551
|
+
...refreshed,
|
|
552
|
+
qty: existing.qty,
|
|
553
|
+
sellUnitsOverride: existing.sellUnitsOverride ?? null,
|
|
554
|
+
});
|
|
529
555
|
}
|
|
530
556
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
531
557
|
}, [scheduleResult]);
|
|
@@ -588,14 +614,19 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
588
614
|
pdp_photo_url: candidate.pdp_photo_url ?? null,
|
|
589
615
|
conversion: candidate.conversion,
|
|
590
616
|
measurements_mm: candidate.measurements_mm ?? [],
|
|
591
|
-
scores: {
|
|
617
|
+
scores: {
|
|
618
|
+
dimension: 0,
|
|
619
|
+
grade: 0,
|
|
620
|
+
brand: 0,
|
|
621
|
+
fuzzy: 0,
|
|
622
|
+
bm25: 0,
|
|
623
|
+
overall: 0,
|
|
624
|
+
},
|
|
592
625
|
method: "catalogue_search",
|
|
593
626
|
}));
|
|
594
627
|
const reviewCandidates = reviewQuery ? catalogueSearchCandidates : llmReviewCandidates;
|
|
595
628
|
const savedSearchCandidate = activeReviewRow ? savedSearchCandidates[activeReviewRow.item.line_id] : undefined;
|
|
596
|
-
const reviewDisplayCandidates = !reviewQuery && savedSearchCandidate && !reviewCandidates.some((candidate) => candidate.product_id === savedSearchCandidate.product_id)
|
|
597
|
-
? [...reviewCandidates, savedSearchCandidate]
|
|
598
|
-
: reviewCandidates;
|
|
629
|
+
const reviewDisplayCandidates = !reviewQuery && savedSearchCandidate && !reviewCandidates.some((candidate) => candidate.product_id === savedSearchCandidate.product_id) ? [...reviewCandidates, savedSearchCandidate] : reviewCandidates;
|
|
599
630
|
const reviewOptionCandidates = (() => {
|
|
600
631
|
const topCandidates = reviewDisplayCandidates.slice(0, reviewQuery ? 12 : 3);
|
|
601
632
|
if (reviewQuery || !savedSearchCandidate || topCandidates.some((candidate) => candidate.product_id === savedSearchCandidate.product_id))
|
|
@@ -604,13 +635,27 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
604
635
|
})();
|
|
605
636
|
const activeReviewCartMatches = !!activeReviewRow?.best && cart[activeReviewRow.item.line_id]?.productId === activeReviewRow.best.product_id;
|
|
606
637
|
const activeReviewHasPendingUpdate = !!activeReviewRow?.best && !!cart[activeReviewRow.item.line_id] && !activeReviewCartMatches;
|
|
607
|
-
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: {
|
|
638
|
+
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: {
|
|
639
|
+
display: "flex",
|
|
640
|
+
gap: 8,
|
|
641
|
+
flexWrap: "wrap",
|
|
642
|
+
alignItems: "center",
|
|
643
|
+
}, children: [_jsx("input", { type: "text", className: "results-search-input", placeholder: "Search description...", value: search, onChange: (e) => setSearch(e.target.value), "aria-label": "Search line items" }), !internal && (_jsxs("div", { className: "results-view-toggle", "aria-label": "Results view", children: [_jsx("button", { type: "button", className: `results-view-toggle__btn${viewMode === "tile" ? " is-active" : ""}`, "aria-pressed": viewMode === "tile", "aria-label": "Tile view", title: "Tile view", onClick: () => setViewMode("tile"), children: _jsxs("svg", { width: "18", height: "12", viewBox: "0 0 15 10", fill: "currentColor", children: [_jsx("rect", { x: "0.5", y: "0.5", width: "4", height: "4" }), _jsx("rect", { x: "5.5", y: "0.5", width: "4", height: "4" }), _jsx("rect", { x: "10.5", y: "0.5", width: "4", height: "4" }), _jsx("rect", { x: "0.5", y: "5.5", width: "4", height: "4" }), _jsx("rect", { x: "5.5", y: "5.5", width: "4", height: "4" }), _jsx("rect", { x: "10.5", y: "5.5", width: "4", height: "4" })] }) }), _jsx("button", { type: "button", className: `results-view-toggle__btn${viewMode === "list" ? " is-active" : ""}`, "aria-pressed": viewMode === "list", "aria-label": "List view", title: "List view", onClick: () => setViewMode("list"), children: _jsxs("svg", { width: "17", height: "17", viewBox: "0 0 15 15", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", children: [_jsx("line", { x1: "0.5", y1: "2.5", x2: "14.5", y2: "2.5" }), _jsx("line", { x1: "0.5", y1: "7.5", x2: "14.5", y2: "7.5" }), _jsx("line", { x1: "0.5", y1: "12.5", x2: "14.5", y2: "12.5" })] }) })] })), debugEnabled && (_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setDetailMode((mode) => (mode === "simple" ? "detailed" : "simple")), children: detailMode === "detailed" ? "Hide detailed view" : "Show detailed view" })), _jsxs("details", { ref: sortMenuRef, style: { position: "relative" }, children: [_jsx("summary", { className: "bom-toolbar-btn sort-menu-summary", "aria-label": "Sort line items", style: { fontSize: 16 }, children: "\u21C5" }), _jsx("div", { className: "sort-menu-panel", children: SORT_OPTIONS.map((opt) => {
|
|
608
644
|
const active = sortConfig === opt.dir;
|
|
609
645
|
return (_jsxs("button", { type: "button", className: `sort-menu-item${active ? " is-active" : ""}`, onClick: () => {
|
|
610
646
|
setSortConfig(active ? null : opt.dir);
|
|
611
647
|
sortMenuRef.current?.removeAttribute("open");
|
|
612
648
|
}, children: [_jsx("span", { className: "sort-menu-item__check", children: active ? "โ" : "" }), opt.label] }, opt.dir));
|
|
613
|
-
}) })] })] })] })) }), viewMode === "tile" || activeReviewRow ? (_jsxs(_Fragment, { children: [activeReviewRow ? (_jsx("div", { className: "modal-backdrop quick-order-modal-backdrop", role: "dialog", "aria-modal": "true", "aria-label": "Product options", onClick: () => closeReview(), children: _jsxs("section", { className: "quick-order-workflow-modal results-review-modal", onClick: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", className: "quick-order-workflow-close results-review-modal__close", onClick: () => closeReview(), "aria-label": "Close product options", children: _jsx(CloseIcon, {}) }), _jsx("div", { className: "results-review-page", children: internal ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "results-review-page__top", children: [_jsxs("button", { type: "button", className: "bom-toolbar-btn bom-toolbar-btn--accent", onClick: () => closeReview(), children: [_jsx(BackArrow, {}), "Back to results"] }), _jsxs("div", { style: {
|
|
649
|
+
}) })] })] })] })) })), viewMode === "tile" || activeReviewRow ? (_jsxs(_Fragment, { children: [activeReviewRow ? (_jsx("div", { className: "modal-backdrop quick-order-modal-backdrop", role: "dialog", "aria-modal": "true", "aria-label": "Product options", onClick: () => closeReview(), children: _jsxs("section", { className: "quick-order-workflow-modal results-review-modal", onClick: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", className: "quick-order-workflow-close results-review-modal__close", onClick: () => closeReview(), "aria-label": "Close product options", children: _jsx(CloseIcon, {}) }), _jsx("div", { className: "results-review-page", children: internal ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "results-review-page__top", children: [_jsxs("button", { type: "button", className: "bom-toolbar-btn bom-toolbar-btn--accent", onClick: () => closeReview(), children: [_jsx(BackArrow, {}), "Back to results"] }), _jsxs("div", { style: {
|
|
650
|
+
display: "flex",
|
|
651
|
+
gap: 8,
|
|
652
|
+
flexWrap: "wrap",
|
|
653
|
+
}, children: [debugEnabled && (_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setDetailMode((mode) => (mode === "simple" ? "detailed" : "simple")), children: detailMode === "detailed" ? "Hide detailed info" : "Show detailed info" })), _jsx("div", { className: "results-review-searchbar", children: _jsx("input", { type: "text", className: "results-search-input", placeholder: "Search products in catalogue...", value: reviewSearch, onChange: (e) => setReviewSearch(e.target.value), "aria-label": "Search database candidates" }) }), _jsx("div", { className: "results-candidate-actions", children: _jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => clearPickedProduct(activeReviewRow.item.line_id), children: "Clear selected product" }) })] })] }), _jsxs("div", { className: "results-review-page__hero", children: [_jsx("div", { className: "results-review-page__image-wrap", children: _jsx("img", { className: "fill-img results-review-page__image", src: productImageSrc(previewCandidate(activeReviewRow)?.description || activeReviewRow.item.description_original, previewCandidate(activeReviewRow)?.product_id, previewCandidate(activeReviewRow)?.pdp_photo_url), alt: "" }) }), _jsxs("div", { className: "results-review-page__summary", children: [_jsx("h2", { children: displayLineLabel(activeReviewRow.item, lineItems) }), _jsxs("p", { className: "results-review-page__meta", children: [fmtQty(activeReviewRow.qty), " ", renderDisplayUnit(activeReviewRow.item.unit, activeReviewRow.qty)] }), activeReviewRow.best ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "results-review-page__product", children: activeReviewRow.best.description }), activeReviewRow.best.price != null ? _jsx("div", { className: "results-row-meta", children: fmtPrice(activeReviewRow.best.price, activeReviewRow.best.price_basis) }) : null] })) : previewCandidate(activeReviewRow) ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "results-review-page__product", children: previewCandidate(activeReviewRow)?.description }), previewCandidate(activeReviewRow)?.price != null ? (_jsx("div", { className: "results-row-meta", children: fmtPrice(previewCandidate(activeReviewRow)?.price ?? null, previewCandidate(activeReviewRow)?.price_basis ?? null) })) : null, _jsx("div", { className: "results-row-meta", children: "Best reviewed alternative - not yet selected" })] })) : (_jsx("div", { className: "results-empty-hint", children: "No product currently selected for this line item." })), hasVisibleLinkedGroup(activeReviewRow.item) ? (_jsxs("div", { style: {
|
|
654
|
+
display: "flex",
|
|
655
|
+
alignItems: "center",
|
|
656
|
+
gap: 8,
|
|
657
|
+
marginTop: 8,
|
|
658
|
+
}, children: [_jsx(LinkedGroupPill, { meta: linkedGroupMetaById.get(activeReviewRow.item.linked_group_id) }), _jsx("div", { className: "results-row-meta", style: { marginTop: 0 }, children: linkedGroupSummary(activeReviewRow.item) })] })) : null, _jsx("div", { className: "results-review-page__actions", children: _jsx("button", { type: "button", className: `bom-toolbar-btn${cart[activeReviewRow.item.line_id] ? " bom-toolbar-btn--added" : " bom-toolbar-btn--accent"}`, onClick: () => {
|
|
614
659
|
if (cart[activeReviewRow.item.line_id])
|
|
615
660
|
onRemoveFromCart(activeReviewRow.item.line_id);
|
|
616
661
|
else {
|
|
@@ -627,7 +672,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
627
672
|
: "Search the catalogue to choose a product." })] }) }), !reviewQuery && reviewDisplayCandidates.length > 0 ? (_jsx("div", { className: "customer-review-option-grid", children: reviewOptionCandidates.map((candidate) => {
|
|
628
673
|
const selected = candidate.product_id === activeReviewRow.best?.product_id;
|
|
629
674
|
return (_jsxs("button", { type: "button", className: `customer-review-option${selected ? " is-selected" : ""}`, onClick: () => handlePickProduct(activeReviewRow.item.line_id, candidate), "aria-pressed": selected, children: [_jsx("img", { className: "customer-review-option__image", src: productImageSrc(candidate.description, candidate.product_id, candidate.pdp_photo_url), alt: "", width: 64, height: 64 }), _jsx("span", { className: "customer-review-option__copy", children: _jsx("strong", { children: candidate.description }) }), _jsx("span", { className: "customer-review-option__price", children: candidate.price != null ? `ยฃ${fmtPrice(candidate.price, candidate.price_basis)}` : "-" }), _jsx("span", { className: "customer-review-option__state", children: selected ? "Selected" : "Choose" })] }, candidate.product_id));
|
|
630
|
-
}) })) : null, reviewDisplayCandidates.length > 0 && _jsx("div", { className: "customer-review-divider", children: _jsx("span", { children: "or" }) }), _jsxs("label", { className: "customer-review-search", children: [_jsx("span", { children: "Search our full catalogue" }), _jsx("input", { type: "text", className: "results-search-input", placeholder: "Search products...", value: reviewSearch, onChange: (e) => setReviewSearch(e.target.value), "aria-label": "Search catalogue" })] }), reviewQuery && catalogueSearchLoading ? _jsx("div", { className: "results-empty-card", children: "Searching catalogue\u2026" }) : null, reviewQuery && !catalogueSearchLoading && catalogueSearchError ? _jsx("div", { className: "results-empty-card", children: catalogueSearchError }) : null, reviewQuery && !catalogueSearchLoading && !catalogueSearchError && reviewDisplayCandidates.length === 0 ?
|
|
675
|
+
}) })) : null, reviewDisplayCandidates.length > 0 && (_jsx("div", { className: "customer-review-divider", children: _jsx("span", { children: "or" }) })), _jsxs("label", { className: "customer-review-search", children: [_jsx("span", { children: "Search our full catalogue" }), _jsx("input", { type: "text", className: "results-search-input", placeholder: "Search products...", value: reviewSearch, onChange: (e) => setReviewSearch(e.target.value), "aria-label": "Search catalogue" })] }), reviewQuery && catalogueSearchLoading ? _jsx("div", { className: "results-empty-card", children: "Searching catalogue\u2026" }) : null, reviewQuery && !catalogueSearchLoading && catalogueSearchError ? _jsx("div", { className: "results-empty-card", children: catalogueSearchError }) : null, reviewQuery && !catalogueSearchLoading && !catalogueSearchError && reviewDisplayCandidates.length === 0 ? _jsx("div", { className: "results-empty-card", children: "No products match this search." }) : null, reviewQuery && !catalogueSearchLoading && !catalogueSearchError && reviewDisplayCandidates.length > 0 ? (_jsx("div", { className: "customer-review-option-grid", children: reviewOptionCandidates.map((candidate) => {
|
|
631
676
|
const selected = candidate.product_id === activeReviewRow.best?.product_id;
|
|
632
677
|
return (_jsxs("button", { type: "button", className: `customer-review-option${selected ? " is-selected" : ""}`, onClick: () => handlePickProduct(activeReviewRow.item.line_id, candidate), "aria-pressed": selected, children: [_jsx("img", { className: "customer-review-option__image", src: productImageSrc(candidate.description, candidate.product_id, candidate.pdp_photo_url), alt: "", width: 64, height: 64 }), _jsx("span", { className: "customer-review-option__copy", children: _jsx("strong", { children: candidate.description }) }), _jsx("span", { className: "customer-review-option__price", children: candidate.price != null ? `ยฃ${fmtPrice(candidate.price, candidate.price_basis)}` : "-" }), _jsx("span", { className: "customer-review-option__state", children: selected ? "Selected" : "Choose" })] }, candidate.product_id));
|
|
633
678
|
}) })) : null] }), activeReviewRow.best ? (activeReviewHasPendingUpdate ? (_jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsx("button", { type: "button", className: "cta-btn customer-review-add", onClick: () => {
|
|
@@ -665,7 +710,12 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
665
710
|
}, onMouseLeave: () => {
|
|
666
711
|
hoveredRowRef.current = null;
|
|
667
712
|
setHoveredRowBounds(null);
|
|
668
|
-
}, children: [_jsxs("td", { children: [_jsx("div", { className: "bom-component-name", children: displayLineLabel(item, lineItems) }), hasVisibleLinkedGroup(item) ? (_jsxs("div", { style: {
|
|
713
|
+
}, children: [_jsxs("td", { children: [_jsx("div", { className: "bom-component-name", children: displayLineLabel(item, lineItems) }), hasVisibleLinkedGroup(item) ? (_jsxs("div", { style: {
|
|
714
|
+
display: "flex",
|
|
715
|
+
alignItems: "center",
|
|
716
|
+
gap: 8,
|
|
717
|
+
marginTop: 4,
|
|
718
|
+
}, children: [_jsx(LinkedGroupPill, { meta: linkedGroupMetaById.get(item.linked_group_id) }), _jsx("div", { className: "results-row-meta", style: { marginTop: 0 }, children: linkedGroupSummary(item) })] })) : null] }), _jsx("td", { children: best ? (_jsxs("div", { children: [_jsx("div", { className: "results-match-cell-text", title: best.description, children: best.description }), detailMode === "detailed" && decision?.technical_classification && (_jsxs("div", { className: "results-row-meta", children: [classificationLabel(decision.technical_classification), decision.approval_required ? " - approval required" : ""] }))] })) : (_jsx("span", { className: "results-empty-hint" })) }), _jsx("td", { style: { textAlign: "center" }, children: best?.price != null ? fmtPrice(best.price, best.price_basis) : "-" }), detailMode === "simple" && (_jsx("td", { style: { textAlign: "center" }, children: _jsx("button", { type: "button", className: `bom-toolbar-btn${cart[item.line_id] ? " bom-toolbar-btn--added" : " bom-toolbar-btn--accent"}`, style: { width: 84, justifyContent: "center" }, onClick: (event) => {
|
|
669
719
|
event.stopPropagation();
|
|
670
720
|
if (cart[item.line_id])
|
|
671
721
|
onRemoveFromCart(item.line_id);
|
|
@@ -673,10 +723,17 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
673
723
|
onAddToCart(toCartItem(row));
|
|
674
724
|
}
|
|
675
725
|
}, "aria-label": cart[item.line_id] ? (internal ? "Remove from selection" : `Remove from ${customerCollection}`) : internal ? "Add to selection" : `Add to ${customerCollection}`, children: cart[item.line_id] ? "โ Added" : internal ? "+ Add" : `Add to ${customerCollection}` }) }))] }, item.line_id));
|
|
676
|
-
})] })] }) }), unmatchedSection] })), !activeReviewRow && totalPages > 1 && (_jsxs("div", { className: "results-pagination", children: [_jsxs("span", { className: "results-pagination-count", children: ["Showing ", (currentPage - 1) * PAGE_SIZE + 1, "\u2013", Math.min(currentPage * PAGE_SIZE, visibleRows.length), " of ", visibleRows.length] }), _jsxs("div", { className: "results-pagination-controls", children: [_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage - 1), disabled: currentPage === 1, children: "Previous" }), _jsxs("span", { className: "results-pagination-page", children: ["Page ", currentPage, " of ", totalPages] }), _jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage + 1), disabled: currentPage === totalPages, children: "Next" })] })] })), (parseReport.itemCount != null || parseReport.headingCount != null || parseReport.skippedCount != null) && (_jsxs("div", { className: "results-summary", style: { marginTop: 16 }, children: [parseReport.itemCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.itemCount }), " items parsed"] })), parseReport.headingCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.headingCount }), " headings"] })), parseReport.skippedCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.skippedCount }), " rows skipped"] }))] })), _jsxs("div", { className: "disclaimer-box", children: [_jsx("span", { className: "disclaimer-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "disclaimer-text", children: "These matches are generated automatically and should be reviewed before purchase." })] }), !activeReviewRow && (_jsxs("div", { className: "btn-row results-actions", children: [_jsxs("button", { className: "cta-btn cta-btn-secondary", style: {
|
|
726
|
+
})] })] }) }), unmatchedSection] })), !activeReviewRow && totalPages > 1 && (_jsxs("div", { className: "results-pagination", children: [_jsxs("span", { className: "results-pagination-count", children: ["Showing ", (currentPage - 1) * PAGE_SIZE + 1, "\u2013", Math.min(currentPage * PAGE_SIZE, visibleRows.length), " of ", visibleRows.length] }), _jsxs("div", { className: "results-pagination-controls", children: [_jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage - 1), disabled: currentPage === 1, children: "Previous" }), _jsxs("span", { className: "results-pagination-page", children: ["Page ", currentPage, " of ", totalPages] }), _jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => setPage(currentPage + 1), disabled: currentPage === totalPages, children: "Next" })] })] })), (parseReport.itemCount != null || parseReport.headingCount != null || parseReport.skippedCount != null) && (_jsxs("div", { className: "results-summary", style: { marginTop: 16 }, children: [parseReport.itemCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.itemCount }), " items parsed"] })), parseReport.headingCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.headingCount }), " headings"] })), parseReport.skippedCount != null && (_jsxs("span", { className: "summary-chip", children: [_jsx("strong", { children: parseReport.skippedCount }), " rows skipped"] }))] })), _jsxs("div", { className: "disclaimer-box", children: [_jsx("span", { className: "disclaimer-icon", children: "\u26A0\uFE0F" }), _jsx("p", { className: "disclaimer-text", children: "These matches are generated automatically and should be reviewed before purchase." })] }), !activeReviewRow && (_jsxs("div", { className: "btn-row results-actions", children: [_jsxs("button", { className: "cta-btn cta-btn-secondary", style: {
|
|
727
|
+
flex: cartCount > 0 ? "0 1 30%" : "0 1 30%",
|
|
728
|
+
gap: 10,
|
|
729
|
+
justifyContent: "center",
|
|
730
|
+
}, onClick: () => setConfirmStartOver(true), "aria-label": "Start over", children: [_jsx(RefreshIcon, {}), _jsx("span", { children: "Start Over" })] }), cartCount > 0 && (_jsxs("button", { className: "cta-btn", style: { flex: "0 1 70%" }, onClick: onGoToBasket, "aria-label": internal ? "View selection" : qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [internal ? "View Selection" : qzaQuoteRequest ? "View Quote" : "View Basket", " (", cartCount, ")"] }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] }))] }))] })] }), reviewGateOpen && (_jsx("div", { className: "modal-backdrop quick-order-modal-backdrop review-gate-modal-backdrop", role: "dialog", "aria-modal": "true", "aria-labelledby": "review-gate-title", children: _jsxs("section", { className: "quick-order-workflow-modal review-gate-modal", children: [_jsxs("div", { className: "review-gate-modal__header", children: [_jsx("h2", { id: "review-gate-title", children: "Choose products for these items" }), _jsx("p", { children: "We couldn\u2019t find a clear match for these items, so we need your help. Select an option from the suggestions, search the catalogue, or leave the item out." })] }), _jsx("div", { className: "review-gate-list", children: pendingReviewRows.map((row) => {
|
|
677
731
|
const reviewed = reviewedReviewIds.has(row.item.line_id);
|
|
678
732
|
const partial = hasBestAlternative(row);
|
|
679
|
-
return (_jsx("div", { className: `review-gate-item${reviewed ? " is-reviewed" : ""}`, children: _jsxs("button", { type: "button", className: "review-gate-item__review", onClick: () => {
|
|
733
|
+
return (_jsx("div", { className: `review-gate-item${reviewed ? " is-reviewed" : ""}`, children: _jsxs("button", { type: "button", className: "review-gate-item__review", onClick: () => {
|
|
734
|
+
setReviewFlowActive(true);
|
|
735
|
+
openReview(row.item.line_id);
|
|
736
|
+
}, children: [_jsxs("span", { className: "review-gate-item__copy", children: [_jsx("strong", { children: displayLineLabel(row.item, lineItems) }), _jsx("span", { children: row.decision?.reason || (partial ? "A close alternative is available but needs confirmation." : "No product has been selected for this item yet.") })] }), _jsx("span", { className: "review-gate-item__action", children: reviewed ? "Choice made" : "Choose a product" })] }) }, row.item.line_id));
|
|
680
737
|
}) }), _jsx("button", { type: "button", className: "review-gate-skip", onClick: skipAllReviews, children: "Skip all" }), skippedReviewRows.length > 0 ? (_jsxs("div", { className: "review-gate-skipped", children: [_jsx("strong", { children: "Skipped" }), skippedReviewRows.map((row) => (_jsxs("button", { type: "button", className: "review-gate-item is-skipped", onClick: () => {
|
|
681
738
|
setSkippedReviewIds((ids) => {
|
|
682
739
|
const next = new Set(ids);
|