@metron-labs/quote-react 0.1.59 → 0.1.61

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.
@@ -1,6 +1,7 @@
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 { offersOption } from "../../lib/estimators/fixedProducts";
4
5
  const PROFILES = [
5
6
  { id: "ogee", label: "Ogee", blurb: "Traditional curved profile" },
6
7
  { id: "torus", label: "Torus", blurb: "Soft rounded detail" },
@@ -46,5 +47,5 @@ export default function ArchitraveEstimator({ onBack, onReview, submitLabel = "A
46
47
  },
47
48
  ];
48
49
  const openingRow = (label, opening, onChange, onRemove) => (_jsxs("div", { className: "architrave-calculator__opening-row", children: [_jsx("span", { children: label }), _jsx("input", { "aria-label": `${label} quantity`, type: "number", min: "0", step: "1", value: opening.quantity, onChange: (event) => onChange("quantity", event.target.value) }), _jsx("input", { "aria-label": `${label} width`, type: "number", min: "0", step: "0.01", value: opening.width, onChange: (event) => onChange("width", event.target.value) }), _jsx("input", { "aria-label": `${label} height`, type: "number", min: "0", step: "0.01", value: opening.height, onChange: (event) => onChange("height", event.target.value) }), onRemove ? _jsx("button", { type: "button", className: "paint-calculator__remove", onClick: onRemove, children: "Remove" }) : null] }));
49
- 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: "Architrave Calculator" }), _jsx("p", { children: "Estimate architrave lengths and board quantities for door openings." })] }), _jsxs("div", { className: "truss-estimator__card architrave-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Measure your openings" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter each door or window opening. Mitred corners are allowed for, and board lengths are worked out from the net run." }), _jsxs("div", { className: "architrave-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Door openings" }), _jsx("p", { className: "architrave-calculator__hint", children: "Each opening is calculated as two side pieces plus one head piece." }), _jsxs("div", { className: "architrave-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" })] })] }), _jsxs("div", { className: "architrave-calculator__dimensions-header", children: [_jsx("span", { children: "Opening" }), _jsx("span", { children: "Qty" }), _jsxs("span", { children: ["Width (", unit, ")"] }), _jsxs("span", { children: ["Height (", unit, ")"] }), _jsx("span", {})] }), openingRow("Single doors", singleDoor, (key, value) => updateOpening(setSingleDoor, key, value)), openingRow("Double doors", doubleDoor, (key, value) => updateOpening(setDoubleDoor, key, value)), alternativeDoors.map((opening, index) => openingRow(`Alternative door ${index + 1}`, opening, (key, value) => setAlternativeDoors((items) => items.map((item, itemIndex) => itemIndex === index ? { ...item, [key]: value } : item)), () => setAlternativeDoors((items) => items.filter((_, itemIndex) => itemIndex !== index)))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setAlternativeDoors((items) => [...items, emptyOpening()]), children: "+ Add alternative-sized door" })] }), _jsxs("div", { className: "architrave-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Standard profile" }), _jsx("p", { className: "architrave-calculator__hint", children: "Choose the profile you plan to use. Length calculations are the same for each profile." }), _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: "architrave-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Board and waste" }), _jsx("div", { className: "architrave-calculator__options", children: _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Board length (m)" }), _jsx("input", { value: "4.40 m", disabled: true })] }) }), _jsx("p", { className: "architrave-calculator__hint", children: "Waste allows for mitred corners, cuts and damaged lengths." })] }), _jsxs("div", { className: "architrave-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "architrave-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Architrave needed" }), _jsxs("strong", { children: [baseLengthM.toFixed(2), " m"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Board lengths" }), _jsx("strong", { children: boardCount })] })] })] }), _jsxs("div", { className: "architrave-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: baseLengthM <= 0, children: submitLabel })] })] })] }) }));
50
+ 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: "Architrave Calculator" }), _jsx("p", { children: "Estimate architrave lengths and board quantities for door openings." })] }), _jsxs("div", { className: "truss-estimator__card architrave-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Measure your openings" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Enter each door or window opening. Mitred corners are allowed for, and board lengths are worked out from the net run." }), _jsxs("div", { className: "architrave-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Door openings" }), _jsx("p", { className: "architrave-calculator__hint", children: "Each opening is calculated as two side pieces plus one head piece." }), _jsxs("div", { className: "architrave-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" })] })] }), _jsxs("div", { className: "architrave-calculator__dimensions-header", children: [_jsx("span", { children: "Opening" }), _jsx("span", { children: "Qty" }), _jsxs("span", { children: ["Width (", unit, ")"] }), _jsxs("span", { children: ["Height (", unit, ")"] }), _jsx("span", {})] }), openingRow("Single doors", singleDoor, (key, value) => updateOpening(setSingleDoor, key, value)), openingRow("Double doors", doubleDoor, (key, value) => updateOpening(setDoubleDoor, key, value)), alternativeDoors.map((opening, index) => openingRow(`Alternative door ${index + 1}`, opening, (key, value) => setAlternativeDoors((items) => items.map((item, itemIndex) => itemIndex === index ? { ...item, [key]: value } : item)), () => setAlternativeDoors((items) => items.filter((_, itemIndex) => itemIndex !== index)))), _jsx("button", { type: "button", className: "paint-calculator__add", onClick: () => setAlternativeDoors((items) => [...items, emptyOpening()]), children: "+ Add alternative-sized door" })] }), _jsxs("div", { className: "architrave-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Standard profile" }), _jsx("p", { className: "architrave-calculator__hint", children: "Choose the profile you plan to use. Length calculations are the same for each profile." }), _jsx("div", { className: "architrave-calculator__profiles", children: PROFILES.filter((option) => offersOption("architrave", `architrave-${option.id}`)).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: "architrave-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Board and waste" }), _jsx("div", { className: "architrave-calculator__options", children: _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Board length (m)" }), _jsx("input", { value: "4.40 m", disabled: true })] }) }), _jsx("p", { className: "architrave-calculator__hint", children: "Waste allows for mitred corners, cuts and damaged lengths." })] }), _jsxs("div", { className: "architrave-calculator__totals", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Totals" }), _jsxs("div", { className: "architrave-calculator__result-grid", children: [_jsxs("div", { children: [_jsx("span", { children: "Architrave needed" }), _jsxs("strong", { children: [baseLengthM.toFixed(2), " m"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Board lengths" }), _jsx("strong", { children: boardCount })] })] })] }), _jsxs("div", { className: "architrave-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: baseLengthM <= 0, children: submitLabel })] })] })] }) }));
50
51
  }
@@ -5,7 +5,14 @@ const BRICK_COVERAGE = 60;
5
5
  const BLOCK_COVERAGE = 10;
6
6
  const BRICK_MORTAR_M3_PER_M2 = 0.02;
7
7
  const BLOCK_MORTAR_M3_PER_M2 = 0.012;
8
- const MORTAR_MIX_YIELD_M3 = 0.01;
8
+ // Mortar is mixed on site from general purpose cement and building sand, 1:4 by volume. Dry
9
+ // materials bulk up by about a third before water goes in, so each m³ of mortar takes 1.33 m³ of
10
+ // dry mix. Both are asked for by weight; the basket turns that into the merchant's bags.
11
+ const MORTAR_CEMENT_PARTS = 1;
12
+ const MORTAR_SAND_PARTS = 4;
13
+ const MORTAR_DRY_VOLUME_FACTOR = 1.33;
14
+ const CEMENT_KG_PER_M3 = 1440;
15
+ const SAND_KG_PER_M3 = 1600;
9
16
  const toNumber = (value) => Number.parseFloat(value) || 0;
10
17
  const formatNumber = (value, decimals = 2) => value.toFixed(decimals);
11
18
  export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
@@ -20,7 +27,9 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
20
27
  const baseUnits = areaM2 * coverage;
21
28
  const units = Math.ceil(baseUnits);
22
29
  const mortarM3 = areaM2 * (masonryType === "brick" ? BRICK_MORTAR_M3_PER_M2 : BLOCK_MORTAR_M3_PER_M2);
23
- const mortarBags = Math.ceil(mortarM3 / MORTAR_MIX_YIELD_M3);
30
+ const dryMixM3 = (mortarM3 * MORTAR_DRY_VOLUME_FACTOR) / (MORTAR_CEMENT_PARTS + MORTAR_SAND_PARTS);
31
+ const cementKg = Math.ceil(dryMixM3 * MORTAR_CEMENT_PARTS * CEMENT_KG_PER_M3);
32
+ const sandKg = Math.ceil(dryMixM3 * MORTAR_SAND_PARTS * SAND_KG_PER_M3);
24
33
  const displayedArea = unit === "metric" ? areaM2 : areaM2 * 10.7639;
25
34
  // Masonry requested as wall area - units per m² is a property of the brick or block.
26
35
  const requirements = [
@@ -34,7 +43,12 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
34
43
  query: masonryType === "brick" ? "facing brick" : "concrete block",
35
44
  category: "masonry",
36
45
  },
37
- ...(addMortar ? [{ key: "mortar", label: "Blue Circle Quality Assured Mortar Mix 20kg", qty: mortarBags, unit: "bag", query: "ready mixed mortar", category: "mortar" }] : []),
46
+ ...(addMortar
47
+ ? [
48
+ { key: "mortar-cement", label: "General purpose cement", qty: cementKg, unit: "kg", query: "general purpose cement", category: "cement" },
49
+ { key: "mortar-sand", label: "Building sand", qty: sandKg, unit: "kg", query: "building sand", category: "aggregate" },
50
+ ]
51
+ : []),
38
52
  ];
