diff --git a/src/components/PageTransition.jsx b/src/components/PageTransition.jsx index a39804e..5abf58e 100644 --- a/src/components/PageTransition.jsx +++ b/src/components/PageTransition.jsx @@ -13,7 +13,7 @@ const PageTransition = ({ children }) => { return (
{children}
diff --git a/src/components/kanban/KanbanCard.jsx b/src/components/kanban/KanbanCard.jsx index d3ed3e4..3276c1a 100644 --- a/src/components/kanban/KanbanCard.jsx +++ b/src/components/kanban/KanbanCard.jsx @@ -29,33 +29,23 @@ function avatarColor(name) { return AVATAR_COLORS[idx]; } -export default function KanbanCard({ lead, columnColor, onClick, isDragOverlay }) { - const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ - id: lead.id, - data: { lead }, - }); - - const style = { - transform: CSS.Translate.toString(transform), - opacity: isDragging ? 0.35 : 1, - zIndex: isDragging ? 999 : 'auto', - }; - +// ── Pure visual display — no dnd bindings. Used both inside KanbanCard and +// inside DragOverlay (so the dragged clone renders above overflow containers). +export function KanbanCardDisplay({ lead, columnColor, isOverlay = false, onClick, forwardRef, dragProps = {} }) { const days = getDaysInStage(lead.activity); const initials = getInitials(lead.name); const avatarCls = avatarColor(lead.name); return (
{/* Left color accent bar */}
); } + +// ── Draggable wrapper — ghost stays frozen in place (no transform applied), +// DragOverlay handles all visual movement above overflow containers. +export default function KanbanCard({ lead, columnColor, onClick }) { + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ + id: lead.id, + data: { lead, columnColor }, + }); + + return ( +
+ +
+ ); +} diff --git a/src/components/kanban/KanbanColumn.jsx b/src/components/kanban/KanbanColumn.jsx index 97374a3..7d6f28c 100644 --- a/src/components/kanban/KanbanColumn.jsx +++ b/src/components/kanban/KanbanColumn.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { useDroppable } from '@dnd-kit/core'; import { motion, AnimatePresence } from 'framer-motion'; -import { Plus, Settings, Trash2, Pencil } from 'lucide-react'; +import { Trash2, Pencil } from 'lucide-react'; import KanbanCard from './KanbanCard'; export default function KanbanColumn({ @@ -16,9 +16,9 @@ export default function KanbanColumn({ const isBucket = column.type === 'bucket'; return ( -
+
{/* Column header */} -
+
@@ -72,16 +72,17 @@ export default function KanbanColumn({ )}
- {/* Drop zone */} + {/* Drop zone — flex-1 + min-h-0 so it fills remaining column height */}
-
+ {/* Inner scroll container — cards scroll within the column */} +
{leads.map((lead, i) => (
- {/* Add column button after last stage column */} - {isLast && canManage && ( - - )}
); } diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 25a23e7..0382e58 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -4319,6 +4319,528 @@ const KANBAN_LEADS_INITIAL = [ }, ]; +// ── Enriched project data for signed/in-progress/complete leads ────────────── +// Keyed by lead ID. Only present for leads that have become active jobs. +const KANBAN_PROJECT_DATA = { + // ── SIGNED LEADS ────────────────────────────────────────────────────────── + kl_023: { + estimatedAmount: 18200, actualCost: 0, variancePercent: 0, completionPct: 25, healthScore: 88, + contractorName: 'Plano Roofing Crew A', contractorPhone: '(972) 555-9001', + budgetBreakdown: [ + { category: 'Materials', allocated: 9800, committed: 9800, actual: 0 }, + { category: 'Labor', allocated: 6200, committed: 6200, actual: 0 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 0 }, + { category: 'Contingency',allocated: 1600, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-03-22', notes: 'Deposit received.' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-03-24', notes: 'GAF Timberline HDZ — Charcoal ordered.' }, + { id: 'm3', name: 'Permits Pulled', status: 'in_progress',date: '2026-04-05', notes: 'City permit submitted, pending approval.' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'pending', date: null, notes: '' }, + { id: 'm5', name: 'Roofing Install', status: 'pending', date: null, notes: '' }, + { id: 'm6', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 30%', amount: 5460, status: 'paid', dueDate: '2026-03-22', paidDate: '2026-03-22' }, + { id: 'inv2', description: 'Progress — 40%', amount: 7280, status: 'pending', dueDate: '2026-04-14', paidDate: null }, + { id: 'inv3', description: 'Final — 30%', amount: 5460, status: 'pending', dueDate: '2026-04-15', paidDate: null }, + ], + workTimeline: [ + { date: '2026-03-22', action: 'Contract Executed', user: 'Felicity Fast', details: 'Client signed. 30% deposit collected.' }, + { date: '2026-03-24', action: 'Materials Ordered', user: 'Adam Admin', details: 'GAF Timberline HDZ Charcoal — 24 squares ordered.' }, + { date: '2026-04-05', action: 'Permit Submitted', user: 'Adam Admin', details: 'Plano city permit application filed.' }, + ], + }, + kl_024: { + estimatedAmount: 31500, actualCost: 0, variancePercent: 0, completionPct: 20, healthScore: 85, + contractorName: 'Plano Siding Crew', contractorPhone: '(972) 555-9002', + budgetBreakdown: [ + { category: 'Materials', allocated: 17000, committed: 17000, actual: 0 }, + { category: 'Labor', allocated: 11500, committed: 11500, actual: 0 }, + { category: 'Permits', allocated: 800, committed: 800, actual: 0 }, + { category: 'Contingency',allocated: 2200, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-03-20', notes: 'Insurance approved full scope.' }, + { id: 'm2', name: 'Materials Ordered',status: 'completed', date: '2026-03-23', notes: 'James Hardie Board & Batten ordered.' }, + { id: 'm3', name: 'Permit Pulled', status: 'pending', date: null, notes: '' }, + { id: 'm4', name: 'Prep & Demo', status: 'pending', date: null, notes: '' }, + { id: 'm5', name: 'Siding Install', status: 'pending', date: null, notes: '' }, + { id: 'm6', name: 'Trim & Caulk', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm8', name: 'Invoice & Payment',status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [ + { id: 'rfi1', subject: 'HOA Color Approval', status: 'open', submittedBy: 'Fiona Field', openedDate: '2026-03-25', closedDate: null, description: 'Need written HOA approval for Cobblestone color before install.' }, + ], + invoices: [ + { id: 'inv1', description: 'Deposit — 25%', amount: 7875, status: 'paid', dueDate: '2026-03-20', paidDate: '2026-03-20' }, + { id: 'inv2', description: 'Progress — 50%', amount: 15750,status: 'pending', dueDate: '2026-04-25', paidDate: null }, + { id: 'inv3', description: 'Final — 25%', amount: 7875, status: 'pending', dueDate: '2026-05-01', paidDate: null }, + ], + workTimeline: [ + { date: '2026-03-20', action: 'Contract Executed', user: 'Fiona Field', details: 'Insurance-approved scope. Full 2,400 sq ft siding.' }, + { date: '2026-03-23', action: 'Materials Ordered', user: 'Adam Admin', details: 'James Hardie Board & Batten — Cobblestone, 2,400 sq ft.' }, + { date: '2026-03-25', action: 'RFI Opened', user: 'Fiona Field', details: 'HOA color approval required before start.' }, + ], + }, + kl_025: { + estimatedAmount: 27800, actualCost: 0, variancePercent: 0, completionPct: 28, healthScore: 92, + contractorName: 'Plano Roofing Crew B', contractorPhone: '(972) 555-9003', + budgetBreakdown: [ + { category: 'Materials', allocated: 15200, committed: 15200, actual: 0 }, + { category: 'Labor', allocated: 9800, committed: 9800, actual: 0 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 0 }, + { category: 'Contingency',allocated: 2200, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-03-11', notes: '50% deposit received.' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-03-12', notes: 'Owens Corning TruDefinition — Driftwood.' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-03-20', notes: 'Permit approved.' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'pending', date: null, notes: '' }, + { id: 'm5', name: 'Roofing Install', status: 'pending', date: null, notes: '' }, + { id: 'm6', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 50%', amount: 13900, status: 'paid', dueDate: '2026-03-11', paidDate: '2026-03-11' }, + { id: 'inv2', description: 'Final — 50%', amount: 13900, status: 'pending', dueDate: '2026-04-20', paidDate: null }, + ], + workTimeline: [ + { date: '2026-03-11', action: 'Contract Executed', user: 'Frank Agent', details: 'Cash client. 50% deposit paid same day.' }, + { date: '2026-03-12', action: 'Materials Ordered', user: 'Adam Admin', details: 'Owens Corning TruDefinition Duration — 28 squares.' }, + { date: '2026-03-20', action: 'Permit Approved', user: 'Adam Admin', details: 'City of Plano residential roofing permit #RFP-2026-1148.' }, + ], + }, + kl_026: { + estimatedAmount: 22400, actualCost: 0, variancePercent: 0, completionPct: 22, healthScore: 86, + contractorName: 'Plano Roofing Crew A', contractorPhone: '(972) 555-9001', + budgetBreakdown: [ + { category: 'Roofing Materials', allocated: 10800, committed: 10800, actual: 0 }, + { category: 'Gutter Materials', allocated: 3400, committed: 3400, actual: 0 }, + { category: 'Labor', allocated: 6400, committed: 6400, actual: 0 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 0 }, + { category: 'Contingency', allocated: 1200, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-03-25', notes: 'Install Apr 18.' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-03-27', notes: 'Roofing + gutters ordered.' }, + { id: 'm3', name: 'Permits Pulled', status: 'pending', date: null, notes: '' }, + { id: 'm4', name: 'Roofing Install', status: 'pending', date: null, notes: '' }, + { id: 'm5', name: 'Gutter Install', status: 'pending', date: null, notes: '' }, + { id: 'm6', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 30%', amount: 6720, status: 'paid', dueDate: '2026-03-25', paidDate: '2026-03-25' }, + { id: 'inv2', description: 'Progress — 40%', amount: 8960, status: 'pending', dueDate: '2026-04-18', paidDate: null }, + { id: 'inv3', description: 'Final — 30%', amount: 6720, status: 'pending', dueDate: '2026-04-22', paidDate: null }, + ], + workTimeline: [ + { date: '2026-03-25', action: 'Contract Executed', user: 'Felix Fixer', details: 'Insurance approved full roof + gutter scope.' }, + { date: '2026-03-27', action: 'Materials Ordered', user: 'Adam Admin', details: 'GAF shingles + K-style 6" gutters ordered.' }, + ], + }, + kl_027: { + estimatedAmount: 19600, actualCost: 0, variancePercent: 0, completionPct: 24, healthScore: 80, + contractorName: 'Plano Roofing Crew C', contractorPhone: '(972) 555-9004', + budgetBreakdown: [ + { category: 'Materials', allocated: 10500, committed: 10500, actual: 0 }, + { category: 'Labor', allocated: 7000, committed: 7000, actual: 0 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 0 }, + { category: 'Contingency',allocated: 1500, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-03-28', notes: 'Supplement approved after adjuster re-inspection.' }, + { id: 'm2', name: 'Materials Ordered', status: 'in_progress',date: '2026-04-02', notes: 'Waiting on special color availability.' }, + { id: 'm3', name: 'Permits Pulled', status: 'pending', date: null, notes: '' }, + { id: 'm4', name: 'Tear-off & Decking',status: 'pending', date: null, notes: '' }, + { id: 'm5', name: 'Roofing Install', status: 'pending', date: null, notes: '' }, + { id: 'm6', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [ + { id: 'co1', title: 'Supplemental — Ice & Water Barrier', amount: 1800, status: 'approved', requestedBy: 'Felicity Fast', date: '2026-03-10', description: 'Insurance adjuster agreed to add ice/water shield on all valleys and eaves.' }, + ], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 30%', amount: 5880, status: 'paid', dueDate: '2026-03-28', paidDate: '2026-03-28' }, + { id: 'inv2', description: 'Progress — 40%', amount: 7840, status: 'pending', dueDate: '2026-04-25', paidDate: null }, + { id: 'inv3', description: 'Final — 30%', amount: 5880, status: 'pending', dueDate: '2026-04-28', paidDate: null }, + ], + workTimeline: [ + { date: '2026-03-28', action: 'Contract Executed', user: 'Felicity Fast', details: 'Signed after 6-week negotiation on supplement items.' }, + { date: '2026-04-02', action: 'Materials Ordered', user: 'Adam Admin', details: 'Special color availability causing 1-week delay.' }, + ], + }, + + // ── IN PROGRESS LEADS ───────────────────────────────────────────────────── + kl_028: { + estimatedAmount: 16800, actualCost: 9200, variancePercent: -3.6, completionPct: 65, healthScore: 82, + contractorName: 'Plano Roofing Crew A', contractorPhone: '(972) 555-9001', + budgetBreakdown: [ + { category: 'Materials', allocated: 9000, committed: 9000, actual: 8600 }, + { category: 'Labor', allocated: 5800, committed: 5800, actual: 600 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 600 }, + { category: 'Contingency',allocated: 1400, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-02-28', notes: '' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-03-02', notes: '' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-03-10', notes: '' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'completed', date: '2026-03-18', notes: 'Tear-off complete. Two rotted deck boards replaced.' }, + { id: 'm5', name: 'Roofing Install', status: 'in_progress',date: '2026-04-07', notes: 'New decking being installed today.' }, + { id: 'm6', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [ + { id: 'co1', title: 'Decking Replacement — 2 Sheets', amount: 480, status: 'approved', requestedBy: 'Frank Agent', date: '2026-03-18', description: 'Two rotted OSB sheets discovered during tear-off. Replaced at cost.' }, + ], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 30%', amount: 5040, status: 'paid', dueDate: '2026-02-28', paidDate: '2026-02-28' }, + { id: 'inv2', description: 'Progress — 40%', amount: 6720, status: 'paid', dueDate: '2026-03-20', paidDate: '2026-03-20' }, + { id: 'inv3', description: 'Final — 30%', amount: 5040, status: 'pending', dueDate: '2026-04-10', paidDate: null }, + ], + workTimeline: [ + { date: '2026-02-28', action: 'Contract Executed', user: 'Frank Agent', details: 'Signed. Production team notified.' }, + { date: '2026-03-18', action: 'Production Started', user: 'Adam Admin', details: 'Tear-off crew on site. Two rotted deck boards found and replaced.' }, + { date: '2026-03-20', action: 'Progress Invoice Paid', user: 'Adam Admin', details: 'Client paid progress invoice on time.' }, + { date: '2026-04-07', action: 'Decking Install', user: 'Adam Admin', details: 'New decking going on today. Roofing shingles start tomorrow.' }, + ], + }, + kl_029: { + estimatedAmount: 28500, actualCost: 14800, variancePercent: 1.4, completionPct: 55, healthScore: 78, + contractorName: 'Plano Siding Crew', contractorPhone: '(972) 555-9002', + budgetBreakdown: [ + { category: 'Materials', allocated: 15400, committed: 15400, actual: 14200 }, + { category: 'Labor', allocated: 10400, committed: 10400, actual: 600 }, + { category: 'Permits', allocated: 800, committed: 800, actual: 800 }, + { category: 'Contingency', allocated: 1900, committed: 400, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-02-25', notes: '' }, + { id: 'm2', name: 'Materials Ordered',status: 'completed', date: '2026-02-27', notes: '' }, + { id: 'm3', name: 'Permit Pulled', status: 'completed', date: '2026-03-05', notes: '' }, + { id: 'm4', name: 'Prep & Demo', status: 'completed', date: '2026-03-25', notes: 'Old siding removed. Minor rot on 2 panels fixed.' }, + { id: 'm5', name: 'Siding Install', status: 'in_progress',date: '2026-04-05', notes: 'Day 3 of install. On track.' }, + { id: 'm6', name: 'Trim & Caulk', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm8', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [ + { id: 'co1', title: 'Rot Repair — 2 Panels', amount: 380, status: 'approved', requestedBy: 'Fiona Field', date: '2026-03-25', description: 'Two panels of rotted OSB substrate discovered during demo. Repaired.' }, + ], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 25%', amount: 7125, status: 'paid', dueDate: '2026-02-25', paidDate: '2026-02-25' }, + { id: 'inv2', description: 'Progress — 50%', amount: 14250,status: 'pending', dueDate: '2026-04-08', paidDate: null }, + { id: 'inv3', description: 'Final — 25%', amount: 7125, status: 'pending', dueDate: '2026-04-15', paidDate: null }, + ], + workTimeline: [ + { date: '2026-02-25', action: 'Contract Executed', user: 'Fiona Field', details: 'Insurance approved full scope.' }, + { date: '2026-03-25', action: 'Production Started', user: 'Adam Admin', details: 'Demo complete. Minor rot on 2 panels — change order raised.' }, + { date: '2026-04-05', action: 'Install Underway', user: 'Adam Admin', details: 'Siding install Day 3. On track for Friday completion.' }, + ], + }, + kl_030: { + estimatedAmount: 21200, actualCost: 16100, variancePercent: -4.2, completionPct: 78, healthScore: 88, + contractorName: 'Plano Roofing Crew B', contractorPhone: '(972) 555-9003', + budgetBreakdown: [ + { category: 'Roofing Materials', allocated: 10200, committed: 10200, actual: 9800 }, + { category: 'Gutter Materials', allocated: 3200, committed: 3200, actual: 3100 }, + { category: 'Labor', allocated: 6000, committed: 6000, actual: 3200 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 600 }, + { category: 'Contingency', allocated: 1200, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-02-24', notes: '' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-02-26', notes: '' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-03-05', notes: '' }, + { id: 'm4', name: 'Tear-off & Decking',status: 'completed', date: '2026-03-20', notes: 'Clean tear-off. No deck issues.' }, + { id: 'm5', name: 'Roofing Install', status: 'completed', date: '2026-03-22', notes: 'Roofing complete.' }, + { id: 'm6', name: 'Gutter Install', status: 'in_progress',date: '2026-04-07', notes: 'Gutter crew arriving today.' }, + { id: 'm7', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm8', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 30%', amount: 6360, status: 'paid', dueDate: '2026-02-24', paidDate: '2026-02-24' }, + { id: 'inv2', description: 'Progress — 40%', amount: 8480, status: 'paid', dueDate: '2026-03-22', paidDate: '2026-03-22' }, + { id: 'inv3', description: 'Final — 30%', amount: 6360, status: 'pending', dueDate: '2026-04-10', paidDate: null }, + ], + workTimeline: [ + { date: '2026-02-24', action: 'Contract Executed', user: 'Felicity Fast', details: '' }, + { date: '2026-03-20', action: 'Roofing Started', user: 'Amanda Manager', details: 'Production underway — roofing crew Day 1.' }, + { date: '2026-03-22', action: 'Roofing Complete', user: 'Amanda Manager', details: 'Roof done. Progress invoice sent.' }, + { date: '2026-04-07', action: 'Gutter Install', user: 'Amanda Manager', details: 'Gutter crew arriving today for 1-day install.' }, + ], + }, + kl_031: { + estimatedAmount: 34500, actualCost: 22000, variancePercent: 2.9, completionPct: 60, healthScore: 90, + contractorName: 'Plano Roofing Crew C', contractorPhone: '(972) 555-9004', + budgetBreakdown: [ + { category: 'Premium Materials', allocated: 19200, committed: 19200, actual: 18800 }, + { category: 'Labor', allocated: 12000, committed: 12000, actual: 3200 }, + { category: 'Permits', allocated: 700, committed: 700, actual: 700 }, + { category: 'Contingency', allocated: 2600, committed: 1000, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-03-03', notes: 'Cash client. Barkwood color selected.' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-03-05', notes: 'Special order — GAF Timberline HDZ Barkwood. 3-week lead time.' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-03-12', notes: '' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'completed', date: '2026-03-28', notes: 'Premium deck prep complete. No issues.' }, + { id: 'm5', name: 'Roofing Install', status: 'in_progress',date: '2026-03-28', notes: 'Premium HDZ install underway.' }, + { id: 'm6', name: 'Ridge & Detail', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm8', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [ + { id: 'rfi1', subject: 'Lifetime Warranty Registration', status: 'open', submittedBy: 'Felix Fixer', openedDate: '2026-03-05', closedDate: null, description: 'GAF factory warranty registration requires installer certification — need to confirm with crew.' }, + ], + invoices: [ + { id: 'inv1', description: 'Deposit — 100%', amount: 34500, status: 'paid', dueDate: '2026-03-03', paidDate: '2026-03-03' }, + ], + workTimeline: [ + { date: '2026-03-03', action: 'Contract Executed', user: 'Felix Fixer', details: 'Cash client — full payment upfront.' }, + { date: '2026-03-28', action: 'Production Started', user: 'Amanda Manager',details: 'Materials arrived. Premium install started.' }, + ], + }, + kl_032: { + estimatedAmount: 9400, actualCost: 6200, variancePercent: -1.1, completionPct: 70, healthScore: 84, + contractorName: 'Plano Siding Crew', contractorPhone: '(972) 555-9002', + budgetBreakdown: [ + { category: 'Materials', allocated: 4800, committed: 4800, actual: 4600 }, + { category: 'Labor', allocated: 3600, committed: 3600, actual: 1600 }, + { category: 'Permits', allocated: 500, committed: 500, actual: 500 }, + { category: 'Contingency',allocated: 500, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-03-07', notes: '' }, + { id: 'm2', name: 'Materials Ordered',status: 'completed', date: '2026-03-09', notes: '' }, + { id: 'm3', name: 'Permit Pulled', status: 'completed', date: '2026-03-20', notes: '' }, + { id: 'm4', name: 'South Face', status: 'completed', date: '2026-04-01', notes: 'South face complete.' }, + { id: 'm5', name: 'West Face', status: 'in_progress',date: '2026-04-06', notes: '2 days remaining.' }, + { id: 'm6', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Invoice & Payment',status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 40%', amount: 3760, status: 'paid', dueDate: '2026-03-07', paidDate: '2026-03-07' }, + { id: 'inv2', description: 'Progress — 30%', amount: 2820, status: 'paid', dueDate: '2026-04-01', paidDate: '2026-04-02' }, + { id: 'inv3', description: 'Final — 30%', amount: 2820, status: 'pending', dueDate: '2026-04-10', paidDate: null }, + ], + workTimeline: [ + { date: '2026-03-07', action: 'Contract Executed', user: 'Frank Agent', details: 'Partial repair — south and west faces only.' }, + { date: '2026-04-01', action: 'South Face Done', user: 'Adam Admin', details: 'South face siding complete. Starting west face.' }, + ], + }, + kl_033: { + estimatedAmount: 42000, actualCost: 18000, variancePercent: 0.8, completionPct: 45, healthScore: 74, + contractorName: 'Plano Roofing Crew A', contractorPhone: '(972) 555-9001', + budgetBreakdown: [ + { category: 'Materials', allocated: 22000, committed: 22000, actual: 17200 }, + { category: 'Labor', allocated: 15500, committed: 15500, actual: 800 }, + { category: 'Permits', allocated: 900, committed: 900, actual: 900 }, + { category: 'Contingency', allocated: 3600, committed: 2000, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-02-15', notes: 'Largest job this quarter.' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-02-18', notes: '3 pallets — 36 squares.' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-03-01', notes: '' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'in_progress',date: '2026-04-05', notes: 'Day 1 of 3. Two crews deployed.' }, + { id: 'm5', name: 'Roofing Install', status: 'pending', date: null, notes: '' }, + { id: 'm6', name: 'Final Inspection', status: 'pending', date: null, notes: '' }, + { id: 'm7', name: 'Invoice & Payment', status: 'pending', date: null, notes: '' }, + ], + changeOrders: [], + rfis: [ + { id: 'rfi1', subject: 'Structural Load — Chimney Flashing', status: 'open', submittedBy: 'Fiona Field', openedDate: '2026-04-05', closedDate: null, description: 'Large chimney requires custom step flashing — need architect sign-off for load specs.' }, + ], + invoices: [ + { id: 'inv1', description: 'Deposit — 25%', amount: 10500, status: 'paid', dueDate: '2026-02-15', paidDate: '2026-02-15' }, + { id: 'inv2', description: 'Progress — 50%', amount: 21000, status: 'pending', dueDate: '2026-04-08', paidDate: null }, + { id: 'inv3', description: 'Final — 25%', amount: 10500, status: 'pending', dueDate: '2026-04-15', paidDate: null }, + ], + workTimeline: [ + { date: '2026-02-15', action: 'Contract Executed', user: 'Fiona Field', details: 'Largest job this quarter — $42,000, 3,600 sq ft.' }, + { date: '2026-04-05', action: 'Production Started', user: 'Adam Admin', details: 'Day 1 of 3-day job. Two crews on site.' }, + { date: '2026-04-05', action: 'RFI Opened', user: 'Fiona Field', details: 'Custom chimney flashing — architect sign-off required.' }, + ], + }, + + // ── COMPLETE LEADS ──────────────────────────────────────────────────────── + kl_034: { + estimatedAmount: 17200, actualCost: 16950, variancePercent: -1.5, completionPct: 100, healthScore: 95, + contractorName: 'Plano Roofing Crew B', contractorPhone: '(972) 555-9003', + budgetBreakdown: [ + { category: 'Materials', allocated: 9200, committed: 9200, actual: 8980 }, + { category: 'Labor', allocated: 6000, committed: 6000, actual: 6000 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 600 }, + { category: 'Contingency', allocated: 1400, committed: 400, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-02-01', notes: '' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-02-03', notes: '' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-02-12', notes: '' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'completed', date: '2026-02-20', notes: 'Clean tear-off.' }, + { id: 'm5', name: 'Roofing Install', status: 'completed', date: '2026-02-21', notes: 'Install complete in 1 day.' }, + { id: 'm6', name: 'Final Inspection', status: 'completed', date: '2026-02-28', notes: 'Passed. Client gave 5-star review.' }, + { id: 'm7', name: 'Invoice & Payment', status: 'completed', date: '2026-03-01', notes: 'Full payment received.' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 30%', amount: 5160, status: 'paid', dueDate: '2026-02-01', paidDate: '2026-02-01' }, + { id: 'inv2', description: 'Progress — 40%', amount: 6880, status: 'paid', dueDate: '2026-02-21', paidDate: '2026-02-22' }, + { id: 'inv3', description: 'Final — 30%', amount: 5160, status: 'paid', dueDate: '2026-03-01', paidDate: '2026-03-01' }, + ], + workTimeline: [ + { date: '2026-02-01', action: 'Contract Executed', user: 'Frank Agent', details: '' }, + { date: '2026-02-20', action: 'Production Started', user: 'Adam Admin', details: 'Tear-off crew on site.' }, + { date: '2026-02-21', action: 'Install Complete', user: 'Adam Admin', details: 'Full install done in 1 day.' }, + { date: '2026-02-28', action: 'Final Inspection', user: 'Adam Admin', details: 'Passed. Client extremely happy. 5-star review.' }, + { date: '2026-03-01', action: 'Project Closed', user: 'Adam Admin', details: 'Final payment received. File closed.' }, + ], + }, + kl_035: { + estimatedAmount: 26400, actualCost: 26100, variancePercent: -1.1, completionPct: 100, healthScore: 93, + contractorName: 'Plano Siding Crew', contractorPhone: '(972) 555-9002', + budgetBreakdown: [ + { category: 'Materials', allocated: 14200, committed: 14200, actual: 13900 }, + { category: 'Labor', allocated: 9600, committed: 9600, actual: 9600 }, + { category: 'Permits', allocated: 800, committed: 800, actual: 800 }, + { category: 'Contingency', allocated: 1800, committed: 200, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-01-28', notes: '' }, + { id: 'm2', name: 'Materials Ordered',status: 'completed', date: '2026-01-30', notes: '' }, + { id: 'm3', name: 'Permit Pulled', status: 'completed', date: '2026-02-05', notes: '' }, + { id: 'm4', name: 'Prep & Demo', status: 'completed', date: '2026-02-15', notes: 'Old siding removed cleanly.' }, + { id: 'm5', name: 'Siding Install', status: 'completed', date: '2026-02-19', notes: '' }, + { id: 'm6', name: 'Trim & Caulk', status: 'completed', date: '2026-02-20', notes: '' }, + { id: 'm7', name: 'Final Inspection', status: 'completed', date: '2026-02-21', notes: 'Insurance proceeds released.' }, + { id: 'm8', name: 'Invoice & Payment',status: 'completed', date: '2026-02-22', notes: 'Full payment received.' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 25%', amount: 6600, status: 'paid', dueDate: '2026-01-28', paidDate: '2026-01-28' }, + { id: 'inv2', description: 'Progress — 50%', amount: 13200,status: 'paid', dueDate: '2026-02-19', paidDate: '2026-02-19' }, + { id: 'inv3', description: 'Final — 25%', amount: 6600, status: 'paid', dueDate: '2026-02-22', paidDate: '2026-02-22' }, + ], + workTimeline: [ + { date: '2026-01-28', action: 'Contract Executed', user: 'Felicity Fast', details: '' }, + { date: '2026-02-15', action: 'Production Started', user: 'Amanda Manager', details: 'Demo and install started same day.' }, + { date: '2026-02-22', action: 'Project Closed', user: 'Amanda Manager', details: 'Insurance check received and deposited. File closed.' }, + ], + }, + kl_036: { + estimatedAmount: 22100, actualCost: 21800, variancePercent: -1.4, completionPct: 100, healthScore: 97, + contractorName: 'Plano Roofing Crew C', contractorPhone: '(972) 555-9004', + budgetBreakdown: [ + { category: 'Materials', allocated: 12000, committed: 12000, actual: 11800 }, + { category: 'Labor', allocated: 7800, committed: 7800, actual: 7800 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 600 }, + { category: 'Contingency', allocated: 1700, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-01-30', notes: 'Full payment upfront.' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-01-31', notes: '' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-02-07', notes: '' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'completed', date: '2026-02-18', notes: 'Clean. No deck issues.' }, + { id: 'm5', name: 'Roofing Install', status: 'completed', date: '2026-02-19', notes: '2-day install complete.' }, + { id: 'm6', name: 'Final Inspection', status: 'completed', date: '2026-02-20', notes: 'Outstanding quality noted.' }, + { id: 'm7', name: 'Invoice & Payment', status: 'completed', date: '2026-01-30', notes: 'Pre-paid.' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Full Payment — 100%', amount: 22100, status: 'paid', dueDate: '2026-01-30', paidDate: '2026-01-30' }, + ], + workTimeline: [ + { date: '2026-01-30', action: 'Contract Executed', user: 'Fiona Field', details: 'Cash client. Full payment upfront.' }, + { date: '2026-02-18', action: 'Production Started', user: 'Adam Admin', details: 'Tear-off started.' }, + { date: '2026-02-20', action: 'Project Closed', user: 'Adam Admin', details: 'Outstanding quality. Referral bonus owed per agreement.' }, + ], + }, + kl_037: { + estimatedAmount: 8900, actualCost: 8650, variancePercent: -2.8, completionPct: 100, healthScore: 96, + contractorName: 'Plano Roofing Crew A', contractorPhone: '(972) 555-9001', + budgetBreakdown: [ + { category: 'Gutter Materials', allocated: 3800, committed: 3800, actual: 3600 }, + { category: 'Fascia Materials', allocated: 1800, committed: 1800, actual: 1800 }, + { category: 'Labor', allocated: 2600, committed: 2600, actual: 2600 }, + { category: 'Permits', allocated: 400, committed: 400, actual: 400 }, + { category: 'Contingency', allocated: 300, committed: 0, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-02-10', notes: '' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-02-12', notes: '' }, + { id: 'm3', name: 'Fascia Repair', status: 'completed', date: '2026-02-28', notes: 'Fascia replaced on all 4 sides.' }, + { id: 'm4', name: 'Gutter Install', status: 'completed', date: '2026-03-01', notes: '1-day install.' }, + { id: 'm5', name: 'Final Walkthrough', status: 'completed', date: '2026-03-02', notes: 'Customer gave 3 referral cards.' }, + { id: 'm6', name: 'Invoice & Payment', status: 'completed', date: '2026-03-02', notes: '' }, + ], + changeOrders: [], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 50%', amount: 4450, status: 'paid', dueDate: '2026-02-10', paidDate: '2026-02-10' }, + { id: 'inv2', description: 'Final — 50%', amount: 4450, status: 'paid', dueDate: '2026-03-02', paidDate: '2026-03-02' }, + ], + workTimeline: [ + { date: '2026-02-10', action: 'Contract Executed', user: 'Felix Fixer', details: '' }, + { date: '2026-02-28', action: 'Production Started', user: 'Adam Admin', details: 'Fascia repair started.' }, + { date: '2026-03-02', action: 'Project Closed', user: 'Adam Admin', details: 'Complete. Customer gave referral cards to 3 neighbors.' }, + ], + }, + kl_038: { + estimatedAmount: 19400, actualCost: 20100, variancePercent: 3.6, completionPct: 100, healthScore: 87, + contractorName: 'Plano Roofing Crew B', contractorPhone: '(972) 555-9003', + budgetBreakdown: [ + { category: 'Materials', allocated: 10400, committed: 10400, actual: 11200 }, + { category: 'Labor', allocated: 6800, committed: 6800, actual: 6800 }, + { category: 'Permits', allocated: 600, committed: 600, actual: 600 }, + { category: 'Contingency', allocated: 1600, committed: 1600, actual: 0 }, + ], + milestones: [ + { id: 'm1', name: 'Contract Signed', status: 'completed', date: '2026-02-04', notes: '' }, + { id: 'm2', name: 'Materials Ordered', status: 'completed', date: '2026-02-06', notes: '' }, + { id: 'm3', name: 'Permits Pulled', status: 'completed', date: '2026-02-15', notes: '' }, + { id: 'm4', name: 'Tear-off & Decking', status: 'completed', date: '2026-03-10', notes: '3 rotted deck sheets replaced — CO raised.' }, + { id: 'm5', name: 'Roofing Install', status: 'completed', date: '2026-03-12', notes: '' }, + { id: 'm6', name: 'Final Inspection', status: 'completed', date: '2026-03-18', notes: '' }, + { id: 'm7', name: 'Invoice & Payment', status: 'completed', date: '2026-03-20', notes: 'Complex insurance supplemental took 2 extra weeks.' }, + ], + changeOrders: [ + { id: 'co1', title: 'Decking Replacement — 3 Sheets', amount: 720, status: 'approved', requestedBy: 'Felicity Fast', date: '2026-03-10', description: 'Three rotted OSB sheets discovered during tear-off. Replaced.' }, + ], + rfis: [], + invoices: [ + { id: 'inv1', description: 'Deposit — 30%', amount: 5820, status: 'paid', dueDate: '2026-02-04', paidDate: '2026-02-04' }, + { id: 'inv2', description: 'Progress — 40%', amount: 7760, status: 'paid', dueDate: '2026-03-12', paidDate: '2026-03-14' }, + { id: 'inv3', description: 'Final — 30%', amount: 5820, status: 'paid', dueDate: '2026-03-20', paidDate: '2026-03-20' }, + ], + workTimeline: [ + { date: '2026-02-04', action: 'Contract Executed', user: 'Felicity Fast', details: '' }, + { date: '2026-03-10', action: 'Production Started', user: 'Adam Admin', details: 'Tear-off found 3 rotted deck sheets. CO raised.' }, + { date: '2026-03-12', action: 'Install Complete', user: 'Adam Admin', details: 'Roofing complete.' }, + { date: '2026-03-20', action: 'Project Closed', user: 'Adam Admin', details: 'Longest close — 7 weeks due to complex insurance supplemental.' }, + ], + }, +}; + // --- CONTEXT SETUP --- const MockStoreContext = createContext(); @@ -4501,6 +5023,7 @@ export const MockStoreProvider = ({ children }) => { updateKanbanLead: (id, data) => { setKanbanLeads(prev => prev.map(l => l.id === id ? { ...l, ...data } : l)); }, + kanbanProjectData: KANBAN_PROJECT_DATA, // Estimates estimates, diff --git a/src/pages/KanbanPage.jsx b/src/pages/KanbanPage.jsx index ec6a012..48e59c6 100644 --- a/src/pages/KanbanPage.jsx +++ b/src/pages/KanbanPage.jsx @@ -1,16 +1,16 @@ import React, { useState, useMemo, useRef, useCallback } from 'react'; import { DndContext, DragOverlay, PointerSensor, TouchSensor, - useSensor, useSensors, closestCorners + useSensor, useSensors, pointerWithin, rectIntersection } from '@dnd-kit/core'; import { motion, AnimatePresence } from 'framer-motion'; import { useTheme } from '../context/ThemeContext'; import { useAuth, ROLES } from '../context/AuthContext'; import { useMockStore } from '../data/mockStore'; import { toast } from 'sonner'; -import { LayoutGrid, Search, Filter, X, Send, Pencil } from 'lucide-react'; +import { LayoutGrid, Search, Filter, X, Send, Pencil, Plus } from 'lucide-react'; -import KanbanCard from '../components/kanban/KanbanCard'; +import KanbanCard, { KanbanCardDisplay } from '../components/kanban/KanbanCard'; import KanbanColumn from '../components/kanban/KanbanColumn'; import LeadInfoDrawer from '../components/kanban/LeadInfoDrawer'; import ColumnManagerModal from '../components/kanban/ColumnManagerModal'; @@ -20,45 +20,43 @@ function fireCampaignToast(lead, column, onEdit) { if (!column?.campaignMessage) return; const msg = column.campaignMessage.replace('{name}', lead.name.split(' ')[0]); - toast( - ({ id }) => { - const [secs, setSecs] = React.useState(20); - React.useEffect(() => { - const t = setInterval(() => setSecs(s => { - if (s <= 1) { clearInterval(t); toast.dismiss(id); return 0; } - return s - 1; - }), 1000); - return () => clearInterval(t); - }, []); - return ( -
-
-
- -

Campaign Message

-
- Sending in {secs}s -
-

{msg}

-
- - + // Sonner v2: toast.custom(fn) where fn receives the toast ID directly (string) + toast.custom((toastId) => { + const [secs, setSecs] = React.useState(20); + React.useEffect(() => { + const t = setInterval(() => setSecs(s => { + if (s <= 1) { clearInterval(t); toast.dismiss(toastId); return 0; } + return s - 1; + }), 1000); + return () => clearInterval(t); + }, []); + return ( +
+
+
+ +

Campaign Message

+ Sending in {secs}s
- ); - }, - { duration: 20000, position: 'bottom-right' } - ); +

{msg}

+
+ + +
+
+ ); + }, { duration: 20000, position: 'bottom-right' }); } // Campaign Edit Modal @@ -110,7 +108,6 @@ export default function KanbanPage() { const canManage = user?.role === ROLES.OWNER || user?.role === ROLES.ADMIN; const [search, setSearch] = useState(''); - const [draggingLead, setDraggingLead] = useState(null); const [selectedLead, setSelectedLead] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); @@ -120,6 +117,9 @@ export default function KanbanPage() { // Campaign edit modal const [campaignEdit, setCampaignEdit] = useState(null); // { message, lead, column } + // Active drag — tracked so DragOverlay can render the floating clone + const [draggingLead, setDraggingLead] = useState(null); // { lead, columnColor } + const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 8 } }) @@ -156,7 +156,9 @@ export default function KanbanPage() { const handleDragStart = ({ active }) => { const lead = kanbanLeads.find(l => l.id === active.id); - setDraggingLead(lead ?? null); + if (!lead) return; + const col = kanbanColumns.find(c => c.id === lead.columnId); + setDraggingLead({ lead, columnColor: col?.color ?? '#6366f1' }); }; const handleDragEnd = ({ active, over }) => { @@ -217,11 +219,17 @@ export default function KanbanPage() { }; return ( -
+ pointerWithin(args).length > 0 ? pointerWithin(args) : rectIntersection(args)} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + > +
{/* Page header */} -
-
-
+
+
+
@@ -230,12 +238,22 @@ export default function KanbanPage() { style={{ fontFamily: 'Barlow Condensed, sans-serif' }}> Pipeline -

{kanbanLeads.length} leads across {stageColumns.length} stages

+

{kanbanLeads.length} leads · {stageColumns.length} stages

+ {/* Add Stage — header shortcut for owners/admins */} + {canManage && ( + + )} + {/* Search */} -
+
- {/* Board */} -
- -
+ {/* Board — fills remaining height, horizontal scroll only */} +
+
{/* Stage columns */} {stageColumns.map((col, i) => ( -
+
+
{/* Bucket columns */} @@ -297,19 +309,7 @@ export default function KanbanPage() { isLast={false} /> ))} -
- - {/* Drag overlay */} - - {draggingLead ? ( - c.id === draggingLead.columnId)?.color ?? '#3B82F6'} - isDragOverlay - /> - ) : null} - - +
{/* Lead info drawer */} @@ -343,5 +343,18 @@ export default function KanbanPage() { )}
+ + {/* DragOverlay — dnd-kit already portals this to document.body internally, + so position:fixed is always viewport-relative regardless of ancestors. */} + + {draggingLead ? ( + + ) : null} + + ); } diff --git a/src/pages/LeadProjectPage.jsx b/src/pages/LeadProjectPage.jsx index e0c7f98..68fec7b 100644 --- a/src/pages/LeadProjectPage.jsx +++ b/src/pages/LeadProjectPage.jsx @@ -1,214 +1,247 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { motion } from 'framer-motion'; -import { useTheme } from '../context/ThemeContext'; -import { useAuth, ROLES } from '../context/AuthContext'; +import { useAuth } from '../context/AuthContext'; import { useMockStore } from '../data/mockStore'; +import { SpotlightCard } from '../components/SpotlightCard'; +import { AnimatedCounter } from '../components/AnimatedCounter'; import { toast } from 'sonner'; import { - ArrowLeft, Phone, Mail, MapPin, User, Briefcase, - Shield, Clock, FileText, ChevronRight, ArrowUpRight, - ArrowDownLeft, ArrowRight, MessageSquare, CheckCircle, - LayoutGrid, ChevronDown + 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 } from 'lucide-react'; +// ── Neomorphic constants (mirrors OwnerProjectDetail) ───────────────────────── +const NEO_PANEL_CLASS = "bg-zinc-900/60 backdrop-blur-3xl border border-white/5 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-[#39ff14] drop-shadow-[0_0_8px_rgba(57,255,20,0.4)]"; +const NEON_GOLD = "text-[#fda913] drop-shadow-[0_0_8px_rgba(253,169,19,0.4)]"; +const NEON_ORANGE = "text-[#ff4500] drop-shadow-[0_0_8px_rgba(255,69,0,0.4)]"; +const NEON_BLUE = "text-[#00f0ff] 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 ''; + 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'; +} -function InfoRow({ icon: Icon, label, value }) { - if (!value) return null; +// ── NeoCard ─────────────────────────────────────────────────────────────────── +function NeoCard({ children, className = '', spotlightColor = 'rgba(255,255,255,0.05)' }) { return ( -
- -
-

