feat(projects): Estimate Versions tab (history + full document view)

This commit is contained in:
Satyam Rastogi
2026-05-29 17:48:23 +05:30
parent 35ff7a2e05
commit 6c5d9ea5e8
+97
View File
@@ -17,6 +17,7 @@ import {
} from 'lucide-react';
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
import EstimateDetailModal from '../../components/estimates/EstimateDetailModal';
import CreateItemModal from '../../components/owner/CreateItemModal';
import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
import TeamManagementPanel from '../../components/owner/TeamManagementPanel';
@@ -73,6 +74,7 @@ const tabs = [
{ id: 'rfis', label: 'RFIs', icon: FileText },
{ id: 'milestones', label: 'Milestones', icon: Milestone },
{ id: 'invoices', label: 'Invoices', icon: DollarSign },
{ id: 'estimates', label: 'Estimates', icon: FileText },
{ id: 'risks', label: 'Risk & Issues', icon: AlertCircle },
{ id: 'inspections', label: 'Inspections', icon: ShieldCheck },
{ id: 'activity', label: 'Activity', icon: Clock },
@@ -200,6 +202,7 @@ const OwnerProjectDetail = () => {
const [activeTab, setActiveTab] = useState('overview');
const [selectedCO, setSelectedCO] = useState(null);
const [selectedInvoice, setSelectedInvoice] = useState(null);
const [selectedVersion, setSelectedVersion] = useState(null);
const [createModalConfig, setCreateModalConfig] = useState(null);
// ── Payments state ─────────────────────────────────────────────────────────
@@ -1651,6 +1654,83 @@ const OwnerProjectDetail = () => {
</NeoCard>
)}
{/* ESTIMATES TAB */}
{activeTab === 'estimates' && (() => {
const buildEstimate = (v) => ({
id: `${project.id}-v${v.version}`,
clientName: project.customerName,
address: project.address,
date: v.date,
status: v.status,
total: v.total,
templateId: v.templateId,
templateName: v.templateName,
estimatedBy: (project.teamMembers || []).find(t => /Sales|Estimator/i.test(t.jobRole || ''))?.name || 'Sales Team',
roofArea: v.roofArea,
notes: v.note,
});
const versions = [...(project.estimateVersions || [])].sort((a, b) => a.version - b.version);
return (
<NeoCard spotlightColor="rgba(0, 240, 255, 0.1)" innerClassName="p-0 lg:p-0">
<div className="p-5 border-b border-zinc-200 dark:border-white/5 flex items-center gap-3">
<FileText className={NEON_BLUE} size={20} />
<h3 className="text-lg font-bold text-zinc-900 dark:text-white tracking-widest uppercase">Estimate Versions</h3>
<span className="text-xs font-bold bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/5 px-2 py-0.5 rounded-full text-zinc-500 dark:text-zinc-400">
{versions.length}
</span>
</div>
{versions.length > 0 ? (
<div className="divide-y divide-zinc-100 dark:divide-white/5">
{versions.map((v) => (
<div
key={v.version}
className="flex flex-col sm:flex-row sm:items-center gap-3 px-5 py-4 hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors cursor-pointer"
tabIndex="0"
role="button"
onClick={() => setSelectedVersion(v)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedVersion(v); } }}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<span className="shrink-0 w-8 h-8 rounded-full bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-xs font-black text-sky-600 dark:text-[#00f0ff]">
v{v.version}
</span>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="font-bold text-sm text-zinc-900 dark:text-white">Version {v.version}</span>
<span className="text-xs font-mono text-zinc-500 dark:text-zinc-400">{v.date}</span>
<span className="text-xs font-mono font-bold text-[#00f0ff]">{formatCurrency(v.total)}</span>
</div>
{v.note && (
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5 truncate">{v.note}</p>
)}
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest border ${
v.status === 'signed'
? 'text-emerald-600 dark:text-[#39ff14] bg-emerald-50 dark:bg-[#39ff14]/10 border-emerald-200 dark:border-[#39ff14]/20'
: v.status === 'revised'
? 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border-amber-200 dark:border-amber-500/20'
: 'text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 border-zinc-200 dark:border-white/10'
}`}>
{v.status === 'signed' ? 'Signed' : v.status === 'revised' ? 'Revised' : 'Sent'}
</span>
<ChevronRight size={14} className="text-zinc-400" />
</div>
</div>
))}
</div>
) : (
<div className="p-16 text-center text-zinc-500">
<FileText size={48} className="mx-auto mb-4 text-zinc-400 dark:text-zinc-600" />
<p className="font-bold text-lg text-zinc-900 dark:text-white">No estimate versions for this project.</p>
<p className="text-sm mt-2 text-zinc-400">Estimate history will appear here once versions are created.</p>
</div>
)}
</NeoCard>
);
})()}
{/* RISK & ISSUES TAB */}
{activeTab === 'risks' && (
<div className="space-y-6">
@@ -2220,6 +2300,23 @@ const OwnerProjectDetail = () => {
onClose={() => setSelectedInvoice(null)}
invoice={selectedInvoice}
/>
<EstimateDetailModal
isOpen={!!selectedVersion}
onClose={() => setSelectedVersion(null)}
estimate={selectedVersion ? {
id: `${project.id}-v${selectedVersion.version}`,
clientName: project.customerName,
address: project.address,
date: selectedVersion.date,
status: selectedVersion.status,
total: selectedVersion.total,
templateId: selectedVersion.templateId,
templateName: selectedVersion.templateName,
estimatedBy: (project.teamMembers || []).find(t => /Sales|Estimator/i.test(t.jobRole || ''))?.name || 'Sales Team',
roofArea: selectedVersion.roofArea,
notes: selectedVersion.note,
} : null}
/>
{createModalConfig && (
<CreateItemModal
isOpen={!!createModalConfig}