import React, { useState, useMemo, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; import { useAuth } from '../context/AuthContext'; import { useMockStore } from '../data/mockStore'; import { useHailHistory, hailSeverityColor, daysAgo } from '../hooks/useHailRecon'; import { SpotlightCard } from '../components/SpotlightCard'; import { AnimatedCounter } from '../components/AnimatedCounter'; import { toast } from 'sonner'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; import { ArrowLeft, Phone, Mail, MapPin, User, Briefcase, Shield, Clock, FileText, ChevronRight, ArrowUpRight, ArrowDownLeft, ArrowRight, MessageSquare, CheckCircle, ChevronDown, Activity, AlertTriangle, TrendingUp, Calendar, DollarSign, GitPullRequest, AlertCircle, Milestone, FolderOpen, Upload, Trash2, Eye, Pencil, X as XIcon, File, StickyNote, Download, Image as ImageIcon, CloudLightning } from 'lucide-react'; // ── Neomorphic constants (mirrors OwnerProjectDetail) ───────────────────────── const NEO_PANEL_CLASS = "dark:bg-zinc-900/60 dark:backdrop-blur-3xl border border-zinc-200 dark:border-white/5 dark:shadow-[8px_8px_16px_rgba(0,0,0,0.6),-8px_-8px_16px_rgba(255,255,255,0.02)] rounded-3xl relative overflow-hidden transition-all duration-300"; const NEON_GREEN = "text-green-600 dark:text-[#39ff14] dark:drop-shadow-[0_0_8px_rgba(57,255,20,0.4)]"; const NEON_GOLD = "text-amber-600 dark:text-[#fda913] dark:drop-shadow-[0_0_8px_rgba(253,169,19,0.4)]"; const NEON_ORANGE = "text-orange-600 dark:text-[#ff4500] dark:drop-shadow-[0_0_8px_rgba(255,69,0,0.4)]"; const NEON_BLUE = "text-sky-600 dark:text-[#00f0ff] dark:drop-shadow-[0_0_8px_rgba(0,240,255,0.4)]"; function formatCurrency(n) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n || 0); } function formatDate(d) { if (!d) return '—'; return new Date(d + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } function getInitials(name) { return (name || '').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); } function getHealthColor(s) { return s >= 70 ? 'text-emerald-500' : s >= 40 ? 'text-amber-500' : 'text-red-500'; } function getVarianceColor(v) { if (!v) return 'text-zinc-400'; return v < 0 ? NEON_GREEN : NEON_ORANGE; } function getMilestoneColor(status) { if (status === 'completed') return 'bg-emerald-500'; if (status === 'in_progress') return 'bg-blue-500'; return 'bg-zinc-700'; } // ── NeoCard ─────────────────────────────────────────────────────────────────── function NeoCard({ children, className = '', spotlightColor = 'rgba(255,255,255,0.05)' }) { return (
{children}
); } // ── Animated progress bar ───────────────────────────────────────────────────── function NeoProgress({ pct, color = '#3B82F6', height = 'h-1.5' }) { return (
); } // ── Stage mover dropdown ────────────────────────────────────────────────────── function StageMoverDropdown({ lead, columns, onStageChange }) { const [open, setOpen] = useState(false); const stageColumns = columns.filter(c => c.type === 'stage').sort((a, b) => a.order - b.order); const bucketColumns = columns.filter(c => c.type === 'bucket'); return (
{open && (

Stages

{stageColumns.map(col => ( ))}

Buckets

{bucketColumns.map(col => ( ))} {lead.bucketId && ( <>
)}
)}
); } // ── Activity entry (stage movement) ────────────────────────────────────────── function StageActivityItem({ entry, isLast }) { const isFirst = !entry.from; const isBucket = entry.to === 'Stuck' || entry.to === 'Follow-up'; const isRegress = entry.activity_type === 'regression'; const dotColor = isFirst ? '#3B82F6' : isBucket ? '#F59E0B' : isRegress ? '#F97316' : '#10B981'; const bgClass = isFirst ? 'bg-blue-500/15' : isBucket ? 'bg-amber-500/15' : isRegress ? 'bg-orange-500/15' : 'bg-emerald-500/15'; return (
{isFirst ? : isBucket ? : isRegress ? : }
{!isLast &&
}
{entry.from ? (

{entry.from} {entry.to}

) : (

Lead Created

)}