39
53
  const changeUnit = (nextUnit) => {
40
54
  if (nextUnit === unit)
@@ -44,12 +58,12 @@ export default function BrickBlockEstimator({ onBack, onReview, submitLabel = "A
44
58
  setWidth((toNumber(width) * factor).toFixed(2));
45
59
  setUnit(nextUnit);
46
60
  };
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: [
61
+ 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 the cement and sand for the 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: [
48
62
  ["brick", "Brick"],
49
63
  ["block", "Block"],
50
64
  ].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
65
  ["common", "Common concrete", "General-purpose grey concrete brick"],
52
66
  ["engineering-red", "Engineering red", "Class B perforated, 65mm"],
53
67
  ["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 })] })] })] }) }));
68
+ ].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 mortar (cement and building sand, mixed 1:4)" })] }) }), _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(_Fragment, { children: [_jsxs("div", { children: [_jsx("span", { children: "Cement" }), _jsxs("strong", { children: [cementKg, " kg"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Building sand" }), _jsxs("strong", { children: [sandKg, " kg"] })] })] })) : null] }), _jsx("p", { className: "brick-calculator__note", children: "Estimate based on standard nominal sizes and 10mm joints. Mortar is a general purpose 1:4 cement and sand mix; quantities are approximate, so confirm the 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 })] })] })] }) }));
55
69
  }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useRef, useState } from "react";
