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
+71 -3
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
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 { useAuth } from '../context/AuthContext';
import { useMockStore } from '../data/mockStore';
@@ -8,7 +8,7 @@ import { toast } from 'sonner';
import { useRegisterAddress } from '../hooks/useHailRecon';
import {
User, MapPin, Briefcase, Shield, Users,
ChevronLeft, Zap, FileText, Save, AlertCircle,
ChevronLeft, Zap, FileText, Save, AlertCircle, CloudLightning, X,
} from 'lucide-react';
import LeadSectionWrapper from '../components/leads/LeadSectionWrapper';
import LeadJobSection from '../components/leads/LeadJobSection';
@@ -113,8 +113,13 @@ export default function CreateLeadPage() {
const registerAddress = useRegisterAddress();
const isDark = theme === 'dark';
const navigate = useNavigate();
const location = useLocation();
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 [openSections, setOpenSections] = useState({
contact: true,
@@ -123,7 +128,24 @@ export default function CreateLeadPage() {
insurance: 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);
@@ -400,6 +422,52 @@ export default function CreateLeadPage() {
</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 --- */}
<div className="space-y-3 mb-8">
<AnimatePresence mode="popLayout">