diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 4192be8..f36549e 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -5701,3 +5701,57 @@ export const MOCK_STORM_EVENTS = [ ], }, ]; + +// ── Storm-sourced leads — canvasser door-knock results linked to storm events ─ +// Each lead has a stormSource { id, areaName, date, maxHailSize, severity } and +// a contractValue (number) for Closed leads. createdAt is shortly after storm date. +export const MOCK_STORM_ATTRIBUTION_LEADS = [ + // ── SE-001 · Apr 28 2026 · Severe · NE Plano ────────────────────────────── + { id: 'SAL-001', firstName: 'John', lastName: 'Martinez', address: '4821 Spring Creek Pkwy', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-04-29T10:14:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + { id: 'SAL-002', firstName: 'Sarah', lastName: 'Kim', address: '4905 Custer Rd', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Appointed', contractValue: null, createdAt: '2026-04-29T14:32:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + { id: 'SAL-003', firstName: 'Robert', lastName: 'Chen', address: '5012 Independence Pkwy', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Closed', contractValue: 18500, createdAt: '2026-04-30T09:05:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + { id: 'SAL-004', firstName: 'Maria', lastName: 'Garcia', address: '4720 Alma Dr', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Closed', contractValue: 24200, createdAt: '2026-04-30T11:48:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + { id: 'SAL-005', firstName: 'David', lastName: 'Thompson', address: '5130 Spring Creek Pkwy', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Appointed', contractValue: null, createdAt: '2026-05-01T08:22:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + { id: 'SAL-006', firstName: 'Lisa', lastName: 'Anderson', address: '4845 Custer Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-05-01T13:10:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + { id: 'SAL-007', firstName: 'James', lastName: 'Wilson', address: '5020 Alma Dr', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'New', contractValue: null, createdAt: '2026-05-02T10:00:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + { id: 'SAL-008', firstName: 'Emily', lastName: 'Davis', address: '4980 Spring Creek Pkwy', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-05-03T09:45:00Z', stormSource: { id: 'SE-001', areaName: 'NE Plano / Spring Creek Pkwy', date: '2026-04-28', maxHailSize: 2.5, severity: 'severe' } }, + + // ── SE-002 · Mar 15 2026 · Moderate · Central Plano ────────────────────── + { id: 'SAL-009', firstName: 'Michael', lastName: 'Brown', address: '1520 W 14th St', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-03-16T10:20:00Z', stormSource: { id: 'SE-002', areaName: 'Central Plano / W 15th St', date: '2026-03-15', maxHailSize: 1.2, severity: 'moderate' } }, + { id: 'SAL-010', firstName: 'Jennifer', lastName: 'Taylor', address: '1608 W 15th St', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Appointed', contractValue: null, createdAt: '2026-03-17T11:05:00Z', stormSource: { id: 'SE-002', areaName: 'Central Plano / W 15th St', date: '2026-03-15', maxHailSize: 1.2, severity: 'moderate' } }, + { id: 'SAL-011', firstName: 'Christopher',lastName: 'Moore', address: '1542 Ave K', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Closed', contractValue: 16800, createdAt: '2026-03-18T09:30:00Z', stormSource: { id: 'SE-002', areaName: 'Central Plano / W 15th St', date: '2026-03-15', maxHailSize: 1.2, severity: 'moderate' } }, + { id: 'SAL-012', firstName: 'Amanda', lastName: 'Jackson', address: '1615 W 15th St', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-03-19T14:00:00Z', stormSource: { id: 'SE-002', areaName: 'Central Plano / W 15th St', date: '2026-03-15', maxHailSize: 1.2, severity: 'moderate' } }, + { id: 'SAL-013', firstName: 'Daniel', lastName: 'White', address: '1480 W 14th St', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'New', contractValue: null, createdAt: '2026-03-20T10:15:00Z', stormSource: { id: 'SE-002', areaName: 'Central Plano / W 15th St', date: '2026-03-15', maxHailSize: 1.2, severity: 'moderate' } }, + + // ── SE-003 · Feb 3 2026 · Significant · SE Plano ───────────────────────── + { id: 'SAL-014', firstName: 'Jessica', lastName: 'Harris', address: '3201 Parker Rd', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Appointed', contractValue: null, createdAt: '2026-02-04T09:00:00Z', stormSource: { id: 'SE-003', areaName: 'SE Plano / Parker Rd Corridor', date: '2026-02-03', maxHailSize: 1.75, severity: 'significant' } }, + { id: 'SAL-015', firstName: 'Ryan', lastName: 'Martinez', address: '3150 Parker Rd', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Closed', contractValue: 21400, createdAt: '2026-02-05T11:30:00Z', stormSource: { id: 'SE-003', areaName: 'SE Plano / Parker Rd Corridor', date: '2026-02-03', maxHailSize: 1.75, severity: 'significant' } }, + { id: 'SAL-016', firstName: 'Ashley', lastName: 'Thompson', address: '3280 Heritage Pkwy', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Closed', contractValue: 19900, createdAt: '2026-02-05T14:20:00Z', stormSource: { id: 'SE-003', areaName: 'SE Plano / Parker Rd Corridor', date: '2026-02-03', maxHailSize: 1.75, severity: 'significant' } }, + { id: 'SAL-017', firstName: 'Kevin', lastName: 'Johnson', address: '3190 Parker Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-02-06T10:00:00Z', stormSource: { id: 'SE-003', areaName: 'SE Plano / Parker Rd Corridor', date: '2026-02-03', maxHailSize: 1.75, severity: 'significant' } }, + { id: 'SAL-018', firstName: 'Stephanie', lastName: 'Davis', address: '3220 Heritage Pkwy', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Appointed', contractValue: null, createdAt: '2026-02-07T09:30:00Z', stormSource: { id: 'SE-003', areaName: 'SE Plano / Parker Rd Corridor', date: '2026-02-03', maxHailSize: 1.75, severity: 'significant' } }, + { id: 'SAL-019', firstName: 'Brian', lastName: 'Wilson', address: '3160 Parker Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-02-08T11:00:00Z', stormSource: { id: 'SE-003', areaName: 'SE Plano / Parker Rd Corridor', date: '2026-02-03', maxHailSize: 1.75, severity: 'significant' } }, + + // ── SE-004 · Jan 20 2026 · Moderate · W Plano ──────────────────────────── + { id: 'SAL-020', firstName: 'Nicole', lastName: 'Garcia', address: '7200 Legacy Dr', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2026-01-21T10:00:00Z', stormSource: { id: 'SE-004', areaName: 'W Plano / Legacy West', date: '2026-01-20', maxHailSize: 1.0, severity: 'moderate' } }, + { id: 'SAL-021', firstName: 'Andrew', lastName: 'Kim', address: '7150 Legacy Dr', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Appointed', contractValue: null, createdAt: '2026-01-22T13:45:00Z', stormSource: { id: 'SE-004', areaName: 'W Plano / Legacy West', date: '2026-01-20', maxHailSize: 1.0, severity: 'moderate' } }, + { id: 'SAL-022', firstName: 'Michelle', lastName: 'Lee', address: '7210 Legacy Dr', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'New', contractValue: null, createdAt: '2026-01-23T09:15:00Z', stormSource: { id: 'SE-004', areaName: 'W Plano / Legacy West', date: '2026-01-20', maxHailSize: 1.0, severity: 'moderate' } }, + + // ── SE-005 · Dec 10 2025 · Trace · Allen / N Plano ─────────────────────── + { id: 'SAL-023', firstName: 'Steven', lastName: 'Clark', address: '405 N Greenville Ave', city: 'Allen', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Closed', contractValue: 12400, createdAt: '2025-12-11T10:30:00Z', stormSource: { id: 'SE-005', areaName: 'Allen / N Plano Border', date: '2025-12-10', maxHailSize: 0.8, severity: 'trace' } }, + { id: 'SAL-024', firstName: 'Melissa', lastName: 'Robinson', address: '415 N Greenville Ave', city: 'Allen', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2025-12-12T11:00:00Z', stormSource: { id: 'SE-005', areaName: 'Allen / N Plano Border', date: '2025-12-10', maxHailSize: 0.8, severity: 'trace' } }, + + // ── SE-006 · Nov 5 2025 · Significant · S Plano ────────────────────────── + { id: 'SAL-025', firstName: 'Timothy', lastName: 'Walker', address: '1801 K Ave', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Closed', contractValue: 31200, createdAt: '2025-11-06T09:00:00Z', stormSource: { id: 'SE-006', areaName: 'S Plano / Haggard Park Area', date: '2025-11-05', maxHailSize: 1.6, severity: 'significant' } }, + { id: 'SAL-026', firstName: 'Kimberly', lastName: 'Hall', address: '1750 J Ave', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Closed', contractValue: 18700, createdAt: '2025-11-07T10:15:00Z', stormSource: { id: 'SE-006', areaName: 'S Plano / Haggard Park Area', date: '2025-11-05', maxHailSize: 1.6, severity: 'significant' } }, + { id: 'SAL-027', firstName: 'Joshua', lastName: 'Allen', address: '1820 K Ave', city: 'Plano', state: 'TX', urgency: 'high', leadSource: 'Door Knock', status: 'Closed', contractValue: 22900, createdAt: '2025-11-08T11:30:00Z', stormSource: { id: 'SE-006', areaName: 'S Plano / Haggard Park Area', date: '2025-11-05', maxHailSize: 1.6, severity: 'significant' } }, + { id: 'SAL-028', firstName: 'Amy', lastName: 'Young', address: '1780 J Ave', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Appointed', contractValue: null, createdAt: '2025-11-09T09:45:00Z', stormSource: { id: 'SE-006', areaName: 'S Plano / Haggard Park Area', date: '2025-11-05', maxHailSize: 1.6, severity: 'significant' } }, + { id: 'SAL-029', firstName: 'Brandon', lastName: 'King', address: '1810 K Ave', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2025-11-10T10:00:00Z', stormSource: { id: 'SE-006', areaName: 'S Plano / Haggard Park Area', date: '2025-11-05', maxHailSize: 1.6, severity: 'significant' } }, + { id: 'SAL-030', firstName: 'Heather', lastName: 'Wright', address: '1760 J Ave', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2025-11-11T13:20:00Z', stormSource: { id: 'SE-006', areaName: 'S Plano / Haggard Park Area', date: '2025-11-05', maxHailSize: 1.6, severity: 'significant' } }, + { id: 'SAL-031', firstName: 'Tyler', lastName: 'Scott', address: '1795 K Ave', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2025-11-12T09:00:00Z', stormSource: { id: 'SE-006', areaName: 'S Plano / Haggard Park Area', date: '2025-11-05', maxHailSize: 1.6, severity: 'significant' } }, + + // ── SE-007 · Sep 18 2025 · Moderate · Frisco / N Plano ─────────────────── + { id: 'SAL-032', firstName: 'Amber', lastName: 'Green', address: '12001 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Closed', contractValue: 17600, createdAt: '2025-09-19T10:00:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } }, + { id: 'SAL-033', firstName: 'Nathan', lastName: 'Adams', address: '12050 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Closed', contractValue: 14300, createdAt: '2025-09-20T11:15:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } }, + { id: 'SAL-034', firstName: 'Brittany', lastName: 'Baker', address: '12020 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'Contacted', contractValue: null, createdAt: '2025-09-21T09:30:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } }, + { id: 'SAL-035', firstName: 'Dylan', lastName: 'Nelson', address: '12080 Coit Rd', city: 'Plano', state: 'TX', urgency: 'standard', leadSource: 'Door Knock', status: 'New', contractValue: null, createdAt: '2025-09-22T10:45:00Z', stormSource: { id: 'SE-007', areaName: 'Frisco / N Plano', date: '2025-09-18', maxHailSize: 1.1, severity: 'moderate' } }, +]; diff --git a/src/hooks/useStormAttribution.js b/src/hooks/useStormAttribution.js new file mode 100644 index 0000000..02140fa --- /dev/null +++ b/src/hooks/useStormAttribution.js @@ -0,0 +1,79 @@ +/** + * Aggregates storm-sourced leads into per-storm revenue attribution stats. + * + * Merges MOCK_STORM_ATTRIBUTION_LEADS (historical) with any live storm leads + * created in the current session via the Storm Intel → Create Lead flow. + * + * Returns summary + per-storm breakdown sorted by revenue descending. + */ + +import { useMemo } from 'react'; +import { MOCK_STORM_ATTRIBUTION_LEADS } from '../data/mockStore'; + +// Estimated average pipeline value for an Appointed (not yet closed) lead +const AVG_PIPELINE_VALUE = 17500; + +export function useStormAttribution(events = [], sessionLeads = []) { + return useMemo(() => { + // Merge: mock historical leads + any new storm leads from this session + const mockIds = new Set(MOCK_STORM_ATTRIBUTION_LEADS.map(l => l.id)); + const newSessionLeads = sessionLeads.filter(l => l.stormSource && !mockIds.has(l.id)); + const allLeads = [...MOCK_STORM_ATTRIBUTION_LEADS, ...newSessionLeads]; + + // Build a quick lookup of storm metadata from the events array + const stormMeta = Object.fromEntries(events.map(e => [e.id, e])); + + // Aggregate per storm + const buckets = {}; + allLeads.forEach(lead => { + const sid = lead.stormSource?.id; + if (!sid) return; + if (!buckets[sid]) { + buckets[sid] = { leads: [], closed: [], appointed: [], contacted: [] }; + } + buckets[sid].leads.push(lead); + if (lead.status === 'Closed') buckets[sid].closed.push(lead); + if (lead.status === 'Appointed') buckets[sid].appointed.push(lead); + if (lead.status === 'Contacted') buckets[sid].contacted.push(lead); + }); + + const perStorm = Object.entries(buckets).map(([sid, b]) => { + const storm = stormMeta[sid] ?? b.leads[0]?.stormSource ?? { id: sid }; + const revenue = b.closed.reduce((s, l) => s + (l.contractValue ?? 0), 0); + const pipeline = b.appointed.length * AVG_PIPELINE_VALUE; + const conversionRate = b.leads.length > 0 + ? Math.round((b.closed.length / b.leads.length) * 100) + : 0; + const revenuePerLead = b.leads.length > 0 + ? Math.round(revenue / b.leads.length) + : 0; + + return { + storm, + leadsCount: b.leads.length, + closedCount: b.closed.length, + appointedCount: b.appointed.length, + contactedCount: b.contacted.length, + revenue, + pipeline, + conversionRate, + revenuePerLead, + }; + }).sort((a, b) => b.revenue - a.revenue); + + // Overall summary + const totalLeads = allLeads.length; + const closedLeads = allLeads.filter(l => l.status === 'Closed').length; + const appointedLeads = allLeads.filter(l => l.status === 'Appointed').length; + const totalRevenue = allLeads.reduce((s, l) => s + (l.contractValue ?? 0), 0); + const pipelineValue = appointedLeads * AVG_PIPELINE_VALUE; + const conversionRate = totalLeads > 0 ? Math.round((closedLeads / totalLeads) * 100) : 0; + const avgDealSize = closedLeads > 0 ? Math.round(totalRevenue / closedLeads) : 0; + + return { + summary: { totalLeads, closedLeads, conversionRate, totalRevenue, pipelineValue, avgDealSize }, + perStorm, + allLeads, + }; + }, [events, sessionLeads]); +} diff --git a/src/pages/StormIntelPage.jsx b/src/pages/StormIntelPage.jsx index d7287b6..6b8af34 100644 --- a/src/pages/StormIntelPage.jsx +++ b/src/pages/StormIntelPage.jsx @@ -4,13 +4,20 @@ import { motion, AnimatePresence } from 'framer-motion'; import { MapContainer, TileLayer, Polygon, Tooltip, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import { + BarChart, Bar, XAxis, YAxis, Tooltip as RechartsTooltip, + ResponsiveContainer, CartesianGrid, Cell, +} from 'recharts'; import { CloudLightning, Wind, Zap, Droplets, Filter, X, ChevronRight, Home, AlertTriangle, TrendingUp, Users, Calendar, MapPin, Flame, Snowflake, Clock, ArrowRight, Eye, UserPlus, Loader2, + DollarSign, Target, BarChart2, Map, } from 'lucide-react'; import { useTheme } from '../context/ThemeContext'; +import { useMockStore } from '../data/mockStore'; import { useStormEvents, conversionWindow } from '../hooks/useStormEvents'; +import { useStormAttribution } from '../hooks/useStormAttribution'; // ─── Constants ─────────────────────────────────────────────────────────────── @@ -428,6 +435,199 @@ const StormDetailDrawer = ({ storm, open, onClose, onFlyTo }) => { ); }; +// ─── Attribution view ──────────────────────────────────────────────────────── + +const SEVERITY_DOT = { severe: '#DC2626', significant: '#EF4444', moderate: '#F97316', trace: '#EAB308' }; + +const CustomTooltip = ({ active, payload, label }) => { + if (!active || !payload?.length) return null; + return ( +
+