by {entry.by} · {formatDate(entry.date)}

{entry.note &&

{entry.note}

}
); } // ── Docs helpers ────────────────────────────────────────────────────────────── const DOC_TYPE_CONFIG = { pdf: { color: '#ef4444', bg: 'rgba(239,68,68,0.15)', icon: FileText, label: 'PDF' }, docx: { color: '#3b82f6', bg: 'rgba(59,130,246,0.15)', icon: FileText, label: 'DOCX' }, doc: { color: '#3b82f6', bg: 'rgba(59,130,246,0.15)', icon: FileText, label: 'DOC' }, md: { color: '#a855f7', bg: 'rgba(168,85,247,0.15)', icon: File, label: 'MD' }, txt: { color: '#6b7280', bg: 'rgba(107,114,128,0.15)',icon: File, label: 'TXT' }, jpg: { color: '#10b981', bg: 'rgba(16,185,129,0.15)', icon: ImageIcon,label: 'JPG' }, jpeg: { color: '#10b981', bg: 'rgba(16,185,129,0.15)', icon: ImageIcon,label: 'JPG' }, png: { color: '#06b6d4', bg: 'rgba(6,182,212,0.15)', icon: ImageIcon,label: 'PNG' }, webp: { color: '#06b6d4', bg: 'rgba(6,182,212,0.15)', icon: ImageIcon,label: 'IMG' }, }; function getDocType(filename) { const ext = (filename || '').split('.').pop().toLowerCase(); return DOC_TYPE_CONFIG[ext] || { color: '#9ca3af', bg: 'rgba(156,163,175,0.15)', icon: File, label: ext.toUpperCase() || 'FILE' }; } function seedLeadDocs(leadId) { const base = [ { id: 'ld1', name: 'Insurance Claim Form.pdf', uploadedBy: 'Owner', uploadedDate: '2026-02-01', size: '245 KB', notes: 'Original claim. Reference for all supplements.' }, { id: 'ld2', name: 'Signed Contract.pdf', uploadedBy: 'Frank Agent',uploadedDate: '2026-02-05', size: '512 KB', notes: 'Fully executed. See payment schedule p.4.' }, { id: 'ld3', name: 'Inspection Report.pdf', uploadedBy: 'Frank Agent',uploadedDate: '2026-01-28', size: '1.1 MB', notes: 'Hail damage confirmed. Photos on p.7-12.' }, { id: 'ld4', name: 'Before Photo - Front.jpg', uploadedBy: 'Frank Agent',uploadedDate: '2026-02-06', size: '2.8 MB', notes: '' }, { id: 'ld5', name: 'Material Spec Sheet.md', uploadedBy: 'Owner', uploadedDate: '2026-02-07', size: '11 KB', notes: 'GAF Timberline HDZ — Class 4 IR.' }, { id: 'ld6', name: 'Adjuster Notes.docx', uploadedBy: 'Owner', uploadedDate: '2026-02-10', size: '42 KB', notes: '' }, ]; const n = 3 + (leadId.charCodeAt(leadId.length - 1) % 3); return base.slice(0, n); } // ── Tabs ────────────────────────────────────────────────────────────────────── const TABS_FULL = [ { id: 'overview', label: 'Overview', icon: Activity }, { id: 'budget', label: 'Budget & Costs', icon: DollarSign }, { id: 'milestones', label: 'Milestones', icon: Milestone }, { id: 'changeOrders', label: 'Change Orders', icon: GitPullRequest }, { id: 'rfis', label: 'RFIs', icon: FileText }, { id: 'invoices', label: 'Invoices', icon: DollarSign }, { id: 'docs', label: 'Docs', icon: FolderOpen }, { id: 'activity', label: 'Activity', icon: Clock }, ]; const TABS_SIMPLE = [ { id: 'overview', label: 'Overview', icon: Activity }, { id: 'contact', label: 'Contact', icon: User }, { id: 'docs', label: 'Docs', icon: FolderOpen}, { id: 'activity', label: 'Activity', icon: Clock }, ]; // ── Main component ──────────────────────────────────────────────────────────── // ── Hail History Section ────────────────────────────────────────────────────── function HailHistorySection({ leadId, lat, lng }) { const { history, loading, hitCount, lastHit, maxSize } = useHailHistory(leadId, lat, lng, 12); const summaryColor = maxSize >= 2.0 ? 'text-red-600 dark:text-red-400' : maxSize >= 1.5 ? 'text-orange-600 dark:text-orange-400' : maxSize >= 1.0 ? 'text-yellow-600 dark:text-yellow-400' : 'text-zinc-400'; return (
{/* Header */}

