"use client"; // ============================================================ // Leads — storm-restoration pipeline board. // · Hero head : storm banner + headline stats (by status) // · Toolbar : search + status filter tabs // · Board : lead cards (avatar, priority ring, status, // address, phone, source, rep, updated) // · Detail : click a lead → rich popup with Contact, // Property, Job Details, Insurance, Assignment // Data comes from leads-data.ts (client-side mock). // ============================================================ import { useMemo, useState, type ReactNode } from "react"; import { Avatar, Btn, Field, Icon, Modal, PageHead, Pill, Segmented, SegTabs, useToast } from "./ui"; import { LEADS, TOTAL_LEADS, STATUS_META, PRIORITY_META, REPS, LEAD_SOURCES, WORK_TYPES, TRADE_TYPES, CLAIM_STATUSES, type Lead, type LeadStatus, } from "./leads-data"; const STATUS_TABS: { value: "all" | LeadStatus; label: string }[] = [ { value: "all", label: "All" }, { value: "new", label: "New" }, { value: "contacted", label: "Contacted" }, { value: "appointed", label: "Appointed" }, { value: "closed", label: "Closed" }, ]; export function Leads() { const toast = useToast(); const [query, setQuery] = useState(""); const [filter, setFilter] = useState<"all" | LeadStatus>("all"); const [selected, setSelected] = useState(null); const [newOpen, setNewOpen] = useState(false); const countByStatus = useMemo(() => { const m: Record = {}; for (const l of LEADS) m[l.status] = (m[l.status] ?? 0) + 1; return m; }, []); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); return LEADS.filter((l) => { const matchStatus = filter === "all" || l.status === filter; const hay = `${l.name} ${l.property.address} ${l.property.city} ${l.job.source} ${l.job.canvasser}`.toLowerCase(); return matchStatus && (!q || hay.includes(q)); }); }, [query, filter]); return (
setNewOpen(true)}>New Lead} /> {/* ---- stat strip ---------------------------------------- */}
{/* ---- toolbar ------------------------------------------- */}
setQuery(e.target.value)} placeholder="Search by name, address, source…" aria-label="Search leads" /> {query && }
{STATUS_TABS.map((t) => ( ))}
{/* ---- board --------------------------------------------- */} {filtered.length === 0 ? (

No leads match

Try a different search or clear the status filter.

) : (
{filtered.map((l) => ( setSelected(l)} /> ))}
)} setSelected(null)} /> setNewOpen(false)} />
); } /* ---------------------------------------------------------- */ /* Stat card */ /* ---------------------------------------------------------- */ function StatCard({ label, value, icon, tone }: { label: string; value: number; icon: string; tone: string }) { return (
{value}
{label}
); } /* ---------------------------------------------------------- */ /* Lead card */ /* ---------------------------------------------------------- */ function LeadCard({ lead, onOpen }: { lead: Lead; onOpen: () => void }) { const status = STATUS_META[lead.status]; const priority = PRIORITY_META[lead.priority]; const primaryPhone = lead.phones.find((p) => p.primary) ?? lead.phones[0]; return ( ); } /* ---------------------------------------------------------- */ /* Lead detail popup */ /* ---------------------------------------------------------- */ function LeadDetail({ lead, onClose }: { lead: Lead | null; onClose: () => void }) { if (!lead) return null; const status = STATUS_META[lead.status]; const priority = PRIORITY_META[lead.priority]; return ( Call Email Update Status } >
{/* identity strip */}
{lead.name}
{status.label} {priority.label}
{/* storm banner */}
{lead.storm.zone}
{lead.storm.date} · {lead.storm.detail}
{/* Contact */}
Phone Numbers
{lead.phones.map((p, i) => (
{p.number} {p.type} {p.primary && Primary}
))}
Email Addresses
{lead.emails.map((e, i) => (
{e.address} {e.primary && Primary}
))}
{/* Property */}
{/* Job Details */}
Field Notes

{lead.job.notes}

{/* Insurance */}
{/* Assignment */}
); } function Section({ title, icon, children, wide }: { title: string; icon: string; children: ReactNode; wide?: boolean }) { return (
{title}
{children}
); } function Dl({ label, value }: { label: string; value: string }) { return (
{label} {value}
); } /* ---------------------------------------------------------- */ /* New Lead — Quick / Full Form intake */ /* ---------------------------------------------------------- */ type Priority = "Low" | "Medium" | "High"; type Urgency = "Standard" | "High" | "Emergency"; type PhoneRow = { number: string; type: string }; type EmailRow = { address: string }; const FULL_STEPS = [ { value: "contact", label: "Contact", icon: "user" }, { value: "property", label: "Property", icon: "owners" }, { value: "job", label: "Job Details", icon: "projects" }, { value: "insurance", label: "Insurance", icon: "shield" }, { value: "assignment", label: "Assignment", icon: "team" }, ]; const LEAD_TYPE_OPTS = ["Residential", "Commercial", "Multi-Family"]; const PROPERTY_TYPE_OPTS = ["Residential", "Commercial", "Multi-Family", "Industrial"]; const BLANK = { firstName: "", lastName: "", phones: [{ number: "", type: "Mobile" }] as PhoneRow[], emails: [] as EmailRow[], address: "", city: "", state: "TX", zip: "", propertyType: "", photos: [] as string[], source: "", referralNote: "", canvasser: "", leadType: "", workType: "", tradeType: "", urgency: "Standard" as Urgency, notes: "", insCompany: "", claimNumber: "", claimStatus: "", adjusterName: "", adjusterPhone: "", policyNumber: "", assignRep: "", priority: "Medium" as Priority, followUp: "", }; function NewLead({ open, onClose }: { open: boolean; onClose: () => void }) { const toast = useToast(); const [mode, setMode] = useState<"quick" | "full">("quick"); const [section, setSection] = useState("contact"); const [f, setF] = useState({ ...BLANK }); const set = (k: string) => (e: { target: { value: string } }) => setF((s) => ({ ...s, [k]: e.target.value }) as typeof BLANK); // multi-value handlers const addPhone = () => setF((s) => ({ ...s, phones: [...s.phones, { number: "", type: "Mobile" }] })); const setPhone = (i: number, key: "number" | "type", v: string) => setF((s) => ({ ...s, phones: s.phones.map((p, j) => (j === i ? { ...p, [key]: v } : p)) })); const removePhone = (i: number) => setF((s) => ({ ...s, phones: s.phones.filter((_, j) => j !== i) })); const addEmail = () => setF((s) => ({ ...s, emails: [...s.emails, { address: "" }] })); const setEmail = (i: number, v: string) => setF((s) => ({ ...s, emails: s.emails.map((e, j) => (j === i ? { address: v } : e)) })); const removeEmail = (i: number) => setF((s) => ({ ...s, emails: s.emails.filter((_, j) => j !== i) })); const addPhoto = () => setF((s) => ({ ...s, photos: [...s.photos, `Photo ${s.photos.length + 1}`] })); function reset() { setF({ ...BLANK, phones: [{ number: "", type: "Mobile" }], emails: [], photos: [] }); setMode("quick"); setSection("contact"); } function close() { reset(); onClose(); } function submit() { const name = `${f.firstName} ${f.lastName}`.trim(); if (!name) { toast.push({ tone: "error", title: "Name required", desc: "Enter the homeowner's first or last name." }); return; } toast.push({ tone: "success", title: "Lead created", desc: `${name} added to the Plano pipeline.` }); close(); } const repOptions = [{ id: "", initials: "—", name: "Unassigned" }, ...REPS]; const stepIdx = FULL_STEPS.findIndex((s) => s.value === section); const isFirstStep = stepIdx <= 0; const isLastStep = stepIdx === FULL_STEPS.length - 1; const goNext = () => { if (!isLastStep) setSection(FULL_STEPS[stepIdx + 1].value); }; const goBack = () => { if (!isFirstStep) setSection(FULL_STEPS[stepIdx - 1].value); }; return ( Cancel {!isFirstStep && Back} {isLastStep ? Create Lead : Next} ) : ( <> Cancel Create Lead ) } >
setMode(v as "quick" | "full")} options={[{ value: "quick", label: "Quick", icon: "star" }, { value: "full", label: "Full Form", icon: "edit" }]} /> {mode === "quick" ? (
setPhone(0, "number", e.target.value)} placeholder="(555) 000-0000" />