{label}

+ {payload.map(p => ( +
+
+ {p.name}: {p.value} +
+ ))} +
+ ); +}; + +const AttributionView = ({ events, sessionLeads }) => { + const { theme } = useTheme(); + const { summary, perStorm } = useStormAttribution(events, sessionLeads); + const isDark = theme === 'dark'; + + const maxRevenue = Math.max(...perStorm.map(p => p.revenue), 1); + + const chartData = perStorm + .filter(p => p.leadsCount > 0) + .map(p => ({ + name: (p.storm.areaName ?? 'Unknown').split('/')[0].trim().replace('Plano', '').replace('Plano', '').trim() || p.storm.areaName, + Leads: p.leadsCount, + Closed: p.closedCount, + stormId: p.storm.id, + severity: p.storm.severity, + })); + + const statCards = [ + { label: 'Storm Leads', value: summary.totalLeads, icon: Users, color: '#3B82F6' }, + { label: 'Closed', value: summary.closedLeads, icon: Target, color: '#10B981' }, + { label: 'Conv. Rate', value: `${summary.conversionRate}%`, icon: TrendingUp, color: '#F59E0B' }, + { label: 'Revenue', value: `$${(summary.totalRevenue/1000).toFixed(0)}k`, icon: DollarSign, color: '#8B5CF6' }, + { label: 'Pipeline', value: `$${(summary.pipelineValue/1000).toFixed(0)}k`, icon: BarChart2, color: '#F97316' }, + { label: 'Avg Deal', value: `$${(summary.avgDealSize/1000).toFixed(1)}k`, icon: CloudLightning, color: '#EC4899' }, + ]; + + return ( +
+ + {/* ── Summary cards ── */} +
+ {statCards.map(({ label, value, icon: Icon, color }) => ( +
+
+ +

{label}

+
+

{value}

+
+ ))} +
+ + {/* ── Leads by storm chart ── */} +
+

Leads Generated Per Storm

+ + + + + + } cursor={{ fill: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)' }} /> + + {chartData.map((entry, i) => ( + + ))} + + + + +
+
Generated (by severity)
+
Closed
+
+
+ + {/* ── Per-storm breakdown ── */} +
+

Storm Revenue Breakdown

+
+ {perStorm.map((item, idx) => { + const sty = SEVERITY_STYLES[item.storm.severity] ?? SEVERITY_STYLES.trace; + const revenuePercent = item.revenue > 0 ? Math.round((item.revenue / maxRevenue) * 100) : 0; + const stormDate = item.storm.date + ? new Date(item.storm.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + : '—'; + + return ( +
+ {/* Severity bar */} +
+
+
+
+ {/* Rank */} +
+ #{idx + 1} +
+
+
+

+ {item.storm.areaName ?? 'Unknown Zone'} +

+ + {sty.label} + +
+

{stormDate}{item.storm.maxHailSize ? ` · ${item.storm.maxHailSize}"` : ''}

+
+
+ {/* Revenue */} +
+

+ {item.revenue > 0 ? `$${item.revenue.toLocaleString()}` : '—'} +

+

revenue

+
+
+ + {/* Stats row */} +
+ {[ + { label: 'Leads', value: item.leadsCount, color: null }, + { label: 'Closed', value: item.closedCount, color: '#10B981' }, + { label: 'Appointed', value: item.appointedCount, color: '#F59E0B' }, + { label: 'Conv %', value: `${item.conversionRate}%`, color: null }, + { label: 'Pipeline', value: item.pipeline > 0 ? `$${(item.pipeline/1000).toFixed(0)}k` : '—', color: '#F97316' }, + ].map(({ label, value, color }) => ( +
+

+ {value} +

+

{label}

+
+ ))} +
+ + {/* Revenue progress bar */} + {item.revenue > 0 && ( +
+ +
+ )} +
+
+ ); + })} +
+
+ + {/* ── Best zone insight ── */} + {perStorm.length > 0 && perStorm[0].revenue > 0 && ( +
+
+ +
+
+

Top Performing Zone

+

+ {perStorm[0].storm.areaName} generated ${perStorm[0].revenue.toLocaleString()} from {perStorm[0].closedCount} closed deal{perStorm[0].closedCount !== 1 ? 's' : ''} ({perStorm[0].conversionRate}% conversion). + {perStorm[0].pipeline > 0 && ` $${(perStorm[0].pipeline/1000).toFixed(0)}k more in pipeline.`} +

+
+
+ )} +
+ ); +}; + // ─── Filter bar ─────────────────────────────────────────────────────────────── const FilterChip = ({ active, onClick, children }) => ( @@ -447,7 +647,9 @@ const FilterChip = ({ active, onClick, children }) => ( const StormIntelPage = () => { const { theme } = useTheme(); + const { leads: storeLeads = [] } = useMockStore(); + const [activeTab, setActiveTab] = useState('map'); const [dateRange, setDateRange] = useState('6m'); const [typeFilter, setTypeFilter] = useState('all'); const [severityFilter, setSeverityFilter] = useState('all'); @@ -457,7 +659,9 @@ const StormIntelPage = () => { const [flyCount, setFlyCount] = useState(0); const [showFilters, setShowFilters] = useState(false); + // Always load all events (attribution uses the full set regardless of date filter) const { events, loading, error, totalHomes } = useStormEvents({ dateRange, typeFilter, severityFilter, sortBy }); + const { events: allEvents } = useStormEvents({ dateRange: '1y' }); const handleSelect = useCallback((storm) => { setSelectedStorm(storm); @@ -525,23 +729,53 @@ const StormIntelPage = () => {
- {/* Mobile filter toggle */} - +
+ {/* Tab switcher */} +
+ + +
+ + {/* Mobile filter toggle — map tab only */} + {activeTab === 'map' && ( + + )} +
- {/* ── Filter bar ── */} + {/* ── Filter bar — map tab only ── */} - {(showFilters || true) && ( + {activeTab === 'map' && (showFilters || true) && ( = 1024 ? '' : 'hidden lg:flex'}`} @@ -578,8 +812,13 @@ const StormIntelPage = () => { )} - {/* ── Body ── */} -
+ {/* ── Attribution tab ── */} + {activeTab === 'attribution' && ( + + )} + + {/* ── Map tab body ── */} +
{/* Map */}