feat(storm-intel): Phase 4+5 — Tomorrow.io forecast overlay and canvasser zone assignment
- Tomorrow.io 7-day forecast strip with weather icons, storm score bar, and per-day risk color coding - High-risk alert banner in forecast strip when any day scores >=65 - api/storm-forecast.js: Vercel proxy that reads TOMORROW_API_KEY server-side; graceful empty response when key unset - useStormForecast hook: USE_MOCK=false with realistic MOCK_FORECAST fallback - ZoneDrawingLayer: click-to-place polygon vertices using react-leaflet useMapEvents; disables double-click zoom while active; shows live vertex dots + dashed preview polygon - ZoneLayer: renders stormZones as colored semi-transparent polygons with agent initials at centroid - ZoneAssignmentModal: name + canvasser picker + color selector + optional storm link; portaled modal - StormIntelPage: Draw Zone button (Admin/Owner only); drawing state/cancel flow; stormZones displayed on map; zone summary strip in sidebar; StormDetailDrawer has working "Draw Canvasser Zone" action - FieldStormZonePage: canvasser mobile view at /field/storm-zones showing assigned zones with map, linked storm details, conversion window badge, and peak window alert - Layout: Storm Zones entry in FIELD_AGENT nav - App: /field/storm-zones route (FIELD_AGENT + Admin + Owner)
This commit is contained in:
@@ -5080,6 +5080,7 @@ export const MockStoreProvider = ({ children }) => {
|
||||
const [templateAccessExcluded, setTemplateAccessExcluded] = useState([]);
|
||||
const [kanbanColumns, setKanbanColumns] = useState(KANBAN_COLUMNS_INITIAL);
|
||||
const [kanbanLeads, setKanbanLeads] = useState(KANBAN_LEADS_INITIAL);
|
||||
const [stormZones, setStormZones] = useState(INITIAL_STORM_ZONES);
|
||||
|
||||
// Commission Settings — Org defaults + per-user overrides
|
||||
const [orgCommissionDefaults, setOrgCommissionDefaults] = useState({
|
||||
@@ -5396,6 +5397,20 @@ export const MockStoreProvider = ({ children }) => {
|
||||
removeUserCommissionOverride: (userId) => {
|
||||
setUserCommissionOverrides(prev => prev.filter(o => o.userId !== userId));
|
||||
},
|
||||
|
||||
// Storm Zones
|
||||
stormZones,
|
||||
addStormZone: (zone) => {
|
||||
const newZone = { ...zone, id: `sz-${Date.now()}`, createdAt: new Date().toISOString() };
|
||||
setStormZones(prev => [...prev, newZone]);
|
||||
return newZone;
|
||||
},
|
||||
updateStormZone: (id, data) => {
|
||||
setStormZones(prev => prev.map(z => z.id === id ? { ...z, ...data } : z));
|
||||
},
|
||||
deleteStormZone: (id) => {
|
||||
setStormZones(prev => prev.filter(z => z.id !== id));
|
||||
},
|
||||
updateProject: (projectId, data) => {
|
||||
setProjects(prev => prev.map(p => p.id === projectId ? { ...p, ...data } : p));
|
||||
},
|
||||
@@ -5705,6 +5720,52 @@ 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.
|
||||
// ── Canvasser zones — polygons drawn on Storm Intel map, assigned to agents ───
|
||||
export const INITIAL_STORM_ZONES = [
|
||||
{
|
||||
id: 'sz-001',
|
||||
name: 'Spring Creek Zone A',
|
||||
stormId: 'SE-001',
|
||||
polygon: [
|
||||
[33.108, -96.720], [33.100, -96.702], [33.086, -96.702],
|
||||
[33.080, -96.720], [33.086, -96.738], [33.100, -96.738],
|
||||
],
|
||||
assignedAgentId: 'REP-01',
|
||||
assignedAgentName: 'Carlos Mendez',
|
||||
color: '#F59E0B',
|
||||
status: 'active',
|
||||
createdAt: '2026-04-29T08:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'sz-002',
|
||||
name: 'Parker Rd Zone B',
|
||||
stormId: 'SE-003',
|
||||
polygon: [
|
||||
[33.033, -96.712], [33.025, -96.697], [33.011, -96.697],
|
||||
[33.003, -96.712], [33.011, -96.727], [33.025, -96.727],
|
||||
],
|
||||
assignedAgentId: 'REP-02',
|
||||
assignedAgentName: 'Aisha Kumar',
|
||||
color: '#8B5CF6',
|
||||
status: 'active',
|
||||
createdAt: '2026-02-04T09:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'sz-003',
|
||||
name: 'Haggard Park Zone C',
|
||||
stormId: 'SE-006',
|
||||
polygon: [
|
||||
[33.047, -96.774], [33.039, -96.757], [33.025, -96.757],
|
||||
[33.017, -96.774], [33.025, -96.791], [33.039, -96.791],
|
||||
],
|
||||
assignedAgentId: 'REP-04',
|
||||
assignedAgentName: 'Nina Patel',
|
||||
color: '#10B981',
|
||||
status: 'completed',
|
||||
createdAt: '2025-11-06T07:30:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
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' } },
|
||||
|
||||
Reference in New Issue
Block a user