{label}

-

{value}

+ +
+ {children}
+
+ ); +} + +// ── Animated progress bar ───────────────────────────────────────────────────── +function NeoProgress({ pct, color = '#3B82F6', height = 'h-1.5' }) { + return ( +
+
); } -function ProgressHeader({ lead, columns, onStageChange }) { - const [stageOpen, setStageOpen] = useState(false); +// ── 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'); - const stageIdx = stageColumns.findIndex(c => c.id === lead.columnId); - const totalStages = stageColumns.length; - const pct = totalStages > 0 ? Math.round(((stageIdx + 1) / totalStages) * 100) : 0; - const currentStageCol = stageColumns[stageIdx]; - const bucketCol = lead.bucketId ? columns.find(c => c.id === lead.bucketId) : null; - return ( -
-
-
- {currentStageCol && ( - - {currentStageCol.name} - - )} - {bucketCol && ( - - {bucketCol.name} - - )} - {pct}% complete -
- - {/* Stage mover */} -
- - {stageOpen && ( -
-

Stages

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

Buckets

- {bucketColumns.map(col => ( - - ))} - {lead.bucketId && ( - <> -
- - - )} -
- )} -
-
- - {/* Progress bar */} -
- -
- - {/* Stage steps breadcrumb */} -
- {stageColumns.map((col, i) => ( - - +
+ + {open && ( +
+

Stages

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

Buckets

+ {bucketColumns.map(col => ( + + ))} + {lead.bucketId && ( + <> +
+ + + )} +
+ )}
); } -function ActivityItem({ entry, isLast }) { - const isFirst = !entry.from; - const isRegression = entry.from && ( - entry.to === 'Contacted' || entry.to === 'New Lead' || - (entry.from !== null && entry.activity_type === 'regression') - ); - const isBucket = entry.to === 'Stuck' || entry.to === 'Follow-up'; - +// ── 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 ? : - isRegression ? : - } +
+ {isFirst ? : + isBucket ? : + isRegress ? : + }
- {!isLast &&
} + {!isLast &&
}
- -
-
-
-
- {entry.from ? ( -

- {entry.from} - - - {entry.to} - -

- ) : ( -

Lead Created

- )} -

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

-
+
+
+
+ {entry.from ? ( +

+ {entry.from} + + {entry.to} +

+ ) : ( +

Lead Created

+ )} +

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

- {entry.note && ( -

- {entry.note} -

- )} + {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 ──────────────────────────────────────────────────────────── export default function LeadProjectPage() { const { leadId } = useParams(); const navigate = useNavigate(); - const { theme } = useTheme(); const { user } = useAuth(); - const { kanbanLeads, kanbanColumns, moveKanbanLead } = useMockStore(); + 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; @@ -219,131 +252,812 @@ export default function LeadProjectPage() { if (!lead) { return ( -
+
- -

Lead not found

- + +

Lead not found

+
); } - const stageColumns = kanbanColumns.filter(c => c.type === 'stage').sort((a, b) => a.order - b.order); - const currentStageCol = stageColumns.find(c => c.id === lead.columnId); + 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 ( -
- {/* Header */} -
-
- -
-
- {getInitials(lead.name)} -
-
-

+
+
+

{lead.name}

-

{lead.address}

+

{lead.address}

-
- - {currentStageCol && ( - - {currentStageCol.name} - - )} -

-
- - {/* Content */} -
- {/* Progress */} - - -
- {/* Left — info panels */} -
- {/* Contact */} -
-

Contact

- - - -
- - {/* Job details */} -
-

Job Details

- - - - -
- - {/* Notes */} - {lead.notes && ( -
-

Notes

-

{lead.notes}

-
- )} - - {/* Quick stats */} -
-

Stats

-
-
-

- {lead.activity?.length ?? 0} -

-

Stage Moves

-
-
-

- {lead.activity?.[0] - ? Math.max(0, Math.floor((new Date('2026-04-07') - new Date(lead.activity[0].date + 'T00:00:00')) / 86400000)) - : 0} -

-

Days in Stage

-
-
-
-
- - {/* Right — activity log */} -
-
-
-

Activity Log

- - {lead.activity?.length ?? 0} entries +
+ {currentStage && ( + + {currentStage.name} -
-
- {(lead.activity || []).map((entry, i) => ( - + {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 ( + + + + + + + + ); + })} + + + + + + + + + + +
CategoryAllocatedCommittedActualRemaining
{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) => ( + + + + + + + + + ))} + +
IDSubjectStatusSubmitted ByOpenedClosed
{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 => ( + + + + + + + + ))} + +
DescriptionAmountStatusDuePaid
{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" /> + )} +