From a0af9946140695fdd38ed8a72b2a4a891d90b84f Mon Sep 17 00:00:00 2001 From: Satyam <95536056+Satyam-Rastogi@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:29:13 +0530 Subject: [PATCH] feat(estimate): redesign estimate builder UI and fix theme conflicts - Restructure section/group/item hierarchy to match reference designs: collapse chevron, cost + green price box, ellipsis menu per section; trash on hover + cost/price per group; inline trash, orange qty=0 border, read-only cost/unit, SRS warning rows per item - Add compact horizontal margin slider with orange accent - Add section total banner with white/30 bordered price box - Rewrite MaterialDetailsModal to match reference (3-row formula layout: Measurement @ Waste = Qty, Cost/Unit with conversion note, Total formula) - Fix MaterialDetailsModal always rendering in light mode: add dark: variants - Fix MeasurementsModal always rendering in light mode: add dark: variants - Fix Financial Summary sidebar always rendering in dark mode: base bg-white with dark: gradient, all inner elements get proper dark: variants - Add new estimate component files: MaterialDetailsModal, MeasurementsModal, ImageUploadModal, InitialChoiceModal, TemplateSelectionModal --- src/components/estimates/ImageUploadModal.jsx | 78 ++ .../estimates/InitialChoiceModal.jsx | 56 ++ .../estimates/MaterialDetailsModal.jsx | 271 ++++++ .../estimates/MeasurementsModal.jsx | 154 ++++ .../estimates/TemplateSelectionModal.jsx | 669 +++++++++++++++ src/pages/EstimateBuilder.jsx | 796 ++++++++++++++---- 6 files changed, 1847 insertions(+), 177 deletions(-) create mode 100644 src/components/estimates/ImageUploadModal.jsx create mode 100644 src/components/estimates/InitialChoiceModal.jsx create mode 100644 src/components/estimates/MaterialDetailsModal.jsx create mode 100644 src/components/estimates/MeasurementsModal.jsx create mode 100644 src/components/estimates/TemplateSelectionModal.jsx diff --git a/src/components/estimates/ImageUploadModal.jsx b/src/components/estimates/ImageUploadModal.jsx new file mode 100644 index 0000000..e105af9 --- /dev/null +++ b/src/components/estimates/ImageUploadModal.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { X, UploadCloud, FileImage, FileText, CheckCircle2 } from 'lucide-react'; + +export default function ImageUploadModal({ isOpen, onClose, onSelectImage }) { + if (!isOpen) return null; + + const dummyImages = [ + { id: 1, name: 'Measurement_Section_1.png', type: 'image' }, + { id: 2, name: 'Measurement_Section_2.png', type: 'image' }, + { id: 3, name: 'Measurement_Section_3.png', type: 'image' } + ]; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

Upload Measurements

+

Upload an image or document, or select a sample to begin.

+
+ +
+ + {/* Content */} +
+ {/* Drag and drop zone */} +
+
+ +
+

Click to upload

+

+ Drag and drop your measurement screenshots, aerial reports, or PDF documents here. +

+
+ + {/* Or Separator */} +
+ +
+ Or choose a mock example +
+
+ + {/* Dummy Options */} +
+ {dummyImages.map((img) => ( + + ))} +
+
+
+
+ ); +} diff --git a/src/components/estimates/InitialChoiceModal.jsx b/src/components/estimates/InitialChoiceModal.jsx new file mode 100644 index 0000000..2702e40 --- /dev/null +++ b/src/components/estimates/InitialChoiceModal.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { PackageOpen, FileEdit } from 'lucide-react'; + +export default function InitialChoiceModal({ isOpen, onSelectTemplate, onStartScratch }) { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

How would you like to proceed?

+

Choose how to build this estimate from your uploaded image.

+
+ + {/* Content */} +
+ + + +
+
+
+ ); +} diff --git a/src/components/estimates/MaterialDetailsModal.jsx b/src/components/estimates/MaterialDetailsModal.jsx new file mode 100644 index 0000000..f5e3952 --- /dev/null +++ b/src/components/estimates/MaterialDetailsModal.jsx @@ -0,0 +1,271 @@ +import React, { useState, useEffect } from 'react'; +import { X, Calculator, FileText } from 'lucide-react'; + +export default function MaterialDetailsModal({ isOpen, onClose, item, onSave }) { + const [measurement, setMeasurement] = useState(item?.baseMeasurement || 0); + const [wastePercent, setWastePercent] = useState(item?.wastePercent || 0); + const [conversionFactor, setConversionFactor] = useState(item?.conversionFactor || 1); + const [unitCost, setUnitCost] = useState(item?.unitCost || 0); + const [marginPercent, setMarginPercent] = useState(item?.marginPercent || 30); + const [pricePerUnitOverride, setPricePerUnitOverride] = useState(false); + const [manualPricePerUnit, setManualPricePerUnit] = useState(0); + + const [calculatedQuantity, setCalculatedQuantity] = useState(0); + const [finalTotalUnits, setFinalTotalUnits] = useState(0); + const [totalCost, setTotalCost] = useState(0); + const [clientPrice, setClientPrice] = useState(0); + + useEffect(() => { + if (item) { + setMeasurement(item.baseMeasurement || 0); + setWastePercent(item.wastePercent || 0); + setConversionFactor(item.conversionFactor || 1); + setUnitCost(item.unitCost || 0); + setMarginPercent(item.marginPercent || 30); + } + }, [item]); + + useEffect(() => { + const withWaste = measurement * (1 + (wastePercent / 100)); + const computedQty = conversionFactor > 0 ? (withWaste / conversionFactor) : withWaste; + setCalculatedQuantity(computedQty); + const finalUnits = Math.ceil(computedQty); + setFinalTotalUnits(finalUnits); + const cost = finalUnits * unitCost; + setTotalCost(cost); + if (marginPercent < 100) { + setClientPrice(cost / (1 - (marginPercent / 100))); + } else { + setClientPrice(cost); + } + }, [measurement, wastePercent, conversionFactor, unitCost, marginPercent]); + + const computedPricePerUnit = finalTotalUnits > 0 ? clientPrice / finalTotalUnits : 0; + + const handleSave = () => { + onSave({ + ...item, + baseMeasurement: measurement, + wastePercent, + conversionFactor, + unitCost, + marginPercent, + qty: finalTotalUnits, + clientPrice + }); + onClose(); + }; + + if (!isOpen || !item) return null; + + const measurementUomLabel = item.measurementUom || item.uom; + const showConversion = conversionFactor > 1 || (item.measurementUom && item.measurementUom !== item.uom); + const conversionNote = showConversion + ? `${conversionFactor} ${measurementUomLabel} = 1 ${item.uom}` + : `1 ${item.uom} = 1 ${item.uom}`; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

Material Details

+
+ + {/* Sub-header: item name + name input + add description */} +
+

{item.desc}

+ + +
+ + {/* Main Content */} +
+ + {/* Row 1: Measurement @ Waste = Quantity */} +
+ {/* Measurement */} +
+ +
+ setMeasurement(parseFloat(e.target.value) || 0)} + className="w-24 bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-600 rounded px-3 py-2 text-sm font-mono text-zinc-800 dark:text-zinc-200 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400" + /> + {measurementUomLabel} +
+
+ + @ + + {/* Waste */} +
+ +
+ setWastePercent(parseFloat(e.target.value) || 0)} + placeholder="0" + className="w-12 bg-transparent outline-none text-sm font-mono text-zinc-800 dark:text-zinc-200 text-right" + /> + % +
+
+ + = + + {/* Quantity (read-only, post-waste pre-ceil) */} +
+ +
+
+ {calculatedQuantity.toFixed(2)} +
+ {item.uom} +
+
+
+ +
+ + {/* Row 2: Cost/Unit | Price/Unit */} +
+ {/* Cost/Unit */} +
+ +
+
+ $ + setUnitCost(parseFloat(e.target.value) || 0)} + className="w-20 px-2 py-2 text-sm font-mono text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 outline-none text-right" + /> +
+ / {item.uom} +
+ {/* Conversion note */} +

