feat(storm-intel): Phase 3 — Revenue Attribution dashboard
Adds an Attribution tab to Storm Intel showing full ROI breakdown of storm-sourced canvassing.
- MOCK_STORM_ATTRIBUTION_LEADS: 35 realistic leads across 7 storms, with addresses, statuses
(New / Contacted / Appointed / Closed) and contractValues for closed deals — $218,900 total revenue
- useStormAttribution hook: merges historical mock leads with any session-created storm leads,
aggregates per-storm stats (leads, closed, conversion rate, revenue, pipeline estimate, avg deal size)
- Attribution tab in StormIntelPage:
- 6 summary stat cards: Total Leads, Closed, Conversion Rate, Revenue, Pipeline, Avg Deal
- Grouped bar chart (Recharts) — leads generated vs closed per storm, bars colored by severity
- Per-storm revenue cards ranked by revenue: severity badge, stats row, animated revenue
progress bar relative to top performer
- "Top Performing Zone" insight card at bottom
- Tab switcher (Map / Attribution) added to header; filter bar only shows on Map tab
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
Reference in New Issue
Block a user