- {/* 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 (
-
-
-
{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 (
+
+
- );
- },
- { 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 => (
+
+ {col.id === lead.columnId && !lead.bucketId &&
}
+
+ ))}
+
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 */}
-
-
-