{conversionNote}

+ {/* Editable conversion factor (small, subtle) */} +
+ setConversionFactor(parseFloat(e.target.value) || 1)} + className="w-10 border-b border-zinc-200 dark:border-zinc-600 bg-transparent outline-none text-center font-mono focus:border-blue-400 text-zinc-500 dark:text-zinc-400" + /> + {measurementUomLabel} = 1 {item.uom} +
+
+ + {/* Price/Unit checkbox */} +
+ +
+ +
+
+ $ + {pricePerUnitOverride ? ( + setManualPricePerUnit(parseFloat(e.target.value) || 0)} + className="w-20 px-2 py-2 text-sm font-mono text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-900 outline-none text-right" + /> + ) : ( + {computedPricePerUnit.toFixed(2)} + )} +
+ / {item.uom} +
+
+
+
+ +
+ + {/* Row 3: Total summary formula */} +
+ {/* Total */} +
+ +
+ {finalTotalUnits} + {item.uom} +
+
+ + × + + {/* Cost/Unit summary */} +
+ + ${unitCost.toFixed(2)} / {item.uom} +
+ + @ + + {/* Section Margin */} +
+ +
+ setMarginPercent(parseFloat(e.target.value) || 0)} + className="w-14 border-b-2 border-zinc-300 dark:border-zinc-600 bg-transparent outline-none text-center font-mono text-base text-zinc-700 dark:text-zinc-300 focus:border-blue-500 pb-1" + /> + % +
+
+ + = + + {/* Price result */} +
+ + ${clientPrice.toFixed(2)} +
+
+ +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/src/components/estimates/MeasurementsModal.jsx b/src/components/estimates/MeasurementsModal.jsx new file mode 100644 index 0000000..b3b75d5 --- /dev/null +++ b/src/components/estimates/MeasurementsModal.jsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from 'react'; +import { ChevronDown, Info } from 'lucide-react'; + +const MOCK_MEASUREMENTS = { + 'Measurement_Section_1.png': { + totalRoofArea: 87.87, + ridges: 189.09, + hips: 395.62, + valleys: 284.06, + rakes: 184.69, + eaves: 600.90, + stepFlashing: 112.00 + }, + 'Measurement_Section_2.png': { + totalRoofArea: 33.91, + ridges: 73.62, + hips: 304.65, + valleys: 140.44, + rakes: 53.57, + eaves: 275.52, + stepFlashing: 39.48 + }, + 'Measurement_Section_3.png': { + totalRoofArea: 81.79, + ridges: 161.85, + hips: 275.53, + valleys: 172.23, + rakes: 69.31, + eaves: 459.65, + stepFlashing: 6.49 + } +}; + +export default function MeasurementsModal({ isOpen, onCancel, onNext, selectedImageId }) { + const [measurements, setMeasurements] = useState({ + totalRoofArea: 0, ridges: 0, hips: 0, valleys: 0, rakes: 0, eaves: 0, stepFlashing: 0 + }); + + useEffect(() => { + if (selectedImageId && MOCK_MEASUREMENTS[selectedImageId]) { + setMeasurements(MOCK_MEASUREMENTS[selectedImageId]); + } + }, [selectedImageId]); + + const handleChange = (field, value) => { + setMeasurements(prev => ({ ...prev, [field]: parseFloat(value) || 0 })); + }; + + if (!isOpen) return null; + + const fields = [ + { label: 'Ridges', key: 'ridges', uom: 'LF' }, + { label: 'Hips', key: 'hips', uom: 'LF' }, + { label: 'Valleys', key: 'valleys', uom: 'LF' }, + { label: 'Rakes', key: 'rakes', uom: 'LF' }, + { label: 'Eaves', key: 'eaves', uom: 'LF' }, + { label: 'Step Flashing', key: 'stepFlashing', uom: 'LF' }, + ]; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

Measurements

+
+ + {/* Info */} +
+

+ + The actual measurements below will be used to calculate your item quantities. +

+
+ + {/* Content */} +
+ {/* Measurement Dropdown */} +
+ +
+ + +
+
+ + {/* Measurements list */} +
+
+ Actual +
+ + {/* Total Roof Area - prominent */} +
+ Total Roof Area +
+ handleChange('totalRoofArea', e.target.value)} + className="w-24 bg-white dark:bg-zinc-800 border-2 border-zinc-800 dark:border-zinc-300 rounded px-3 py-1.5 text-right font-mono text-sm text-zinc-900 dark:text-zinc-100 outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" + /> + SQ +
+
+ + {fields.map(item => ( +
+ {item.label} +
+ handleChange(item.key, e.target.value)} + className="w-24 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded px-3 py-1.5 text-right font-mono text-sm text-zinc-700 dark:text-zinc-300 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400 transition-all" + /> + {item.uom} +
+
+ ))} +
+ + +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/src/components/estimates/TemplateSelectionModal.jsx b/src/components/estimates/TemplateSelectionModal.jsx new file mode 100644 index 0000000..58dc849 --- /dev/null +++ b/src/components/estimates/TemplateSelectionModal.jsx @@ -0,0 +1,669 @@ +import React, { useState } from 'react'; + +const STUBBED_TEMPLATES = [ + { + id: 'atlas_glassmaster', + name: 'Atlas GlassMaster', + category: 'Roofing', + brand: 'Atlas', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood. If bad or rotten wood is discovered, it will be replaced at a price of $50 per sheet.", + "Install 3' of Atlas WeatherMaster ice and water shield at all gutter lines, rakes edges, and valleys.", + "Install Atlas Summit 60 Synthetic underlayment.", + "Install Atlas Glassmaster AR Shingles." + ], + materials: [ + { desc: 'Atlas Glassmaster AR', baseUnitCost: 37.67, uom: 'SQ' }, + { desc: 'Atlas Pro-Cut Hip & Ridge', baseUnitCost: 78.50, uom: 'BD' }, + { desc: 'Atlas 6X Pro-Cut Starter', baseUnitCost: 70.80, uom: 'BD' }, + { desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' }, + { desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'atlas_glassmaster_srs', + name: 'Atlas GlassMaster - SRS', + category: 'Roofing', + brand: 'SRS', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install 3' of Atlas WeatherMaster ice and water shield at all gutter lines, rakes edges, and valleys.", + "Install Atlas Summit 60 Synthetic underlayment.", + "Install Atlas Glassmaster AR Shingles (SRS distribution)." + ], + materials: [ + { desc: 'Atlas Glassmaster AR', baseUnitCost: 37.67, uom: 'SQ' }, + { desc: 'Atlas Pro-Cut Hip & Ridge', baseUnitCost: 78.50, uom: 'BD' }, + { desc: 'Atlas 6X Pro-Cut Starter', baseUnitCost: 70.80, uom: 'BD' }, + { desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' }, + { desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'atlas_pinnacle_pristine', + name: 'Atlas Pinnacle Pristine', + category: 'Roofing', + brand: 'Atlas', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install 3' of Atlas WeatherMaster ice and water shield.", + "Install Atlas Summit 60 Synthetic underlayment.", + "Install Atlas Pinnacle Pristine Lifetime Dimensional Shingles." + ], + materials: [ + { desc: 'Atlas Pinnacle Pristine', baseUnitCost: 42.50, uom: 'SQ' }, + { desc: 'Atlas Pro-Cut High Profile Hip & Ridge', baseUnitCost: 85.00, uom: 'BD' }, + { desc: 'Atlas Pro-Cut Starter', baseUnitCost: 72.00, uom: 'BD' }, + { desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' }, + { desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'atlas_pinnacle_pristine_srs', + name: 'Atlas Pinnacle Pristine - SRS', + category: 'Roofing', + brand: 'SRS', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install 3' of Atlas WeatherMaster ice and water shield.", + "Install Atlas Summit 60 Synthetic underlayment.", + "Install Atlas Pinnacle Pristine Lifetime Dimensional Shingles (SRS distribution)." + ], + materials: [ + { desc: 'Atlas Pinnacle Pristine', baseUnitCost: 42.50, uom: 'SQ' }, + { desc: 'Atlas Pro-Cut High Profile Hip & Ridge', baseUnitCost: 85.00, uom: 'BD' }, + { desc: 'Atlas Pro-Cut Starter', baseUnitCost: 72.00, uom: 'BD' }, + { desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' }, + { desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'atlas_stormmaster_shake', + name: 'Atlas StormMaster Shake', + category: 'Roofing', + brand: 'Atlas', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install 3' of Atlas WeatherMaster ice and water shield at all eaves and valleys.", + "Install Atlas Summit 60 Synthetic underlayment.", + "Install Atlas StormMaster Shake Impact Resistant Shingles." + ], + materials: [ + { desc: 'Atlas StormMaster Shake', baseUnitCost: 55.00, uom: 'SQ' }, + { desc: 'Atlas Pro-Cut Hip & Ridge', baseUnitCost: 78.50, uom: 'BD' }, + { desc: 'Atlas 6X Pro-Cut Starter', baseUnitCost: 70.80, uom: 'BD' }, + { desc: 'Atlas Summit 60 Synthetic Underlayment', baseUnitCost: 116.00, uom: 'RL' }, + { desc: 'Atlas WeatherMaster Ice & Water', baseUnitCost: 65.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'certainteed_landmark', + name: 'CertainTeed Landmark AR', + category: 'Roofing', + brand: 'CertainTeed', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install CertainTeed WinterGuard ice and water shield.", + "Install CertainTeed RoofRunner Synthetic underlayment.", + "Install CertainTeed Landmark AR Shingles." + ], + materials: [ + { desc: 'CertainTeed Landmark AR', baseUnitCost: 44.00, uom: 'SQ' }, + { desc: 'CertainTeed ShadowRidge', baseUnitCost: 82.00, uom: 'BD' }, + { desc: 'CertainTeed SwiftStart', baseUnitCost: 75.00, uom: 'BD' }, + { desc: 'CertainTeed RoofRunner', baseUnitCost: 125.00, uom: 'RL' }, + { desc: 'CertainTeed WinterGuard', baseUnitCost: 70.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'certainteed_landmark_srs', + name: 'CertainTeed Landmark AR - SRS', + category: 'Roofing', + brand: 'SRS', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install CertainTeed WinterGuard ice and water shield.", + "Install CertainTeed RoofRunner Synthetic underlayment.", + "Install CertainTeed Landmark AR Shingles (SRS distribution)." + ], + materials: [ + { desc: 'CertainTeed Landmark AR', baseUnitCost: 44.00, uom: 'SQ' }, + { desc: 'CertainTeed ShadowRidge', baseUnitCost: 82.00, uom: 'BD' }, + { desc: 'CertainTeed SwiftStart', baseUnitCost: 75.00, uom: 'BD' }, + { desc: 'CertainTeed RoofRunner', baseUnitCost: 125.00, uom: 'RL' }, + { desc: 'CertainTeed WinterGuard', baseUnitCost: 70.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'certainteed_presidential_shake', + name: 'CertainTeed Presidential Shake TL', + category: 'Roofing', + brand: 'CertainTeed', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install CertainTeed WinterGuard ice and water shield at all eaves and valleys.", + "Install CertainTeed RoofRunner Synthetic underlayment.", + "Install CertainTeed Presidential Shake TL triple-layer shingles." + ], + materials: [ + { desc: 'CertainTeed Presidential Shake TL', baseUnitCost: 72.00, uom: 'SQ' }, + { desc: 'CertainTeed Presidential Ridge', baseUnitCost: 92.00, uom: 'BD' }, + { desc: 'CertainTeed SwiftStart', baseUnitCost: 75.00, uom: 'BD' }, + { desc: 'CertainTeed RoofRunner', baseUnitCost: 125.00, uom: 'RL' }, + { desc: 'CertainTeed WinterGuard', baseUnitCost: 70.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'gaf_royal_sovereign', + name: 'GAF Royal Sovereign', + category: 'Roofing', + brand: 'GAF', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install GAF WeatherWatch ice and water shield at eaves and valleys.", + "Install GAF FeltBuster Synthetic underlayment.", + "Install GAF Royal Sovereign 3-tab shingles." + ], + materials: [ + { desc: 'GAF Royal Sovereign', baseUnitCost: 32.00, uom: 'SQ' }, + { desc: 'GAF Seal-A-Ridge', baseUnitCost: 72.00, uom: 'BD' }, + { desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' }, + { desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' }, + { desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'gaf_timberline_hdz', + name: 'GAF Timberline HDZ', + category: 'Roofing', + brand: 'GAF', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install GAF WeatherWatch ice and water shield at eaves, rakes, and valleys.", + "Install GAF FeltBuster Synthetic underlayment.", + "Install GAF Timberline HDZ Laminated Architectural Shingles." + ], + materials: [ + { desc: 'GAF Timberline HDZ', baseUnitCost: 45.00, uom: 'SQ' }, + { desc: 'GAF TimberTex Premium Ridge Cap', baseUnitCost: 85.00, uom: 'BD' }, + { desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' }, + { desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' }, + { desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'gaf_timberline_ultra_hd', + name: 'GAF Timberline Ultra HD', + category: 'Roofing', + brand: 'GAF', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install GAF WeatherWatch ice and water shield at eaves, rakes, and valleys.", + "Install GAF FeltBuster Synthetic underlayment.", + "Install GAF Timberline Ultra HD extra-thick dimensional shingles." + ], + materials: [ + { desc: 'GAF Timberline Ultra HD', baseUnitCost: 52.00, uom: 'SQ' }, + { desc: 'GAF TimberTex Premium Ridge Cap', baseUnitCost: 85.00, uom: 'BD' }, + { desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' }, + { desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' }, + { desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'gaf_timberline_hdz_srs', + name: 'GAF Timberline HDZ - SRS', + category: 'Roofing', + brand: 'SRS', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install GAF WeatherWatch ice and water shield at eaves, rakes, and valleys.", + "Install GAF FeltBuster Synthetic underlayment.", + "Install GAF Timberline HDZ Laminated Architectural Shingles (SRS distribution)." + ], + materials: [ + { desc: 'GAF Timberline HDZ', baseUnitCost: 45.00, uom: 'SQ' }, + { desc: 'GAF TimberTex Premium Ridge Cap', baseUnitCost: 85.00, uom: 'BD' }, + { desc: 'GAF Pro-Start Starter Strip', baseUnitCost: 68.00, uom: 'BD' }, + { desc: 'GAF FeltBuster Synthetic Underlayment', baseUnitCost: 110.00, uom: 'RL' }, + { desc: 'GAF WeatherWatch Ice & Water', baseUnitCost: 68.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'iko_armourshake', + name: 'IKO Armourshake', + category: 'Roofing', + brand: 'IKO', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install IKO ArmourGard ice and water shield at eaves and valleys.", + "Install IKO RoofGard-SB Synthetic underlayment.", + "Install IKO Armourshake heavyweight laminated shingles." + ], + materials: [ + { desc: 'IKO Armourshake', baseUnitCost: 58.00, uom: 'SQ' }, + { desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' }, + { desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' }, + { desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' }, + { desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'iko_armourshake_srs', + name: 'IKO Armourshake - SRS', + category: 'Roofing', + brand: 'SRS', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install IKO ArmourGard ice and water shield at eaves and valleys.", + "Install IKO RoofGard-SB Synthetic underlayment.", + "Install IKO Armourshake heavyweight laminated shingles (SRS distribution)." + ], + materials: [ + { desc: 'IKO Armourshake', baseUnitCost: 58.00, uom: 'SQ' }, + { desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' }, + { desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' }, + { desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' }, + { desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'iko_cambridge', + name: 'IKO Cambridge', + category: 'Roofing', + brand: 'IKO', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install IKO ArmourGard ice and water shield.", + "Install IKO RoofGard-SB Synthetic underlayment.", + "Install IKO Cambridge Architectural Shingles." + ], + materials: [ + { desc: 'IKO Cambridge', baseUnitCost: 42.00, uom: 'SQ' }, + { desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' }, + { desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' }, + { desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' }, + { desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'iko_dynasty', + name: 'IKO Dynasty', + category: 'Roofing', + brand: 'IKO', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install IKO ArmourGard ice and water shield at eaves, rakes, and valleys.", + "Install IKO RoofGard-SB Synthetic underlayment.", + "Install IKO Dynasty Impact Resistant shingles." + ], + materials: [ + { desc: 'IKO Dynasty', baseUnitCost: 54.00, uom: 'SQ' }, + { desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' }, + { desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' }, + { desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' }, + { desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'iko_dynasty_srs', + name: 'IKO Dynasty - SRS', + category: 'Roofing', + brand: 'SRS', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install IKO ArmourGard ice and water shield at eaves, rakes, and valleys.", + "Install IKO RoofGard-SB Synthetic underlayment.", + "Install IKO Dynasty Impact Resistant shingles (SRS distribution)." + ], + materials: [ + { desc: 'IKO Dynasty', baseUnitCost: 54.00, uom: 'SQ' }, + { desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' }, + { desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' }, + { desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' }, + { desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'iko_nordic_per_square', + name: 'IKO Nordic Per Square', + category: 'Roofing', + brand: 'IKO', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install IKO ArmourGard ice and water shield.", + "Install IKO RoofGard-SB Synthetic underlayment.", + "Install IKO Nordic Architectural Shingles (priced per square)." + ], + materials: [ + { desc: 'IKO Nordic', baseUnitCost: 40.00, uom: 'SQ' }, + { desc: 'IKO Hip & Ridge 12', baseUnitCost: 80.00, uom: 'BD' }, + { desc: 'IKO Leading Edge Plus Starter Strip', baseUnitCost: 71.00, uom: 'BD' }, + { desc: 'IKO RoofGard-SB Synthetic Underlayment', baseUnitCost: 118.00, uom: 'RL' }, + { desc: 'IKO ArmourGard Ice & Water', baseUnitCost: 67.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'malarkey_vista_ar', + name: 'Malarkey Vista AR', + category: 'Roofing', + brand: 'Malarkey', + description: [ + "Remove existing shingles down to deck.", + "Re-nail any loose wood.", + "Install Malarkey EverGuard ice and water shield at eaves and valleys.", + "Install Malarkey UDL 40 Synthetic underlayment.", + "Install Malarkey Vista AR Architectural Shingles." + ], + materials: [ + { desc: 'Malarkey Vista AR', baseUnitCost: 46.00, uom: 'SQ' }, + { desc: 'Malarkey Ridglass Hip & Ridge', baseUnitCost: 83.00, uom: 'BD' }, + { desc: 'Malarkey Windsor Starter Strip', baseUnitCost: 73.00, uom: 'BD' }, + { desc: 'Malarkey UDL 40 Synthetic Underlayment', baseUnitCost: 122.00, uom: 'RL' }, + { desc: 'Malarkey EverGuard Ice & Water', baseUnitCost: 69.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'mulehide_epdm', + name: 'Mulehide .060 EPDM System', + category: 'Roofing', + brand: 'Mulehide', + description: [ + "Remove existing flat roof membrane down to deck.", + "Inspect and repair decking as needed.", + "Install Mulehide .060 EPDM membrane fully adhered system.", + "Install Mulehide bonding adhesive.", + "Install drip edge and termination bars at perimeter." + ], + materials: [ + { desc: 'Mulehide .060 EPDM Membrane', baseUnitCost: 85.00, uom: 'SQ' }, + { desc: 'Mulehide Bonding Adhesive', baseUnitCost: 48.00, uom: 'EA' }, + { desc: 'Mulehide EPDM Lap Sealant', baseUnitCost: 12.00, uom: 'EA' }, + { desc: 'Mulehide Termination Bar', baseUnitCost: 4.50, uom: 'LF' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'owens_corning_duration', + name: 'Owens Corning Duration', + category: 'Roofing', + brand: 'Owens Corning', + description: [ + "Remove existing shingles down to deck.", + "Inspect and re-nail decking.", + "Install Owens Corning WeatherLock G ice and water shield.", + "Install Owens Corning ProArmor Synthetic underlayment.", + "Install Owens Corning TruDefinition Duration Shingles." + ], + materials: [ + { desc: 'OC TruDefinition Duration', baseUnitCost: 46.50, uom: 'SQ' }, + { desc: 'OC ProEdge Hip & Ridge', baseUnitCost: 88.00, uom: 'BD' }, + { desc: 'OC Starter Strip Plus', baseUnitCost: 78.00, uom: 'BD' }, + { desc: 'OC ProArmor Underlayment', baseUnitCost: 130.00, uom: 'RL' }, + { desc: 'OC WeatherLock G', baseUnitCost: 75.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'owens_corning_duration_srs', + name: 'Owens Corning Duration - SRS', + category: 'Roofing', + brand: 'SRS', + description: [ + "Remove existing shingles down to deck.", + "Inspect and re-nail decking.", + "Install Owens Corning WeatherLock G ice and water shield.", + "Install Owens Corning ProArmor Synthetic underlayment.", + "Install Owens Corning TruDefinition Duration Shingles (SRS distribution)." + ], + materials: [ + { desc: 'OC TruDefinition Duration', baseUnitCost: 46.50, uom: 'SQ' }, + { desc: 'OC ProEdge Hip & Ridge', baseUnitCost: 88.00, uom: 'BD' }, + { desc: 'OC Starter Strip Plus', baseUnitCost: 78.00, uom: 'BD' }, + { desc: 'OC ProArmor Underlayment', baseUnitCost: 130.00, uom: 'RL' }, + { desc: 'OC WeatherLock G', baseUnitCost: 75.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + }, + { + id: 'owens_corning_oakridge', + name: 'Owens Corning Oakridge', + category: 'Roofing', + brand: 'Owens Corning', + description: [ + "Remove existing shingles down to deck.", + "Inspect and re-nail decking.", + "Install Owens Corning WeatherLock G ice and water shield.", + "Install Owens Corning ProArmor Synthetic underlayment.", + "Install Owens Corning Oakridge Architectural Shingles." + ], + materials: [ + { desc: 'OC Oakridge', baseUnitCost: 38.00, uom: 'SQ' }, + { desc: 'OC ProEdge Hip & Ridge', baseUnitCost: 88.00, uom: 'BD' }, + { desc: 'OC Starter Strip Plus', baseUnitCost: 78.00, uom: 'BD' }, + { desc: 'OC ProArmor Underlayment', baseUnitCost: 130.00, uom: 'RL' }, + { desc: 'OC WeatherLock G', baseUnitCost: 75.00, uom: 'RL' }, + { desc: 'Coil Nails 1 1/4"', baseUnitCost: 35.00, uom: 'BX' }, + { desc: 'Plastic Cap Nails', baseUnitCost: 28.50, uom: 'BX' }, + { desc: 'Geocel 2300 Clear Sealant', baseUnitCost: 8.95, uom: 'EA' } + ] + } +]; + +export default function TemplateSelectionModal({ isOpen, onClose, onSelectTemplate }) { + const [searchTerm, setSearchTerm] = useState(''); + const [selected, setSelected] = useState(null); + + const filtered = STUBBED_TEMPLATES.filter(t => + t.name.toLowerCase().includes(searchTerm.toLowerCase()) || + t.category.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleNext = () => { + if (selected) { + onSelectTemplate(selected); + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

Select a Template

+

Choose a base template to calculate your item quantities.

+
+
+ + {/* Search Bar */} +
+ setSearchTerm(e.target.value)} + placeholder="Search for a Template..." + className="w-full bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-blue-500 transition-shadow text-zinc-800 dark:text-zinc-200" + /> +
+ + {/* Templates List */} +
+
+ + {/* Category Header */} +
+

+ Roofing ({filtered.length}) +

+
+ + {/* List items */} +
+ {filtered.map((template) => { + const isSelected = selected?.id === template.id; + return ( + + ); + })} + + {filtered.length === 0 && ( +
+ No templates match your search. +
+ )} +
+ +
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/src/pages/EstimateBuilder.jsx b/src/pages/EstimateBuilder.jsx index d8fea34..f9936e9 100644 --- a/src/pages/EstimateBuilder.jsx +++ b/src/pages/EstimateBuilder.jsx @@ -1,17 +1,30 @@ import React, { useState, useEffect } from 'react'; import { Plus, Trash2, Calculator, Settings, AlertCircle, Save, ArrowLeft, - FileText, User, MapPin, Phone, Mail, CheckCircle2, MoreHorizontal, ChevronDown, DollarSign, Maximize, Minimize + FileText, User, MapPin, Phone, Mail, CheckCircle2, MoreHorizontal, ChevronDown, ChevronRight, DollarSign, Maximize, Minimize, UploadCloud, GripVertical, Tag } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; +import ImageUploadModal from '../components/estimates/ImageUploadModal'; +import TemplateSelectionModal from '../components/estimates/TemplateSelectionModal'; +import MeasurementsModal from '../components/estimates/MeasurementsModal'; +import MaterialDetailsModal from '../components/estimates/MaterialDetailsModal'; +import InitialChoiceModal from '../components/estimates/InitialChoiceModal'; const generateId = () => Math.random().toString(36).substr(2, 9); -const UOM_OPTIONS = ['SQ', 'EA', 'BD', 'RL', 'LF', 'HR', 'LS']; +const UOM_OPTIONS = ['SQ', 'EA', 'BD', 'RL', 'LF', 'HR', 'LS', 'PC', 'BX']; export default function EstimateBuilder() { const navigate = useNavigate(); + // --- Zone E: Workflow State --- + // Workflow: 'upload' → 'initialChoice' → 'template' (or 'none') → 'measurements' → 'none' + const [workflowStep, setWorkflowStep] = useState('upload'); + const [selectedImage, setSelectedImage] = useState(null); + const [selectedTemplate, setSelectedTemplate] = useState(null); + const [measurementsData, setMeasurementsData] = useState(null); + const [activeDetailsItem, setActiveDetailsItem] = useState(null); + // --- Zone A: Client Info --- const [clientInfo, setClientInfo] = useState({ name: '', @@ -60,6 +73,8 @@ export default function EstimateBuilder() { { id: generateId(), name: 'Roofing Section', + description: [], + marginPercent: 30, groups: [ { id: generateId(), @@ -79,6 +94,12 @@ export default function EstimateBuilder() { } ]); + // Collapsed state for section descriptions + const [collapsedDescriptions, setCollapsedDescriptions] = useState({}); + const toggleDescription = (sectionId) => { + setCollapsedDescriptions(prev => ({ ...prev, [sectionId]: !prev[sectionId] })); + }; + // --- Zone D: Financials --- const [financials, setFinancials] = useState({ taxRate: 0, @@ -97,11 +118,131 @@ export default function EstimateBuilder() { return () => window.removeEventListener('keydown', handleKeyDown); }, [isFullViewMode]); + // --- Workflow Handlers --- + const handleImageSelect = (imageName) => { + setSelectedImage(imageName); + setWorkflowStep('initialChoice'); + }; + + const handleTemplateSelect = (template) => { + setSelectedTemplate(template); + setWorkflowStep('measurements'); + }; + + const handleMeasurementsNext = (measurements) => { + setMeasurementsData(measurements); + + // Auto-generate sections based on template + if (selectedTemplate) { + const newSection = { + id: generateId(), + name: `${selectedTemplate.name} Section`, + description: selectedTemplate.description || [], + marginPercent: 30, + groups: [ + { + id: generateId(), + name: 'Materials', + items: selectedTemplate.materials.map(mat => { + let baseQty = 0; + let conversionFactor = 1; + let measurementUom = mat.uom; + + // Logic to handle different UOMs based on typical roofing measurements + if (mat.uom === 'SQ') { + baseQty = measurements.totalRoofArea || 0; + measurementUom = 'SQ'; + } else if (mat.uom === 'BD') { + if (mat.desc.toLowerCase().includes('ridge')) { + baseQty = measurements.ridges || 0; + conversionFactor = 30; // 30 LF per Bundle of Ridge + } else { + baseQty = measurements.eaves || 0; + conversionFactor = 100; // 100 LF per Bundle of Starter + } + measurementUom = 'LF'; + } else if (mat.uom === 'RL') { + if (mat.desc.toLowerCase().includes('ice')) { + baseQty = measurements.eaves || 0; + conversionFactor = 65; // 65 LF per Roll of Ice & Water + measurementUom = 'LF'; + } else { + baseQty = measurements.totalRoofArea || 0; + conversionFactor = 10; // 10 SQ per Roll of Synthetic + measurementUom = 'SQ'; + } + } else if (mat.uom === 'BX') { + baseQty = 1; + measurementUom = 'BX'; + } else if (mat.uom === 'EA') { + baseQty = 1; + measurementUom = 'EA'; + } else if (mat.uom === 'PC') { + baseQty = 1; + measurementUom = 'PC'; + } else { + baseQty = measurements.totalRoofArea || 0; + } + + const calculatedPreCeil = (baseQty * 1.05) / conversionFactor; // 5% flat waste for initial generation + const finalQty = Math.ceil(calculatedPreCeil); + + return { + id: generateId(), + desc: mat.desc, + uom: mat.uom, + unitCost: mat.baseUnitCost, + qty: finalQty, + clientPrice: finalQty * mat.baseUnitCost * 1.428, // ~30% margin default + baseMeasurement: baseQty, + wastePercent: 5, + conversionFactor: conversionFactor, + marginPercent: 30, + measurementUom: measurementUom + }; + }) + } + ] + }; + // Replace the dummy default section with the generated one + setSections([newSection]); + } + setWorkflowStep('none'); + }; + + const handleItemClick = (sectionId, groupId, item) => { + setActiveDetailsItem({ sectionId, groupId, item }); + }; + + const handleSaveMaterialDetails = (updatedItem) => { + if (!activeDetailsItem) return; + const { sectionId, groupId } = activeDetailsItem; + setSections(sections.map(s => { + if (s.id === sectionId) { + return { + ...s, + groups: s.groups.map(g => { + if (g.id === groupId) { + return { + ...g, + items: g.items.map(i => i.id === updatedItem.id ? updatedItem : i) + }; + } + return g; + }) + }; + } + return s; + })); + }; + // Handlers for Zone C const addSection = () => { setSections([...sections, { id: generateId(), name: `New Section ${sections.length + 1}`, + description: [], + marginPercent: 30, groups: [] }]); }; @@ -185,6 +326,27 @@ export default function EstimateBuilder() { setSections(sections.map(s => s.id === sectionId ? { ...s, name } : s)); }; + const updateSectionMargin = (sectionId, newMarginPercent) => { + setSections(sections.map(s => { + if (s.id === sectionId) { + return { + ...s, + margin: newMarginPercent, + marginPercent: newMarginPercent, + groups: s.groups.map(g => ({ + ...g, + items: g.items.map(i => { + const cost = i.qty * i.unitCost; + const clientPrice = newMarginPercent < 100 ? cost / (1 - (newMarginPercent / 100)) : cost; + return { ...i, marginPercent: newMarginPercent, clientPrice }; + }) + })) + }; + } + return s; + })); + }; + const updateGroupName = (sectionId, groupId, name) => { setSections(sections.map(s => { if (s.id === sectionId) { @@ -213,9 +375,6 @@ export default function EstimateBuilder() { const ohpAmount = subtotal * (financials.ohp / 100); const grandTotal = subtotal + tax + ohpAmount; - // Profit margin calculation (based on Revenue - Cost) - // Here we consider total revenue as the Subtotal + OHP. - // If OHP is part of revenue, Final Gross Margin % = ((GrandTotal(excluding tax) - InternalCost) / GrandTotal(excluding tax)) * 100 const totalRevenueNoTax = subtotal + ohpAmount; const netProfit = totalRevenueNoTax - internalCost; const grossMarginPercent = totalRevenueNoTax > 0 ? (netProfit / totalRevenueNoTax) * 100 : 0; @@ -236,6 +395,46 @@ export default function EstimateBuilder() { return { cost, rev }; }; + // Helper calculation for an entire section + const calcSectionTotals = (section) => { + let cost = 0; + let rev = 0; + section.groups.forEach(g => { + g.items.forEach(i => { + cost += (i.qty * i.unitCost); + rev += i.clientPrice; + }); + }); + const margin = rev > 0 ? ((rev - cost) / rev) * 100 : 0; + const profitAmount = rev - cost; + return { cost, rev, margin, profitAmount }; + }; + + // Bottom action bar component + const BottomActionBar = () => ( +
+ + +
+ + +
+ ); + return (
@@ -252,6 +451,9 @@ export default function EstimateBuilder() {

Create and configure project estimates for clients.

+ @@ -362,13 +564,13 @@ export default function EstimateBuilder() { )}
{/* Blueprint background for construction feel */}
-
+
@@ -398,161 +600,368 @@ export default function EstimateBuilder() {
-
- {sections.map(section => ( -
-
- updateSectionName(section.id, e.target.value)} - className="text-lg font-bold bg-transparent border-b border-transparent focus:border-emerald-500 outline-none text-zinc-900 dark:text-white pb-1 focus:ring-0" - /> - -
+
+ {sections.map((section, sectionIndex) => { + const sectionTotals = calcSectionTotals(section); + const currentMargin = section.marginPercent !== undefined ? section.marginPercent : sectionTotals.margin; + const isDescCollapsed = collapsedDescriptions[section.id] !== false; // default collapsed -
- {section.groups.map(group => { - const groupTotals = calcGroupTotals(group); - return ( -
- {/* Group Header */} -
-
- updateGroupName(section.id, group.id, e.target.value)} - className="font-semibold text-sm bg-transparent border-b border-transparent focus:border-emerald-500 outline-none w-48 text-zinc-800 dark:text-zinc-200 pb-0.5" - /> + return ( +
+ {/* Section Header */} +
+ + updateSectionName(section.id, e.target.value)} + className="text-base font-bold bg-transparent border-b border-transparent focus:border-emerald-500 outline-none text-zinc-900 dark:text-white pb-0.5 focus:ring-0 flex-1" + /> + ${sectionTotals.cost.toFixed(2)} +
+ $ + {sectionTotals.rev.toFixed(2)} +
+ + +
+ + {/* Section Description Block (collapsible) */} + {section.description && section.description.length > 0 && !isDescCollapsed && ( +
+
    + {section.description.map((line, i) => ( +
  • + - + {line} +
  • + ))} +
+
+ )} + + {/* Groups */} +
+ {section.groups.map(group => { + const groupTotals = calcGroupTotals(group); + return ( +
+ {/* Group Header */} +
+
+ + updateGroupName(section.id, group.id, e.target.value)} + className="font-semibold text-sm bg-transparent border-b border-transparent focus:border-emerald-500 outline-none text-zinc-700 dark:text-zinc-300 pb-0.5 focus:ring-0" + /> +
+
+ ${groupTotals.cost.toFixed(2)} +
+ $ + {groupTotals.rev.toFixed(2)} +
+
- -
- {/* Items Table - Responsive horizontal scroll container */} -
- - - - - - - - - - - - - - - {group.items.map((item, index) => ( - - - - - - - - - + {/* Items Table */} +
+
Sr No.DescriptionQtyUOMUnit Cost ($)Total CostClient Price ($)Act
- {index + 1} - - updateItem(section.id, group.id, item.id, 'desc', e.target.value)} - className="w-full bg-transparent border-none focus:ring-2 focus:ring-emerald-500 rounded-lg px-3 py-2 outline-none text-zinc-800 dark:text-zinc-200 hover:bg-black/5 dark:hover:bg-white/5 transition-colors" - placeholder="Item description..." - /> - - updateItem(section.id, group.id, item.id, 'qty', e.target.value)} - className="w-full bg-transparent border border-zinc-200 dark:border-zinc-700 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500 rounded-lg px-3 py-2 outline-none font-mono text-zinc-800 dark:text-zinc-200 hover:border-zinc-300 dark:hover:border-zinc-600 transition-colors" - /> - -
- - -
-
- updateItem(section.id, group.id, item.id, 'unitCost', e.target.value)} - className="w-full bg-transparent border border-zinc-200 dark:border-zinc-700 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500 rounded-lg px-3 py-2 outline-none font-mono text-zinc-800 dark:text-zinc-200 hover:border-zinc-300 dark:hover:border-zinc-600 transition-colors" - /> - - ${(item.qty * item.unitCost).toFixed(2)} - -
- $ - updateItem(section.id, group.id, item.id, 'clientPrice', e.target.value)} - className="w-full bg-emerald-50/50 dark:bg-emerald-900/10 border border-emerald-200 dark:border-emerald-800/30 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500 rounded-lg pl-8 pr-3 py-2 outline-none font-mono font-bold text-emerald-900 dark:text-emerald-100 transition-colors" - /> -
-
- -
+ + + + + + + + + - ))} - - {/* Add Item Row Button */} - - - - -
DescriptionUnitCost/UnitCostPrice···
- -
-
- - {/* Group Subtotal Footer */} -
-
- Group Cost: - ${groupTotals.cost.toFixed(2)} + + + {group.items.map((item, index) => ( + + + {/* Drag handle */} + + + + {/* Description + inline trash */} + +
+ updateItem(section.id, group.id, item.id, 'desc', e.target.value)} + className="flex-1 min-w-[150px] bg-transparent border-none focus:ring-2 focus:ring-emerald-500 rounded-lg px-3 py-2 outline-none text-zinc-800 dark:text-zinc-200 hover:bg-black/5 dark:hover:bg-white/5 transition-colors text-sm" + placeholder="Item description..." + /> + +
+ + {/* Unit = Qty + UOM side by side */} + +
+ updateItem(section.id, group.id, item.id, 'qty', e.target.value)} + className={`w-20 bg-transparent border focus:ring-1 rounded-lg px-2 py-2 outline-none font-mono text-sm text-zinc-800 dark:text-zinc-200 transition-colors ${ + !item.qty || item.qty === 0 + ? 'border-orange-400 focus:border-orange-500 focus:ring-orange-400' + : 'border-zinc-200 dark:border-zinc-700 focus:border-emerald-500 focus:ring-emerald-500 hover:border-zinc-300 dark:hover:border-zinc-600' + }`} + /> +
+ + +
+
+ + {/* Cost/Unit (read-only computed display) */} + + ${(item.unitCost || 0).toFixed(2)} + + {/* Cost (read-only calculated) */} + + ${(item.qty * item.unitCost).toFixed(2)} + + {/* Price */} + +
+ $ + updateItem(section.id, group.id, item.id, 'clientPrice', e.target.value)} + className="w-full min-w-[110px] bg-green-50 dark:bg-emerald-900/10 border border-emerald-200 dark:border-emerald-800/30 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 rounded-lg pl-7 pr-3 py-2 outline-none font-mono text-sm font-bold text-emerald-900 dark:text-emerald-100 transition-colors" + /> +
+ + {/* Actions ··· */} + + + + + {item.srsUnavailable && ( + + + ⚠ This item is not available from SRS + + + )} +
+ ))} + +
-
- Group Revenue: - ${groupTotals.rev.toFixed(2)} + + {/* Add Item to Group button */} +
+
+ ); + })} +
+ + {/* Add Item to Section search bar area */} +
+
+ +
+ + +
+ + {/* Section Profit Margin Slider */} +
+
+ + Section Profit Margin + updateSectionMargin(section.id, parseFloat(e.target.value))} + className="flex-1 h-2 rounded-lg appearance-none cursor-pointer accent-emerald-500" + style={{ background: `linear-gradient(to right, #10b981 0%, #10b981 ${(currentMargin/60)*100}%, #e4e4e7 ${(currentMargin/60)*100}%, #e4e4e7 100%)` }} + /> + + ${sectionTotals.profitAmount.toFixed(2)} + +
+ updateSectionMargin(section.id, parseFloat(e.target.value) || 0)} + className="w-10 bg-transparent outline-none font-mono text-sm text-amber-600 dark:text-amber-500 text-right" + /> + % +
+ ${sectionTotals.cost.toFixed(2)} +
+ ${sectionTotals.rev.toFixed(2)} +
+
+ + {/* Taxes + OH&P row */} +
+ Taxes + + OH & P + ${(sectionTotals.rev * (financials.taxRate/100) + sectionTotals.rev * (financials.ohp/100)).toFixed(2)} +
+ + {/* Section Total Banner */} +
+ {section.name.toUpperCase()} TOTAL +
+ Cost: ${sectionTotals.cost.toFixed(2)} +
+ $ + {sectionTotals.rev.toFixed(2)}
- ); - })} +
+
+
+ ); + })} - + {/* Add New Section Button */} +
+ +
+ + {/* Add Discount */} +
+ +
+ + {/* Estimate Total Banner */} +
+ Estimate Total +
+ Cost: ${totals.internalCost.toFixed(2)} + ${totals.grandTotal.toFixed(2)} +
+
+ + {/* Summary grid after estimate total */} +
+ {/* Total OH&P */} +
+
Total OH&P
+
+
+ Overhead + ${totals.ohpAmount.toFixed(2)} +
+
+ Profit + $0.00 +
- ))} + {/* Taxes & Discounts */} +
+
Taxes & Discounts
+
+
+ Taxes + ${totals.tax.toFixed(2)} +
+
+ Discounts + $0.00 +
+
+
+ {/* Profit */} +
+
Profit
+
+
+ Net Profit + +${totals.netProfit.toFixed(2)} +
+
+ Total Margin + {totals.grossMarginPercent.toFixed(1)}% +
+
+
+
+
+ + {/* Bottom Action Bar (inside section, sticky when full view) */} + +
@@ -562,24 +971,24 @@ export default function EstimateBuilder() { {/* --- Zone D: Financial Summary & Profit Analysis --- */}
-
+
-
-

+
+

Financial Summary

- Subtotal (Client Price) - ${totals.subtotal.toFixed(2)} + Subtotal (Client Price) + ${totals.subtotal.toFixed(2)}
- + Tax Rate - % + %
@@ -587,17 +996,17 @@ export default function EstimateBuilder() { type="number" value={financials.taxRate || ''} onChange={(e) => setFinancials({ ...financials, taxRate: parseFloat(e.target.value) || 0 })} - className="w-full bg-white/5 border border-white/10 rounded px-2 py-1 text-right text-sm font-mono focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none" + className="w-full bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/10 rounded px-2 py-1 text-right text-sm font-mono text-zinc-800 dark:text-white focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none" />
- ${totals.tax.toFixed(2)} + ${totals.tax.toFixed(2)}
- + OH&P Modifier - % + %
@@ -605,20 +1014,20 @@ export default function EstimateBuilder() { type="number" value={financials.ohp || ''} onChange={(e) => setFinancials({ ...financials, ohp: parseFloat(e.target.value) || 0 })} - className="w-full bg-white/5 border border-white/10 rounded px-2 py-1 text-right text-sm font-mono focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none" + className="w-full bg-zinc-50 dark:bg-white/5 border border-zinc-200 dark:border-white/10 rounded px-2 py-1 text-right text-sm font-mono text-zinc-800 dark:text-white focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none" />
- ${totals.ohpAmount.toFixed(2)} + ${totals.ohpAmount.toFixed(2)}
{/* Grand Total */} -
+
- Grand Total - + Grand Total + ${totals.grandTotal.toFixed(2)}
@@ -626,43 +1035,42 @@ export default function EstimateBuilder() { {/* Profit Analysis Gauge */}
-

- +

+ Profit Analysis

- Net Profit - +${totals.netProfit.toFixed(2)} + Net Profit + +${totals.netProfit.toFixed(2)}
- Gross Margin - + Gross Margin + {totals.grossMarginPercent.toFixed(1)}%
{/* Gauge Bar */} -
+
30 ? 'bg-emerald-500' : totals.grossMarginPercent > 15 ? 'bg-amber-400' : 'bg-red-500' }`} style={{ width: `${Math.min(Math.max(totals.grossMarginPercent, 0), 100)}%` }} /> - {/* Tick Marks for aesthetics */}
{[...Array(9)].map((_, i) => ( -
+
))}
{/* Warning message if margin is low */} {totals.grossMarginPercent < 20 && totals.grandTotal > 0 && ( -
+

Margin is under 20%. Please review costs or markup strategy.

@@ -676,6 +1084,40 @@ export default function EstimateBuilder() {
+ + {/* --- Modals --- */} + setWorkflowStep('none')} + onSelectImage={handleImageSelect} + /> + + setWorkflowStep('template')} + onStartScratch={() => setWorkflowStep('none')} + /> + + setWorkflowStep('none')} + onSelectTemplate={handleTemplateSelect} + /> + + setWorkflowStep('none')} + onNext={handleMeasurementsNext} + selectedImageId={selectedImage} + /> + + setActiveDetailsItem(null)} + item={activeDetailsItem?.item} + onSave={handleSaveMaterialDetails} + /> +
); }