@metron-labs/quote-react 0.1.45 → 0.1.47

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.
@@ -12,5 +12,5 @@ interface BasketStageProps {
12
12
  onQuoteRequested?: () => void;
13
13
  internal?: boolean;
14
14
  }
15
- export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, onQuoteRequested, internal }: BasketStageProps): import("react").JSX.Element;
15
+ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, onQuoteRequested, internal, }: BasketStageProps): import("react").JSX.Element;
16
16
  export {};
@@ -37,7 +37,7 @@ function TrashIcon() {
37
37
  function ChevronIcon() {
38
38
  return (_jsx("svg", { width: "8", height: "5", viewBox: "0 0 8 5", fill: "none", children: _jsx("path", { d: "M1 4L4 1L7 4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }));
39
39
  }
40
- export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, onQuoteRequested, internal = false }) {
40
+ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, onQuoteRequested, internal = false, }) {
41
41
  // Both widgets can render a basket on the same page, so element ids are made unique.
42
42
  const uid = useId();
43
43
  const qzaBasketLayout = !internal && getBoqClientId() === "QZA6-MRA4";
@@ -191,7 +191,9 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
191
191
  // The API's own message is written for developers (e.g. "items.0: Unrecognized key(s)"),
192
192
  // so the customer gets a plain one and the console keeps the detail.
193
193
  console.error("Quote request failed", res.status, payload?.message);
194
- setQuoteContactError(res.status === 429 ? "You've sent a few requests already. Please try again in a few minutes." : "Your request couldn't be sent. Please try again, or contact us if it keeps happening.");
194
+ setQuoteContactError(res.status === 429
195
+ ? "You've sent a few requests already. Please try again in a few minutes."
196
+ : "Your request couldn't be sent. Please try again, or contact us if it keeps happening.");
195
197
  return;
196
198
  }
197
199
  track("quote_requested", { items: pricedItems.length, total: Math.round(total * 100) / 100 });
@@ -237,7 +239,7 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
237
239
  return (_jsx("div", { className: `wizard-step entry-stage${qzaBasketLayout ? " basket-layout--qza6-mra4" : ""}`, children: _jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsxs("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: [internal && _jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "basket-hero-copy", children: [qzaBasketLayout && (_jsxs("button", { type: "button", className: "cta-btn cta-btn-secondary basket-hero-back", onClick: onBack, children: [_jsx(BackArrow, {}), _jsx("span", { children: "Back to results" })] })), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: internal ? "Your Selection" : qzaBasketLayout ? "Request a quote" : "Your Basket" }), internal && (_jsxs("span", { className: "summary-chip summary-chip--scope", children: [_jsx("strong", { children: items.length }), " ", items.length === 1 ? "item" : "items"] }))] }), _jsx("p", { children: internal
238
240
  ? "Review the items you've added before exporting them for review."
239
241
  : qzaBasketLayout
240
- ? "Check your items & quantities below, and we'll do our best to beat list prices."
242
+ ? "Check your items and quantities below, and we'll do our best to beat list prices."
241
243
  : "Review your quantities and order summary before you buy." })] })] }), _jsx("div", { className: "quick-order-panel", style: { paddingTop: 16 }, children: items.length === 0 ? (_jsxs("div", { className: "quick-order-history-empty", children: [_jsx("div", { className: "basket-empty-icon", "aria-hidden": "true", children: "\uD83D\uDCCB" }), _jsx("p", { children: "Your selection is empty." }), _jsx("p", { className: "basket-empty-hint", children: "Go back to your results and add quoted items." }), _jsx("div", { className: "btn-row", style: { marginTop: 20, justifyContent: "center" }, children: _jsx("button", { className: "cta-btn", style: { width: "auto", maxWidth: 280 }, onClick: onBack, "aria-label": "Back to results", children: _jsx("span", { children: "Back to Results" }) }) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "basket-layout", children: [_jsxs("div", { className: "basket-main", children: [_jsx("div", { className: "basket-list", "aria-label": internal ? "Selection items" : qzaBasketLayout ? "Quote request items" : "Basket items", children: pagedItems.map(({ item, pricing }) => {
242
244
  const requestedQtyValue = qtyDrafts[item.lineId] ?? String(pricing.requestedAmount);
243
245
  const requestedLocked = pricing.requestedUnit === pricing.sellUnit || pricing.costingMode === "review";
@@ -262,7 +264,10 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
262
264
  setReference(e.target.value);
263
265
  if (qzaBasketLayout)
264
266
  setQuoteContactError("");
265
- }, placeholder: qzaBasketLayout ? "you@example.com" : "PO number or job reference", "aria-invalid": qzaBasketLayout && quoteContactError === INVALID_EMAIL_MESSAGE, "aria-describedby": qzaBasketLayout && quoteContactError ? `${uid}-basket-reference-error` : undefined }), qzaBasketLayout && quoteContactError && (_jsx("p", { id: `${uid}-basket-reference-error`, className: "basket-quote-request-error", role: "alert", children: quoteContactError })), qzaBasketLayout ? (_jsx("button", { type: "button", className: "cta-btn", onClick: () => void submitQuoteRequest(), disabled: quoteRequestSending, "aria-busy": quoteRequestSending, children: _jsx("span", { children: quoteRequestSending ? "Sending…" : "Request our best quote" }) })) : (_jsx("button", { className: "cta-btn", onClick: () => { track("downloaded", { type: "basket", items: items.length }); void exportBasketExcel(items); }, "aria-label": internal ? "Download selection" : "Download basket", children: _jsx("span", { children: internal ? "Download Quote" : "Buy Now" }) })), _jsxs("p", { className: "basket-sidebar-disclaimer", children: [_jsx("span", { "aria-hidden": "true", children: "\u26A0\uFE0F" }), qzaBasketLayout
267
+ }, placeholder: qzaBasketLayout ? "you@example.com" : "PO number or job reference", "aria-invalid": qzaBasketLayout && quoteContactError === INVALID_EMAIL_MESSAGE, "aria-describedby": qzaBasketLayout && quoteContactError ? `${uid}-basket-reference-error` : undefined }), qzaBasketLayout && quoteContactError && (_jsx("p", { id: `${uid}-basket-reference-error`, className: "basket-quote-request-error", role: "alert", children: quoteContactError })), qzaBasketLayout ? (_jsx("button", { type: "button", className: "cta-btn", onClick: () => void submitQuoteRequest(), disabled: quoteRequestSending, "aria-busy": quoteRequestSending, children: _jsx("span", { children: quoteRequestSending ? "Sending…" : "Request our best quote" }) })) : (_jsx("button", { className: "cta-btn", onClick: () => {
268
+ track("downloaded", { type: "basket", items: items.length });
269
+ void exportBasketExcel(items);
270
+ }, "aria-label": internal ? "Download selection" : "Download basket", children: _jsx("span", { children: internal ? "Download Quote" : "Buy Now" }) })), _jsxs("p", { className: "basket-sidebar-disclaimer", children: [_jsx("span", { "aria-hidden": "true", children: "\u26A0\uFE0F" }), qzaBasketLayout
266
271
  ? "This is a quote request, not an order. We'll confirm our best price, availability and delivery before you approve."
267
272
  : "Prices and availability are estimates and should be confirmed before procurement."] })] })] }), _jsx("div", { className: "btn-row results-actions", children: _jsxs("button", { className: "cta-btn cta-btn-secondary", style: { flex: "0 1 30%", gap: 10, justifyContent: "center" }, onClick: onBack, "aria-label": "Back to results", children: [_jsx(BackArrow, {}), _jsx("span", { children: "Back to results" })] }) })] })) })] }) }));