Hail Impact History

Last 12 months · Powered by Hail Recon

{!loading && hitCount > 0 && (

{hitCount}x

{lastHit ? `Last: ${daysAgo(lastHit.date)}d ago` : ''}

)}
{/* Body */}
{loading ? (

Checking hail history...

) : hitCount === 0 ? (

No hail impacts recorded in the last 12 months.

) : (
{history.map((event, i) => { const textCls = hailSeverityColor(event.hailSize, 'text'); const bgCls = hailSeverityColor(event.hailSize, 'bg'); const brdCls = hailSeverityColor(event.hailSize, 'border'); const days = daysAgo(event.date); const dateObj = new Date(event.date); const dateLabel = dateObj.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); return (
{/* Date */}

{dateLabel}

{days === 0 ? 'Today' : `${days}d ago`}

{/* Size badge */} {event.hailSize?.toFixed(2)}" {/* Severity label */} {event.hailSize >= 2.0 ? 'Severe' : event.hailSize >= 1.5 ? 'Significant' : event.hailSize >= 1.0 ? 'Moderate' : 'Trace'} {i === 0 && ( Most Recent )}
); })}
)}
); } export default function LeadProjectPage() { const { leadId } = useParams(); const navigate = useNavigate(); const { user } = useAuth(); const { kanbanLeads, kanbanColumns, kanbanProjectData, moveKanbanLead } = useMockStore(); const basePath = user?.role === 'OWNER' ? '/owner' : user?.role === 'ADMIN' ? '/admin' : '/emp/fa'; const lead = kanbanLeads.find(l => l.id === leadId); const proj = kanbanProjectData?.[leadId] ?? null; const tabs = proj ? TABS_FULL : TABS_SIMPLE; const [activeTab, setActiveTab] = useState('overview'); // ── Docs state ──────────────────────────────────────────────────────────── const [docs, setDocs] = useState(() => seedLeadDocs(leadId)); const [viewDoc, setViewDoc] = useState(null); const [editNoteDoc, setEditNoteDoc] = useState(null); const [deleteDocId, setDeleteDocId] = useState(null); const [uploadOpen, setUploadOpen] = useState(false); const [uploadForm, setUploadForm] = useState({ name: '', notes: '' }); const fileInputRef = useRef(null); const handleDocUpload = () => { if (!uploadForm.name.trim()) return; setDocs(prev => [{ id: `ld_${Date.now()}`, name: uploadForm.name.trim(), uploadedBy: user?.name || 'Owner', uploadedDate: '2026-04-08', size: '—', notes: uploadForm.notes.trim(), }, ...prev]); setUploadOpen(false); setUploadForm({ name: '', notes: '' }); }; const handleFileSelect = (e) => { const f = e.target.files?.[0]; if (f) setUploadForm(p => ({ ...p, name: f.name })); }; const handleSaveNote = () => { setDocs(prev => prev.map(d => d.id === editNoteDoc.doc.id ? { ...d, notes: editNoteDoc.noteText } : d)); setEditNoteDoc(null); }; const handleDeleteDoc = () => { setDocs(prev => prev.filter(d => d.id !== deleteDocId)); setDeleteDocId(null); }; const handleStageChange = (newColumnId, newBucketId) => { if (!lead) return; if (newColumnId === lead.columnId && newBucketId === lead.bucketId) return; moveKanbanLead(lead.id, newColumnId, newBucketId); toast.success('Stage updated'); }; if (!lead) { return (

Lead not found

); } const stageColumns = [...kanbanColumns].filter(c => c.type === 'stage').sort((a, b) => a.order - b.order); const currentStage = stageColumns.find(c => c.id === lead.columnId); const bucketCol = lead.bucketId ? kanbanColumns.find(c => c.id === lead.bucketId) : null; const stageIdx = stageColumns.findIndex(c => c.id === lead.columnId); const stagePct = stageColumns.length > 0 ? Math.round(((stageIdx + 1) / stageColumns.length) * 100) : 0; const completionPct = proj?.completionPct ?? stagePct; // Budget chart const budgetChartData = (proj?.budgetBreakdown || []).map(b => ({ name: b.category.length > 12 ? b.category.slice(0, 10) + '…' : b.category, allocated: b.allocated / 1000, committed: b.committed / 1000, actual: b.actual / 1000, })); const CustomTooltip = ({ active, payload, label }) => { if (active && payload?.length) return (

