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
This commit is contained in:
@@ -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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div
|
||||
className="bg-white dark:bg-zinc-900 w-full max-w-xl rounded-xl shadow-2xl border border-zinc-200 dark:border-zinc-700/50 overflow-hidden flex flex-col max-h-[92vh]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-center p-6 border-b border-zinc-200 dark:border-zinc-700/50 shrink-0 relative">
|
||||
<h2 className="text-3xl font-light text-zinc-400 dark:text-zinc-500 tracking-tight" style={{ fontFamily: 'Georgia, serif' }}>Measurements</h2>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="px-8 py-4 bg-zinc-50 dark:bg-zinc-800/30 border-b border-zinc-100 dark:border-zinc-700/50 shrink-0">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 flex items-start gap-2">
|
||||
<Info className="w-4 h-4 text-zinc-400 dark:text-zinc-500 shrink-0 mt-0.5" />
|
||||
The actual measurements below will be used to calculate your item quantities.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-8 overflow-y-auto flex-1 space-y-6 bg-white dark:bg-zinc-900">
|
||||
{/* Measurement Dropdown */}
|
||||
<div className="border border-zinc-300 dark:border-zinc-600 rounded-lg px-4 py-2 relative focus-within:border-blue-400 focus-within:ring-1 focus-within:ring-blue-400 bg-white dark:bg-zinc-800/50">
|
||||
<label className="block text-xs text-blue-500 font-semibold mb-0.5">Measurement:</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="w-full appearance-none bg-transparent outline-none text-zinc-800 dark:text-zinc-200 text-sm font-medium pr-6"
|
||||
defaultValue="primary"
|
||||
>
|
||||
<option value="primary">Steep Slope Roofing Measurement</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-0 top-1/2 -translate-y-1/2 w-4 h-4 text-blue-500 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Measurements list */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end pr-16">
|
||||
<span className="text-xs text-zinc-400 dark:text-zinc-500 font-medium tracking-wide uppercase">Actual</span>
|
||||
</div>
|
||||
|
||||
{/* Total Roof Area - prominent */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300 w-36">Total Roof Area</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="number"
|
||||
value={measurements.totalRoofArea || ''}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-sm text-blue-500 w-8 font-medium">SQ</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{fields.map(item => (
|
||||
<div key={item.key} className="flex items-center justify-between">
|
||||
<span className="text-sm text-blue-600 dark:text-blue-400 w-36">{item.label}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="number"
|
||||
value={measurements[item.key] || ''}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-sm text-blue-500 w-8 font-medium">{item.uom}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className="text-sm text-blue-500 hover:text-blue-600 hover:underline mt-2">
|
||||
Show Measurements Not Included on Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-zinc-200 dark:border-zinc-700/50 shrink-0 bg-white dark:bg-zinc-900/80">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-5 py-2 text-sm font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-lg transition-colors uppercase tracking-wide"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onNext(measurements)}
|
||||
className="px-8 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition-colors shadow-sm uppercase tracking-wide"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user