268
273
  }
@@ -26,7 +26,9 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
26
26
  const requirements = [
27
27
  {
28
28
  key: masonryType === "brick" ? `brick-${brickOption}` : "block",
29
- label: masonryType === "brick" ? { common: "Common concrete bricks", "engineering-red": "Red engineering bricks", "engineering-blue": "Blue engineering bricks" }[brickOption] : "Blocks",
29
+ label: masonryType === "brick"
30
+ ? { common: "Common concrete bricks", "engineering-red": "Red engineering bricks", "engineering-blue": "Blue engineering bricks" }[brickOption]
31
+ : "Blocks",
30
32
  qty: units,
31
33
  unit: "each",
32
34
  query: masonryType === "brick" ? "facing brick" : "concrete block",
@@ -42,8 +44,12 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
42
44
  setWidth((toNumber(width) * factor).toFixed(2));
43
45
  setUnit(nextUnit);
44
46
  };
45
- 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: "Brick & Block Calculator" }), _jsx("p", { children: "Estimate masonry units, sand and cement for a wall." })] }), _jsxs("div", { className: "truss-estimator__card brick-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Wall area and masonry type" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Pick brick or block and enter the wall dimensions for unit counts plus ready-mixed mortar." }), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Masonry type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
47
+ 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: "Brick and Block Calculator" }), _jsx("p", { children: "Estimate masonry units, sand and cement for a wall." })] }), _jsxs("div", { className: "truss-estimator__card brick-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Wall area and masonry type" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Pick brick or block and enter the wall dimensions for unit counts plus ready-mixed mortar." }), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Masonry type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
46
48
  ["brick", "Brick"],
47
49
  ["block", "Block"],
48
- ].map(([id, label]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${masonryType === id ? " is-selected" : ""}`, onClick: () => setMasonryType(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsxs("div", { className: "truss-estimator__option-copy", children: ["Approx. ", id === "brick" ? BRICK_COVERAGE : BLOCK_COVERAGE, " per m\u00B2"] })] }, id))) })] }), masonryType === "brick" && _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Brick type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--three", children: [["common", "Common concrete", "General-purpose grey concrete brick"], ["engineering-red", "Engineering red", "Class B perforated, 65mm"], ["engineering-blue", "Engineering blue", "Class B solid, 65mm"]].map(([id, label, blurb]) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${brickOption === id ? " is-selected" : ""}`, onClick: () => setBrickOption(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id)) })] }), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall dimensions" }), _jsxs("div", { className: "brick-calculator__unit-toggle", role: "group", "aria-label": "Measurement units", children: [_jsx("button", { type: "button", className: unit === "metric" ? "is-selected" : "", onClick: () => changeUnit("metric"), children: "Metric" }), _jsx("button", { type: "button", className: unit === "imperial" ? "is-selected" : "", onClick: () => changeUnit("imperial"), children: "Imperial" })] }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Height (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: height, onChange: (event) => setHeight(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Width (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] })] }), _jsx("div", { className: "brick-calculator__options", children: _jsxs("label", { className: "brick-calculator__checkbox", children: [_jsx("input", { type: "checkbox", checked: addMortar, onChange: (event) => setAddMortar(event.target.checked) }), _jsx("span", { children: "Include ready-mixed mortar" })] }) }), _jsxs("div", { className: "brick-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "brick-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Wall area" }), _jsxs("strong", { children: [formatNumber(displayedArea), " ", unit === "metric" ? "m²" : "ft²"] })] }), _jsxs("div", { children: [_jsx("span", { children: masonryType === "brick" ? "Bricks" : "Blocks" }), _jsx("strong", { children: units.toLocaleString("en-GB") })] }), addMortar ? _jsxs("div", { children: [_jsx("span", { children: "Ready-mixed mortar" }), _jsxs("strong", { children: [mortarBags, " x 20kg bags"] })] }) : null] }), _jsx("p", { className: "brick-calculator__note", children: "Estimate based on standard nominal sizes and 10mm joints. Mortar bags are approximate; confirm the required mortar specification before ordering." })] }), _jsxs("div", { className: "brick-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: areaM2 <= 0, children: submitLabel })] })] })] }) }));
50
+ ].map(([id, label]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${masonryType === id ? " is-selected" : ""}`, onClick: () => setMasonryType(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsxs("div", { className: "truss-estimator__option-copy", children: ["Approx. ", id === "brick" ? BRICK_COVERAGE : BLOCK_COVERAGE, " per m\u00B2"] })] }, id))) })] }), masonryType === "brick" && (_jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Brick type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--three", children: [
51
+ ["common", "Common concrete", "General-purpose grey concrete brick"],
52
+ ["engineering-red", "Engineering red", "Class B perforated, 65mm"],
53
+ ["engineering-blue", "Engineering blue", "Class B solid, 65mm"],
54
+ ].map(([id, label, blurb]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${brickOption === id ? " is-selected" : ""}`, onClick: () => setBrickOption(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id))) })] })), _jsxs("div", { className: "brick-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall dimensions" }), _jsxs("div", { className: "brick-calculator__unit-toggle", role: "group", "aria-label": "Measurement units", children: [_jsx("button", { type: "button", className: unit === "metric" ? "is-selected" : "", onClick: () => changeUnit("metric"), children: "Metric" }), _jsx("button", { type: "button", className: unit === "imperial" ? "is-selected" : "", onClick: () => changeUnit("imperial"), children: "Imperial" })] }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Height (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: height, onChange: (event) => setHeight(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsxs("span", { children: ["Width (", unit === "metric" ? "m" : "ft", ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] })] }), _jsx("div", { className: "brick-calculator__options", children: _jsxs("label", { className: "brick-calculator__checkbox", children: [_jsx("input", { type: "checkbox", checked: addMortar, onChange: (event) => setAddMortar(event.target.checked) }), _jsx("span", { children: "Include ready-mixed mortar" })] }) }), _jsxs("div", { className: "brick-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "brick-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Wall area" }), _jsxs("strong", { children: [formatNumber(displayedArea), " ", unit === "metric" ? "m²" : "ft²"] })] }), _jsxs("div", { children: [_jsx("span", { children: masonryType === "brick" ? "Bricks" : "Blocks" }), _jsx("strong", { children: units.toLocaleString("en-GB") })] }), addMortar ? (_jsxs("div", { children: [_jsx("span", { children: "Ready-mixed mortar" }), _jsxs("strong", { children: [mortarBags, " x 20kg bags"] })] })) : null] }), _jsx("p", { className: "brick-calculator__note", children: "Estimate based on standard nominal sizes and 10mm joints. Mortar bags are approximate; confirm the required mortar specification before ordering." })] }), _jsxs("div", { className: "brick-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: areaM2 <= 0, children: submitLabel })] })] })] }) }));
49
55
  }