{label}

{payload.map((p, i) => (

{p.name}: {formatCurrency(p.value * 1000)}

))}
); return null; }; const invoiceTotalPaid = (proj?.invoices || []).filter(i => i.status === 'paid').reduce((s, i) => s + i.amount, 0); const invoiceTotalPending = (proj?.invoices || []).filter(i => i.status !== 'paid').reduce((s, i) => s + i.amount, 0); return (
{/* Ambient glow */}
{/* ── Header ── */}

{lead.name}

{lead.address}

{currentStage && ( {currentStage.name} )} {bucketCol && ( {bucketCol.name} )}
{/* ── Key Metrics Row ── */}
{proj ? [ { label: 'Estimated', value: formatCurrency(proj.estimatedAmount) }, { label: 'Actual Cost', value: formatCurrency(proj.actualCost) }, { label: 'Variance', value: `${(proj.variancePercent || 0) > 0 ? '+' : ''}${(proj.variancePercent || 0).toFixed(1)}%`, color: getVarianceColor(proj.variancePercent) }, { label: 'Completion', value: `${proj.completionPct}%` }, { label: 'Health', value: proj.healthScore, color: getHealthColor(proj.healthScore) }, { label: 'Stage', value: currentStage?.name || '—' }, ].map((m, i) => (
{m.value}
{m.label}
)) : [ { label: 'Stage', value: currentStage?.name || '—' }, { label: 'Job Type', value: lead.jobType }, { label: 'Category', value: lead.insuranceType }, { label: 'Assigned To', value: lead.assignedAgentName || 'Unassigned' }, { label: 'Created', value: formatDate(lead.createdDate) }, { label: 'Stage Moves', value: lead.activity?.length ?? 0 }, ].map((m, i) => (
{m.value}
{m.label}
))}
{/* ── Tabs ── */}
{tabs.map(tab => { const TabIcon = tab.icon; const isActive = activeTab === tab.id; return ( ); })}
{/* ── Tab Content ── */}
{/* ── OVERVIEW ── */} {activeTab === 'overview' && proj && (
{/* Project Progression */}

Project Progression

{['Signed', 'Materials', 'On Site', 'Install', 'Complete'].map((step, i) => { const isActive = (i * 25) <= completionPct; return (
{isActive ? : }
{step}
); })}
{/* Row: Contractor + Summary */}

Contact & Contractor

{[ { icon: User, label: 'Client', value: lead.name }, { icon: Phone, label: 'Phone', value: lead.phone }, { icon: Mail, label: 'Email', value: lead.email }, { icon: MapPin, label: 'Address', value: lead.address }, { icon: Briefcase, label: 'Contractor', value: proj.contractorName }, { icon: Phone, label: 'Crew Phone', value: proj.contractorPhone }, { icon: Shield, label: 'Assigned Agent', value: lead.assignedAgentName }, ].map(({ icon: Icon, label, value }) => value ? (

{label}

{value}

) : null)}

Open Items

{[ { label: 'Change Orders', count: proj.changeOrders?.length || 0, color: '#fda913' }, { label: 'Open RFIs', count: (proj.rfis || []).filter(r => r.status === 'open').length, color: '#ff4500' }, { label: 'Pending Invoices', count: (proj.invoices || []).filter(i => i.status === 'pending').length, color: '#00f0ff' }, { label: 'Milestones Done', count: (proj.milestones || []).filter(m => m.status === 'completed').length + '/' + (proj.milestones?.length || 0), color: '#39ff14' }, ].map(item => (

{item.count}

{item.label}

))}
{lead.notes && (

Field Notes

{lead.notes}

)}
)} {/* Overview for simple (no project data) */} {activeTab === 'overview' && !proj && (

Contact

{[ { icon: User, label: 'Name', value: lead.name }, { icon: Phone, label: 'Phone', value: lead.phone }, { icon: Mail, label: 'Email', value: lead.email }, { icon: MapPin, label: 'Address', value: lead.address }, { icon: Briefcase, label: 'Job Type', value: lead.jobType }, { icon: Shield, label: 'Category', value: lead.insuranceType }, { icon: User, label: 'Assigned Agent', value: lead.assignedAgentName }, ].map(({ icon: Icon, label, value }) => value ? (

{label}

{value}

) : null)}

Pipeline Position

{stageColumns.map((col, i) => ( {col.name} {i < stageColumns.length - 1 && } ))}
{lead.notes && (

Notes

{lead.notes}

)}

Full project details (budget, milestones, invoices) become available once the lead is Signed.

)} {/* ── CONTACT (simple only) ── */} {activeTab === 'contact' && !proj && (

Contact Information

{[ { icon: User, label: 'Full Name', value: lead.name }, { icon: Phone, label: 'Phone', value: lead.phone }, { icon: Mail, label: 'Email', value: lead.email }, { icon: MapPin, label: 'Address', value: lead.address }, ].map(({ icon: Icon, label, value }) => value ? (

{label}

{value}

) : null)}

Quick Actions

{lead.phone && ( Call {lead.phone} )} {lead.email && ( Email {lead.email} )} {lead.address && ( View on Maps )}
)} {/* ── BUDGET & COSTS ── */} {activeTab === 'budget' && proj && (
{/* Summary cards */}
{[ { label: 'Estimated', value: formatCurrency(proj.estimatedAmount), color: NEON_BLUE }, { label: 'Committed', value: formatCurrency(proj.budgetBreakdown?.reduce((s, b) => s + b.committed, 0)), color: NEON_GOLD }, { label: 'Actual Spent', value: formatCurrency(proj.actualCost), color: proj.actualCost > proj.estimatedAmount ? NEON_ORANGE : NEON_GREEN }, { label: 'Variance', value: `${(proj.variancePercent || 0) > 0 ? '+' : ''}${(proj.variancePercent || 0).toFixed(1)}%`, color: getVarianceColor(proj.variancePercent) }, ].map((m, i) => (
{m.value}
{m.label}
))}
{/* Chart */} {budgetChartData.length > 0 && (

Budget Breakdown

`$${v}k`} /> } />
{[['#3b82f6','Allocated'],['#fda913','Committed'],['#39ff14','Actual']].map(([c, l]) => (
{l}
))}
)} {/* Table */}

Line Item Breakdown

{(proj.budgetBreakdown || []).map((b, i) => { const rem = b.allocated - b.actual; return ( ); })}
Category Allocated Committed Actual Remaining
{b.category} {formatCurrency(b.allocated)} {formatCurrency(b.committed)} {formatCurrency(b.actual)} = 0 ? 'text-emerald-400' : 'text-red-400'}`}>{formatCurrency(rem)}
Total {formatCurrency(proj.estimatedAmount)} {formatCurrency(proj.budgetBreakdown?.reduce((s, b) => s + b.committed, 0))} {formatCurrency(proj.actualCost)} = 0 ? 'text-emerald-400' : 'text-red-400'}`}> {formatCurrency(proj.estimatedAmount - proj.actualCost)}
)} {/* ── MILESTONES ── */} {activeTab === 'milestones' && proj && (

Project Milestones

{(proj.milestones || []).filter(m => m.status === 'completed').length} / {proj.milestones?.length || 0} complete
{(proj.milestones || []).map((m, i) => { const dotColor = m.status === 'completed' ? '#39ff14' : m.status === 'in_progress' ? '#00f0ff' : '#3f3f46'; return (

{m.name}

{m.status.replace('_', ' ')}
{m.date &&

{formatDate(m.date)}

} {m.notes &&

{m.notes}

}
); })}
)} {/* ── CHANGE ORDERS ── */} {activeTab === 'changeOrders' && proj && (

Change Orders

{proj.changeOrders?.length || 0}
{(proj.changeOrders || []).length === 0 ? (

No change orders

) : (
{proj.changeOrders.map(co => (

{co.title}

{co.status} +{formatCurrency(co.amount)}

{co.description}

Requested by {co.requestedBy} · {formatDate(co.date)}

))}
)}
)} {/* ── RFIs ── */} {activeTab === 'rfis' && proj && (

Requests for Information

{proj.rfis?.length || 0}
{(proj.rfis || []).length === 0 ? (

No RFIs

) : (
{proj.rfis.map((r, i) => ( ))}
ID Subject Status Submitted By Opened Closed
{r.id.toUpperCase()} {r.subject} {r.status} {r.submittedBy} {r.openedDate} {r.closedDate || '—'}
)}
)} {/* ── INVOICES ── */} {activeTab === 'invoices' && proj && (
{[ { label: 'Total Contract', value: formatCurrency(proj.estimatedAmount), color: NEON_BLUE }, { label: 'Collected', value: formatCurrency(invoiceTotalPaid), color: NEON_GREEN }, { label: 'Outstanding', value: formatCurrency(invoiceTotalPending), color: invoiceTotalPending > 0 ? NEON_GOLD : 'text-zinc-400' }, ].map((m, i) => (
{m.value}
{m.label}
))}

Invoice Schedule

{(proj.invoices || []).map(inv => ( ))}
Description Amount Status Due Paid
{inv.description} {formatCurrency(inv.amount)} {inv.status} {inv.dueDate || '—'} {inv.paidDate || '—'}
)} {/* ── DOCS (shared across simple + full) ── */} {activeTab === 'docs' && (

Project Documents

{docs.length}
{docs.length === 0 ? (

No documents yet

) : (
{docs.map(doc => { const cfg = getDocType(doc.name); const DocIcon = cfg.icon; return (

{doc.name}

{cfg.label}

{doc.size} · {doc.uploadedBy} · {doc.uploadedDate}

{doc.notes ? (

{doc.notes}

) : ( )}
); })}
)}
)} {/* ── ACTIVITY ── */} {activeTab === 'activity' && (
{/* Work timeline (project-level) */} {proj && (proj.workTimeline || []).length > 0 && (

Work Timeline

{proj.workTimeline.map((a, i) => (
{a.action}
{a.details &&
{a.details}
}
{a.date}
{a.user &&
{a.user}
}
))}
)} {/* Stage movement log */}

Stage Movement Log

{lead.activity?.length ?? 0} entries
{(lead.activity || []).length > 0 ? ( (lead.activity || []).map((entry, i) => ( )) ) : (

No stage movements recorded.

)}
)}
{/* ── Upload Modal ── */} {uploadOpen && (
e.stopPropagation()}>

Upload Document

Supports PDF, DOCX, MD, TXT, JPG, PNG, WEBP

fileInputRef.current?.click()} className="border-2 border-dashed border-white/10 hover:border-blue-500/50 rounded-2xl p-8 text-center cursor-pointer transition-colors mb-4 bg-white/[0.02] hover:bg-blue-500/5">

Click to select a file

{uploadForm.name &&

{uploadForm.name}

}
{!uploadForm.name && ( setUploadForm(p => ({ ...p, name: e.target.value }))} className="w-full px-4 py-2.5 rounded-xl bg-black/40 border border-white/10 text-zinc-200 text-sm placeholder-zinc-600 outline-none focus:ring-2 focus:ring-blue-500/30 mb-4" /> )}