Files
LynkedUpPro_CRM/src/components/estimates/MaterialDetailsModal.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

272 lines
16 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<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-3xl 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">
<h2 className="text-3xl font-light tracking-tight text-zinc-400 dark:text-zinc-500" style={{ fontFamily: 'Georgia, serif' }}>Material Details</h2>
</div>
{/* Sub-header: item name + name input + add description */}
<div className="px-8 py-5 border-b border-zinc-200 dark:border-zinc-700/50 shrink-0 bg-zinc-50 dark:bg-zinc-800/40">
<p className="text-sm font-semibold text-zinc-800 dark:text-zinc-200 mb-3">{item.desc}</p>
<input
type="text"
placeholder="Estimate Item Name"
className="w-full bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded px-3 py-2.5 text-sm outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400 text-zinc-600 dark:text-zinc-300 placeholder-zinc-300 dark:placeholder-zinc-600 transition-all"
defaultValue=""
/>
<button className="mt-2 text-blue-500 text-sm flex items-center gap-1.5 hover:text-blue-600 font-medium">
<FileText className="w-3.5 h-3.5" /> Add Description
</button>
</div>
{/* Main Content */}
<div className="p-8 overflow-y-auto flex-1 space-y-8 bg-white dark:bg-zinc-900">
{/* Row 1: Measurement @ Waste = Quantity */}
<div className="flex items-end gap-4 flex-wrap">
{/* Measurement */}
<div className="flex-1 min-w-[120px]">
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Measurement:</label>
<div className="flex items-center gap-2">
<input
type="number"
value={measurement || ''}
onChange={(e) => 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"
/>
<span className="text-sm text-zinc-500 dark:text-zinc-400 font-medium">{measurementUomLabel}</span>
</div>
</div>
<span className="text-zinc-400 dark:text-zinc-500 text-lg mb-2">@</span>
{/* Waste */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Waste</label>
<div className="flex items-center gap-1 border-2 border-blue-300 dark:border-blue-500/60 rounded px-3 py-2 focus-within:border-blue-500">
<input
type="number"
value={wastePercent === 0 ? '' : wastePercent}
onChange={(e) => 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"
/>
<span className="text-sm text-zinc-500 dark:text-zinc-400">%</span>
</div>
</div>
<span className="text-zinc-400 dark:text-zinc-500 text-lg mb-2">=</span>
{/* Quantity (read-only, post-waste pre-ceil) */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Quantity</label>
<div className="flex items-center gap-2">
<div className="w-24 border border-zinc-200 dark:border-zinc-700 rounded px-3 py-2 bg-zinc-50 dark:bg-zinc-800/50">
<span className="text-sm font-mono text-zinc-700 dark:text-zinc-300">{calculatedQuantity.toFixed(2)}</span>
</div>
<span className="text-sm text-zinc-500 dark:text-zinc-400 font-medium">{item.uom}</span>
</div>
</div>
</div>
<div className="border-t border-zinc-200 dark:border-zinc-700/50" />
{/* Row 2: Cost/Unit | Price/Unit */}
<div className="flex items-start gap-12 flex-wrap">
{/* Cost/Unit */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Cost / Unit</label>
<div className="flex items-center gap-2">
<div className="flex items-center border border-zinc-300 dark:border-zinc-600 rounded overflow-hidden focus-within:border-blue-400 focus-within:ring-1 focus-within:ring-blue-400">
<span className="px-2 py-2 text-sm text-zinc-500 dark:text-zinc-400 border-r border-zinc-200 dark:border-zinc-600 bg-zinc-50 dark:bg-zinc-800">$</span>
<input
type="number"
value={unitCost || ''}
onChange={(e) => 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"
/>
</div>
<span className="text-sm text-zinc-500 dark:text-zinc-400">/ {item.uom}</span>
</div>
{/* Conversion note */}
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-1.5">{conversionNote}</p>
{/* Editable conversion factor (small, subtle) */}
<div className="flex items-center gap-1 mt-1 text-xs text-zinc-400 dark:text-zinc-500">
<input
type="number"
value={conversionFactor}
onChange={(e) => 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"
/>
<span>{measurementUomLabel} = 1 {item.uom}</span>
</div>
</div>
{/* Price/Unit checkbox */}
<div className="flex items-start gap-3">
<button
onClick={() => setPricePerUnitOverride(!pricePerUnitOverride)}
className={`w-5 h-5 mt-5 rounded border-2 flex items-center justify-center shrink-0 transition-colors ${pricePerUnitOverride ? 'border-orange-500 bg-orange-50 dark:bg-orange-900/20' : 'border-orange-400'}`}
>
{pricePerUnitOverride && <div className="w-2.5 h-2.5 bg-orange-500 rounded-sm" />}
</button>
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1.5">Price / Unit</label>
<div className="flex items-center gap-2">
<div className="flex items-center border border-zinc-200 dark:border-zinc-700 rounded overflow-hidden focus-within:border-blue-400">
<span className="px-2 py-2 text-sm text-zinc-400 dark:text-zinc-500 border-r border-zinc-100 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800">$</span>
{pricePerUnitOverride ? (
<input
type="number"
value={manualPricePerUnit || ''}
onChange={(e) => 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"
/>
) : (
<span className="w-20 px-2 py-2 text-sm font-mono text-zinc-300 dark:text-zinc-600 text-right">{computedPricePerUnit.toFixed(2)}</span>
)}
</div>
<span className="text-sm text-zinc-500 dark:text-zinc-400">/ {item.uom}</span>
</div>
</div>
</div>
</div>
<div className="border-t border-zinc-200 dark:border-zinc-700/50" />
{/* Row 3: Total summary formula */}
<div className="flex items-end gap-5 flex-wrap">
{/* Total */}
<div>
<label className="block text-xs text-blue-500 dark:text-blue-400 font-medium mb-1">Total</label>
<div className="flex items-baseline gap-1.5">
<span className="text-2xl font-bold text-zinc-800 dark:text-zinc-200">{finalTotalUnits}</span>
<span className="text-sm text-zinc-500 dark:text-zinc-400">{item.uom}</span>
</div>
</div>
<span className="text-zinc-300 dark:text-zinc-600 text-xl mb-1">×</span>
{/* Cost/Unit summary */}
<div>
<label className="block text-xs text-blue-500 dark:text-blue-400 font-medium mb-1">Cost/Unit</label>
<span className="text-base font-mono text-zinc-600 dark:text-zinc-400">${unitCost.toFixed(2)} / {item.uom}</span>
</div>
<span className="text-zinc-300 dark:text-zinc-600 text-xl mb-1">@</span>
{/* Section Margin */}
<div>
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1">Section Margin</label>
<div className="flex items-center gap-1">
<input
type="number"
value={marginPercent || ''}
onChange={(e) => 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"
/>
<span className="text-zinc-500 dark:text-zinc-400 text-sm">%</span>
</div>
</div>
<span className="text-zinc-300 dark:text-zinc-600 text-xl mb-1">=</span>
{/* Price result */}
<div className="bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-700 rounded-lg px-6 py-3 text-center">
<label className="block text-xs text-zinc-400 dark:text-zinc-500 mb-1">Price</label>
<span className="text-2xl font-bold text-zinc-700 dark:text-zinc-200">${clientPrice.toFixed(2)}</span>
</div>
</div>
</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={onClose}
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={handleSave}
className="px-10 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"
>
Done
</button>
</div>
</div>
</div>
);
}