@@ -93,7 +93,9 @@ export default function ConcreteAggregateEstimator({ onBack, onReview, submitLab
93
93
  }
94
94
  };
95
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 & 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" })] }) })] }) }));
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
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 & 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" ? "Based on a 1:2:4 mix. Confirm the mix design for structural work." : "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 })] })] })] }) }));
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"
99
+ ? "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 })] })] })] }) }));
99
101
  }
@@ -26,7 +26,6 @@ const CONFIG = {
26
26
  wastage: { straight: 0.05, diagonal: 0.15 },
27
27
  pictureFrame: { extraWastage: 0.08 },
28
28
  joist: {
29
- lengthM: 2.4,
30
29
  spacingOptions: [
31
30
  { id: "300", label: "300mm centres (heavy duty)", spacingM: 0.3 },
32
31
  { id: "400", label: "400mm centres (standard)", spacingM: 0.4 },
@@ -145,13 +144,15 @@ export default function DeckingEstimator({ onBack, onReview, submitLabel = "Add
145
144
  const screwBoxes = Math.ceil((area * 30 * (1 + wastageRate + pfExtra)) / 200);
146
145
  const spacing = CONFIG.joist.spacingOptions.find((option) => option.id === state.joistSpacing) ?? CONFIG.joist.spacingOptions[1];
147
146
  const joistRunTotalM = area / spacing.spacingM;
148
- const joistLengthsNeeded = Math.ceil(joistRunTotalM / CONFIG.joist.lengthM);
147
+ // Asked for in metres: the matched joist's own length turns it into lengths, so a merchant
148
+ // selling 2.4m and one selling 3.6m both get the right count.
149
+ const joistMetres = Math.ceil(joistRunTotalM);
149
150
  return {
150
151
  range,
151
152
  area,
152
153
  boardCount,
153
154
  screwBoxes,
154
- joistLengthsNeeded,
155
+ joistMetres,
155
156
  spacing,
156
157
  };
157
158
  };
@@ -159,7 +160,7 @@ export default function DeckingEstimator({ onBack, onReview, submitLabel = "Add
159
160
  const requirements = results
160
161
  ? [
161
162
  { key: `boards-${state.range ?? "meridian"}`, label: `Deck boards (${results.range.label})`, qty: results.boardCount, unit: "each", query: "timber decking board", category: "decking", calculatorWasteIncluded: true },
162
- { key: "joists", label: "Treated C24 joists", qty: results.joistLengthsNeeded, unit: "each", query: "treated joist", category: "timber", calculatorWasteIncluded: true },
163
+ { key: "joists", label: "Treated C24 joists", qty: results.joistMetres, unit: "m", query: "treated joist", category: "timber", calculatorWasteIncluded: true },
163
164
  { key: "screws", label: "Green decking screws (200/box)", qty: results.screwBoxes, unit: "box", query: "green decking screws", category: "fixings", calculatorWasteIncluded: true },
164
165
  ]
165
166
  : [];
@@ -1,6 +1,7 @@
1
1
  import type { EstimatorRequirement } from "../../lib/estimators/types";
2
+ import type { EstimatorToolId } from "./registry";
2
3
  interface EstimatorResultsProps {
3
- toolId: string;
4
+ toolId: EstimatorToolId;
4
5
  requirements: EstimatorRequirement[];
5
6
  /** Mirrors the estimator's own `calculated` flag - nothing is fetched before Calculate. */
6
7
  ready: boolean;
@@ -96,7 +96,7 @@ export default function EstimatorResults({ toolId, requirements, ready, onBack,
96
96
  (async () => {
97
97
  let next = null;
98
98
  try {
99
- const resolved = await resolveRequirements(requirements);
99
+ const resolved = await resolveRequirements(requirements, undefined, toolId);
100
100
  next = resolved.map(({ requirement, candidates }) => ({
101
101
  requirement,
102
102
  candidates,
@@ -1,6 +1,7 @@
1
+ import type { EstimatorToolId } from "./registry";
1
2
  import type { EstimatorRequirement } from "../../lib/estimators/types";
2
3
  interface EstimatorReviewStageProps {
3
- toolId: string;
4
+ toolId: EstimatorToolId;
4
5
  toolTitle: string;
5
6
  requirements: EstimatorRequirement[];
6
7
  onBack: () => void;
@@ -1,11 +1,16 @@
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
+ // byArea: ask for the area and let the matched product's own coverage turn it into packs, which
5
+ // is right whichever client's product it is. Floor and wall still ask for packs worked out from
6
+ // the coverage here.
7
+ // ponytail: floor/wall stay on packs because FORT's catalogue coverage for its floor roll and wall
8
+ // slab is wrong (0.06 and 0.546 m2); switch them to byArea once that data is fixed.
4
9
  const APPLICATIONS = {
5
10
  floor: { label: "Floor", product: "Knauf Acoustic Roll", thickness: "100mm", coverage: 12.36, salesUnit: "roll" },
6
11
  wall: { label: "Cavity wall", product: "Celotex CW4100", thickness: "100mm", coverage: 3.24, salesUnit: "pack" },
7
- roof: { label: "Pitched roof", product: "Kingspan K107", thickness: "100mm", coverage: 8.64, salesUnit: "pack" },
8
- loft: { label: "Loft", product: "Knauf Loft Roll 44", thickness: "100mm", coverage: 13.89, salesUnit: "roll" },
12
+ roof: { label: "Pitched roof", product: "Kingspan K107", thickness: "100mm", coverage: 8.64, salesUnit: "pack", byArea: true },
13
+ loft: { label: "Loft", product: "Knauf Loft Roll 44", thickness: "100mm", coverage: 13.89, salesUnit: "roll", byArea: true },
9
14
  };
10
15
  export default function InsulationEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
11
16
  const [application, setApplication] = useState("floor");
@@ -14,13 +19,12 @@ export default function InsulationEstimator({ onBack, onReview, submitLabel = "A
14
19
  const area = Number.parseFloat(length) * Number.parseFloat(width) || 0;
15
20
  const selected = APPLICATIONS[application];
16
21
  const packs = Math.ceil(area / selected.coverage);
17
- // Requested as area - pack coverage comes from the product, not PRODUCT_COVERAGE.
18
22
  const requirements = [
19
23
  {
20
24
  key: `insulation-${application}`,
21
25
  label: `${selected.product} ${selected.thickness}`,
22
- qty: packs,
23
- unit: selected.salesUnit,
26
+ qty: selected.byArea ? Math.round(area * 100) / 100 : packs,
27
+ unit: selected.byArea ? "m2" : selected.salesUnit,
24
28
  query: selected.product,
25
29
  category: "insulation",
26
30
  },
@@ -3,8 +3,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useState } from "react";
4
4
  const toNumber = (value) => Number.parseFloat(value) || 0;
5
5
  const FINISHES = {
6
- paving: [{ id: "council", label: "Council paving slab (600 × 600mm)" }, { id: "raj", label: "Raj blend sandstone (600 × 600mm)" }],
7
- block: [{ id: "brindle", label: "Brindle block paving (60mm)" }, { id: "charcoal", label: "Charcoal block paving (60mm)" }],
6
+ paving: [
7
+ { id: "council", label: "Council paving slab (600 × 600mm)" },
8
+ { id: "raj", label: "Raj blend sandstone (600 × 600mm)" },
9
+ ],
10
+ block: [
11
+ { id: "brindle", label: "Brindle block paving (60mm)" },
12
+ { id: "charcoal", label: "Charcoal block paving (60mm)" },
13
+ ],
8
14
  gravel: [{ id: "pea", label: "20mm pea shingle" }],
9
15
  };
10
16
  export default function PavingEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
@@ -26,17 +32,27 @@ export default function PavingEstimator({ onBack, onReview, submitLabel = "Add t
26
32
  const motBags = Math.ceil((subBaseVolume * 1700) / 800);
27
33
  const sandBags = Math.ceil((beddingVolume * 1600) / 800);
28
34
  const gravelBags = Math.ceil((gravelVolume * 1600) / 800);
29
- const surfaceOrder = surface === "paving"
30
- ? { qty: slabs, unit: "each" }
31
- : surface === "block"
32
- ? { qty: pavingArea, unit: "m2" }
33
- : { qty: gravelBags, unit: "bag" };
35
+ const surfaceOrder = surface === "paving" ? { qty: slabs, unit: "each" } : surface === "block" ? { qty: pavingArea, unit: "m2" } : { qty: gravelBags, unit: "bag" };
34
36
  const surfaceQuery = { paving: "paving slab", block: "block paver", gravel: "decorative gravel" }[surface];
35
37
  const requirements = [
36
- { key: `surface-${finish}`, label: FINISHES[surface].find((item) => item.id === finish)?.label ?? "Surface material", qty: surfaceOrder.qty, unit: surfaceOrder.unit, query: surfaceQuery, category: "landscaping" },
38
+ {
39
+ key: `surface-${finish}`,
40
+ label: FINISHES[surface].find((item) => item.id === finish)?.label ?? "Surface material",
41
+ qty: surfaceOrder.qty,
42
+ unit: surfaceOrder.unit,
43
+ query: surfaceQuery,
44
+ category: "landscaping",
45
+ },
37
46
  ...(includeSubBase ? [{ key: "sub-base", label: "MOT Type 1 sub-base", qty: motBags, unit: "bag", query: "MOT Type 1 sub base", category: "aggregate" }] : []),
38
47
  ...(includeBedding ? [{ key: "bedding", label: "Sharp sand bedding", qty: sandBags, unit: "bag", query: "sharp sand", category: "aggregate" }] : []),
39
48
  ];
40
- const options = [["paving", "Paving slabs", "For patios and paths"], ["block", "Block paving", "For driveways and paths"], ["gravel", "Decorative gravel", "For paths and landscaping"]];
41
- 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: "Paving & Driveway Calculator" }), _jsx("p", { children: "Estimate paving, block paving, sub-base and bedding materials for outdoor areas." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Surface and ground build-up" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose the surface material and enter the area. Sub-base and bedding sand are estimated alongside the surface units." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Surface material" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--three", children: options.map(([id, label, blurb]) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${surface === id ? " is-selected" : ""}`, onClick: () => { setSurface(id); setFinish(FINISHES[id][0].id); }, children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id)) }), _jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsx("span", { children: "Finish" }), _jsx("select", { value: finish, onChange: (event) => setFinish(event.target.value), children: FINISHES[surface].map((option) => _jsx("option", { value: option.id, children: option.label }, option.id)) })] })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area and groundwork" }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Length (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: length, onChange: (event) => setLength(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Width (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] }), surface === "gravel" && _jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsx("span", { children: "Gravel depth (mm)" }), _jsx("input", { type: "number", min: "0", step: "10", value: gravelDepth, onChange: (event) => setGravelDepth(event.target.value) })] }), includeSubBase && _jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsx("span", { children: "Sub-base depth (mm)" }), _jsx("input", { type: "number", min: "0", step: "10", value: subBaseDepth, onChange: (event) => setSubBaseDepth(event.target.value) })] }), _jsxs("label", { className: "brick-calculator__checkbox materials-calculator__waste", children: [_jsx("input", { type: "checkbox", checked: includeSubBase, onChange: (event) => setIncludeSubBase(event.target.checked) }), _jsx("span", { children: "Include MOT Type 1 sub-base" })] }), _jsxs("label", { className: "brick-calculator__checkbox materials-calculator__waste", children: [_jsx("input", { type: "checkbox", checked: includeBedding, onChange: (event) => setIncludeBedding(event.target.checked) }), _jsx("span", { children: "Include sharp sand bedding" })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "materials-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Surface area" }), _jsxs("strong", { children: [area.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: surface === "paving" ? "Paving slabs" : surface === "block" ? "Block paving" : "Gravel" }), _jsx("strong", { children: surface === "paving" ? `${slabs} slabs` : surface === "block" ? `${pavingArea.toFixed(2)} m²` : `${gravelBags} bulk bags` })] }), includeSubBase && _jsxs("div", { children: [_jsx("span", { children: "MOT Type 1" }), _jsxs("strong", { children: [motBags, " bulk bags"] })] }), includeBedding && _jsxs("div", { children: [_jsx("span", { children: "Sharp sand" }), _jsxs("strong", { children: [sandBags, " bulk bags"] })] })] }), _jsx("p", { className: "materials-calculator__note", children: "Slab and block quantities use typical sizes. Confirm pack coverage and laying pattern 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: area <= 0, children: submitLabel })] })] })] }) }));
49
+ const options = [
50
+ ["paving", "Paving slabs", "For patios and paths"],
51
+ ["block", "Block paving", "For driveways and paths"],
52
+ ["gravel", "Decorative gravel", "For paths and landscaping"],
53
+ ];
54
+ 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: "Paving and Driveway Calculator" }), _jsx("p", { children: "Estimate paving, block paving, sub-base and bedding materials for outdoor areas." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Surface and ground build-up" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose the surface material and enter the area. Sub-base and bedding sand are estimated alongside the surface units." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Surface material" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--three", children: options.map(([id, label, blurb]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${surface === id ? " is-selected" : ""}`, onClick: () => {
55
+ setSurface(id);
56
+ setFinish(FINISHES[id][0].id);
57
+ }, children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id))) }), _jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsx("span", { children: "Finish" }), _jsx("select", { value: finish, onChange: (event) => setFinish(event.target.value), children: FINISHES[surface].map((option) => (_jsx("option", { value: option.id, children: option.label }, option.id))) })] })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area and groundwork" }), _jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Length (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: length, onChange: (event) => setLength(event.target.value) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Width (m)" }), _jsx("input", { type: "number", min: "0", step: "0.01", value: width, onChange: (event) => setWidth(event.target.value) })] })] }), surface === "gravel" && (_jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsx("span", { children: "Gravel depth (mm)" }), _jsx("input", { type: "number", min: "0", step: "10", value: gravelDepth, onChange: (event) => setGravelDepth(event.target.value) })] })), includeSubBase && (_jsxs("label", { className: "truss-estimator__field materials-calculator__depth", children: [_jsx("span", { children: "Sub-base depth (mm)" }), _jsx("input", { type: "number", min: "0", step: "10", value: subBaseDepth, onChange: (event) => setSubBaseDepth(event.target.value) })] })), _jsxs("label", { className: "brick-calculator__checkbox materials-calculator__waste", children: [_jsx("input", { type: "checkbox", checked: includeSubBase, onChange: (event) => setIncludeSubBase(event.target.checked) }), _jsx("span", { children: "Include MOT Type 1 sub-base" })] }), _jsxs("label", { className: "brick-calculator__checkbox materials-calculator__waste", children: [_jsx("input", { type: "checkbox", checked: includeBedding, onChange: (event) => setIncludeBedding(event.target.checked) }), _jsx("span", { children: "Include sharp sand bedding" })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "materials-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Surface area" }), _jsxs("strong", { children: [area.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: surface === "paving" ? "Paving slabs" : surface === "block" ? "Block paving" : "Gravel" }), _jsx("strong", { children: surface === "paving" ? `${slabs} slabs` : surface === "block" ? `${pavingArea.toFixed(2)} m²` : `${gravelBags} bulk bags` })] }), includeSubBase && (_jsxs("div", { children: [_jsx("span", { children: "MOT Type 1" }), _jsxs("strong", { children: [motBags, " bulk bags"] })] })), includeBedding && (_jsxs("div", { children: [_jsx("span", { children: "Sharp sand" }), _jsxs("strong", { children: [sandBags, " bulk bags"] })] }))] }), _jsx("p", { className: "materials-calculator__note", children: "Slab and block quantities use typical sizes. Confirm pack coverage and laying pattern 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: area <= 0, children: submitLabel })] })] })] }) }));
42
58
  }
@@ -26,11 +26,16 @@ export default function PlasterboardEstimator({ onBack, onReview, submitLabel =
26
26
  const boardLabel = BOARD_TYPES.find((option) => option.id === boardType)?.label ?? "Standard";
27
27
  const requirements = [
28
28
  { key: "board", label: `${boardLabel} plasterboard`, qty: boardCount, unit: "sheet", query: `${boardLabel} plasterboard`, category: "plasterboard" },
29
- ...(fixingMethod === "screws" ? [{ key: "screws", label: "Drywall screws", qty: screwBoxes, unit: "box", query: "drywall screws", category: "fixings" }] : [{ key: "adhesive", label: "Board adhesive", qty: adhesiveBags, unit: "bag", query: "drywall board adhesive", category: "plasterboard" }]),
29
+ ...(fixingMethod === "screws"
30
+ ? [{ key: "screws", label: "Drywall screws", qty: screwBoxes, unit: "box", query: "drywall screws", category: "fixings" }]
31
+ : [{ key: "adhesive", label: "Board adhesive", qty: adhesiveBags, unit: "bag", query: "drywall board adhesive", category: "plasterboard" }]),
30
32
  { key: "tape", label: "Joint tape", qty: tapeRolls, unit: "roll", query: "plasterboard joint tape", category: "plasterboard" },
31
33
  ];
32
34
  const updateSurface = (index, key, nextValue) => {
33
- setSurfaces((items) => items.map((surface, surfaceIndex) => surfaceIndex === index ? { ...surface, [key]: nextValue } : surface));
35
+ setSurfaces((items) => items.map((surface, surfaceIndex) => (surfaceIndex === index ? { ...surface, [key]: nextValue } : surface)));
34
36
  };
35
- 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: "Plasterboard & Drylining Calculator" }), _jsx("p", { children: "Estimate boards, fixings and jointing materials for walls and ceilings." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Boards, fixings and jointing" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter the surfaces you are lining and the board specification. Screws, tape and adhesive are estimated with the boards." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Board type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: BOARD_TYPES.map((option) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${boardType === option.id ? " is-selected" : ""}`, onClick: () => setBoardType(option.id), children: [_jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsx("div", { className: "truss-estimator__option-copy", children: option.blurb })] }, option.id))) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Fixing method" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [["screws", "Drywall screws", "For timber or metal framing"], ["adhesive", "Board adhesive", "For masonry dot-and-dab"]].map(([id, label, blurb]) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${fixingMethod === id ? " is-selected" : ""}`, onClick: () => setFixingMethod(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id)) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall and ceiling areas" }), _jsx("p", { className: "materials-calculator__hint", children: "Add each surface separately so different wall sizes are easy to measure." }), _jsxs("div", { className: "materials-calculator__dimensions-header", children: [_jsx("span", { children: "Surface" }), _jsx("span", { children: "Length (m)" }), _jsx("span", { children: "Height (m)" }), _jsx("span", {})] }), surfaces.map((surface, index) => (_jsxs("div", { className: "materials-calculator__surface-row", children: [_jsxs("span", { children: ["Surface ", index + 1] }), _jsx("input", { "aria-label": `Surface ${index + 1} length`, type: "number", min: "0", step: "0.01", value: surface.length, onChange: (event) => updateSurface(index, "length", event.target.value) }), _jsx("input", { "aria-label": `Surface ${index + 1} height`, type: "number", min: "0", step: "0.01", value: surface.height, onChange: (event) => updateSurface(index, "height", event.target.value) }), surfaces.length > 1 ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setSurfaces((items) => items.filter((_, surfaceIndex) => surfaceIndex !== index)), children: "Remove" }) : null] }, `surface-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setSurfaces((items) => [...items, emptySurface()]), children: "+ Add another surface" }), _jsxs("label", { className: "truss-estimator__field materials-calculator__openings", children: [_jsxs("span", { children: ["Openings to deduct (m\u00B2) ", _jsx("small", { children: "optional" })] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: openings, onChange: (event) => setOpenings(event.target.value) })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "materials-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Board area" }), _jsxs("strong", { children: [netAreaM2.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: "2.4 x 1.2 m boards" }), _jsx("strong", { children: boardCount })] }), _jsxs("div", { children: [_jsx("span", { children: fixingMethod === "screws" ? "Drywall screws" : "Board adhesive" }), _jsx("strong", { children: fixingMethod === "screws" ? `${screwCount} (${screwBoxes} boxes)` : `${adhesiveBags} bags` })] }), _jsxs("div", { children: [_jsx("span", { children: "Joint tape" }), _jsxs("strong", { children: [jointTapeM, " m (", tapeRolls, " rolls)"] })] })] }), _jsx("p", { className: "materials-calculator__note", children: "Figures are estimates. Confirm board specification, fixing patterns and fire or acoustic requirements 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: netAreaM2 <= 0, children: submitLabel })] })] })] }) }));
37
+ 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: "Plasterboard and Drylining Calculator" }), _jsx("p", { children: "Estimate boards, fixings and jointing materials for walls and ceilings." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Boards, fixings and jointing" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter the surfaces you are lining and the board specification. Screws, tape and adhesive are estimated with the boards." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Board type" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: BOARD_TYPES.map((option) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${boardType === option.id ? " is-selected" : ""}`, onClick: () => setBoardType(option.id), children: [_jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsx("div", { className: "truss-estimator__option-copy", children: option.blurb })] }, option.id))) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Fixing method" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
38
+ ["screws", "Drywall screws", "For timber or metal framing"],
39
+ ["adhesive", "Board adhesive", "For masonry dot-and-dab"],
40
+ ].map(([id, label, blurb]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${fixingMethod === id ? " is-selected" : ""}`, onClick: () => setFixingMethod(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: label }), _jsx("div", { className: "truss-estimator__option-copy", children: blurb })] }, id))) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Wall and ceiling areas" }), _jsx("p", { className: "materials-calculator__hint", children: "Add each surface separately so different wall sizes are easy to measure." }), _jsxs("div", { className: "materials-calculator__dimensions-header", children: [_jsx("span", { children: "Surface" }), _jsx("span", { children: "Length (m)" }), _jsx("span", { children: "Height (m)" }), _jsx("span", {})] }), surfaces.map((surface, index) => (_jsxs("div", { className: "materials-calculator__surface-row", children: [_jsxs("span", { children: ["Surface ", index + 1] }), _jsx("input", { "aria-label": `Surface ${index + 1} length`, type: "number", min: "0", step: "0.01", value: surface.length, onChange: (event) => updateSurface(index, "length", event.target.value) }), _jsx("input", { "aria-label": `Surface ${index + 1} height`, type: "number", min: "0", step: "0.01", value: surface.height, onChange: (event) => updateSurface(index, "height", event.target.value) }), surfaces.length > 1 ? (_jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setSurfaces((items) => items.filter((_, surfaceIndex) => surfaceIndex !== index)), children: "Remove" })) : null] }, `surface-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setSurfaces((items) => [...items, emptySurface()]), children: "+ Add another surface" }), _jsxs("label", { className: "truss-estimator__field materials-calculator__openings", children: [_jsxs("span", { children: ["Openings to deduct (m\u00B2) ", _jsx("small", { children: "optional" })] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: openings, onChange: (event) => setOpenings(event.target.value) })] })] }), _jsxs("div", { className: "materials-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "materials-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Board area" }), _jsxs("strong", { children: [netAreaM2.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: "2.4 x 1.2 m boards" }), _jsx("strong", { children: boardCount })] }), _jsxs("div", { children: [_jsx("span", { children: fixingMethod === "screws" ? "Drywall screws" : "Board adhesive" }), _jsx("strong", { children: fixingMethod === "screws" ? `${screwCount} (${screwBoxes} boxes)` : `${adhesiveBags} bags` })] }), _jsxs("div", { children: [_jsx("span", { children: "Joint tape" }), _jsxs("strong", { children: [jointTapeM, " m (", tapeRolls, " rolls)"] })] })] }), _jsx("p", { className: "materials-calculator__note", children: "Figures are estimates. Confirm board specification, fixing patterns and fire or acoustic requirements 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: netAreaM2 <= 0, children: submitLabel })] })] })] }) }));
36
41
  }
