Files
LynkedUpPro_CRM/src/components/estimates/MeasurementsModal.jsx
T
Satyam a0af994614 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
2026-03-13 00:29:13 +05:30

155 lines
7.7 KiB
React

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>
);
}