4
4
  import { assetUrl } from "../../lib/assets";
5
+ import { offersOption } from "../../lib/estimators/fixedProducts";
5
6
  import ForwardArrow from "../ForwardArrow";
6
7
  const CANVAS_WIDTH_M = 12;
7
8
  const CANVAS_HEIGHT_M = 8;
@@ -159,7 +160,7 @@ export default function DeckingEstimator({ onBack, onReview, submitLabel = "Add
159
160
  const results = computeResults();
160
161
  const requirements = results
161
162
  ? [
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 },
163
+ { key: `boards-${state.range ?? "meridian"}`, label: `Deck boards (${results.range.label})`, qty: Math.round(results.boardCount * results.range.boardLengthM * 100) / 100, unit: "m", query: "timber decking board", category: "decking", calculatorWasteIncluded: true },
163
164
  { key: "joists", label: "Treated C24 joists", qty: results.joistMetres, unit: "m", query: "treated joist", category: "timber", calculatorWasteIncluded: true },
164
165
  { key: "screws", label: "Green decking screws (200/box)", qty: results.screwBoxes, unit: "box", query: "green decking screws", category: "fixings", calculatorWasteIncluded: true },
165
166
  ]
@@ -272,7 +273,7 @@ export default function DeckingEstimator({ onBack, onReview, submitLabel = "Add
272
273
  else
273
274
  setStepIndex((value) => value + 1);
274
275
  };
275
- 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: "Decking Calculator" }), _jsx("p", { children: "Estimate timber boards, joists and screws for your deck." })] }), _jsxs("div", { className: "truss-estimator__card", children: [_jsxs("div", { className: "truss-estimator__body", children: [currentStep === "range" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Choose your timber decking" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Both options are fixed, orderable pressure-treated timber boards." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.entries(CONFIG.ranges).map(([id, range]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${state.range === id ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, range: id })), children: [_jsx("div", { className: "truss-estimator__option-title", children: range.label }), _jsx("div", { className: "truss-estimator__option-copy", children: range.blurb }), _jsxs("div", { className: "truss-estimator__option-copy", children: [range.boardLengthM, "m boards"] })] }, id))) })] })) : null, currentStep === "grid" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Tell us your deck size" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Start with a quick rectangle estimate, or draw an irregular deck for a more precise result." }), _jsxs("div", { className: "truss-estimator__toggle-row", children: [_jsx("button", { type: "button", className: `truss-estimator__toggle${state.deckSizeMode === "quick" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, deckSizeMode: "quick" })), children: "Quick estimate" }), _jsx("button", { type: "button", className: `truss-estimator__toggle${state.deckSizeMode === "draw" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, deckSizeMode: "draw", drawMode: "outline" })), children: "Draw your space" })] }), state.deckSizeMode === "quick" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Length (m)" }), _jsx("input", { type: "number", inputMode: "decimal", min: "0.1", step: "0.1", value: state.quickLengthM, onChange: (event) => setState((value) => ({ ...value, quickLengthM: event.target.value })) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Width (m)" }), _jsx("input", { type: "number", inputMode: "decimal", min: "0.1", step: "0.1", value: state.quickWidthM, onChange: (event) => setState((value) => ({ ...value, quickWidthM: event.target.value })) })] })] }), _jsx("div", { className: "truss-estimator__grid-hint", children: "For L-shaped decks, cut-outs, or sides against a wall, use Draw your space." }), hasQuickDimensions ? (_jsxs("div", { className: "truss-estimator__grid-stats", children: [_jsxs("div", { children: [(quickLengthM * quickWidthM).toFixed(2), " m\u00B2 deck"] }), _jsxs("div", { children: [(2 * (quickLengthM + quickWidthM)).toFixed(2), " m perimeter"] })] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Draw your deck" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Click to add corners, then close the outline. Drag any handle to refine it." }), _jsxs("div", { className: "deck-outline__toolbar", children: [state.isOutlineClosed ? (_jsxs("div", { className: "deck-outline__summary", children: [_jsxs("div", { children: [_jsx("span", { children: "Deck area" }), _jsxs("strong", { children: [drawingArea.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Perimeter" }), _jsxs("strong", { children: [drawingPerimeter.toFixed(2), " m"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Wall edges" }), _jsx("strong", { children: state.wallSegments.size > 0 ? state.wallSegments.size : "None" })] }), _jsxs("div", { children: [_jsx("span", { children: "Visible edge" }), _jsxs("strong", { children: [visibleDrawingEdgeLength.toFixed(2), " m"] })] })] })) : null, _jsxs("div", { className: "deck-outline__actions", children: [_jsx("button", { type: "button", className: "deck-outline__action", onClick: () => setState((value) => ({ ...value, points: value.points.slice(0, -1) })), disabled: state.points.length === 0 || state.isOutlineClosed, children: "Undo" }), _jsx("button", { type: "button", className: "deck-outline__action", onClick: () => {
276
+ 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: "Decking Calculator" }), _jsx("p", { children: "Estimate timber boards, joists and screws for your deck." })] }), _jsxs("div", { className: "truss-estimator__card", children: [_jsxs("div", { className: "truss-estimator__body", children: [currentStep === "range" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Choose your timber decking" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Both options are fixed, orderable pressure-treated timber boards." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.entries(CONFIG.ranges).filter(([id]) => offersOption("decking", `boards-${id}`)).map(([id, range]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${state.range === id ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, range: id })), children: [_jsx("div", { className: "truss-estimator__option-title", children: range.label }), _jsx("div", { className: "truss-estimator__option-copy", children: range.blurb }), _jsxs("div", { className: "truss-estimator__option-copy", children: [range.boardLengthM, "m boards"] })] }, id))) })] })) : null, currentStep === "grid" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Tell us your deck size" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Start with a quick rectangle estimate, or draw an irregular deck for a more precise result." }), _jsxs("div", { className: "truss-estimator__toggle-row", children: [_jsx("button", { type: "button", className: `truss-estimator__toggle${state.deckSizeMode === "quick" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, deckSizeMode: "quick" })), children: "Quick estimate" }), _jsx("button", { type: "button", className: `truss-estimator__toggle${state.deckSizeMode === "draw" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, deckSizeMode: "draw", drawMode: "outline" })), children: "Draw your space" })] }), state.deckSizeMode === "quick" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__field-grid", children: [_jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Length (m)" }), _jsx("input", { type: "number", inputMode: "decimal", min: "0.1", step: "0.1", value: state.quickLengthM, onChange: (event) => setState((value) => ({ ...value, quickLengthM: event.target.value })) })] }), _jsxs("label", { className: "truss-estimator__field", children: [_jsx("span", { children: "Width (m)" }), _jsx("input", { type: "number", inputMode: "decimal", min: "0.1", step: "0.1", value: state.quickWidthM, onChange: (event) => setState((value) => ({ ...value, quickWidthM: event.target.value })) })] })] }), _jsx("div", { className: "truss-estimator__grid-hint", children: "For L-shaped decks, cut-outs, or sides against a wall, use Draw your space." }), hasQuickDimensions ? (_jsxs("div", { className: "truss-estimator__grid-stats", children: [_jsxs("div", { children: [(quickLengthM * quickWidthM).toFixed(2), " m\u00B2 deck"] }), _jsxs("div", { children: [(2 * (quickLengthM + quickWidthM)).toFixed(2), " m perimeter"] })] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Draw your deck" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Click to add corners, then close the outline. Drag any handle to refine it." }), _jsxs("div", { className: "deck-outline__toolbar", children: [state.isOutlineClosed ? (_jsxs("div", { className: "deck-outline__summary", children: [_jsxs("div", { children: [_jsx("span", { children: "Deck area" }), _jsxs("strong", { children: [drawingArea.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Perimeter" }), _jsxs("strong", { children: [drawingPerimeter.toFixed(2), " m"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Wall edges" }), _jsx("strong", { children: state.wallSegments.size > 0 ? state.wallSegments.size : "None" })] }), _jsxs("div", { children: [_jsx("span", { children: "Visible edge" }), _jsxs("strong", { children: [visibleDrawingEdgeLength.toFixed(2), " m"] })] })] })) : null, _jsxs("div", { className: "deck-outline__actions", children: [_jsx("button", { type: "button", className: "deck-outline__action", onClick: () => setState((value) => ({ ...value, points: value.points.slice(0, -1) })), disabled: state.points.length === 0 || state.isOutlineClosed, children: "Undo" }), _jsx("button", { type: "button", className: "deck-outline__action", onClick: () => {
276
277
  setDrawCursorPoint(null);
277
278
  setState((value) => ({ ...value, points: [], isOutlineClosed: false, wallSegments: new Set(), drawMode: "outline", drawError: "" }));
278
279
  }, disabled: state.points.length === 0, children: "Clear" }), _jsx("button", { type: "button", className: "deck-outline__action", onClick: () => setState((value) => ({
@@ -7,7 +7,7 @@ import ForwardArrow from "../ForwardArrow";
7
7
  import { renderDisplayUnit, formatPriceBasis } from "../../app/components/UnitDisplay";
8
8
  import { useOptionalCart } from "../../lib/cart/CartContext";
9
9
  import { candidateToCartItem } from "../../lib/boq/toCartItem";
10
- import { fetchCatalogueStatus, resolveRequirements, searchCatalogue } from "../../lib/estimators/resolveProducts";
10
+ import { estimateRequestUnit, fetchCatalogueStatus, resolveRequirements, searchCatalogue } from "../../lib/estimators/resolveProducts";
11
11
  import { fmtCurrency, fmtQty } from "../../lib/formatters";
12
12
  import { productImageSrc } from "../../lib/productImage";
13
13
  import { getPricingBreakdown } from "../../util/pricing";
@@ -64,7 +64,7 @@ export default function EstimatorResults({ toolId, requirements, ready, onBack,
64
64
  const toCartLine = (row) => candidateToCartItem(row.chosen, {
65
65
  lineId: lineIdFor(row.requirement.key),
66
66
  description: row.requirement.label,
67
- unit: row.requirement.unit,
67
+ unit: estimateRequestUnit(row.requirement.unit, row.chosen),
68
68
  qty: row.requirement.qty,
69
69
  });
70
70
  // Requirements are rebuilt on every render; only their content should retrigger a lookup.
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useRef, useState } from "react";
4
4
  import { assetUrl } from "../../lib/assets";
5
+ import { offersOption } from "../../lib/estimators/fixedProducts";
5
6
  import ForwardArrow from "../ForwardArrow";
6
7
  const GRID_SIZE = 12;
7
8
  const CELL_PX = 30;
@@ -23,7 +24,7 @@ const CONFIG = {
23
24
  },
24
25
  heights: ["0.9", "1.2", "1.5", "1.8"],
25
26
  gate: {
26
- single: { label: "Single lap gate", widthDefault: 0.9 },
27
+ single: { label: "Single gate", widthDefault: 0.9 },
27
28
  },
28
29
  mounting: {
29
30
  concrete: { label: "Concrete posts", blurb: "Slotted concrete posts with matching panels." },
@@ -31,6 +32,11 @@ const CONFIG = {
31
32
  },
32
33
  };
33
34
  const STEPS = ["range", "height", "gate", "posts"];
35
+ // Fences 1.2m and lower use 6ft posts; taller ones 8ft.
36
+ const postLengthFor = (height) => (Number(height) <= 1.2 ? "6" : "8");
37
+ // Only the panels and posts the merchant stocks are offered (see offersOption).
38
+ const heightOffered = (range, height) => offersOption("fencing", `panels-${range ?? "boundary"}-${height}`);
39
+ const mountingOffered = (mounting, height) => !height || offersOption("fencing", mounting === "concrete" ? `concrete-intermediate-${postLengthFor(height)}` : `timber-post-${postLengthFor(height)}`);
34
40
  const INITIAL_STATE = {
35
41
  range: null,
36
42
  colour: null,
@@ -199,7 +205,7 @@ export default function FencingEstimator({ onBack, onReview, submitLabel = "Add
199
205
  };
200
206
  const results = computeResults();
201
207
  const gateLabel = state.gateType && state.gateType !== "none" ? CONFIG.gate[state.gateType].label : null;
202
- const postLength = Number(results.height) <= 1.2 ? "6" : "8";
208
+ const postLength = postLengthFor(results.height);
203
209
  const requirements = results
204
210
  ? [
205
211
  { key: `panels-${state.range ?? "boundary"}-${results.height}`, label: `${results.range.label} panels (${results.height}m)`, qty: results.panelsCount, unit: "each", query: "fence panel", category: "fencing" },
@@ -212,7 +218,8 @@ export default function FencingEstimator({ onBack, onReview, submitLabel = "Add
212
218
  { key: `timber-post-${postLength}`, label: "Timber fence posts", qty: results.totalPostCount, unit: "each", query: "timber fence post", category: "fencing" },
213
219
  { key: "postcrete", label: "Postcrete", qty: results.bagsCount, unit: "bag", query: "postcrete", category: "cement" },
214
220
  ]),
215
- ...(gateLabel ? [{ key: "gate", label: gateLabel, qty: 1, unit: "each", query: "lap gate", category: "fencing" }] : []),
221
+ // The gate matches the panels: a lap gate in a lap fence, a feather edge gate in a feather edge one.
222
+ ...(gateLabel ? [{ key: `gate-${state.range ?? "boundary"}`, label: state.range === "skyline" ? "Single feather edge gate" : "Single lap gate", qty: 1, unit: "each", query: state.range === "skyline" ? "feather edge gate" : "lap gate", category: "fencing" }] : []),
216
223
  ...(results.gravelCount > 0
217
224
  ? [{ key: "gravel-boards", label: "Concrete gravel boards", qty: results.gravelCount, unit: "each", query: "gravel board", category: "fencing" }]
218
225
  : []),
@@ -418,7 +425,7 @@ export default function FencingEstimator({ onBack, onReview, submitLabel = "Add
418
425
  setStepIndex((value) => value + 1);
419
426
  };
420
427
  const perimeterEdges = getPerimeterEdges(state.grid);
421
- 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: "Fencing Calculator" }), _jsx("p", { children: "Estimate panels, posts and cost for your fence line in a few steps." })] }), _jsxs("div", { className: "truss-estimator__card", children: [_jsxs("div", { className: "truss-estimator__body", children: [currentStep === "range" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Choose your fence panels" }), _jsx("p", { className: "truss-estimator__step-sub", children: "The calculator uses fixed, orderable timber panels from the catalogue." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.entries(CONFIG.ranges).map(([id, range]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${state.range === id ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, range: id })), children: [_jsx("div", { className: "truss-estimator__option-title", children: range.label }), _jsx("div", { className: "truss-estimator__option-copy", children: range.blurb })] }, id))) })] })) : null, currentStep === "range" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Measure your fence line" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Use a quick total, or draw the boundary to mark existing edges that do not need new fencing." }), _jsxs("div", { className: "truss-estimator__toggle-row", children: [_jsx("button", { type: "button", className: `truss-estimator__toggle${state.boundaryMode === "quick" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, boundaryMode: "quick" })), children: "Quick estimate" }), _jsx("button", { type: "button", className: `truss-estimator__toggle${state.boundaryMode === "draw" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, boundaryMode: "draw" })), children: "Draw your boundary" })] }), state.boundaryMode === "quick" ? (_jsxs("label", { className: "truss-estimator__field truss-estimator__field--compact fence-boundary__quick-field", children: [_jsx("span", { children: "Total fence length (m)" }), _jsx("input", { type: "number", min: "0", step: "0.1", value: state.quickLengthM, onChange: (event) => setState((value) => ({ ...value, quickLengthM: event.target.value })) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "fence-boundary__toolbar", children: _jsxs("div", { className: "fence-boundary__actions", children: [_jsx("button", { type: "button", className: "deck-outline__action", onClick: () => setState((value) => ({ ...value, boundaryPoints: value.boundaryPoints.slice(0, -1), boundaryClosed: false, excludedBoundaryEdges: new Set() })), disabled: state.boundaryPoints.length === 0 || state.boundaryClosed, children: "Undo" }), _jsx("button", { type: "button", className: "deck-outline__action", onClick: () => {
428
+ 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: "Fencing Calculator" }), _jsx("p", { children: "Estimate panels, posts and cost for your fence line in a few steps." })] }), _jsxs("div", { className: "truss-estimator__card", children: [_jsxs("div", { className: "truss-estimator__body", children: [currentStep === "range" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Choose your fence panels" }), _jsx("p", { className: "truss-estimator__step-sub", children: "The calculator uses fixed, orderable timber panels from the catalogue." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.entries(CONFIG.ranges).map(([id, range]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${state.range === id ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, range: id, height: value.height && heightOffered(id, value.height) ? value.height : null })), children: [_jsx("div", { className: "truss-estimator__option-title", children: range.label }), _jsx("div", { className: "truss-estimator__option-copy", children: range.blurb })] }, id))) })] })) : null, currentStep === "range" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Measure your fence line" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Use a quick total, or draw the boundary to mark existing edges that do not need new fencing." }), _jsxs("div", { className: "truss-estimator__toggle-row", children: [_jsx("button", { type: "button", className: `truss-estimator__toggle${state.boundaryMode === "quick" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, boundaryMode: "quick" })), children: "Quick estimate" }), _jsx("button", { type: "button", className: `truss-estimator__toggle${state.boundaryMode === "draw" ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, boundaryMode: "draw" })), children: "Draw your boundary" })] }), state.boundaryMode === "quick" ? (_jsxs("label", { className: "truss-estimator__field truss-estimator__field--compact fence-boundary__quick-field", children: [_jsx("span", { children: "Total fence length (m)" }), _jsx("input", { type: "number", min: "0", step: "0.1", value: state.quickLengthM, onChange: (event) => setState((value) => ({ ...value, quickLengthM: event.target.value })) })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "fence-boundary__toolbar", children: _jsxs("div", { className: "fence-boundary__actions", children: [_jsx("button", { type: "button", className: "deck-outline__action", onClick: () => setState((value) => ({ ...value, boundaryPoints: value.boundaryPoints.slice(0, -1), boundaryClosed: false, excludedBoundaryEdges: new Set() })), disabled: state.boundaryPoints.length === 0 || state.boundaryClosed, children: "Undo" }), _jsx("button", { type: "button", className: "deck-outline__action", onClick: () => {
422
429
  setBoundaryPan({ x: 0, y: 0 });
423
430
  setBoundaryCursorPoint(null);
424
431
  setState((value) => ({ ...value, boundaryPoints: [], boundaryClosed: false, excludedBoundaryEdges: new Set() }));
@@ -486,7 +493,7 @@ export default function FencingEstimator({ onBack, onReview, submitLabel = "Add
486
493
  next.add(edge.key);
487
494
  return { ...value, excludedEdges: next };
488
495
  }) }, edge.key));
489
- }) })) : null] }) }), state.gridMode === "walls" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "truss-estimator__grid-hint", children: "Click an edge on the shape above to toggle it." }), _jsxs("div", { className: "truss-estimator__legend", children: [_jsxs("span", { className: "truss-estimator__legend-item", children: [_jsx("svg", { className: "truss-estimator__legend-line", viewBox: "0 0 26 10", "aria-hidden": "true", children: _jsx("line", { x1: "2", y1: "5", x2: "24", y2: "5", stroke: "var(--accent)", strokeWidth: "4", strokeLinecap: "round" }) }), "Needs new fencing"] }), _jsxs("span", { className: "truss-estimator__legend-item", children: [_jsx("svg", { className: "truss-estimator__legend-line", viewBox: "0 0 26 10", "aria-hidden": "true", children: _jsx("line", { x1: "2", y1: "5", x2: "24", y2: "5", stroke: "#9aa3b5", strokeWidth: "4", strokeLinecap: "round", strokeDasharray: "3 3" }) }), "Existing boundary"] })] })] })) : (_jsx("div", { className: "truss-estimator__grid-hint", children: "Once you have drawn the plot, switch to boundary mode to exclude any sides that do not need a new fence." })), _jsxs("div", { className: "truss-estimator__grid-stats", children: [_jsxs("div", { children: [state.grid.size, " m\u00B2 plot"] }), state.gridMode === "walls" ? (_jsxs(_Fragment, { children: [_jsxs("div", { children: [results.totalEdgeM, " m fence run"] }), _jsxs("div", { children: [results.cornerPostCount, " corner/end posts"] })] })) : null] })] })) : null, currentStep === "height" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Fence height" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Select the nearest available panel height." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--pitch", children: CONFIG.heights.map((height) => (_jsx("button", { type: "button", className: `truss-estimator__visual-card${state.height === height ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, height })), children: _jsx("img", { className: "truss-estimator__visual-card-img truss-estimator__visual-card-img--tall", src: assetUrl(`/fencing/height/${height}.webp`), alt: "" }) }, height))) }), _jsx("div", { className: "truss-estimator__section-heading", children: "Gravel board?" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
496
+ }) })) : null] }) }), state.gridMode === "walls" ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "truss-estimator__grid-hint", children: "Click an edge on the shape above to toggle it." }), _jsxs("div", { className: "truss-estimator__legend", children: [_jsxs("span", { className: "truss-estimator__legend-item", children: [_jsx("svg", { className: "truss-estimator__legend-line", viewBox: "0 0 26 10", "aria-hidden": "true", children: _jsx("line", { x1: "2", y1: "5", x2: "24", y2: "5", stroke: "var(--accent)", strokeWidth: "4", strokeLinecap: "round" }) }), "Needs new fencing"] }), _jsxs("span", { className: "truss-estimator__legend-item", children: [_jsx("svg", { className: "truss-estimator__legend-line", viewBox: "0 0 26 10", "aria-hidden": "true", children: _jsx("line", { x1: "2", y1: "5", x2: "24", y2: "5", stroke: "#9aa3b5", strokeWidth: "4", strokeLinecap: "round", strokeDasharray: "3 3" }) }), "Existing boundary"] })] })] })) : (_jsx("div", { className: "truss-estimator__grid-hint", children: "Once you have drawn the plot, switch to boundary mode to exclude any sides that do not need a new fence." })), _jsxs("div", { className: "truss-estimator__grid-stats", children: [_jsxs("div", { children: [state.grid.size, " m\u00B2 plot"] }), state.gridMode === "walls" ? (_jsxs(_Fragment, { children: [_jsxs("div", { children: [results.totalEdgeM, " m fence run"] }), _jsxs("div", { children: [results.cornerPostCount, " corner/end posts"] })] })) : null] })] })) : null, currentStep === "height" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Fence height" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Select the nearest available panel height." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--pitch", children: CONFIG.heights.filter((height) => heightOffered(state.range, height)).map((height) => (_jsx("button", { type: "button", className: `truss-estimator__visual-card${state.height === height ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, height, mountingType: value.mountingType && mountingOffered(value.mountingType, height) ? value.mountingType : null })), children: _jsx("img", { className: "truss-estimator__visual-card-img truss-estimator__visual-card-img--tall", src: assetUrl(`/fencing/height/${height}.webp`), alt: "" }) }, height))) }), _jsx("div", { className: "truss-estimator__section-heading", children: "Gravel board?" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
490
497
  { id: "no", label: "No gravel board", img: "/fencing/no-gravel-board.webp" },