@@ -21,7 +21,7 @@ export default function SkirtingEstimator({ onBack, onReview, submitLabel = "Add
21
21
  const [rooms, setRooms] = useState([emptyRoom()]);
22
22
  const [openings, setOpenings] = useState("");
23
23
  const [profile, setProfile] = useState("chamfered");
24
- const boardLength = "4.2";
24
+ const boardLength = "4.4";
25
25
  const baseLengthM = mode === "total"
26
26
  ? toMetres(totalLength, unit)
27
27
  : rooms.reduce((total, room) => total + 2 * (toMetres(room.length, unit) + toMetres(room.width, unit)), 0);
@@ -34,5 +34,5 @@ export default function SkirtingEstimator({ onBack, onReview, submitLabel = "Add
34
34
  const updateRoom = (index, key, nextValue) => {
35
35
  setRooms((items) => items.map((room, roomIndex) => roomIndex === index ? { ...room, [key]: nextValue } : room));
36
36
  };
37
- 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: "Skirting Board Calculator" }), _jsx("p", { children: "Work out how many skirting board lengths you need for one room or a whole project." })] }), _jsxs("div", { className: "truss-estimator__card skirting-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Measure by room or total length" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter a single total or work room by room. The run is converted straight into board lengths." }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "How would you like to measure?" }), _jsxs("div", { className: "brick-calculator__unit-toggle skirting-calculator__mode-toggle", role: "group", "aria-label": "Calculation method", children: [_jsx("button", { type: "button", className: mode === "total" ? "is-selected" : "", onClick: () => setMode("total"), children: "Total length" }), _jsx("button", { type: "button", className: mode === "rooms" ? "is-selected" : "", onClick: () => setMode("rooms"), children: "Room by room" })] })] }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Measurements" }), _jsxs("div", { className: "skirting-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: "Metres" }), _jsx("button", { type: "button", className: unit === "ft" ? "is-selected" : "", onClick: () => setUnit("ft"), children: "Feet" })] })] }), mode === "total" ? (_jsxs("label", { className: "truss-estimator__field skirting-calculator__single-field", children: [_jsxs("span", { children: ["Total skirting length (", unit, ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: totalLength, onChange: (event) => setTotalLength(event.target.value) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "skirting-calculator__dimensions-header", children: [_jsx("span", { children: "Room" }), _jsxs("span", { children: ["Length (", unit, ")"] }), _jsxs("span", { children: ["Width (", unit, ")"] }), _jsx("span", {})] }), rooms.map((room, index) => (_jsxs("div", { className: "skirting-calculator__room-row", children: [_jsxs("span", { className: "skirting-calculator__room-label", children: ["Room ", index + 1] }), _jsx("input", { "aria-label": `Room ${index + 1} length in ${unit}`, type: "number", min: "0", step: "0.01", value: room.length, onChange: (event) => updateRoom(index, "length", event.target.value) }), _jsx("input", { "aria-label": `Room ${index + 1} width in ${unit}`, type: "number", min: "0", step: "0.01", value: room.width, onChange: (event) => updateRoom(index, "width", event.target.value) }), rooms.length > 1 ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setRooms((items) => items.filter((_, roomIndex) => roomIndex !== index)), children: "Remove" }) : null] }, `room-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setRooms((items) => [...items, emptyRoom()]), children: "+ Add another room" })] })), _jsxs("label", { className: "truss-estimator__field skirting-calculator__single-field", children: [_jsxs("span", { children: ["Openings and doorways to deduct (", unit, ") ", _jsx("small", { children: "optional" })] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: openings, onChange: (event) => setOpenings(event.target.value) })] })] }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Profile" }), _jsx("p", { className: "skirting-calculator__hint", children: "Choose the finish you want for the fixed 4.2m white-primed MDF boards." }), _jsx("div", { className: "architrave-calculator__profiles", children: PROFILES.map((option) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${profile === option.id ? " is-selected" : ""}`, onClick: () => setProfile(option.id), children: [_jsx(ProfileDiagram, { profile: option.id }), _jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsx("div", { className: "truss-estimator__option-copy", children: option.blurb })] }, option.id)) })] }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Board and waste" }), _jsx("div", { className: "truss-estimator__field-grid skirting-calculator__options-grid", children: _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Board length" }), _jsx("input", { value: "4.20 m", disabled: true })] }) }), _jsx("p", { className: "skirting-calculator__hint", children: "Longer boards can reduce the number of joins, while 10% waste allows for cuts, corners and mistakes." })] }), _jsxs("div", { className: "skirting-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "skirting-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Net skirting" }), _jsxs("strong", { children: [netLengthM.toFixed(2), " m"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Board lengths" }), _jsx("strong", { children: boardCount })] })] }), _jsx("p", { className: "skirting-calculator__note", children: "Corners and joins vary by room layout. Measure each wall where possible for the most accurate result." })] }), _jsxs("div", { className: "skirting-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: netLengthM <= 0, children: submitLabel })] })] })] }) }));
37
+ 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: "Skirting Board Calculator" }), _jsx("p", { children: "Work out how many skirting board lengths you need for one room or a whole project." })] }), _jsxs("div", { className: "truss-estimator__card skirting-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Measure by room or total length" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter a single total or work room by room. The run is converted straight into board lengths." }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "How would you like to measure?" }), _jsxs("div", { className: "brick-calculator__unit-toggle skirting-calculator__mode-toggle", role: "group", "aria-label": "Calculation method", children: [_jsx("button", { type: "button", className: mode === "total" ? "is-selected" : "", onClick: () => setMode("total"), children: "Total length" }), _jsx("button", { type: "button", className: mode === "rooms" ? "is-selected" : "", onClick: () => setMode("rooms"), children: "Room by room" })] })] }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Measurements" }), _jsxs("div", { className: "skirting-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: "Metres" }), _jsx("button", { type: "button", className: unit === "ft" ? "is-selected" : "", onClick: () => setUnit("ft"), children: "Feet" })] })] }), mode === "total" ? (_jsxs("label", { className: "truss-estimator__field skirting-calculator__single-field", children: [_jsxs("span", { children: ["Total skirting length (", unit, ")"] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: totalLength, onChange: (event) => setTotalLength(event.target.value) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "skirting-calculator__dimensions-header", children: [_jsx("span", { children: "Room" }), _jsxs("span", { children: ["Length (", unit, ")"] }), _jsxs("span", { children: ["Width (", unit, ")"] }), _jsx("span", {})] }), rooms.map((room, index) => (_jsxs("div", { className: "skirting-calculator__room-row", children: [_jsxs("span", { className: "skirting-calculator__room-label", children: ["Room ", index + 1] }), _jsx("input", { "aria-label": `Room ${index + 1} length in ${unit}`, type: "number", min: "0", step: "0.01", value: room.length, onChange: (event) => updateRoom(index, "length", event.target.value) }), _jsx("input", { "aria-label": `Room ${index + 1} width in ${unit}`, type: "number", min: "0", step: "0.01", value: room.width, onChange: (event) => updateRoom(index, "width", event.target.value) }), rooms.length > 1 ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: () => setRooms((items) => items.filter((_, roomIndex) => roomIndex !== index)), children: "Remove" }) : null] }, `room-${index}`))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setRooms((items) => [...items, emptyRoom()]), children: "+ Add another room" })] })), _jsxs("label", { className: "truss-estimator__field skirting-calculator__single-field", children: [_jsxs("span", { children: ["Openings and doorways to deduct (", unit, ") ", _jsx("small", { children: "optional" })] }), _jsx("input", { type: "number", min: "0", step: "0.01", value: openings, onChange: (event) => setOpenings(event.target.value) })] })] }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Profile" }), _jsx("p", { className: "skirting-calculator__hint", children: "Choose the finish you want for the fixed 4.4m white-primed MDF boards." }), _jsx("div", { className: "architrave-calculator__profiles", children: PROFILES.map((option) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${profile === option.id ? " is-selected" : ""}`, onClick: () => setProfile(option.id), children: [_jsx(ProfileDiagram, { profile: option.id }), _jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsx("div", { className: "truss-estimator__option-copy", children: option.blurb })] }, option.id)) })] }), _jsxs("div", { className: "skirting-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Board and waste" }), _jsx("div", { className: "truss-estimator__field-grid skirting-calculator__options-grid", children: _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Board length" }), _jsx("input", { value: "4.40 m", disabled: true })] }) }), _jsx("p", { className: "skirting-calculator__hint", children: "Longer boards can reduce the number of joins, while 10% waste allows for cuts, corners and mistakes." })] }), _jsxs("div", { className: "skirting-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "skirting-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Net skirting" }), _jsxs("strong", { children: [netLengthM.toFixed(2), " m"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Board lengths" }), _jsx("strong", { children: boardCount })] })] }), _jsx("p", { className: "skirting-calculator__note", children: "Corners and joins vary by room layout. Measure each wall where possible for the most accurate result." })] }), _jsxs("div", { className: "skirting-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: netLengthM <= 0, children: submitLabel })] })] })] }) }));
38
38
  }
