feat(projects): Estimate Versions tab (history + full document view)
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
|
import ChangeOrderDrawer from '../../components/owner/ChangeOrderDrawer';
|
||||||
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
|
import InvoiceDetailModal from '../../components/owner/InvoiceDetailModal';
|
||||||
|
import EstimateDetailModal from '../../components/estimates/EstimateDetailModal';
|
||||||
import CreateItemModal from '../../components/owner/CreateItemModal';
|
import CreateItemModal from '../../components/owner/CreateItemModal';
|
||||||
import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
|
import CommissionSettingsModal from '../../components/owner/CommissionSettingsModal';
|
||||||
import TeamManagementPanel from '../../components/owner/TeamManagementPanel';
|
import TeamManagementPanel from '../../components/owner/TeamManagementPanel';
|
||||||
@@ -73,6 +74,7 @@ const tabs = [
|
|||||||
{ id: 'rfis', label: 'RFIs', icon: FileText },
|
{ id: 'rfis', label: 'RFIs', icon: FileText },
|
||||||
{ id: 'milestones', label: 'Milestones', icon: Milestone },
|
{ id: 'milestones', label: 'Milestones', icon: Milestone },
|
||||||
{ id: 'invoices', label: 'Invoices', icon: DollarSign },
|
{ id: 'invoices', label: 'Invoices', icon: DollarSign },
|
||||||
|
{ id: 'estimates', label: 'Estimates', icon: FileText },
|
||||||
{ id: 'risks', label: 'Risk & Issues', icon: AlertCircle },
|
{ id: 'risks', label: 'Risk & Issues', icon: AlertCircle },
|
||||||
{ id: 'inspections', label: 'Inspections', icon: ShieldCheck },
|
{ id: 'inspections', label: 'Inspections', icon: ShieldCheck },
|
||||||
{ id: 'activity', label: 'Activity', icon: Clock },
|
{ id: 'activity', label: 'Activity', icon: Clock },
|
||||||
@@ -200,6 +202,7 @@ const OwnerProjectDetail = () => {
|
|||||||
const [activeTab, setActiveTab] = useState('overview');
|
const [activeTab, setActiveTab] = useState('overview');
|
||||||
const [selectedCO, setSelectedCO] = useState(null);
|
const [selectedCO, setSelectedCO] = useState(null);
|
||||||
const [selectedInvoice, setSelectedInvoice] = useState(null);
|
const [selectedInvoice, setSelectedInvoice] = useState(null);
|
||||||
|
const [selectedVersion, setSelectedVersion] = useState(null);
|
||||||
const [createModalConfig, setCreateModalConfig] = useState(null);
|
const [createModalConfig, setCreateModalConfig] = useState(null);
|
||||||
|
|
||||||
// ── Payments state ─────────────────────────────────────────────────────────
|
// ── Payments state ─────────────────────────────────────────────────────────
|
||||||
@@ -1651,6 +1654,83 @@ const OwnerProjectDetail = () => {
|
|||||||
</NeoCard>
|
</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 */}
|
{/* RISK & ISSUES TAB */}
|
||||||
{activeTab === 'risks' && (
|
{activeTab === 'risks' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -2220,6 +2300,23 @@ const OwnerProjectDetail = () => {
|
|||||||
onClose={() => setSelectedInvoice(null)}
|
onClose={() => setSelectedInvoice(null)}
|
||||||
invoice={selectedInvoice}
|
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 && (
|
{createModalConfig && (
|
||||||
<CreateItemModal
|
<CreateItemModal
|
||||||
isOpen={!!createModalConfig}
|
isOpen={!!createModalConfig}
|
||||||
|
|||||||
Reference in New Issue
Block a user