feat(projects): Estimate Versions tab (history + full document view)
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user