feat(leads): enrich lead data + add clickable lead detail page mirroring the create-lead form

This commit is contained in:
Satyam Rastogi
2026-05-29 18:18:58 +05:30
parent aeabcea850
commit a8ed0c0034
4 changed files with 504 additions and 25 deletions
+9
View File
@@ -34,6 +34,7 @@ import SubContractorDashboard from './pages/subcontractor/SubContractorDashboard
import SubcontractorTaskDetailPage from './pages/subcontractor/SubcontractorTaskDetailPage'; import SubcontractorTaskDetailPage from './pages/subcontractor/SubcontractorTaskDetailPage';
import CreateLeadPage from './pages/CreateLeadPage'; import CreateLeadPage from './pages/CreateLeadPage';
import LeadsListPage from './pages/LeadsListPage'; import LeadsListPage from './pages/LeadsListPage';
import LeadDetailPage from './pages/LeadDetailPage';
import LynkDispatchPage from './pages/LynkDispatchPage'; import LynkDispatchPage from './pages/LynkDispatchPage';
import EstimatesPage from './pages/EstimatesPage'; import EstimatesPage from './pages/EstimatesPage';
import KanbanPage from './pages/KanbanPage'; import KanbanPage from './pages/KanbanPage';
@@ -125,6 +126,14 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/leads/view/:leadId"
element={
<ProtectedRoute allowedRoles={['OWNER', 'ADMIN', 'FIELD_AGENT']}>
<LeadDetailPage />
</ProtectedRoute>
}
/>
<Route <Route
path="/emp/fa/leads/:leadId" path="/emp/fa/leads/:leadId"
element={ element={
+139 -24
View File
@@ -7662,12 +7662,8 @@ const MOCK_NOTIFICATIONS = [
// --- SEED HELPERS --- // --- SEED HELPERS ---
/** /**
* Maps MOCK_STORM_ATTRIBUTION_LEADS into the shape LeadsListPage expects. * Maps MOCK_STORM_ATTRIBUTION_LEADS into the shape LeadsListPage expects,
* Field coverage: * enriched with the full field set mirroring the CreateLeadPage form.
* id, firstName, lastName, address, city, state,
* urgency, status, leadSource, stormSource (truthy obj),
* phones[{ number, isPrimary }],
* canvasserName, createdByName, createdAt
*/ */
function seedLeadsFromAttribution() { function seedLeadsFromAttribution() {
const canvassers = [ const canvassers = [
@@ -7684,32 +7680,151 @@ function seedLeadsFromAttribution() {
'Shelby Greer', 'Shelby Greer',
'Dalton Pruitt', 'Dalton Pruitt',
]; ];
const emailDomains = ['gmail.com', 'yahoo.com', 'outlook.com', 'gmail.com', 'yahoo.com'];
const planoZips = ['75023', '75024', '75025', '75074', '75075', '75093'];
const propertyTypes = ['Single Family', 'Single Family', 'Townhome', 'Single Family', 'Multi-Family', 'Single Family'];
const leadTypes = ['Insurance', 'Insurance', 'Retail', 'Insurance', 'Retail', 'Insurance'];
const workTypes = ['Roof Replacement', 'Roof Repair', 'Roof Replacement', 'Siding', 'Gutters', 'Windows'];
const insuranceCompanies = ['State Farm', 'Allstate', 'Farmers', 'USAA'];
const claimStatuses = ['Filed', 'Under Review', 'Approved'];
const adjusterNames = [
'Marcus Powell', 'Linda Forsythe', 'Craig Delgado', 'Susan Merritt',
'Jeff Okonkwo', 'Diane Castillo',
];
const adjusterAreaCodes = ['972', '469', '214'];
const repNames = ['Jesus Gonzales', 'Sarah Calloway'];
// Near-future 2026 follow-up dates, deterministic by idx
const followUpDates = [
'2026-06-04', '2026-06-06', '2026-06-09', '2026-06-11', '2026-06-13',
'2026-06-16', '2026-06-18', '2026-06-20', '2026-06-23', '2026-06-25',
'2026-06-27', '2026-06-30', '2026-07-02', '2026-07-07', '2026-07-09',
'2026-07-11', '2026-07-14', '2026-07-16', '2026-07-18', '2026-07-21',
'2026-07-23', '2026-07-25', '2026-07-28', '2026-07-30', '2026-08-01',
'2026-08-04', '2026-08-06', '2026-08-08', '2026-08-11', '2026-08-13',
'2026-08-15', '2026-08-18', '2026-08-20', '2026-08-22', '2026-08-25',
];
const canvassingNotes = [
'Homeowner showed significant granule loss on south-facing slopes; agreed to inspection.',
'Visible dents on ridge cap and gutters from recent hail event; very interested.',
'Neighbor referral — two doors down already signed. Motivated to move quickly.',
'Homeowner working with insurer; requested second opinion on adjuster estimate.',
'Front elevation has missing shingles post-storm. Homeowner confirmed claim filed.',
'Spoke with spouse — primary decision-maker out of town until next week.',
'Property flagged on storm map; homeowner unaware of damage. Scheduled walkthrough.',
'Older roof (est. 15 yr); multiple soft spots noted. Prime re-roof candidate.',
'Hail dents visible on AC unit and metal flashing. Homeowner eager to proceed.',
'HOA required repair; homeowner wants professional documentation for board approval.',
];
// Deterministic assignment based on SAL index so output is stable. // Deterministic assignment based on SAL index so output is stable.
return MOCK_STORM_ATTRIBUTION_LEADS.map((sal, idx) => { return MOCK_STORM_ATTRIBUTION_LEADS.map((sal, idx) => {
const canvasserName = canvassers[idx % canvassers.length]; const canvasserName = canvassers[idx % canvassers.length];
const createdByName = creators[(idx + 2) % creators.length]; const createdByName = creators[(idx + 2) % creators.length];
// Build a realistic phone number from the index
// Primary phone (area-code varies by parity)
const areaCode = idx % 2 === 0 ? '469' : '972'; const areaCode = idx % 2 === 0 ? '469' : '972';
const mid = String(500 + idx).padStart(3, '0'); const mid = String(500 + idx).padStart(3, '0');
const last = String(1000 + idx * 37).padStart(4, '0').slice(-4); const last = String(1000 + idx * 37).padStart(4, '0').slice(-4);
const phoneNumber = `(${areaCode}) ${mid}-${last}`; const primaryPhone = `(${areaCode}) ${mid}-${last}`;
// Second phone for every third lead
const phones = [{ id: '1', number: primaryPhone, type: 'Mobile', isPrimary: true }];
if (idx % 3 === 0) {
const homeAC = idx % 2 === 0 ? '214' : '817';
const homeMid = String(600 + idx).padStart(3, '0');
const homeLast = String(2000 + idx * 13).padStart(4, '0').slice(-4);
phones.push({ id: '2', number: `(${homeAC}) ${homeMid}-${homeLast}`, type: 'Home', isPrimary: false });
}
// Email
const domain = emailDomains[idx % emailDomains.length];
const emails = [{
email: `${sal.firstName.toLowerCase()}.${sal.lastName.toLowerCase()}@${domain}`,
isPrimary: true,
}];
// Property
const zip = planoZips[idx % planoZips.length];
const propertyType = propertyTypes[idx % propertyTypes.length];
// Job details
const leadType = leadTypes[idx % leadTypes.length];
const workType = workTypes[idx % workTypes.length];
const tradeType = 'Roofing';
const priority = sal.urgency === 'high' ? 'high' : (idx % 3 === 0 ? 'medium' : 'low');
const notes = canvassingNotes[idx % canvassingNotes.length];
// Assignment
const assignedTo = repNames[idx % repNames.length];
const followUpDate = followUpDates[idx % followUpDates.length];
// Insurance block (only for Insurance leads)
let insuranceBlock = {};
if (leadType === 'Insurance') {
const ins = insuranceCompanies[idx % insuranceCompanies.length];
const claimNum = `CLM-2026-${String(1000 + idx).padStart(4, '0')}`;
const policyNum = `POL-${String(80000 + idx * 7).padStart(6, '0')}`;
const claimStat = claimStatuses[idx % claimStatuses.length];
const adjName = adjusterNames[idx % adjusterNames.length];
const adjAC = adjusterAreaCodes[idx % adjusterAreaCodes.length];
const adjMid = String(700 + idx).padStart(3, '0');
const adjLast = String(3000 + idx * 19).padStart(4, '0').slice(-4);
insuranceBlock = {
insuranceCompany: ins,
claimNumber: claimNum,
claimStatus: claimStat,
adjusterName: adjName,
adjusterPhone: `(${adjAC}) ${adjMid}-${adjLast}`,
policyNumber: policyNum,
};
} else {
insuranceBlock = {
insuranceCompany: '',
claimNumber: '',
claimStatus: '',
adjusterName: '',
adjusterPhone: '',
policyNumber: '',
};
}
return { return {
id: sal.id, // Original fields (kept as-is for LeadsListPage card)
firstName: sal.firstName, id: sal.id,
lastName: sal.lastName, firstName: sal.firstName,
address: sal.address, lastName: sal.lastName,
city: sal.city, address: sal.address,
state: sal.state, city: sal.city,
urgency: sal.urgency, state: sal.state,
status: sal.status, urgency: sal.urgency,
leadSource: sal.leadSource, status: sal.status,
stormSource: sal.stormSource, // truthy object renders Storm Zone badge leadSource: sal.leadSource,
createdAt: sal.createdAt, stormSource: sal.stormSource,
createdAt: sal.createdAt,
canvasserName, canvasserName,
createdByName, createdByName,
phones: [{ number: phoneNumber, isPrimary: true }],
isQuickCapture: false, isQuickCapture: false,
// Enriched phone list
phones,
// Contact
emails,
// Property
zip,
propertyType,
propertyPhotos: [],
// Job Details
leadType,
workType,
tradeType,
referralNote: '',
canvasserId: '',
notes,
// Assignment
assignedTo,
priority,
followUpDate,
// Insurance
...insuranceBlock,
}; };
}); });
} }
+349
View File
@@ -0,0 +1,349 @@
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { useTheme } from '../context/ThemeContext';
import { useMockStore } from '../data/mockStore';
import {
ChevronLeft, User, MapPin, Briefcase, Shield, Users,
Phone, Mail, CloudLightning, Clock, Calendar,
AlertCircle, CheckCircle2, FileText, Zap, Star,
} from 'lucide-react';
// ---------------------------------------------------------------------------
// Badge helpers (mirrors LeadsListPage)
// ---------------------------------------------------------------------------
const URGENCY_CONFIG = {
standard: { label: 'Standard', bg: 'bg-zinc-200 dark:bg-zinc-700', text: 'text-zinc-600 dark:text-zinc-300' },
high: { label: 'High', bg: 'bg-amber-100 dark:bg-amber-500/20', text: 'text-amber-700 dark:text-amber-400' },
emergency: { label: 'Emergency', bg: 'bg-red-100 dark:bg-red-500/20', text: 'text-red-700 dark:text-red-400' },
};
const STATUS_CONFIG = {
New: { bg: 'bg-blue-100 dark:bg-blue-500/20', text: 'text-blue-700 dark:text-blue-400' },
Contacted: { bg: 'bg-purple-100 dark:bg-purple-500/20', text: 'text-purple-700 dark:text-purple-400' },
Appointed: { bg: 'bg-emerald-100 dark:bg-emerald-500/20', text: 'text-emerald-700 dark:text-emerald-400' },
Closed: { bg: 'bg-zinc-100 dark:bg-zinc-700', text: 'text-zinc-500 dark:text-zinc-400' },
};
const PRIORITY_CONFIG = {
high: { label: 'High', bg: 'bg-red-100 dark:bg-red-500/20', text: 'text-red-700 dark:text-red-400' },
medium: { label: 'Medium', bg: 'bg-amber-100 dark:bg-amber-500/20', text: 'text-amber-700 dark:text-amber-400' },
low: { label: 'Low', bg: 'bg-zinc-100 dark:bg-zinc-700', text: 'text-zinc-500 dark:text-zinc-400' },
};
function Badge({ children, bg, text }) {
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-bold uppercase tracking-wider ${bg} ${text}`}>
{children}
</span>
);
}
// ---------------------------------------------------------------------------
// Section card wrapper
// ---------------------------------------------------------------------------
function Section({ icon: Icon, title, accentColor, isDark, children }) {
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: 'spring', stiffness: 260, damping: 26 }}
className={`rounded-2xl border p-5 ${
isDark
? 'bg-zinc-900/70 backdrop-blur-xl border-white/[0.08]'
: 'bg-white border-zinc-200/70 shadow-sm'
}`}
>
<div className="flex items-center gap-2.5 mb-4">
<div
className="w-7 h-7 rounded-lg flex items-center justify-center shrink-0"
style={{ backgroundColor: `${accentColor}20` }}
>
<Icon size={14} style={{ color: accentColor }} />
</div>
<h3 className="font-['Barlow_Condensed'] font-black uppercase tracking-widest text-sm text-zinc-900 dark:text-white">
{title}
</h3>
</div>
<div className="space-y-3">
{children}
</div>
</motion.div>
);
}
// ---------------------------------------------------------------------------
// Label + value row
// ---------------------------------------------------------------------------
function Field({ label, value }) {
const isEmpty = !value || (typeof value === 'string' && value.trim() === '');
return (
<div className="flex flex-col gap-0.5">
<span className="text-[10px] font-bold uppercase tracking-widest text-zinc-400 dark:text-zinc-500">
{label}
</span>
<span className={`text-sm ${isEmpty ? 'text-zinc-300 dark:text-zinc-600 italic' : 'text-zinc-800 dark:text-zinc-200'}`}>
{isEmpty ? '—' : value}
</span>
</div>
);
}
// ---------------------------------------------------------------------------
// Format ISO → human date
// ---------------------------------------------------------------------------
function formatDate(iso) {
if (!iso) return '';
try {
return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
} catch {
return iso;
}
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
export default function LeadDetailPage() {
const { leadId } = useParams();
const navigate = useNavigate();
const { theme } = useTheme();
const { leads } = useMockStore();
const isDark = theme === 'dark';
const lead = leads.find(l => l.id === leadId);
// ── Not found ───────────────────────────────────────────────────────────
if (!lead) {
return (
<div className={`min-h-screen flex flex-col items-center justify-center gap-4 transition-colors duration-300 ${isDark ? 'bg-[#09090b]' : 'bg-zinc-100/80'}`}>
<AlertCircle size={40} className="text-zinc-400" />
<p className="font-['Barlow_Condensed'] font-black uppercase tracking-widest text-2xl text-zinc-900 dark:text-white">
Lead Not Found
</p>
<p className="text-sm text-zinc-500 dark:text-zinc-400">
The lead <span className="font-mono text-zinc-700 dark:text-zinc-300">{leadId}</span> does not exist.
</p>
<button
type="button"
onClick={() => navigate('/emp/fa/leads')}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl font-bold text-sm uppercase tracking-widest text-white transition-all active:scale-[0.97] hover:opacity-90"
style={{ backgroundColor: '#3B82F6' }}
>
<ChevronLeft size={14} />
Back to Leads
</button>
</div>
);
}
// ── Derived values ───────────────────────────────────────────────────────
const urgencyCfg = URGENCY_CONFIG[lead.urgency] ?? URGENCY_CONFIG.standard;
const statusCfg = STATUS_CONFIG[lead.status] ?? STATUS_CONFIG.New;
const priorityCfg = PRIORITY_CONFIG[lead.priority] ?? PRIORITY_CONFIG.medium;
const initials = `${lead.firstName?.[0] ?? ''}${lead.lastName?.[0] ?? ''}`.toUpperCase();
const hasInsurance = !!(lead.insuranceCompany);
// ── Layout ───────────────────────────────────────────────────────────────
return (
<div className={`min-h-screen transition-colors duration-300 ${isDark ? 'bg-[#09090b]' : 'bg-zinc-100/80'}`}>
<div className="max-w-2xl mx-auto px-4 sm:px-6 py-6 lg:py-10 space-y-4">
{/* ── Back button ─────────────────────────────────────────── */}
<button
type="button"
onClick={() => navigate(-1)}
className="flex items-center gap-1.5 text-sm font-semibold text-zinc-500 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-white transition-colors"
>
<ChevronLeft size={16} />
Back
</button>
{/* ── Hero header card ─────────────────────────────────────── */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: 'spring', stiffness: 260, damping: 26 }}
className={`rounded-2xl border p-5 ${
isDark
? 'bg-zinc-900/70 backdrop-blur-xl border-white/[0.08] shadow-[0_4px_24px_rgba(0,0,0,0.4)]'
: 'bg-white border-zinc-200/70 shadow-md'
}`}
>
<div className="flex items-start gap-4">
{/* Avatar */}
<div
className="w-14 h-14 rounded-2xl flex items-center justify-center text-lg font-black text-white shrink-0"
style={{ backgroundColor: '#3B82F6' }}
>
{initials || '?'}
</div>
{/* Name + badges */}
<div className="flex-1 min-w-0">
<h1 className="font-['Barlow_Condensed'] font-black uppercase tracking-widest text-2xl leading-tight text-zinc-900 dark:text-white truncate">
{lead.firstName} {lead.lastName}
</h1>
<div className="flex flex-wrap items-center gap-2 mt-2">
<Badge bg={urgencyCfg.bg} text={urgencyCfg.text}>{urgencyCfg.label}</Badge>
<Badge bg={statusCfg.bg} text={statusCfg.text}>{lead.status}</Badge>
{lead.stormSource && (
<span className="inline-flex items-center gap-1 text-[11px] font-bold uppercase tracking-wider 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-2 py-0.5 rounded-full">
<CloudLightning size={10} />
Storm Zone
</span>
)}
{lead.isQuickCapture && (
<span className="inline-flex items-center gap-1 text-[11px] font-bold uppercase tracking-wider bg-zinc-100 dark:bg-zinc-800 text-zinc-400 px-2 py-0.5 rounded-full">
<Zap size={10} />
Quick
</span>
)}
</div>
{lead.stormSource && (
<p className="mt-2 text-xs text-amber-600 dark:text-amber-400/80">
{lead.stormSource.areaName} &mdash; {lead.stormSource.date} &mdash; {lead.stormSource.maxHailSize}" hail ({lead.stormSource.severity})
</p>
)}
</div>
{/* Lead ID pill */}
<span className="shrink-0 font-mono text-[10px] text-zinc-400 dark:text-zinc-500 bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded-lg">
{lead.id}
</span>
</div>
</motion.div>
{/* ── Contact ─────────────────────────────────────────────── */}
<Section icon={User} title="Contact" accentColor="#3B82F6" isDark={isDark}>
{/* Phones */}
<div className="space-y-2">
<span className="text-[10px] font-bold uppercase tracking-widest text-zinc-400 dark:text-zinc-500">
Phone Numbers
</span>
{lead.phones?.length > 0 ? (
<div className="flex flex-col gap-1.5">
{lead.phones.map((p, i) => (
<div key={i} className="flex items-center gap-2">
<Phone size={12} className="text-zinc-400 dark:text-zinc-500 shrink-0" />
<span className="text-sm text-zinc-800 dark:text-zinc-200">{p.number}</span>
<span className="text-[10px] text-zinc-400 dark:text-zinc-500 bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded-md">
{p.type ?? 'Mobile'}
</span>
{p.isPrimary && (
<span className="text-[10px] text-blue-500 bg-blue-50 dark:bg-blue-500/10 px-1.5 py-0.5 rounded-md font-bold uppercase">
Primary
</span>
)}
</div>
))}
</div>
) : (
<span className="text-sm text-zinc-300 dark:text-zinc-600 italic">—</span>
)}
</div>
{/* Emails */}
<div className="space-y-2">
<span className="text-[10px] font-bold uppercase tracking-widest text-zinc-400 dark:text-zinc-500">
Email Addresses
</span>
{lead.emails?.length > 0 ? (
<div className="flex flex-col gap-1.5">
{lead.emails.map((e, i) => (
<div key={i} className="flex items-center gap-2">
<Mail size={12} className="text-zinc-400 dark:text-zinc-500 shrink-0" />
<span className="text-sm text-zinc-800 dark:text-zinc-200">{e.email}</span>
{e.isPrimary && (
<span className="text-[10px] text-blue-500 bg-blue-50 dark:bg-blue-500/10 px-1.5 py-0.5 rounded-md font-bold uppercase">
Primary
</span>
)}
</div>
))}
</div>
) : (
<span className="text-sm text-zinc-300 dark:text-zinc-600 italic">—</span>
)}
</div>
</Section>
{/* ── Property ────────────────────────────────────────────── */}
<Section icon={MapPin} title="Property" accentColor="#10B981" isDark={isDark}>
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2">
<Field label="Address" value={lead.address} />
</div>
<Field label="City" value={lead.city} />
<Field label="State" value={lead.state} />
<Field label="ZIP" value={lead.zip} />
<Field label="Property Type" value={lead.propertyType} />
</div>
</Section>
{/* ── Job Details ─────────────────────────────────────────── */}
<Section icon={Briefcase} title="Job Details" accentColor="#F59E0B" isDark={isDark}>
<div className="grid grid-cols-2 gap-3">
<Field label="Lead Source" value={lead.leadSource} />
<Field label="Lead Type" value={lead.leadType} />
<Field label="Work Type" value={lead.workType} />
<Field label="Trade Type" value={lead.tradeType} />
<Field label="Urgency" value={lead.urgency ? lead.urgency.charAt(0).toUpperCase() + lead.urgency.slice(1) : ''} />
<Field label="Canvasser" value={lead.canvasserName} />
{lead.referralNote && (
<div className="col-span-2">
<Field label="Referral Note" value={lead.referralNote} />
</div>
)}
{lead.notes && (
<div className="col-span-2">
<Field label="Field Notes" value={lead.notes} />
</div>
)}
</div>
</Section>
{/* ── Insurance (only if present) ──────────────────────────── */}
{hasInsurance && (
<Section icon={Shield} title="Insurance" accentColor="#8B5CF6" isDark={isDark}>
<div className="grid grid-cols-2 gap-3">
<Field label="Insurance Company" value={lead.insuranceCompany} />
<Field label="Claim Status" value={lead.claimStatus} />
<Field label="Claim Number" value={lead.claimNumber} />
<Field label="Policy Number" value={lead.policyNumber} />
<Field label="Adjuster Name" value={lead.adjusterName} />
<Field label="Adjuster Phone" value={lead.adjusterPhone} />
</div>
</Section>
)}
{/* ── Assignment ──────────────────────────────────────────── */}
<Section icon={Users} title="Assignment" accentColor="#06B6D4" isDark={isDark}>
<div className="grid grid-cols-2 gap-3">
<Field label="Assigned To" value={lead.assignedTo} />
<div>
<span className="text-[10px] font-bold uppercase tracking-widest text-zinc-400 dark:text-zinc-500">
Priority
</span>
<div className="mt-1">
{lead.priority ? (
<Badge bg={priorityCfg.bg} text={priorityCfg.text}>
{priorityCfg.label}
</Badge>
) : (
<span className="text-sm text-zinc-300 dark:text-zinc-600 italic">—</span>
)}
</div>
</div>
<Field label="Follow-Up Date" value={formatDate(lead.followUpDate)} />
<Field label="Created By" value={lead.createdByName} />
<div className="col-span-2">
<Field label="Created At" value={formatDate(lead.createdAt)} />
</div>
</div>
</Section>
</div>
</div>
);
}
+7 -1
View File
@@ -62,6 +62,7 @@ function timeAgo(isoString) {
function LeadCard({ lead, index }) { function LeadCard({ lead, index }) {
const { theme } = useTheme(); const { theme } = useTheme();
const isDark = theme === 'dark'; const isDark = theme === 'dark';
const navigate = useNavigate();
const primaryPhone = lead.phones?.find(p => p.isPrimary) ?? lead.phones?.[0]; const primaryPhone = lead.phones?.find(p => p.isPrimary) ?? lead.phones?.[0];
return ( return (
@@ -69,7 +70,12 @@ function LeadCard({ lead, index }) {
initial={{ opacity: 0, y: 16 }} initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ type: 'spring', stiffness: 280, damping: 28, delay: index * 0.04 }} transition={{ type: 'spring', stiffness: 280, damping: 28, delay: index * 0.04 }}
className={`rounded-2xl border p-4 flex items-start gap-4 cursor-pointer transition-shadow duration-200 group onClick={() => navigate(`/leads/view/${lead.id}`)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') navigate(`/leads/view/${lead.id}`); }}
tabIndex={0}
role="button"
aria-label={`View lead: ${lead.firstName} ${lead.lastName}`}
className={`rounded-2xl border p-4 flex items-start gap-4 cursor-pointer transition-shadow duration-200 group focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500
${isDark ${isDark
? 'bg-zinc-900/70 backdrop-blur-xl border-white/[0.08] hover:border-white/20 shadow-[0_4px_16px_rgba(0,0,0,0.3)]' ? 'bg-zinc-900/70 backdrop-blur-xl border-white/[0.08] hover:border-white/20 shadow-[0_4px_16px_rgba(0,0,0,0.3)]'
: 'bg-white border-zinc-200/60 shadow-sm hover:shadow-md' : 'bg-white border-zinc-200/60 shadow-sm hover:shadow-md'