feat(estimate): refine layout, add full view modal, update table columns, and remove reference images

This commit is contained in:
Satyam
2026-03-04 22:46:01 +05:30
parent 628e945ea7
commit b7c9ebb0d0
3 changed files with 707 additions and 1 deletions
+681
View File
@@ -0,0 +1,681 @@
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
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
const generateId = () => Math.random().toString(36).substr(2, 9);
const UOM_OPTIONS = ['SQ', 'EA', 'BD', 'RL', 'LF', 'HR', 'LS'];
export default function EstimateBuilder() {
const navigate = useNavigate();
// --- Zone A: Client Info ---
const [clientInfo, setClientInfo] = useState({
name: '',
phone: '',
email: '',
address: '',
zip: ''
});
// Basic Validation states
const [errors, setErrors] = useState({});
const validateEmail = (email) => {
if (!email) return true;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};
const validatePhone = (phone) => {
if (!phone) return true;
return phone.length >= 10;
};
const handleClientChange = (e) => {
const { name, value } = e.target;
setClientInfo(prev => ({ ...prev, [name]: value }));
// Clear error on change
if (name === 'email' && validateEmail(value)) {
setErrors(prev => ({ ...prev, email: null }));
} else if (name === 'email') {
setErrors(prev => ({ ...prev, email: 'Invalid email address' }));
}
if (name === 'phone' && validatePhone(value)) {
setErrors(prev => ({ ...prev, phone: null }));
} else if (name === 'phone' && value.length > 0) {
setErrors(prev => ({ ...prev, phone: 'Invalid phone (min 10 chars)' }));
}
};
// --- Zone B: Scope of Work ---
const [scopeOfWork, setScopeOfWork] = useState('');
// --- Zone C: Costing Engine ---
const [sections, setSections] = useState([
{
id: generateId(),
name: 'Roofing Section',
groups: [
{
id: generateId(),
name: 'Materials',
items: [
{ id: generateId(), desc: 'CertainTeed Landmark AR', qty: 98.41, uom: 'SQ', unitCost: 42.09, clientPrice: 19182.12 }
]
},
{
id: generateId(),
name: 'Labor',
items: [
{ id: generateId(), desc: 'Tear off and Install Laminated Shingles', qty: 98.41, uom: 'SQ', unitCost: 80.00, clientPrice: 12121.46 }
]
}
]
}
]);
// --- Zone D: Financials ---
const [financials, setFinancials] = useState({
taxRate: 0,
ohp: 0,
});
const [isFullViewMode, setIsFullViewMode] = useState(false);
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'Escape' && isFullViewMode) {
setIsFullViewMode(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isFullViewMode]);
// Handlers for Zone C
const addSection = () => {
setSections([...sections, {
id: generateId(),
name: `New Section ${sections.length + 1}`,
groups: []
}]);
};
const addGroup = (sectionId) => {
setSections(sections.map(s => {
if (s.id === sectionId) {
return {
...s,
groups: [...s.groups, { id: generateId(), name: `New Group`, items: [] }]
};
}
return s;
}));
};
const addItem = (sectionId, groupId) => {
setSections(sections.map(s => {
if (s.id === sectionId) {
return {
...s,
groups: s.groups.map(g => {
if (g.id === groupId) {
return {
...g,
items: [...g.items, { id: generateId(), desc: '', qty: 0, uom: 'EA', unitCost: 0, clientPrice: 0 }]
};
}
return g;
})
};
}
return s;
}));
};
const updateItem = (sectionId, groupId, itemId, field, value) => {
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 => {
if (i.id === itemId) {
const parsedVal = (field === 'qty' || field === 'unitCost' || field === 'clientPrice') ? parseFloat(value) || 0 : value;
return { ...i, [field]: parsedVal };
}
return i;
})
};
}
return g;
})
};
}
return s;
}));
};
const removeItem = (sectionId, groupId, itemId) => {
setSections(sections.map(s => {
if (s.id === sectionId) {
return {
...s,
groups: s.groups.map(g => {
if (g.id === groupId) {
return { ...g, items: g.items.filter(i => i.id !== itemId) };
}
return g;
})
};
}
return s;
}));
};
const updateSectionName = (sectionId, name) => {
setSections(sections.map(s => s.id === sectionId ? { ...s, name } : s));
};
const updateGroupName = (sectionId, groupId, name) => {
setSections(sections.map(s => {
if (s.id === sectionId) {
return { ...s, groups: s.groups.map(g => g.id === groupId ? { ...g, name } : g) };
}
return s;
}));
};
// Calculations
const calculateTotals = () => {
let internalCost = 0;
let clientRevenue = 0;
sections.forEach(s => {
s.groups.forEach(g => {
g.items.forEach(i => {
internalCost += (i.qty * i.unitCost);
clientRevenue += i.clientPrice;
});
});
});
const subtotal = clientRevenue;
const tax = subtotal * (financials.taxRate / 100);
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;
return { internalCost, clientRevenue, subtotal, tax, ohpAmount, grandTotal, netProfit, grossMarginPercent };
};
const totals = calculateTotals();
// Helper calculation for a particular group
const calcGroupTotals = (group) => {
let cost = 0;
let rev = 0;
group.items.forEach(i => {
cost += (i.qty * i.unitCost);
rev += i.clientPrice;
});
return { cost, rev };
};
return (
<div className="min-h-screen bg-zinc-50 dark:bg-[#09090b] text-zinc-900 dark:text-zinc-100 p-6 md:p-10 space-y-10 w-full max-w-[2400px] mx-auto">
{/* --- Page Header --- */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div>
<button
onClick={() => navigate(-1)}
className="flex items-center text-sm font-medium text-zinc-500 hover:text-amber-500 transition-colors mb-2"
>
<ArrowLeft className="w-4 h-4 mr-1" /> Back
</button>
<h1 className="text-3xl font-display font-bold tracking-tight text-zinc-900 dark:text-white">Estimate Builder</h1>
<p className="text-zinc-500 dark:text-zinc-400 text-sm mt-1">Create and configure project estimates for clients.</p>
</div>
<div className="flex items-center gap-4">
<button className="flex items-center px-4 py-2.5 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-white/10 rounded-xl hover:bg-zinc-100 dark:hover:bg-white/5 transition-all text-sm font-medium">
<Settings className="w-4 h-4 mr-2" /> Options
</button>
<button className="flex items-center px-4 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white rounded-xl shadow-lg shadow-blue-500/20 transition-all font-medium text-sm">
<Save className="w-4 h-4 mr-2" /> Save Estimate
</button>
</div>
</div>
<div className="grid grid-cols-1 2xl:grid-cols-12 gap-8">
{/* === MAIN CONTENT (Left Col - 8) === */}
<div className="2xl:col-span-8 space-y-8">
{/* --- Zone A: Client & Project Metadata --- */}
<section className="bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/10 rounded-2xl p-8 shadow-sm relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/5 blur-3xl rounded-full" />
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-blue-100 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded-xl">
<User className="w-5 h-5" />
</div>
<h2 className="text-xl font-semibold">Client Details</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 relative z-10">
<div className="space-y-2 md:col-span-2">
<label className="text-xs font-semibold text-zinc-500 uppercase tracking-wider ml-1">Client Name</label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-400" />
<input
name="name" value={clientInfo.name} onChange={handleClientChange}
className="w-full bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-white/10 rounded-xl pl-12 pr-4 py-3.5 text-base focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all dark:placeholder-zinc-500"
placeholder="E.g. John Doe"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-500 uppercase tracking-wider ml-1">Phone</label>
<div className="relative">
<Phone className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-400" />
<input
name="phone" value={clientInfo.phone} onChange={handleClientChange}
className={`w-full bg-zinc-50 dark:bg-zinc-800/50 border ${errors.phone ? 'border-red-500 focus:ring-red-500' : 'border-zinc-200 dark:border-white/10 focus:ring-blue-500'} rounded-xl pl-12 pr-4 py-3.5 text-base focus:ring-2 focus:border-transparent outline-none transition-all dark:placeholder-zinc-500`}
placeholder="(555) 123-4567"
/>
</div>
{errors.phone && <p className="text-sm text-red-500 ml-1">{errors.phone}</p>}
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-500 uppercase tracking-wider ml-1">Email Address</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-400" />
<input
name="email" value={clientInfo.email} onChange={handleClientChange}
className={`w-full bg-zinc-50 dark:bg-zinc-800/50 border ${errors.email ? 'border-red-500 focus:ring-red-500' : 'border-zinc-200 dark:border-white/10 focus:ring-blue-500'} rounded-xl pl-12 pr-4 py-3.5 text-base focus:ring-2 focus:border-transparent outline-none transition-all dark:placeholder-zinc-500`}
placeholder="client@example.com"
/>
</div>
{errors.email && <p className="text-sm text-red-500 ml-1">{errors.email}</p>}
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-500 uppercase tracking-wider ml-1">Property Address</label>
<div className="relative">
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-400" />
<input
name="address" value={clientInfo.address} onChange={handleClientChange}
className="w-full bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-white/10 rounded-xl pl-12 pr-4 py-3.5 text-base focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all dark:placeholder-zinc-500"
placeholder="123 Main St, City, State"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-zinc-500 uppercase tracking-wider ml-1">Zip / Pincode</label>
<div className="relative">
<input
name="zip" value={clientInfo.zip} onChange={handleClientChange}
className="w-full bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-white/10 rounded-xl px-4 py-3.5 text-base focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all dark:placeholder-zinc-500"
placeholder="75001"
/>
</div>
</div>
</div>
</section>
{/* --- Zone B: Scope of Work --- */}
<section className="bg-white dark:bg-zinc-900/50 border border-zinc-200 dark:border-white/10 rounded-2xl p-8 shadow-sm">
<div className="flex items-center gap-4 mb-6">
<div className="p-3 bg-indigo-100 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 rounded-xl">
<FileText className="w-5 h-5" />
</div>
<h2 className="text-xl font-semibold">Scope of Work</h2>
</div>
<textarea
value={scopeOfWork}
onChange={(e) => setScopeOfWork(e.target.value)}
placeholder="Describe the tasks, inclusions, and exclusions here... (Markup supported)"
className="w-full min-h-[200px] bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-white/10 rounded-xl p-6 text-base focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all resize-y custom-scrollbar font-sans leading-relaxed"
/>
</section>
{/* --- Zone C: Costing Engine --- */}
{isFullViewMode && (
<div className="fixed inset-0 z-40 bg-zinc-950/80 backdrop-blur-sm transition-opacity" onClick={() => setIsFullViewMode(false)} />
)}
<section className={`transition-[inset,padding,border-radius,background-color] duration-300 ease-out border border-zinc-200 dark:border-white/10 ${isFullViewMode ?
`fixed inset-4 md:inset-10 z-50 bg-white dark:bg-[#0a0a0c] rounded-3xl p-6 md:p-10 shadow-[0_0_100px_rgba(0,0,0,0.5)] overflow-hidden flex flex-col`
: `bg-white dark:bg-zinc-900/50 rounded-2xl p-8 shadow-sm relative overflow-hidden`
}`}>
{/* Blueprint background for construction feel */}
<div className="absolute inset-0 blueprint-fine opacity-30 dark:opacity-[0.05] pointer-events-none" />
<div className="flex items-center justify-between mb-8 relative z-10 shrink-0">
<div className="flex items-center gap-4">
<div className="p-3 bg-emerald-100 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 rounded-xl shadow-inner">
<Calculator className="w-5 h-5" />
</div>
<div className="flex flex-col">
<h2 className="text-xl md:text-2xl font-semibold text-zinc-900 dark:text-white">Costing Engine</h2>
{isFullViewMode && <span className="text-xs text-zinc-500 mt-0.5">Press <kbd className="px-1.5 py-0.5 bg-zinc-100 dark:bg-zinc-800 rounded font-mono border border-zinc-200 dark:border-zinc-700">Esc</kbd> to minimize</span>}
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setIsFullViewMode(!isFullViewMode)}
className="flex items-center px-3 md:px-4 py-2 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800/50 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 rounded-xl text-sm font-medium transition-colors"
>
{isFullViewMode ? (
<><Minimize className="w-5 h-5 md:mr-2" /> <span className="hidden md:inline">Minimize</span></>
) : (
<><Maximize className="w-5 h-5 md:mr-2" /> <span className="hidden md:inline">Full View</span></>
)}
</button>
<button
onClick={addSection}
className="flex items-center px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl text-sm font-medium transition-colors shadow-lg shadow-emerald-500/20"
>
<Plus className="w-5 h-5 mr-2" /> Add Section
</button>
</div>
</div>
<div className={`space-y-10 relative z-10 ${isFullViewMode ? 'overflow-y-auto custom-scrollbar flex-1 pr-2' : ''}`}>
{sections.map(section => (
<div key={section.id} className="border-l-2 border-zinc-200 dark:border-zinc-700 pl-4 py-2">
<div className="flex items-center gap-3 mb-4 group">
<input
value={section.name}
onChange={(e) => 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"
/>
<button onClick={() => setSections(sections.filter(s => s.id !== section.id))} className="opacity-0 group-hover:opacity-100 p-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-md transition-all">
<Trash2 className="w-4 h-4" />
</button>
</div>
<div className="space-y-4">
{section.groups.map(group => {
const groupTotals = calcGroupTotals(group);
return (
<div key={group.id} className="bg-zinc-50 dark:bg-zinc-800/40 border border-zinc-200 dark:border-white/5 rounded-xl overflow-hidden">
{/* Group Header */}
<div className="flex items-center justify-between px-4 py-3 bg-zinc-100/50 dark:bg-zinc-800/80 border-b border-zinc-200 dark:border-white/5 group/header">
<div className="flex items-center gap-2">
<input
value={group.name}
onChange={(e) => 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"
/>
</div>
<button onClick={() => setSections(sections.map(s => s.id === section.id ? { ...s, groups: s.groups.filter(g => g.id !== group.id) } : s))} className="p-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-md transition-all opacity-0 group-hover/header:opacity-100">
<Trash2 className="w-4 h-4" />
</button>
</div>
{/* Items Table - Responsive horizontal scroll container */}
<div className="w-full overflow-x-auto custom-scrollbar">
<table className="w-full text-base text-left whitespace-nowrap">
<thead className="text-sm text-zinc-500 dark:text-zinc-400 bg-white/50 dark:bg-zinc-900/50 border-b border-zinc-200 dark:border-white/5 uppercase font-semibold tracking-wider">
<tr>
<th className="px-6 py-4 w-12 text-center">Sr No.</th>
<th className="px-6 py-4 min-w-[300px]">Description</th>
<th className="px-6 py-4 w-28">Qty</th>
<th className="px-6 py-4 w-28">UOM</th>
<th className="px-6 py-4 w-36">Unit Cost ($)</th>
<th className="px-6 py-4 w-36 text-zinc-400">Total Cost</th>
<th className="px-6 py-4 w-44 font-bold text-zinc-900 dark:text-white">Client Price ($)</th>
<th className="px-6 py-4 w-12 text-center text-zinc-400">Act</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-200/50 dark:divide-white/5">
{group.items.map((item, index) => (
<tr key={item.id} className="hover:bg-zinc-100/30 dark:hover:bg-white/5 transition-colors group/row">
<td className="px-6 py-4 text-center font-mono text-zinc-400 dark:text-zinc-500">
{index + 1}
</td>
<td className="px-6 py-4">
<input
value={item.desc}
onChange={(e) => 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..."
/>
</td>
<td className="px-6 py-4">
<input
type="number"
value={item.qty || ''}
onChange={(e) => 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"
/>
</td>
<td className="px-6 py-4">
<div className="relative">
<select
value={item.uom}
onChange={(e) => updateItem(section.id, group.id, item.id, 'uom', e.target.value)}
className="w-full appearance-none bg-transparent border border-zinc-200 dark:border-zinc-700 focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500 rounded-lg pl-3 pr-8 py-2 outline-none font-mono text-zinc-800 dark:text-zinc-200 hover:border-zinc-300 dark:hover:border-zinc-600 transition-colors"
>
{UOM_OPTIONS.map(opt => <option key={opt} value={opt} className="bg-white dark:bg-zinc-900">{opt}</option>)}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-400 pointer-events-none" />
</div>
</td>
<td className="px-6 py-4">
<input
type="number"
value={item.unitCost || ''}
onChange={(e) => 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"
/>
</td>
<td className="px-6 py-4 font-mono text-zinc-500 dark:text-zinc-400">
${(item.qty * item.unitCost).toFixed(2)}
</td>
<td className="px-6 py-4">
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 font-mono">$</span>
<input
type="number"
value={item.clientPrice || ''}
onChange={(e) => 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"
/>
</div>
</td>
<td className="px-6 py-4 flex justify-center">
<button
onClick={() => removeItem(section.id, group.id, item.id)}
className="text-zinc-400 hover:text-red-500 p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors opacity-0 group-hover/row:opacity-100"
>
<Trash2 className="w-5 h-5" />
</button>
</td>
</tr>
))}
{/* Add Item Row Button */}
<tr>
<td colSpan="8" className="px-6 py-4">
<button
onClick={() => addItem(section.id, group.id)}
className="flex items-center text-sm font-medium text-emerald-600 dark:text-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-300 transition-colors py-2 px-3 hover:bg-emerald-50 dark:hover:bg-emerald-500/10 rounded-lg focus:outline-none"
>
<Plus className="w-5 h-5 mr-2" /> Add Line Item
</button>
</td>
</tr>
</tbody>
</table>
</div>
{/* Group Subtotal Footer */}
<div className="bg-white/80 dark:bg-zinc-900/80 px-4 py-2 flex justify-end gap-8 border-t border-zinc-200 dark:border-white/5 text-xs">
<div className="flex items-center gap-2">
<span className="text-zinc-500">Group Cost:</span>
<span className="font-mono font-medium text-zinc-700 dark:text-zinc-300">${groupTotals.cost.toFixed(2)}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-zinc-500">Group Revenue:</span>
<span className="font-mono font-bold text-emerald-600 dark:text-emerald-400">${groupTotals.rev.toFixed(2)}</span>
</div>
</div>
</div>
);
})}
<button
onClick={() => addGroup(section.id)}
className="flex items-center text-sm font-medium text-zinc-500 hover:text-zinc-800 dark:hover:text-white transition-colors"
>
<Plus className="w-4 h-4 mr-1" /> Add Group
</button>
</div>
</div>
))}
</div>
</section>
</div>
{/* === SIDEBAR CONTENT (Right Col - 4) === */}
<div className="2xl:col-span-4 space-y-8">
{/* --- Zone D: Financial Summary & Profit Analysis --- */}
<div className="sticky top-6">
<section className="bg-gradient-to-b from-zinc-800 to-zinc-950 dark:from-[#111115] dark:to-[#09090b] rounded-2xl shadow-xl overflow-hidden text-white border border-zinc-700 dark:border-white/10">
<div className="p-6 border-b border-white/10">
<h3 className="text-lg font-bold flex items-center gap-2 mb-4">
<DollarSign className="w-5 h-5 text-amber-500" />
Financial Summary
</h3>
<div className="space-y-4">
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-400">Subtotal (Client Price)</span>
<span className="font-mono font-medium">${totals.subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center text-sm group">
<span className="text-zinc-400 flex items-center gap-1">
Tax Rate
<span className="opacity-0 group-hover:opacity-100 transition-opacity text-xs bg-white/10 px-1.5 rounded text-zinc-300">%</span>
</span>
<div className="flex flex-col items-end">
<div className="relative w-20">
<input
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"
/>
</div>
<span className="font-mono text-xs text-zinc-500 mt-1">${totals.tax.toFixed(2)}</span>
</div>
</div>
<div className="flex justify-between items-center text-sm group">
<span className="text-zinc-400 flex items-center gap-1">
OH&P Modifier
<span className="opacity-0 group-hover:opacity-100 transition-opacity text-xs bg-white/10 px-1.5 rounded text-zinc-300">%</span>
</span>
<div className="flex flex-col items-end">
<div className="relative w-20">
<input
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"
/>
</div>
<span className="font-mono text-xs text-zinc-500 mt-1">${totals.ohpAmount.toFixed(2)}</span>
</div>
</div>
</div>
</div>
{/* Grand Total */}
<div className="p-6 bg-amber-500/10 border-b border-amber-500/20">
<div className="flex justify-between items-end">
<span className="text-sm font-semibold text-amber-400 uppercase tracking-widest">Grand Total</span>
<span className="text-3xl font-display font-bold text-white tracking-tight">
${totals.grandTotal.toFixed(2)}
</span>
</div>
</div>
{/* Profit Analysis Gauge */}
<div className="p-6">
<h4 className="text-sm font-medium text-zinc-300 mb-4 flex items-center gap-2">
<Calculator className="w-4 h-4 text-emerald-400" />
Profit Analysis
</h4>
<div className="space-y-4">
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-400">Net Profit</span>
<span className="font-mono font-medium text-emerald-400">+${totals.netProfit.toFixed(2)}</span>
</div>
<div className="flex justify-between items-center text-sm mb-1">
<span className="text-zinc-400">Gross Margin</span>
<span className="font-mono font-bold text-white bg-white/10 px-2 py-0.5 rounded">
{totals.grossMarginPercent.toFixed(1)}%
</span>
</div>
{/* Gauge Bar */}
<div className="h-3 w-full bg-white/5 rounded-full overflow-hidden relative">
<div
className={`h-full transition-all duration-500 ease-out ${totals.grossMarginPercent > 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 */}
<div className="absolute inset-0 flex justify-between px-1">
{[...Array(9)].map((_, i) => (
<div key={i} className="w-px h-full bg-black/20" />
))}
</div>
</div>
{/* Warning message if margin is low */}
{totals.grossMarginPercent < 20 && totals.grandTotal > 0 && (
<div className="flex items-start gap-2 text-xs text-amber-400/80 bg-amber-400/10 p-2.5 rounded-lg border border-amber-400/20">
<AlertCircle className="w-4 h-4 shrink-0 mt-0.5" />
<p>Margin is under 20%. Please review costs or markup strategy.</p>
</div>
)}
</div>
</div>
</section>
</div>
</div>
</div>
</div>
);
}