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:
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { MapPin, User, Clock } from 'lucide-react';
|
||||
import { MapPin, User, Clock, CloudLightning } from 'lucide-react';
|
||||
|
||||
function getInitials(name) {
|
||||
return (name || '').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
|
||||
@@ -93,9 +93,17 @@ export function KanbanCardDisplay({ lead, columnColor, isOverlay = false, onClic
|
||||
{lead.assignedAgentName ?? 'Unassigned'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Clock size={10} className="text-zinc-400" />
|
||||
<span className="text-[11px] text-zinc-400 dark:text-zinc-500">{days}d</span>
|
||||
<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" />
|
||||
<span className="text-[11px] text-zinc-400 dark:text-zinc-500">{days}d</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -8,7 +8,7 @@ import PermissionGate from '../components/PermissionGate';
|
||||
import {
|
||||
Plus, Search, MapPin, Phone, Zap, FileText,
|
||||
Clock, User, ChevronRight, Filter, Lock,
|
||||
DoorOpen, MessageSquare,
|
||||
DoorOpen, MessageSquare, CloudLightning,
|
||||
} from 'lucide-react';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -130,6 +130,12 @@ function LeadCard({ lead, index }) {
|
||||
{lead.referralNote}
|
||||
</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>
|
||||
|
||||
|
||||
@@ -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 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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user