491
498
  { id: "yes", label: "Add gravel board", img: "/fencing/gravel-board.webp" },
492
499
  ].map((option) => (_jsxs("button", { type: "button", className: `truss-estimator__visual-card truss-estimator__visual-card--wide${state.gravelBoard === option.id ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, gravelBoard: option.id })), children: [_jsx("img", { className: "truss-estimator__visual-card-img truss-estimator__visual-card-img--mid", src: assetUrl(option.img), alt: "" }), _jsx("span", { children: option.label })] }, option.id))) })] })) : null, currentStep === "gate" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Do you need a gate?" }), _jsx("p", { className: "truss-estimator__step-sub", children: "A gate opening replaces part of the panel run and adds a gate kit with heavy-duty posts." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: [
@@ -496,5 +503,5 @@ export default function FencingEstimator({ onBack, onReview, submitLabel = "Add
496
503
  ...value,
497
504
  gateType: option.id,
498
505
  gateWidthM: option.id === "none" ? "" : String(CONFIG.gate[option.id].widthDefault),
499
- })), children: [_jsx("img", { className: "truss-estimator__visual-card-img truss-estimator__visual-card-img--mid", src: assetUrl(option.img), alt: "" }), _jsx("span", { children: option.label })] }, option.id))) }), state.gateType && state.gateType !== "none" ? (_jsxs("label", { className: "truss-estimator__field truss-estimator__field--compact", children: [_jsx("span", { children: "Gate width (m)" }), _jsx("input", { type: "text", inputMode: "decimal", value: state.gateWidthM, onChange: (event) => setState((value) => ({ ...value, gateWidthM: event.target.value })) })] })) : null] })) : null, currentStep === "posts" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Posts and fixing type" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose how the posts will be set." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.entries(CONFIG.mounting).map(([id, mounting]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${state.mountingType === id ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, mountingType: id })), children: [_jsx("div", { className: "truss-estimator__option-title", children: mounting.label }), _jsx("div", { className: "truss-estimator__option-copy", children: mounting.blurb })] }, id))) })] })) : null] }), _jsxs("div", { className: "truss-estimator__nav", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary", style: { flex: "0 1 30%", justifyContent: "center", marginTop: 0 }, onClick: handleBack, children: "Back" }), _jsxs("button", { type: "button", className: "cta-btn", style: { flex: "0 1 70%", marginTop: 0 }, onClick: handleNext, disabled: !canContinue(), children: [_jsx("span", { children: nextLabel }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] })] })] })] }) }));
506
+ })), children: [_jsx("img", { className: "truss-estimator__visual-card-img truss-estimator__visual-card-img--mid", src: assetUrl(option.img), alt: "" }), _jsx("span", { children: option.label })] }, option.id))) }), state.gateType && state.gateType !== "none" ? (_jsxs("label", { className: "truss-estimator__field truss-estimator__field--compact", children: [_jsx("span", { children: "Gate width (m)" }), _jsx("input", { type: "text", inputMode: "decimal", value: state.gateWidthM, onChange: (event) => setState((value) => ({ ...value, gateWidthM: event.target.value })) })] })) : null] })) : null, currentStep === "posts" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "truss-estimator__step-label", children: ["Step ", stepIndex + 1, " of ", STEPS.length] }), _jsx("h2", { className: "truss-estimator__step-title", children: "Posts and fixing type" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose how the posts will be set." }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: Object.entries(CONFIG.mounting).filter(([id]) => mountingOffered(id, state.height)).map(([id, mounting]) => (_jsxs("button", { type: "button", className: `truss-estimator__option-card${state.mountingType === id ? " is-selected" : ""}`, onClick: () => setState((value) => ({ ...value, mountingType: id })), children: [_jsx("div", { className: "truss-estimator__option-title", children: mounting.label }), _jsx("div", { className: "truss-estimator__option-copy", children: mounting.blurb })] }, id))) })] })) : null] }), _jsxs("div", { className: "truss-estimator__nav", children: [_jsx("button", { type: "button", className: "cta-btn cta-btn-secondary", style: { flex: "0 1 30%", justifyContent: "center", marginTop: 0 }, onClick: handleBack, children: "Back" }), _jsxs("button", { type: "button", className: "cta-btn", style: { flex: "0 1 70%", marginTop: 0 }, onClick: handleNext, disabled: !canContinue(), children: [_jsx("span", { children: nextLabel }), _jsx("span", { className: "arrow", children: _jsx(ForwardArrow, {}) })] })] })] })] }) }));
500
507
  }
@@ -1,16 +1,15 @@
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
+ // The basket is asked for the area; the matched product's own coverage turns it into packs, which
5
+ // is right whichever merchant's product it is. coverage here only feeds the on-screen pack estimate.
9
6
  const APPLICATIONS = {
10
- floor: { label: "Floor", product: "Knauf Acoustic Roll", thickness: "100mm", coverage: 12.36, salesUnit: "roll" },
11
- wall: { label: "Cavity wall", product: "Celotex CW4100", thickness: "100mm", coverage: 3.24, salesUnit: "pack" },
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 },
7
+ // Between joists of an upstairs floor, where the job is sound; a ground floor needs heat kept in.
8
+ floor: { label: "Floor between rooms", product: "Knauf Acoustic Roll", thickness: "100mm", coverage: 12.36 },
9
+ ground: { label: "Ground floor", product: "Rigid PIR insulation board", thickness: "100mm", coverage: 2.88 },
10
+ wall: { label: "Cavity wall", product: "Celotex CW4100", thickness: "100mm", coverage: 3.24 },
11
+ roof: { label: "Pitched roof", product: "Kingspan K107", thickness: "100mm", coverage: 8.64 },
12
+ loft: { label: "Loft", product: "Knauf Loft Roll 44", thickness: "100mm", coverage: 13.89 },
14
13
  };
15
14
  export default function InsulationEstimator({ onBack, onReview, submitLabel = "Add to basket" }) {
16
15
  const [application, setApplication] = useState("floor");
@@ -23,12 +22,12 @@ export default function InsulationEstimator({ onBack, onReview, submitLabel = "A
23
22
  {
24
23
  key: `insulation-${application}`,
25
24
  label: `${selected.product} ${selected.thickness}`,
26
- qty: selected.byArea ? Math.round(area * 100) / 100 : packs,
27
- unit: selected.byArea ? "m2" : selected.salesUnit,
25
+ qty: Math.round(area * 100) / 100,
26
+ unit: "m2",
28
27
  query: selected.product,
29
28
  category: "insulation",
30
29
  },
31
30
  ];
32
31
  const applications = Object.entries(APPLICATIONS);
33
- 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: "Insulation Calculator" }), _jsx("p", { children: "Estimate insulation coverage and pack quantities for floors, walls, roofs and lofts." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Where and what you are insulating" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose the application and product, and enter the area, for coverage and pack quantities." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Application" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--two", children: applications.map(([id, option]) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${application === id ? " is-selected" : ""}`, onClick: () => setApplication(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsxs("div", { className: "truss-estimator__option-copy", children: [option.product, ", ", option.thickness] })] }, id)) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area" }), _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) })] })] })] }), _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: "Insulated area" }), _jsxs("strong", { children: [area.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: selected.product }), _jsxs("strong", { children: [packs, " packs"] })] })] }), _jsx("p", { className: "materials-calculator__note", children: "This is a simple product choice, not thermal or Building Regulations advice. Confirm the specification 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 })] })] })] }) }));
32
+ 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: "Insulation Calculator" }), _jsx("p", { children: "Estimate insulation coverage and pack quantities for floors, walls, roofs and lofts." })] }), _jsxs("div", { className: "truss-estimator__card materials-calculator", children: [_jsx("h2", { className: "truss-estimator__step-title", children: "Where and what you are insulating" }), _jsx("p", { className: "truss-estimator__step-sub", children: "Choose the application and product, and enter the area, for coverage and pack quantities." }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Application" }), _jsx("div", { className: "truss-estimator__option-grid truss-estimator__option-grid--three", children: applications.map(([id, option]) => _jsxs("button", { type: "button", className: `truss-estimator__option-card${application === id ? " is-selected" : ""}`, onClick: () => setApplication(id), children: [_jsx("div", { className: "truss-estimator__option-title", children: option.label }), _jsxs("div", { className: "truss-estimator__option-copy", children: [option.product, ", ", option.thickness] })] }, id)) })] }), _jsxs("div", { className: "materials-calculator__section", children: [_jsx("div", { className: "truss-estimator__section-heading", children: "Area" }), _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) })] })] })] }), _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: "Insulated area" }), _jsxs("strong", { children: [area.toFixed(2), " m\u00B2"] })] }), _jsxs("div", { children: [_jsx("span", { children: selected.product }), _jsxs("strong", { children: [packs, " packs"] })] })] }), _jsx("p", { className: "materials-calculator__note", children: "This is a simple product choice, not thermal or Building Regulations advice. Confirm the specification 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 })] })] })] }) }));
34
33
  }