feat(storm-intel): Phase 2 — storm-sourced lead generation flow

When a canvasser taps "Create Lead from Zone" in the Storm Intel detail drawer,
router state carries the storm context into CreateLeadPage:

- CreateLeadPage reads stormSource from location.state; initialises the form with
  city=Plano, state=TX, leadSource=Door Knock, urgency=high for significant/severe
  storms, and a pre-filled notes field with zone name, hail size, and date
- Amber storm banner renders below the page header showing zone name, hail size,
  and a note that urgency was auto-set; dismissible with X
- stormSource is saved on the lead object via the existing addLead spread
- LeadsListPage LeadCard shows an amber "Storm Zone" badge when lead.stormSource exists
- KanbanCard footer shows a compact amber "Storm" label for storm-sourced leads
This commit is contained in:
Satyam-Rastogi
2026-05-18 17:06:04 +05:30
parent 2e797c89f6
commit 4cface49da
4 changed files with 101 additions and 9 deletions
+10 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useDraggable } from '@dnd-kit/core'; import { useDraggable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { MapPin, User, Clock } from 'lucide-react'; import { MapPin, User, Clock, CloudLightning } from 'lucide-react';
function getInitials(name) { function getInitials(name) {
return (name || '').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); return (name || '').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
@@ -93,13 +93,21 @@ export function KanbanCardDisplay({ lead, columnColor, isOverlay = false, onClic
{lead.assignedAgentName ?? 'Unassigned'} {lead.assignedAgentName ?? 'Unassigned'}
</span> </span>
</div> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1.5 shrink-0">
{lead.stormSource && (
<span className="flex items-center gap-0.5 text-[10px] font-bold text-amber-600 dark:text-amber-400">
<CloudLightning size={9} />
Storm
</span>
)}
<div className="flex items-center gap-1">
<Clock size={10} className="text-zinc-400" /> <Clock size={10} className="text-zinc-400" />
<span className="text-[11px] text-zinc-400 dark:text-zinc-500">{days}d</span> <span className="text-[11px] text-zinc-400 dark:text-zinc-500">{days}d</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
); );
} }
+71 -3
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { useTheme } from '../context/ThemeContext'; import { useTheme } from '../context/ThemeContext';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { useMockStore } from '../data/mockStore'; import { useMockStore } from '../data/mockStore';
@@ -8,7 +8,7 @@ import { toast } from 'sonner';
import { useRegisterAddress } from '../hooks/useHailRecon'; import { useRegisterAddress } from '../hooks/useHailRecon';
import { import {
User, MapPin, Briefcase, Shield, Users, User, MapPin, Briefcase, Shield, Users,
ChevronLeft, Zap, FileText, Save, AlertCircle, ChevronLeft, Zap, FileText, Save, AlertCircle, CloudLightning, X,
} from 'lucide-react'; } from 'lucide-react';
import LeadSectionWrapper from '../components/leads/LeadSectionWrapper'; import LeadSectionWrapper from '../components/leads/LeadSectionWrapper';
import LeadJobSection from '../components/leads/LeadJobSection'; import LeadJobSection from '../components/leads/LeadJobSection';
@@ -113,8 +113,13 @@ export default function CreateLeadPage() {
const registerAddress = useRegisterAddress(); const registerAddress = useRegisterAddress();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [validationError, setValidationError] = useState(''); const [validationError, setValidationError] = useState('');
// Storm zone context — passed from StormIntelPage via router state
const stormSource = location.state?.stormSource ?? null;
const [stormBannerDismissed, setStormBannerDismissed] = useState(false);
const [isQuickCapture, setIsQuickCapture] = useState(true); const [isQuickCapture, setIsQuickCapture] = useState(true);
const [openSections, setOpenSections] = useState({ const [openSections, setOpenSections] = useState({
contact: true, contact: true,
@@ -123,7 +128,24 @@ export default function CreateLeadPage() {
insurance: false, insurance: false,
assignment: false, assignment: false,
}); });
const [formData, setFormData] = useState(INITIAL_FORM);
// Pre-fill form when arriving from Storm Intel
const [formData, setFormData] = useState(() => {
if (!stormSource) return INITIAL_FORM;
const urgency = (stormSource.severity === 'severe' || stormSource.severity === 'significant') ? 'high' : 'standard';
const stormDate = stormSource.date
? new Date(stormSource.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
: '';
return {
...INITIAL_FORM,
city: 'Plano',
state: 'TX',
leadSource: 'Door Knock',
urgency,
notes: `Storm zone canvass — ${stormSource.areaName}.${stormSource.maxHailSize ? ` ${stormSource.maxHailSize}" hail` : ''}${stormDate ? ` on ${stormDate}` : ''}.`,
stormSource,
};
});
const progressBarRef = useRef(null); const progressBarRef = useRef(null);
@@ -400,6 +422,52 @@ export default function CreateLeadPage() {
</div> </div>
</div> </div>
{/* --- Storm zone banner --- */}
<AnimatePresence>
{stormSource && !stormBannerDismissed && (
<motion.div
key="storm-banner"
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.22 }}
className="mb-5 rounded-xl border border-amber-300 dark:border-amber-500/30 bg-amber-50 dark:bg-amber-500/8 overflow-hidden"
>
<div className="flex items-start gap-3 px-4 py-3">
<div className="w-8 h-8 rounded-lg bg-amber-500/15 dark:bg-amber-500/20 flex items-center justify-center shrink-0 mt-0.5">
<CloudLightning size={16} className="text-amber-600 dark:text-amber-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[12px] font-black uppercase tracking-wider text-amber-700 dark:text-amber-400 mb-0.5">
Storm Zone Lead
</p>
<p className="text-[13px] font-semibold text-zinc-800 dark:text-zinc-200">
{stormSource.areaName}
{stormSource.maxHailSize && (
<span className="ml-2 text-[11px] font-bold text-amber-600 dark:text-amber-400">
{stormSource.maxHailSize}" hail
</span>
)}
</p>
<p className="text-[11px] text-zinc-500 dark:text-zinc-400 mt-0.5">
Lead source, urgency, and notes pre-filled from storm data.
{(stormSource.severity === 'severe' || stormSource.severity === 'significant') && (
<span className="ml-1 text-amber-600 dark:text-amber-400 font-semibold">Urgency set to High.</span>
)}
</p>
</div>
<button
type="button"
onClick={() => setStormBannerDismissed(true)}
className="p-1 rounded-lg text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-amber-100 dark:hover:bg-amber-500/10 transition-colors shrink-0"
>
<X size={14} />
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* --- Sections --- */} {/* --- Sections --- */}
<div className="space-y-3 mb-8"> <div className="space-y-3 mb-8">
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
+7 -1
View File
@@ -8,7 +8,7 @@ import PermissionGate from '../components/PermissionGate';
import { import {
Plus, Search, MapPin, Phone, Zap, FileText, Plus, Search, MapPin, Phone, Zap, FileText,
Clock, User, ChevronRight, Filter, Lock, Clock, User, ChevronRight, Filter, Lock,
DoorOpen, MessageSquare, DoorOpen, MessageSquare, CloudLightning,
} from 'lucide-react'; } from 'lucide-react';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -130,6 +130,12 @@ function LeadCard({ lead, index }) {
{lead.referralNote} {lead.referralNote}
</span> </span>
)} )}
{lead.stormSource && (
<span className="inline-flex items-center gap-1 text-xs font-bold text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-500/10 border border-amber-200 dark:border-amber-500/20 px-1.5 py-0.5 rounded-full">
<CloudLightning size={9} className="shrink-0" />
Storm Zone
</span>
)}
</div> </div>
</div> </div>
+11 -1
View File
@@ -254,7 +254,17 @@ const StormDetailDrawer = ({ storm, open, onClose, onFlyTo }) => {
const dateLabel = new Date(storm.date).toLocaleDateString('en-US', { weekday: 'short', month: 'long', day: 'numeric', year: 'numeric' }); const dateLabel = new Date(storm.date).toLocaleDateString('en-US', { weekday: 'short', month: 'long', day: 'numeric', year: 'numeric' });
const handleCreateLead = () => { const handleCreateLead = () => {
navigate('/emp/fa/leads/new'); navigate('/emp/fa/leads/new', {
state: {
stormSource: {
id: storm.id,
date: storm.date,
areaName: storm.areaName,
maxHailSize: storm.maxHailSize,
severity: storm.severity,
},
},
});
}; };
const handleViewInDispatch = () => { const handleViewInDispatch = () => {