@metron-labs/quote-react 0.1.61 → 0.1.63
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/stages/ScheduleResultsStage.d.ts +1 -1
- package/dist/app/stages/ScheduleResultsStage.js +52 -21
- package/dist/components/estimators/ConcreteAggregateEstimator.js +12 -74
- package/dist/components/estimators/EstimatorResults.js +7 -7
- package/dist/components/estimators/PaintEstimator.d.ts +1 -1
- package/dist/components/estimators/PaintEstimator.js +7 -2
- package/dist/embed/quote.js +12 -12
- package/dist/lib/estimators/fixedProducts.js +2 -0
- package/dist/util/pricing.js +46 -0
- package/package.json +1 -1
|
@@ -17,5 +17,5 @@ export declare function candidateFromCart(item: CartItem): MatchCandidate;
|
|
|
17
17
|
export declare function refreshedCartItem(refreshed: CartItem, existing: CartItem): CartItem;
|
|
18
18
|
export declare function displayLineLabel(item: LineItem, allItems: LineItem[]): string;
|
|
19
19
|
export declare function formatResultsTotal(totalValue: number | null, needsPricingCount: number): string | null;
|
|
20
|
-
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal }: ResultsStageProps): import("react").JSX.Element;
|
|
20
|
+
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal, }: ResultsStageProps): import("react").JSX.Element;
|
|
21
21
|
export {};
|
|
@@ -198,7 +198,9 @@ function curatedCandidates(candidates, result, selectedProductId) {
|
|
|
198
198
|
const recommendedIds = result?.rerank?.recommendedCandidateIds ?? [];
|
|
199
199
|
if (recommendedIds.length === 0) {
|
|
200
200
|
const topCandidates = candidates.slice(0, 6);
|
|
201
|
-
const pinnedSelected = !topCandidates.some((candidate) => candidate.product_id === selectedProductId)
|
|
201
|
+
const pinnedSelected = !topCandidates.some((candidate) => candidate.product_id === selectedProductId)
|
|
202
|
+
? candidates.find((candidate) => candidate.product_id === selectedProductId)
|
|
203
|
+
: undefined;
|
|
202
204
|
return pinnedSelected ? [...topCandidates, pinnedSelected] : topCandidates;
|
|
203
205
|
}
|
|
204
206
|
const byId = new Map(candidates.map((candidate) => [candidate.product_id, candidate]));
|
|
@@ -326,7 +328,7 @@ function CandidateBreakdown({ item, candidates, result, decision, unmatchable, s
|
|
|
326
328
|
}, "aria-label": isCurrent ? `${c.description} selected` : `Use ${c.description}` }), _jsx("span", { className: "switch-toggle-track", "aria-hidden": "true" })] }) })] }, `${c.product_id}-${i}`));
|
|
327
329
|
}) })] })] }));
|
|
328
330
|
}
|
|
329
|
-
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal = false }) {
|
|
331
|
+
export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart, onAddToCart, onRemoveFromCart, onGoToBasket, cartSeeded, onCartSeeded, reviewGateComplete, onReviewGateComplete, internal = false, }) {
|
|
330
332
|
const qzaQuoteRequest = !internal && getBoqClientId() === "QZA6-MRA4";
|
|
331
333
|
const customerCollection = qzaQuoteRequest ? "quote" : "basket";
|
|
332
334
|
const { results, lineItems, parseReport, timings } = scheduleResult;
|
|
@@ -368,7 +370,10 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
368
370
|
const pick = pickOverrides[item.line_id];
|
|
369
371
|
const automaticMatch = resolveBest(result, pick);
|
|
370
372
|
const savedCartMatch = cart[item.line_id];
|
|
371
|
-
const matched = pick === NO_SELECTION
|
|
373
|
+
const matched = pick === NO_SELECTION
|
|
374
|
+
? undefined
|
|
375
|
+
: (pickOverrideCandidates[item.line_id] ??
|
|
376
|
+
(savedCartMatch && savedCartMatch.productId !== automaticMatch?.product_id ? candidateFromCart(savedCartMatch) : automaticMatch));
|
|
372
377
|
const best = unmatchable ? undefined : matched;
|
|
373
378
|
const qty = item.qty ?? 0;
|
|
374
379
|
return {
|
|
@@ -671,7 +676,9 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
671
676
|
}));
|
|
672
677
|
const reviewCandidates = reviewQuery ? catalogueSearchCandidates : llmReviewCandidates;
|
|
673
678
|
const savedSearchCandidate = activeReviewRow ? savedSearchCandidates[activeReviewRow.item.line_id] : undefined;
|
|
674
|
-
const reviewDisplayCandidates = !reviewQuery && savedSearchCandidate && !reviewCandidates.some((candidate) => candidate.product_id === savedSearchCandidate.product_id)
|
|
679
|
+
const reviewDisplayCandidates = !reviewQuery && savedSearchCandidate && !reviewCandidates.some((candidate) => candidate.product_id === savedSearchCandidate.product_id)
|
|
680
|
+
? [...reviewCandidates, savedSearchCandidate]
|
|
681
|
+
: reviewCandidates;
|
|
675
682
|
const reviewOptionCandidates = (() => {
|
|
676
683
|
const topCandidates = reviewDisplayCandidates.slice(0, reviewQuery ? 12 : 6);
|
|
677
684
|
if (reviewQuery || !savedSearchCandidate || topCandidates.some((candidate) => candidate.product_id === savedSearchCandidate.product_id))
|
|
@@ -680,22 +687,22 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
680
687
|
})();
|
|
681
688
|
const activeReviewCartMatches = !!activeReviewRow?.best && cart[activeReviewRow.item.line_id]?.productId === activeReviewRow.best.product_id;
|
|
682
689
|
const activeReviewHasPendingUpdate = !!activeReviewRow?.best && !!cart[activeReviewRow.item.line_id] && !activeReviewCartMatches;
|
|
683
|
-
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: totalLabel ?? "Review quantities" }), " 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, disabled: cartCount === 0, "aria-label": qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [_jsx("small", { children: qzaQuoteRequest ? "View Quote" : "View Basket" }), totalLabel != null && _jsx("strong", { children: totalLabel })] }), _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 && (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
690
|
+
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: totalLabel ?? "Review quantities" }), " 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, disabled: cartCount === 0, "aria-label": qzaQuoteRequest ? "View quote" : "View basket", children: [_jsxs("span", { children: [_jsx("small", { children: qzaQuoteRequest ? "View Quote" : "View Basket" }), totalLabel != null && _jsx("strong", { children: totalLabel })] }), _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 && (_jsx("div", { className: "results-summary", style: { marginBottom: 0 }, children: _jsxs("div", { style: {
|
|
691
|
+
display: "flex",
|
|
692
|
+
gap: 8,
|
|
693
|
+
flexWrap: "wrap",
|
|
694
|
+
alignItems: "center",
|
|
695
|
+
}, 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) => {
|
|
696
|
+
const active = sortConfig === opt.dir;
|
|
697
|
+
return (_jsxs("button", { type: "button", className: `sort-menu-item${active ? " is-active" : ""}`, onClick: () => {
|
|
698
|
+
setSortConfig(active ? null : opt.dir);
|
|
699
|
+
sortMenuRef.current?.removeAttribute("open");
|
|
700
|
+
}, children: [_jsx("span", { className: "sort-menu-item__check", children: active ? "✓" : "" }), opt.label] }, opt.dir));
|
|
701
|
+
}) })] })] }) })) })), 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: {
|
|
695
702
|
display: "flex",
|
|
696
703
|
gap: 8,
|
|
697
704
|
flexWrap: "wrap",
|
|
698
|
-
}, 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: {
|
|
705
|
+
}, 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: {
|
|
699
706
|
display: "flex",
|
|
700
707
|
alignItems: "center",
|
|
701
708
|
gap: 8,
|
|
@@ -708,7 +715,19 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
708
715
|
saveSearchCandidate(activeReviewRow.item.line_id, activeReviewRow.best);
|
|
709
716
|
onAddToCart(toCartItem(activeReviewRow));
|
|
710
717
|
}
|
|
711
|
-
}, disabled: !activeReviewRow.best, "aria-label": cart[activeReviewRow.item.line_id]
|
|
718
|
+
}, disabled: !activeReviewRow.best, "aria-label": cart[activeReviewRow.item.line_id]
|
|
719
|
+
? internal
|
|
720
|
+
? "Remove from selection"
|
|
721
|
+
: `Remove from ${customerCollection}`
|
|
722
|
+
: internal
|
|
723
|
+
? "Add to selection"
|
|
724
|
+
: `Add to ${customerCollection}`, children: cart[activeReviewRow.item.line_id] ? "✓ Added" : internal ? "+ Add to selection" : `Add to ${customerCollection}` }) })] })] }), catalogueSearchLoading ? _jsx("div", { className: "results-empty-card", children: "Searching catalogue\u2026" }) : null, !catalogueSearchLoading && catalogueSearchError ? _jsx("div", { className: "results-empty-card", children: catalogueSearchError }) : null, !catalogueSearchLoading && !catalogueSearchError && reviewQuery && reviewCandidates.length === 0 ? (_jsx("div", { className: "results-empty-card", children: "No candidate products match this database search." })) : !catalogueSearchLoading && !catalogueSearchError ? (_jsx(CandidateBreakdown, { item: activeReviewRow.item, candidates: reviewCandidates, result: activeReviewRow.result, decision: activeReviewRow.decision, unmatchable: activeReviewRow.unmatchable, selectedProductId: activeReviewRow.best?.product_id ?? null, detailMode: detailMode, linkedGroupMeta: hasVisibleLinkedGroup(activeReviewRow.item) ? linkedGroupMetaById.get(activeReviewRow.item.linked_group_id) : undefined, onPick: (candidate) => handlePickProduct(activeReviewRow.item.line_id, candidate), onClear: () => clearPickedProduct(activeReviewRow.item.line_id), showAll: internal || debugEnabled || reviewQuery.length > 0, internal: internal, debugEnabled: debugEnabled, forceReviewNote: true })) : null] })) : (_jsxs(_Fragment, { children: [_jsxs("section", { className: "customer-review-request", children: [_jsx("span", { className: "customer-review-eyebrow", children: "Original request" }), _jsx("strong", { children: displayLineLabel(activeReviewRow.item, lineItems) || "Original request" })] }), activeReviewReason ? (_jsxs("section", { className: "customer-review-ai", children: [_jsx("strong", { children: hasNoSuitableCandidate ? "No automatic match:" : "AI review:" }), " ", activeReviewReason] })) : null, _jsxs("section", { className: "customer-review-options", children: [_jsx("div", { className: "customer-review-options__head", children: _jsxs("div", { children: [_jsx("h2", { children: hasNoSuitableCandidate
|
|
725
|
+
? llmReviewCandidates.length > 0
|
|
726
|
+
? "Choose a product"
|
|
727
|
+
: "No suitable product found"
|
|
728
|
+
: hasAiVerdict
|
|
729
|
+
? "Choose a product"
|
|
730
|
+
: "We couldn't review this item" }), _jsx("p", { children: hasNoSuitableCandidate
|
|
712
731
|
? llmReviewCandidates.length > 0
|
|
713
732
|
? "No product was selected automatically. Review these related options before adding one."
|
|
714
733
|
: "Search the catalogue to choose an alternative."
|
|
@@ -717,7 +736,7 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
717
736
|
: "Search the catalogue to choose a product." })] }) }), !reviewQuery && reviewDisplayCandidates.length > 0 ? (_jsx("div", { className: "customer-review-option-grid", children: reviewOptionCandidates.map((candidate) => {
|
|
718
737
|
const selected = candidate.product_id === activeReviewRow.best?.product_id;
|
|
719
738
|
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));
|
|
720
|
-
}) })) : 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) => {
|
|
739
|
+
}) })) : 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) => {
|
|
721
740
|
const selected = candidate.product_id === activeReviewRow.best?.product_id;
|
|
722
741
|
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));
|
|
723
742
|
}) })) : null] }), activeReviewRow.best ? (activeReviewHasPendingUpdate ? (_jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsx("button", { type: "button", className: "cta-btn customer-review-add", onClick: () => {
|
|
@@ -746,7 +765,13 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
746
765
|
else {
|
|
747
766
|
onAddToCart(toCartItem(row));
|
|
748
767
|
}
|
|
749
|
-
}, disabled: !preview, "aria-label": cart[item.line_id]
|
|
768
|
+
}, disabled: !preview, "aria-label": cart[item.line_id]
|
|
769
|
+
? internal
|
|
770
|
+
? "Remove from selection"
|
|
771
|
+
: `Remove from ${customerCollection}`
|
|
772
|
+
: internal
|
|
773
|
+
? "Add to selection"
|
|
774
|
+
: `Add to ${customerCollection}`, children: cart[item.line_id] ? "✓ Added" : internal ? "+ Add" : `Add to ${customerCollection}` })] })] }, item.line_id));
|
|
750
775
|
}) }) })) : null, unmatchedSection] })] })) : (_jsxs(_Fragment, { children: [hoveredRowBounds && (_jsx("div", { className: "results-table__hover-overlay", style: hoveredRowBounds, "aria-hidden": "true", children: "Click to view options" })), _jsx("div", { className: "bom-table-wrap", children: _jsxs("table", { className: "bom-table results-table", "aria-label": "Matched line items", children: [_jsxs("colgroup", { children: [_jsx("col", { className: "col-item" }), _jsx("col", { style: { width: "220px" } }), _jsx("col", { style: { width: "100px" } }), detailMode === "simple" && _jsx("col", { style: { width: "110px" } })] }), _jsx("thead", { children: _jsxs("tr", { className: "bom-col-header", children: [_jsx("th", { style: { textAlign: "left" }, children: "Description" }), _jsx("th", { style: { textAlign: "center" }, children: "Product Matched" }), _jsx("th", { style: { textAlign: "center" }, children: "Price" }), detailMode === "simple" && _jsx("th", { style: { textAlign: "center" }, children: internal ? "Selection" : qzaQuoteRequest ? "Quote" : "Basket" })] }) }), _jsxs("tbody", { children: [visibleRows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: detailMode === "simple" ? 4 : 3, className: "results-empty-hint", style: { padding: "16px 8px" }, children: "No line items match the current search/filter." }) })), matchedDisplayRows.map((row) => {
|
|
751
776
|
const { item, result, decision, best } = row;
|
|
752
777
|
return (_jsxs("tr", { className: "bom-row results-table__review-row", onClick: () => openReview(item.line_id), onMouseEnter: (event) => {
|
|
@@ -767,7 +792,13 @@ export default function ScheduleResultsStage({ scheduleResult, onStartOver, cart
|
|
|
767
792
|
else {
|
|
768
793
|
onAddToCart(toCartItem(row));
|
|
769
794
|
}
|
|
770
|
-
}, "aria-label": cart[item.line_id]
|
|
795
|
+
}, "aria-label": cart[item.line_id]
|
|
796
|
+
? internal
|
|
797
|
+
? "Remove from selection"
|
|
798
|
+
: `Remove from ${customerCollection}`
|
|
799
|
+
: internal
|
|
800
|
+
? "Add to selection"
|
|
801
|
+
: `Add to ${customerCollection}`, children: cart[item.line_id] ? "✓ Added" : internal ? "+ Add" : `Add to ${customerCollection}` }) }))] }, item.line_id));
|
|
771
802
|
})] })] }) }), 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: {
|
|
772
803
|
flex: "0 1 30%",
|
|
773
804
|
gap: 10,
|
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import BrickWall from "../BrickWall";
|
|
5
|
-
import { useOptionalCart } from "../../lib/cart/CartContext";
|
|
6
|
-
import { candidateToCartItem } from "../../lib/boq/toCartItem";
|
|
7
|
-
import { searchCatalogue } from "../../lib/estimators/resolveProducts";
|
|
8
4
|
const SITE_MIX_DRY_VOLUME_FACTOR = 1.54;
|
|
9
5
|
const CEMENT_DENSITY_KG_M3 = 1440;
|
|
10
|
-
const CEMENT_BAG_KG = 25;
|
|
11
|
-
const BULK_BAG_KG = 800;
|
|
12
|
-
const MIN_BASKET_LOADING_MS = 1500;
|
|
13
6
|
const toNumber = (value) => Number.parseFloat(value) || 0;
|
|
14
7
|
const aggregateDetails = {
|
|
15
|
-
mot: { label: "MOT Type 1", blurb: "Sub-base for patios and driveways", density: 1.7,
|
|
16
|
-
"sharp-sand": { label: "Sharp sand", blurb: "Bedding and general groundwork", density: 1.6,
|
|
17
|
-
gravel: { label: "20mm gravel", blurb: "Drainage and landscaping", density: 1.6,
|
|
8
|
+
mot: { label: "MOT Type 1", blurb: "Sub-base for patios and driveways", density: 1.7, query: "MOT type 1" },
|
|
9
|
+
"sharp-sand": { label: "Sharp sand", blurb: "Bedding and general groundwork", density: 1.6, query: "sharp sand" },
|
|
10
|
+
gravel: { label: "20mm gravel", blurb: "Drainage and landscaping", density: 1.6, query: "20mm gravel" },
|
|
18
11
|
};
|
|
19
12
|
export default function ConcreteAggregateEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
|
|
20
|
-
// This calculator knows its exact products, so it normally fills the basket
|
|
21
|
-
// itself. The internal tools render it outside the cart provider and take the
|
|
22
|
-
// quantities through their own review instead, so the cart is optional here.
|
|
23
|
-
const cart = useOptionalCart();
|
|
24
13
|
const [unit, setUnit] = useState("m");
|
|
25
14
|
const [length, setLength] = useState("");
|
|
26
15
|
const [width, setWidth] = useState("");
|
|
27
16
|
const [depth, setDepth] = useState("");
|
|
28
17
|
const [group, setGroup] = useState("concrete");
|
|
29
18
|
const [aggregate, setAggregate] = useState("mot");
|
|
30
|
-
const [adding, setAdding] = useState(false);
|
|
31
|
-
const [basketError, setBasketError] = useState(null);
|
|
32
19
|
const conversion = unit === "m" ? 1 : 0.3048;
|
|
33
20
|
const volumeM3 = toNumber(length) * conversion * toNumber(width) * conversion * toNumber(depth) * (unit === "m" ? 0.01 : 0.0254);
|
|
34
21
|
const selectedAggregate = aggregateDetails[aggregate];
|
|
@@ -36,66 +23,17 @@ export default function ConcreteAggregateEstimator({ onBack, onReview, submitLab
|
|
|
36
23
|
const cementKg = siteMixPartVolume * CEMENT_DENSITY_KG_M3;
|
|
37
24
|
const sandVolumeM3 = siteMixPartVolume * 2;
|
|
38
25
|
const concreteAggregateVolumeM3 = siteMixPartVolume * 4;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const aggregateBulkBags = Math.ceil((volumeM3 * selectedAggregate.density * 1000) / BULK_BAG_KG);
|
|
26
|
+
const kg = (volume, density) => Math.ceil(volume * density * 1000);
|
|
27
|
+
// Asked for by weight: the basket turns it into whatever bags the merchant sells, through the
|
|
28
|
+
// same product table and pricing as the other calculators.
|
|
43
29
|
const requirements = group === "concrete"
|
|
44
30
|
? [
|
|
45
|
-
{ key: "cement", label: "
|
|
46
|
-
{ key: "sharp-sand", label: "Sharp
|
|
47
|
-
{ key: "gravel", label: "20mm
|
|
31
|
+
{ key: "cement", label: "General purpose cement", qty: Math.ceil(cementKg), unit: "kg", query: "general purpose cement" },
|
|
32
|
+
{ key: "sharp-sand", label: "Sharp sand", qty: kg(sandVolumeM3, aggregateDetails["sharp-sand"].density), unit: "kg", query: aggregateDetails["sharp-sand"].query },
|
|
33
|
+
{ key: "gravel", label: "20mm gravel", qty: kg(concreteAggregateVolumeM3, aggregateDetails.gravel.density), unit: "kg", query: aggregateDetails.gravel.query },
|
|
48
34
|
]
|
|
49
|
-
: [{ key: aggregate, label:
|
|
50
|
-
|
|
51
|
-
// product code is the query because that is what resolves these exact lines.
|
|
52
|
-
const submit = () => {
|
|
53
|
-
if (onReview) {
|
|
54
|
-
onReview(requirements.map((requirement) => ({
|
|
55
|
-
key: requirement.key,
|
|
56
|
-
label: requirement.label,
|
|
57
|
-
qty: requirement.qty,
|
|
58
|
-
unit: "each",
|
|
59
|
-
query: requirement.productId,
|
|
60
|
-
})));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
void addToBasket();
|
|
64
|
-
};
|
|
65
|
-
const addToBasket = async () => {
|
|
66
|
-
const startedAt = Date.now();
|
|
67
|
-
setAdding(true);
|
|
68
|
-
setBasketError(null);
|
|
69
|
-
try {
|
|
70
|
-
const items = await Promise.all(requirements.map(async (requirement) => {
|
|
71
|
-
const candidate = (await searchCatalogue(requirement.productId)).find((item) => item.product_id === requirement.productId);
|
|
72
|
-
if (!candidate)
|
|
73
|
-
throw new Error(`${requirement.label} is no longer available in this catalogue.`);
|
|
74
|
-
return candidateToCartItem(candidate, {
|
|
75
|
-
lineId: `concrete-aggregate:${requirement.key}`,
|
|
76
|
-
description: requirement.label,
|
|
77
|
-
unit: "bag",
|
|
78
|
-
qty: requirement.qty,
|
|
79
|
-
});
|
|
80
|
-
}));
|
|
81
|
-
const remaining = MIN_BASKET_LOADING_MS - (Date.now() - startedAt);
|
|
82
|
-
if (remaining > 0)
|
|
83
|
-
await new Promise((resolve) => window.setTimeout(resolve, remaining));
|
|
84
|
-
cart?.clearCart();
|
|
85
|
-
cart?.addManyToCart(items);
|
|
86
|
-
cart?.openBasket();
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
setBasketError(error instanceof Error ? error.message : "Could not add the products to your basket.");
|
|
90
|
-
}
|
|
91
|
-
finally {
|
|
92
|
-
setAdding(false);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
if (adding) {
|
|
96
|
-
return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Your Materials" }), _jsx("p", { children: "Calculating quantities from the Concrete and Aggregate Calculator." })] }), _jsx("div", { className: "truss-estimator__card", children: _jsxs("div", { className: "processing-container processing-variant-banter", role: "status", "aria-label": "Loading", style: { flex: 1, justifyContent: "center" }, children: [_jsx(BrickWall, {}), _jsx("p", { className: "banter-message is-visible", children: "Preparing your materials\u2026" })] }) })] }) }));
|
|
97
|
-
}
|
|
98
|
-
return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Concrete and Aggregate Calculator" }), _jsx("p", { children: "Calculate fixed site-mix concrete ingredients or aggregate bulk bags." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Choose what you need" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter the area and depth. The calculator adds the fixed Materials Market products directly to your basket." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Material type" }), _jsxs("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [_jsxs("button", { type: "button", className: `truss-estimator__option-card${group === "concrete" ? " is-selected" : ""}`, onClick: () => setGroup("concrete"), children: [_jsx("div", { className: "truss-estimator__option-title", children: "Site-mixed concrete" }), _jsx("div", { className: "truss-estimator__option-copy", children: "Cement, sharp sand and 20mm aggregate" })] }), _jsxs("button", { type: "button", className: `truss-estimator__option-card${group === "aggregate" ? " is-selected" : ""}`, onClick: () => setGroup("aggregate"), children: [_jsx("div", { className: "truss-estimator__option-title", children: "Aggregates" }), _jsx("div", { className: "truss-estimator__option-copy", children: "MOT Type 1, sharp sand or 20mm gravel" })] })] })] }), group === "aggregate" && (_jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Aggregate type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.keys(aggregateDetails).map((id) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${aggregate === id ? " is-selected" : ""}`, onClick: () => setAggregate(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: aggregateDetails[id].label }), _jsx("div", { className: "truss-estimator__option-copy", children: aggregateDetails[id].blurb })] }, id))) })] })), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area and depth" }), _jsxs("div", { className: "materials-calculator__unit-row", children: [_jsx("span", { children: "Units" }), _jsxs("div", { className: "brick-calculator__unit-toggle", role: "group", "aria-label": "Measurement units", children: [_jsx("button", { type: "button", className: unit === "m" ? "is-selected" : "", onClick: () => setUnit("m"), children: "Metric" }), _jsx("button", { type: "button", className: unit === "ft" ? "is-selected" : "", onClick: () => setUnit("ft"), children: "Imperial" })] })] }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Length (", unit, ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: length, onChange: (event) => setLength(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Width (", unit, ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] }), _jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsxs("span", { children: ["Depth (", unit === "m" ? "cm" : "inches", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.1", value: depth, onChange: (event) => setDepth(event.target.value) })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Materials required" }), _jsx("div", { className: "materials-calculator__result-grid", children: group === "concrete" ? (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("span", { children: "Cement" }), _jsxs("strong", { children: [cementKg.toFixed(0), " kg"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Sharp sand" }), _jsxs("strong", { children: [sandVolumeM3.toFixed(2), " m\u00B3"] })] }), _jsxs("div", { children: [_jsx("span", { children: "20mm aggregate" }), _jsxs("strong", { children: [concreteAggregateVolumeM3.toFixed(2), " m\u00B3"] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("span", { children: selectedAggregate.label }), _jsxs("strong", { children: [volumeM3.toFixed(2), " m\u00B3"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Approx. weight" }), _jsxs("strong", { children: [(volumeM3 * selectedAggregate.density).toFixed(2), " t"] })] }), _jsx("div", {})] })) }), _jsx("p", { className: "materials-calculator__note", children: group === "concrete"
|
|
35
|
+
: [{ key: aggregate, label: selectedAggregate.label, qty: kg(volumeM3, selectedAggregate.density), unit: "kg", query: selectedAggregate.query }];
|
|
36
|
+
return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Concrete and Aggregate Calculator" }), _jsx("p", { children: "Calculate fixed site-mix concrete ingredients or aggregate bulk bags." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Choose what you need" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter the area and depth. The calculator works out the materials and adds them to your basket." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Material type" }), _jsxs("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [_jsxs("button", { type: "button", className: `truss-estimator__option-card${group === "concrete" ? " is-selected" : ""}`, onClick: () => setGroup("concrete"), children: [_jsx("div", { className: "truss-estimator__option-title", children: "Site-mixed concrete" }), _jsx("div", { className: "truss-estimator__option-copy", children: "Cement, sharp sand and 20mm aggregate" })] }), _jsxs("button", { type: "button", className: `truss-estimator__option-card${group === "aggregate" ? " is-selected" : ""}`, onClick: () => setGroup("aggregate"), children: [_jsx("div", { className: "truss-estimator__option-title", children: "Aggregates" }), _jsx("div", { className: "truss-estimator__option-copy", children: "MOT Type 1, sharp sand or 20mm gravel" })] })] })] }), group === "aggregate" && (_jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Aggregate type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.keys(aggregateDetails).map((id) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${aggregate === id ? " is-selected" : ""}`, onClick: () => setAggregate(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: aggregateDetails[id].label }), _jsx("div", { className: "truss-estimator__option-copy", children: aggregateDetails[id].blurb })] }, id))) })] })), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area and depth" }), _jsxs("div", { className: "materials-calculator__unit-row", children: [_jsx("span", { children: "Units" }), _jsxs("div", { className: "brick-calculator__unit-toggle", role: "group", "aria-label": "Measurement units", children: [_jsx("button", { type: "button", className: unit === "m" ? "is-selected" : "", onClick: () => setUnit("m"), children: "Metric" }), _jsx("button", { type: "button", className: unit === "ft" ? "is-selected" : "", onClick: () => setUnit("ft"), children: "Imperial" })] })] }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Length (", unit, ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: length, onChange: (event) => setLength(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Width (", unit, ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] }), _jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsxs("span", { children: ["Depth (", unit === "m" ? "cm" : "inches", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.1", value: depth, onChange: (event) => setDepth(event.target.value) })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Materials required" }), _jsx("div", { className: "materials-calculator__result-grid", children: group === "concrete" ? (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("span", { children: "Cement" }), _jsxs("strong", { children: [cementKg.toFixed(0), " kg"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Sharp sand" }), _jsxs("strong", { children: [sandVolumeM3.toFixed(2), " m\u00B3"] })] }), _jsxs("div", { children: [_jsx("span", { children: "20mm aggregate" }), _jsxs("strong", { children: [concreteAggregateVolumeM3.toFixed(2), " m\u00B3"] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("span", { children: selectedAggregate.label }), _jsxs("strong", { children: [volumeM3.toFixed(2), " m\u00B3"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Approx. weight" }), _jsxs("strong", { children: [(volumeM3 * selectedAggregate.density).toFixed(2), " t"] })] }), _jsx("div", {})] })) }), _jsx("p", { className: "materials-calculator__note", children: group === "concrete"
|
|
99
37
|
? "Based on a 1:2:4 mix. Confirm the mix design for structural work."
|
|
100
|
-
: "Figures are estimates. Confirm the specification and supplier quantities before ordering." })] }),
|
|
38
|
+
: "Figures are estimates. Confirm the specification and supplier quantities before ordering." })] }), _jsxs("div", { className: "materials-calculator__actions", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary estimator-actions__back", onClick: onBack, children: "Back" }), _jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", onClick: () => onReview?.(requirements), disabled: volumeM3 <= 0, children: submitLabel })] })] })] }) }));
|
|
101
39
|
}
|
|
@@ -272,13 +272,13 @@ export default function EstimatorResults({ toolId, requirements, ready, onBack,
|
|
|
272
272
|
const options = pickerResults.length > 0 ? pickerResults : row.candidates;
|
|
273
273
|
return (_jsxs("div", { className: "estimator-products__picker", children: [_jsx("input", { type: "text", className: "results-search-input", value: pickerSearch, autoFocus: true, placeholder: "Search products in catalogue...", onChange: (event) => setPickerSearch(event.target.value), "aria-label": `Search a product for ${row.requirement.label}` }), _jsxs("div", { className: "estimator-products__options", children: [options.map((candidate) => (_jsxs("button", { type: "button", className: `estimator-products__option${candidate.product_id === row.chosen?.product_id ? " is-selected" : ""}`, onClick: () => chooseCandidate(row.requirement.key, candidate), children: [_jsx("span", { children: candidate.description }), _jsxs("span", { className: "results-row-meta", children: [candidate.product_id, candidate.price != null && ` · ${fmtCurrency(candidate.price)}`] })] }, candidate.product_id))), options.length === 0 && _jsx("p", { className: "results-empty-hint", children: "No matching products." })] })] }));
|
|
274
274
|
};
|
|
275
|
-
return (_jsxs("section", { className: "estimator-products results-tile-section", "aria-label": "Matched products", children: [!loading && (_jsxs("div", { className: "estimator-products__head", children: [_jsxs("div", { className: "results-tile-section__head", children: [_jsx("h3", { children: "Matched Products" }), _jsx("p", { children: "Products matched to your estimate, check before adding to your basket." })] }), !reviewRow && (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
275
|
+
return (_jsxs("section", { className: "estimator-products results-tile-section", "aria-label": "Matched products", children: [!loading && (_jsxs("div", { className: "estimator-products__head", children: [_jsxs("div", { className: "results-tile-section__head", children: [_jsx("h3", { children: "Matched Products" }), _jsx("p", { children: "Products matched to your estimate, check before adding to your basket." })] }), !reviewRow && (_jsx("div", { className: "results-summary estimator-products__toolbar", children: _jsxs("div", { className: "estimator-products__controls", children: [_jsx("input", { type: "text", className: "results-search-input", placeholder: "Search description...", value: search, onChange: (event) => setSearch(event.target.value), "aria-label": "Search matched products" }), _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" })] }) })] }), _jsxs("details", { ref: sortMenuRef, style: { position: "relative" }, children: [_jsx("summary", { className: "bom-toolbar-btn sort-menu-summary", "aria-label": "Sort materials", style: { fontSize: 16 }, children: "\u21C5" }), _jsx("div", { className: "sort-menu-panel", children: SORT_OPTIONS.map((option) => {
|
|
276
|
+
const active = sortConfig === option.dir;
|
|
277
|
+
return (_jsxs("button", { type: "button", className: `sort-menu-item${active ? " is-active" : ""}`, onClick: () => {
|
|
278
|
+
setSortConfig(active ? null : option.dir);
|
|
279
|
+
sortMenuRef.current?.removeAttribute("open");
|
|
280
|
+
}, children: [_jsx("span", { className: "sort-menu-item__check", children: active ? "✓" : "" }), option.label] }, option.dir));
|
|
281
|
+
}) })] })] }) }))] })), reviewRow && (_jsxs("div", { className: "results-review-page", 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: { display: "flex", gap: 8, flexWrap: "wrap" }, children: [_jsx("div", { className: "results-review-searchbar", children: _jsx("input", { type: "text", className: "results-search-input", placeholder: "Search products in catalogue...", value: reviewSearch, onChange: (event) => setReviewSearch(event.target.value), "aria-label": `Search a product for ${reviewRow.requirement.label}` }) }), reviewRow.chosen && (_jsx("div", { className: "results-candidate-actions", children: _jsx("button", { type: "button", className: "bom-toolbar-btn", onClick: () => clearChosen(reviewRow.requirement.key), 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(reviewRow.chosen?.description || reviewRow.requirement.label, reviewRow.chosen?.product_id, reviewRow.chosen?.pdp_photo_url), alt: "" }) }), _jsxs("div", { className: "results-review-page__summary", children: [_jsx("h2", { children: reviewRow.requirement.label }), _jsxs("p", { className: "results-review-page__meta", children: [fmtQty(reviewRow.requirement.qty), " ", renderDisplayUnit(reviewRow.requirement.unit, reviewRow.requirement.qty)] }), reviewRow.chosen ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "results-review-page__product", children: reviewRow.chosen.description }), _jsxs("div", { className: "results-row-meta", children: [reviewRow.chosen.product_id, reviewRow.chosen.price != null ? ` · ${fmtPrice(reviewRow.chosen.price, reviewRow.chosen.price_basis)}` : ""] })] })) : (_jsx("div", { className: "results-empty-hint", children: "No product currently selected for this line." })), _jsx("div", { className: "results-review-page__actions", children: _jsx("button", { type: "button", className: `bom-toolbar-btn${cart?.cart[lineIdFor(reviewRow.requirement.key)] ? " bom-toolbar-btn--added" : " bom-toolbar-btn--accent"}`, onClick: () => toggleInCart(reviewRow), disabled: !reviewRow.chosen || !cart, children: cart?.cart[lineIdFor(reviewRow.requirement.key)] ? "✓ Added" : "+ Add to basket" }) })] })] }), reviewCandidates.length === 0 ? (_jsx("p", { className: "results-empty-hint", children: "No matching products." })) : (_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" }), _jsx("th", { "aria-label": "Actions" })] }) }), _jsx("tbody", { children: reviewCandidates.map((candidate) => {
|
|
282
282
|
const isCurrent = candidate.product_id === reviewRow.chosen?.product_id;
|
|
283
283
|
return (_jsxs("tr", { className: isCurrent ? "score-breakdown-row--best" : "", children: [_jsxs("td", { children: [_jsx("div", { children: candidate.description }), _jsx("div", { className: "results-row-meta", children: candidate.product_id })] }), _jsx("td", { children: _jsx("span", { className: "unit-badge", children: fmtMeasurements(candidate.measurements_mm) }) }), _jsx("td", { children: candidate.price != null ? fmtPrice(candidate.price, candidate.price_basis) : "-" }), _jsx("td", { children: _jsxs("label", { className: "switch-toggle", title: isCurrent ? "Currently selected" : "Use this candidate", children: [_jsx("input", { type: "checkbox", checked: isCurrent, onChange: () => {
|
|
284
284
|
if (isCurrent)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { EstimatorProps } from "./registry";
|
|
2
|
-
export default function PaintEstimator({ onBack }: EstimatorProps): import("react").JSX.Element;
|
|
2
|
+
export default function PaintEstimator({ onBack, onReview, submitLabel }: EstimatorProps): import("react").JSX.Element;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { fixedProductSearch } from "../../lib/estimators/fixedProducts";
|
|
4
5
|
const PAINT_COVERAGE_M2_PER_LITRE = 10;
|
|
5
6
|
const DOOR_AREA_M2 = 1.9 * 0.762;
|
|
6
7
|
const DOUBLE_DOOR_AREA_M2 = 1.9 * 1.524;
|
|
7
8
|
const emptyDimensions = () => ({ height: "", width: "" });
|
|
8
9
|
const value = (input) => Number.parseFloat(input) || 0;
|
|
9
10
|
const area = ({ height, width }) => value(height) * value(width);
|
|
10
|
-
export default function PaintEstimator({ onBack }) {
|
|
11
|
+
export default function PaintEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
|
|
11
12
|
const [walls, setWalls] = useState([emptyDimensions()]);
|
|
12
13
|
const [excludedAreas, setExcludedAreas] = useState([emptyDimensions()]);
|
|
13
14
|
const [doors, setDoors] = useState("0");
|
|
@@ -18,8 +19,12 @@ export default function PaintEstimator({ onBack }) {
|
|
|
18
19
|
const doorArea = value(doors) * DOOR_AREA_M2 + value(doubleDoors) * DOUBLE_DOOR_AREA_M2;
|
|
19
20
|
const paintableArea = Math.max(0, wallArea - excludedArea - doorArea);
|
|
20
21
|
const litres = paintableArea * coats / PAINT_COVERAGE_M2_PER_LITRE;
|
|
22
|
+
// Asked for in litres; the basket rounds up to whole tins of the merchant's paint. Only offered
|
|
23
|
+
// where the merchant has a paint product set up - elsewhere this stays an estimate.
|
|
24
|
+
const requirement = { key: "paint-emulsion", label: "Matt emulsion, brilliant white", qty: Math.round(litres * 10) / 10, unit: "litres", query: "matt emulsion brilliant white" };
|
|
25
|
+
const canOrder = fixedProductSearch("paint", requirement) != null;
|
|
21
26
|
const updateDimensions = (setter, index, key, nextValue) => {
|
|
22
27
|
setter((items) => items.map((item, itemIndex) => itemIndex === index ? { ...item, [key]: nextValue } : item));
|
|
23
28
|
};
|
|
24
|
-
return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Paint Calculator" }), _jsx("p", { children: "Estimate how much paint you need for your walls, including multiple coats and excluded areas." })] }), _jsxs("div", { className: "truss-estimator__card paint-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Walls, exclusions and coats" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter each wall, subtract windows and doors, and choose your number of coats. Quantities assume a coverage of 10 m\u00B2 per litre." }), _jsxs("div", { className: "paint-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall dimensions" }), _jsx("p", { className: "paint-calculator__hint", children: "Enter the height and width of each wall you want to paint." }), _jsxs("div", { className: "paint-calculator__dimensions-header", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", { children: "Height (m)" }), _jsx("span", { children: "Width (m)" }), _jsx("span", {})] }), walls.map((wall, index) => (_jsxs("div", { className: "paint-calculator__row", children: [_jsxs("span", { className: "paint-calculator__row-label", children: ["Wall ", index + 1] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Height (m)" }), _jsx("input", { "aria-label": `Wall ${index + 1} height in metres`, type: "number", min: "0", step: "0.01", value: wall.height, onChange: (event) => updateDimensions(setWalls, index, "height", event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Width (m)" }), _jsx("input", { "aria-label": `Wall ${index + 1} width in metres`, type: "number", min: "0", step: "0.01", value: wall.width, onChange: (event) => updateDimensions(setWalls, index, "width", event.target.value) })] }), walls.length > 1 ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setWalls((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: "Remove" }) : null] }, `wall-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setWalls((items) => [...items, emptyDimensions()]), children: "+ Add another wall" })] }), _jsxs("div", { className: "paint-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Areas to exclude" }), _jsx("p", { className: "paint-calculator__hint", children: "Add windows, sockets, skirting boards or other areas that will not be painted." }), _jsxs("div", { className: "paint-calculator__dimensions-header", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", { children: "Height (m)" }), _jsx("span", { children: "Width (m)" }), _jsx("span", {})] }), excludedAreas.map((excluded, index) => (_jsxs("div", { className: "paint-calculator__row", children: [_jsxs("span", { className: "paint-calculator__row-label", children: ["Area ", index + 1] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Height (m)" }), _jsx("input", { "aria-label": `Area ${index + 1} height in metres`, type: "number", min: "0", step: "0.01", value: excluded.height, onChange: (event) => updateDimensions(setExcludedAreas, index, "height", event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Width (m)" }), _jsx("input", { "aria-label": `Area ${index + 1} width in metres`, type: "number", min: "0", step: "0.01", value: excluded.width, onChange: (event) => updateDimensions(setExcludedAreas, index, "width", event.target.value) })] }), excludedAreas.length > 1 ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setExcludedAreas((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: "Remove" }) : null] }, `excluded-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setExcludedAreas((items) => [...items, emptyDimensions()]), children: "+ Add another area" })] }), _jsxs("div", { className: "paint-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Doors and coats" }), _jsx("p", { className: "paint-calculator__hint", children: "Enter the number of standard single and double doors to leave unpainted. Door sizes are estimated automatically." }), _jsxs("div", { className: "truss-estimator__field-grid paint-calculator__small-fields", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Doors" }), _jsx("input", { type: "number", min: "0", step: "1", value: doors, onChange: (event) => setDoors(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Double doors" }), _jsx("input", { type: "number", min: "0", step: "1", value: doubleDoors, onChange: (event) => setDoubleDoors(event.target.value) })] })] }), _jsxs("div", { className: "paint-calculator__coat-field", children: [_jsx("p", { className: "paint-calculator__hint", children: "Most walls need 2 coats for an even finish. Use 3 coats when changing colour significantly or covering a darker wall." }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Number of coats" }), _jsxs("select", { value: coats, onChange: (event) => setCoats(Number(event.target.value)), children: [_jsx("option", { value: "1", children: "1 coat" }), _jsx("option", { value: "2", children: "2 coats" }), _jsx("option", { value: "3", children: "3 coats" })] })] })] })] }), _jsxs("div", { className: "paint-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "paint-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Paintable area" }), _jsxs("strong", { children: [paintableArea.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Coverage" }), _jsxs("strong", { children: [coats, " ", coats === 1 ? "coat" : "coats"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Paint required" }), _jsxs("strong", { children: [litres.toFixed(1), " litres"] })] })] }), _jsx("p", { className: "paint-calculator__note", children: "Based on paint coverage of 10 m
|
|
29
|
+
return (_jsx(_Fragment, { children: _jsxs("div", { className: "truss-estimator", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Estimating Tools" }), _jsx("h1", { children: "Paint Calculator" }), _jsx("p", { children: "Estimate how much paint you need for your walls, including multiple coats and excluded areas." })] }), _jsxs("div", { className: "truss-estimator__card paint-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Walls, exclusions and coats" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter each wall, subtract windows and doors, and choose your number of coats. Quantities assume a coverage of 10 m\u00B2 per litre." }), _jsxs("div", { className: "paint-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall dimensions" }), _jsx("p", { className: "paint-calculator__hint", children: "Enter the height and width of each wall you want to paint." }), _jsxs("div", { className: "paint-calculator__dimensions-header", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", { children: "Height (m)" }), _jsx("span", { children: "Width (m)" }), _jsx("span", {})] }), walls.map((wall, index) => (_jsxs("div", { className: "paint-calculator__row", children: [_jsxs("span", { className: "paint-calculator__row-label", children: ["Wall ", index + 1] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Height (m)" }), _jsx("input", { "aria-label": `Wall ${index + 1} height in metres`, type: "number", min: "0", step: "0.01", value: wall.height, onChange: (event) => updateDimensions(setWalls, index, "height", event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Width (m)" }), _jsx("input", { "aria-label": `Wall ${index + 1} width in metres`, type: "number", min: "0", step: "0.01", value: wall.width, onChange: (event) => updateDimensions(setWalls, index, "width", event.target.value) })] }), walls.length > 1 ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setWalls((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: "Remove" }) : null] }, `wall-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setWalls((items) => [...items, emptyDimensions()]), children: "+ Add another wall" })] }), _jsxs("div", { className: "paint-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Areas to exclude" }), _jsx("p", { className: "paint-calculator__hint", children: "Add windows, sockets, skirting boards or other areas that will not be painted." }), _jsxs("div", { className: "paint-calculator__dimensions-header", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", { children: "Height (m)" }), _jsx("span", { children: "Width (m)" }), _jsx("span", {})] }), excludedAreas.map((excluded, index) => (_jsxs("div", { className: "paint-calculator__row", children: [_jsxs("span", { className: "paint-calculator__row-label", children: ["Area ", index + 1] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Height (m)" }), _jsx("input", { "aria-label": `Area ${index + 1} height in metres`, type: "number", min: "0", step: "0.01", value: excluded.height, onChange: (event) => updateDimensions(setExcludedAreas, index, "height", event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { className: "sr-only", children: "Width (m)" }), _jsx("input", { "aria-label": `Area ${index + 1} width in metres`, type: "number", min: "0", step: "0.01", value: excluded.width, onChange: (event) => updateDimensions(setExcludedAreas, index, "width", event.target.value) })] }), excludedAreas.length > 1 ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setExcludedAreas((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: "Remove" }) : null] }, `excluded-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setExcludedAreas((items) => [...items, emptyDimensions()]), children: "+ Add another area" })] }), _jsxs("div", { className: "paint-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Doors and coats" }), _jsx("p", { className: "paint-calculator__hint", children: "Enter the number of standard single and double doors to leave unpainted. Door sizes are estimated automatically." }), _jsxs("div", { className: "truss-estimator__field-grid paint-calculator__small-fields", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Doors" }), _jsx("input", { type: "number", min: "0", step: "1", value: doors, onChange: (event) => setDoors(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Double doors" }), _jsx("input", { type: "number", min: "0", step: "1", value: doubleDoors, onChange: (event) => setDoubleDoors(event.target.value) })] })] }), _jsxs("div", { className: "paint-calculator__coat-field", children: [_jsx("p", { className: "paint-calculator__hint", children: "Most walls need 2 coats for an even finish. Use 3 coats when changing colour significantly or covering a darker wall." }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Number of coats" }), _jsxs("select", { value: coats, onChange: (event) => setCoats(Number(event.target.value)), children: [_jsx("option", { value: "1", children: "1 coat" }), _jsx("option", { value: "2", children: "2 coats" }), _jsx("option", { value: "3", children: "3 coats" })] })] })] })] }), _jsxs("div", { className: "paint-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "paint-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Paintable area" }), _jsxs("strong", { children: [paintableArea.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Coverage" }), _jsxs("strong", { children: [coats, " ", coats === 1 ? "coat" : "coats"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Paint required" }), _jsxs("strong", { children: [litres.toFixed(1), " litres"] })] })] }), _jsx("p", { className: "paint-calculator__note", children: canOrder ? "Based on paint coverage of 10 m² per litre. Ordered as brilliant white matt emulsion, rounded up to whole tins." : "Based on paint coverage of 10 m² per litre. Decorative paint is not currently stocked in this catalogue, so this remains an estimate only." })] }), _jsxs("div", { className: "paint-calculator__actions", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary estimator-actions__back", onClick: onBack, children: "Back" }), canOrder ? (_jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", onClick: () => onReview?.([requirement]), disabled: litres <= 0, children: submitLabel })) : (_jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", disabled: true, children: "Paint products unavailable" }))] })] })] }) }));
|
|
25
30
|
}
|