@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.
@@ -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) ? candidates.find((candidate) => candidate.product_id === selectedProductId) : undefined;
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 ? undefined : (pickOverrideCandidates[item.line_id] ?? (savedCartMatch && savedCartMatch.productId !== automaticMatch?.product_id ? candidateFromCart(savedCartMatch) : automaticMatch));
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) ? [...reviewCandidates, savedSearchCandidate] : reviewCandidates;
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 && (_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: {
684
- display: "flex",
685
- gap: 8,
686
- flexWrap: "wrap",
687
- alignItems: "center",
688
- }, 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) => {
689
- const active = sortConfig === opt.dir;
690
- return (_jsxs("button", { type: "button", className: `sort-menu-item${active ? " is-active" : ""}`, onClick: () => {
691
- setSortConfig(active ? null : opt.dir);
692
- sortMenuRef.current?.removeAttribute("open");
693
- }, children: [_jsx("span", { className: "sort-menu-item__check", children: active ? "✓" : "" }), opt.label] }, opt.dir));
694
- }) })] })] })] })) })), 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: {
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] ? (internal ? "Remove from selection" : `Remove from ${customerCollection}`) : internal ? "Add to selection" : `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 ? (llmReviewCandidates.length > 0 ? "Choose a product" : "No suitable product found") : hasAiVerdict ? "Choose a product" : "We couldn't review this item" }), _jsx("p", { children: hasNoSuitableCandidate
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] ? (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));
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] ? (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));
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, productId: "6001007" },
16
- "sharp-sand": { label: "Sharp sand", blurb: "Bedding and general groundwork", density: 1.6, productId: "6001012" },
17
- gravel: { label: "20mm gravel", blurb: "Drainage and landscaping", density: 1.6, productId: "6001004" },
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 cementBags = Math.ceil(cementKg / CEMENT_BAG_KG);
40
- const sandBulkBags = Math.ceil((sandVolumeM3 * aggregateDetails["sharp-sand"].density * 1000) / BULK_BAG_KG);
41
- const concreteAggregateBulkBags = Math.ceil((concreteAggregateVolumeM3 * aggregateDetails.gravel.density * 1000) / BULK_BAG_KG);
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: "Blue Circle General Purpose Grey Cement 25kg", qty: cementBags, productId: "6003004" },
46
- { key: "sharp-sand", label: "Sharp Sand Bulk Bag (800kg)", qty: sandBulkBags, productId: "6001012" },
47
- { key: "gravel", label: "20mm Non Decorative Pea Shingle/Gravel Bulk Bag (800kg)", qty: concreteAggregateBulkBags, productId: "6001004" },
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: `${selectedAggregate.label} Bulk Bag (800kg)`, qty: aggregateBulkBags, productId: selectedAggregate.productId }];
50
- // Handed up as requirements when the host has its own review step; the
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." })] }), basketError && _jsx("p", { className: "results-empty-hint estimator-products__error", children: basketError }), _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: submit, disabled: volumeM3 <= 0 || adding, children: adding ? "Adding products…" : submitLabel })] })] })] }) }));
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 && (_jsxs("div", { className: "results-summary estimator-products__toolbar", children: [needsPricingCount > 0 && (_jsxs("span", { className: "summary-chip summary-chip--neutral", children: [_jsx("strong", { children: needsPricingCount }), " need pricing"] })), _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) => {
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\u00B2 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" }), _jsx("button", { type: "button", className: "cta-btn estimator-actions__calculate", disabled: true, children: "Paint products unavailable" })] })] })] }) }));
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
  }