@@ -39,7 +39,7 @@ export const ESTIMATOR_TOOLS = [
39
39
  { id: "decking", title: "Decking Calculator", desc: "Estimate boards, substructure and fixing quantities.", imageUrl: "/tool-images/decking.webp", Component: DeckingEstimator },
40
40
  {
41
41
  id: "brick-block",
42
- title: "Brick & Block Calculator",
42
+ title: "Brick and Block Calculator",
43
43
  desc: "Estimate masonry units and ready-mixed mortar for a wall.",
44
44
  imageUrl: "/tool-images/brick-block.webp",
45
45
  Component: BrickBlockEstimator,
@@ -61,7 +61,7 @@ export const ESTIMATOR_TOOLS = [
61
61
  },
62
62
  {
63
63
  id: "concrete-aggregate",
64
- title: "Concrete & Aggregate Calculator",
64
+ title: "Concrete and Aggregate Calculator",
65
65
  desc: "Estimate concrete, MOT, sand and gravel quantities.",
66
66
  imageUrl: "/tool-images/concrete-aggregate.webp",
67
67
  Component: ConcreteAggregateEstimator,
@@ -89,7 +89,7 @@ export const ESTIMATOR_TOOLS = [
89
89
  },
90
90
  {
91
91
  id: "paving",
92
- title: "Paving & Driveway Calculator",
92
+ title: "Paving and Driveway Calculator",
93
93
  desc: "Estimate paving, block paving, MOT and sharp sand.",
94
94
  imageUrl: "/tool-images/paving.webp",
95
95
  Component: PavingEstimator,
@@ -1,6 +1,7 @@
1
1
  export type FixedBasketLine = {
2
2
  key: string;
3
3
  productId: string;
4
+ search: string;
4
5
  qty: number;
5
6
  unit?: string;
6
7
  };
@@ -4,7 +4,7 @@ import { useState } from "react";
4
4
  import BrickWall from "../BrickWall";
5
5
  import { useCart } from "../../lib/cart/CartContext";
6
6
  import { candidateToCartItem } from "../../lib/boq/toCartItem";
7
- import { searchCatalogue } from "../../lib/estimators/resolveProducts";
7
+ import { estimateRequestUnit, searchCatalogue } from "../../lib/estimators/resolveProducts";
8
8
  const MIN_BASKET_LOADING_MS = 1500;
9
9
  export function useFixedBasket(toolTitle) {
10
10
  const { addManyToCart, clearCart, openBasket } = useCart();
@@ -16,10 +16,10 @@ export function useFixedBasket(toolTitle) {
16
16
  setBasketError(null);
17
17
  try {
18
18
  const items = await Promise.all(lines.filter((line) => line.qty > 0).map(async (line) => {
19
- const candidate = (await searchCatalogue(line.productId)).find((item) => item.product_id === line.productId);
19
+ const candidate = (await searchCatalogue(line.search)).find((item) => item.product_id === line.productId);
20
20
  if (!candidate)
21
21
  throw new Error("A selected product is no longer available in this catalogue.");
22
- return candidateToCartItem(candidate, { lineId: `${toolTitle}:${line.key}`, description: candidate.description, unit: line.unit ?? "each", qty: line.qty, calculatorWasteIncluded: wasteIncluded });
22
+ return candidateToCartItem(candidate, { lineId: `${toolTitle}:${line.key}`, description: candidate.description, unit: estimateRequestUnit(line.unit ?? "each", candidate), qty: line.qty, calculatorWasteIncluded: wasteIncluded });
23
23
  }));
24
24
  const remaining = MIN_BASKET_LOADING_MS - (Date.now() - startedAt);
25
25
  if (remaining > 0)