d801f54600
Phase 5 — Insurance (purple #8B5CF6): - Insurance Company custom select, Claim Number + Claim Status (2-col grid) - Adjuster Name + Adjuster Phone (2-col grid, flex-wrapper with Phone icon + mask) - Policy Number full-width input, all focus rings use purple accent Phase 6 — Assignment (cyan #06B6D4): - Role-aware rep selector: Field Agents see self-assign toggle, Admins/Owners see scrollable list of all field agents with Unassigned option - Priority pill selector (Low/Medium/High) with Framer Motion layoutId slide - Follow-up Date: flex-wrapper with Calendar icon + color-scheme fix - sectionCompletion wired for both sections Fix: replace Framer Motion variant inheritance with explicit initial/animate on each section motion.div — inherited variants from already-animated parent caused Insurance and Assignment to render stuck at opacity:0 when added dynamically Author: Satyam Rastogi
402 lines
16 KiB
React
402 lines
16 KiB
React
import React, { useState, useRef, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTheme } from '../context/ThemeContext';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import {
|
|
User, MapPin, Briefcase, Shield, Users,
|
|
ChevronLeft, Zap, FileText, Save,
|
|
} from 'lucide-react';
|
|
import LeadSectionWrapper from '../components/leads/LeadSectionWrapper';
|
|
import LeadJobSection from '../components/leads/LeadJobSection';
|
|
import LeadContactSection from '../components/leads/LeadContactSection';
|
|
import LeadPropertySection from '../components/leads/LeadPropertySection';
|
|
import LeadInsuranceSection from '../components/leads/LeadInsuranceSection';
|
|
import LeadAssignmentSection from '../components/leads/LeadAssignmentSection';
|
|
import gsap from 'gsap';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Section definitions
|
|
// ---------------------------------------------------------------------------
|
|
const SECTIONS = [
|
|
{
|
|
id: 'contact',
|
|
title: 'Contact',
|
|
icon: User,
|
|
accentColor: '#3B82F6',
|
|
quickCapture: true,
|
|
description: 'Name, phone numbers, and email addresses',
|
|
},
|
|
{
|
|
id: 'property',
|
|
title: 'Property',
|
|
icon: MapPin,
|
|
accentColor: '#10B981',
|
|
quickCapture: true,
|
|
description: 'Property address, type, and site photos',
|
|
},
|
|
{
|
|
id: 'job',
|
|
title: 'Job Details',
|
|
icon: Briefcase,
|
|
accentColor: '#F59E0B',
|
|
quickCapture: true,
|
|
description: 'Lead source, work type, urgency, and notes',
|
|
},
|
|
{
|
|
id: 'insurance',
|
|
title: 'Insurance',
|
|
icon: Shield,
|
|
accentColor: '#8B5CF6',
|
|
quickCapture: false,
|
|
description: 'Carrier, claim number, adjuster, and policy details',
|
|
},
|
|
{
|
|
id: 'assignment',
|
|
title: 'Assignment',
|
|
icon: Users,
|
|
accentColor: '#06B6D4',
|
|
quickCapture: false,
|
|
description: 'Assign a rep, set priority, and schedule follow-up',
|
|
},
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Initial form state
|
|
// ---------------------------------------------------------------------------
|
|
const INITIAL_FORM = {
|
|
// Job Details
|
|
leadSource: '',
|
|
leadType: '',
|
|
workType: '',
|
|
tradeType: '',
|
|
urgency: 'standard',
|
|
notes: '',
|
|
// Contact
|
|
firstName: '',
|
|
lastName: '',
|
|
phones: [{ id: '1', number: '', type: 'Mobile', isPrimary: true }],
|
|
emails: [],
|
|
// Property
|
|
address: '',
|
|
city: '',
|
|
state: 'TX',
|
|
zip: '',
|
|
propertyType: '',
|
|
propertyPhotos: [],
|
|
// Insurance (Phase 5)
|
|
insuranceCompany: '',
|
|
claimNumber: '',
|
|
claimStatus: '',
|
|
adjusterName: '',
|
|
adjusterPhone: '',
|
|
policyNumber: '',
|
|
// Assignment (Phase 6)
|
|
assignedTo: '',
|
|
priority: 'medium',
|
|
followUpDate: '',
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Component
|
|
// ---------------------------------------------------------------------------
|
|
export default function CreateLeadPage() {
|
|
const { theme } = useTheme();
|
|
const { user } = useAuth();
|
|
const isDark = theme === 'dark';
|
|
const navigate = useNavigate();
|
|
|
|
const [isQuickCapture, setIsQuickCapture] = useState(true);
|
|
const [openSections, setOpenSections] = useState({
|
|
contact: true,
|
|
property: false,
|
|
job: false,
|
|
insurance: false,
|
|
assignment: false,
|
|
});
|
|
const [formData, setFormData] = useState(INITIAL_FORM);
|
|
|
|
const progressBarRef = useRef(null);
|
|
|
|
// Section completion map
|
|
const sectionCompletion = {
|
|
contact: !!(
|
|
formData.firstName &&
|
|
formData.lastName &&
|
|
formData.phones.some(p => p.number.replace(/\D/g, '').length >= 10)
|
|
),
|
|
property: !!(formData.address && formData.city),
|
|
job: isQuickCapture
|
|
? !!formData.leadSource
|
|
: !!(formData.leadSource && formData.leadType && formData.workType),
|
|
insurance: !!(formData.insuranceCompany && formData.claimStatus),
|
|
assignment: !!(formData.assignedTo && formData.followUpDate),
|
|
};
|
|
|
|
// Progress = % of visible sections that are complete
|
|
const visibleSectionIds = (isQuickCapture ? SECTIONS.filter(s => s.quickCapture) : SECTIONS).map(s => s.id);
|
|
const completedCount = visibleSectionIds.filter(id => sectionCompletion[id]).length;
|
|
const progress = visibleSectionIds.length > 0
|
|
? Math.round((completedCount / visibleSectionIds.length) * 100)
|
|
: 0;
|
|
|
|
// GSAP animates the progress bar whenever progress changes
|
|
useEffect(() => {
|
|
if (progressBarRef.current) {
|
|
gsap.to(progressBarRef.current, {
|
|
width: `${progress}%`,
|
|
duration: 0.45,
|
|
ease: 'power2.out',
|
|
});
|
|
}
|
|
}, [progress]);
|
|
|
|
const updateField = (field, value) => {
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const toggleSection = (id) => {
|
|
setOpenSections(prev => ({ ...prev, [id]: !prev[id] }));
|
|
};
|
|
|
|
const visibleSections = isQuickCapture ? SECTIONS.filter(s => s.quickCapture) : SECTIONS;
|
|
|
|
const modeSwitchTo = (quick) => {
|
|
if (quick === isQuickCapture) return;
|
|
setIsQuickCapture(quick);
|
|
if (quick) {
|
|
setOpenSections(prev => ({ ...prev, insurance: false, assignment: false }));
|
|
}
|
|
};
|
|
|
|
// Render the content for a given section (placeholder until its phase is built)
|
|
const renderSectionContent = (section) => {
|
|
if (section.id === 'contact') {
|
|
return (
|
|
<LeadContactSection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
isQuickCapture={isQuickCapture}
|
|
/>
|
|
);
|
|
}
|
|
if (section.id === 'property') {
|
|
return (
|
|
<LeadPropertySection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
isQuickCapture={isQuickCapture}
|
|
/>
|
|
);
|
|
}
|
|
if (section.id === 'job') {
|
|
return (
|
|
<LeadJobSection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
isQuickCapture={isQuickCapture}
|
|
/>
|
|
);
|
|
}
|
|
if (section.id === 'insurance') {
|
|
return (
|
|
<LeadInsuranceSection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
/>
|
|
);
|
|
}
|
|
if (section.id === 'assignment') {
|
|
return (
|
|
<LeadAssignmentSection
|
|
formData={formData}
|
|
updateField={updateField}
|
|
currentUser={user}
|
|
/>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return (
|
|
<div className={`min-h-screen transition-colors duration-300
|
|
${isDark ? 'bg-[#09090b]' : 'bg-zinc-100/80'}
|
|
`}>
|
|
{/* --- Progress bar --- */}
|
|
<div className={`h-[3px] w-full ${isDark ? 'bg-zinc-800' : 'bg-zinc-200'}`}>
|
|
<div
|
|
ref={progressBarRef}
|
|
className="h-full bg-blue-500"
|
|
style={{ width: '0%' }}
|
|
/>
|
|
</div>
|
|
|
|
{/* ----------------------------------------------------------------
|
|
Page content
|
|
---------------------------------------------------------------- */}
|
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-6 lg:py-10 pb-44 md:pb-10">
|
|
|
|
{/* --- Page Header --- */}
|
|
<div className="mb-7">
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate(-1)}
|
|
className="flex items-center gap-1 text-xs uppercase tracking-widest font-bold text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 mb-3 transition-colors"
|
|
>
|
|
<ChevronLeft size={13} strokeWidth={2.5} />
|
|
Leads
|
|
</button>
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
{/* Title */}
|
|
<div>
|
|
<h1 className="font-['Barlow_Condensed'] font-black uppercase tracking-widest text-4xl leading-none text-zinc-900 dark:text-white">
|
|
New Lead
|
|
</h1>
|
|
<p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1.5">
|
|
{isQuickCapture
|
|
? 'Essential fields only — fastest capture at the door.'
|
|
: 'Full lead profile with insurance and assignment details.'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Quick / Full Form toggle */}
|
|
<div className={`flex items-center p-1 rounded-xl gap-0.5 shrink-0
|
|
${isDark ? 'bg-zinc-800' : 'bg-zinc-200/80'}
|
|
`}>
|
|
<button
|
|
type="button"
|
|
onClick={() => modeSwitchTo(true)}
|
|
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-[11px] font-bold uppercase tracking-wider transition-all duration-200
|
|
${isQuickCapture
|
|
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white shadow-sm'
|
|
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'
|
|
}`}
|
|
>
|
|
<Zap size={11} strokeWidth={2.5} />
|
|
Quick
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => modeSwitchTo(false)}
|
|
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-[11px] font-bold uppercase tracking-wider transition-all duration-200
|
|
${!isQuickCapture
|
|
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white shadow-sm'
|
|
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'
|
|
}`}
|
|
>
|
|
<FileText size={11} strokeWidth={2.5} />
|
|
Full Form
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section indicator pills */}
|
|
<div className="flex items-center gap-2 mt-4 flex-wrap">
|
|
{SECTIONS.map(s => (
|
|
<div
|
|
key={s.id}
|
|
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all duration-300
|
|
${(!s.quickCapture && isQuickCapture)
|
|
? 'opacity-30 bg-zinc-200 dark:bg-zinc-800 text-zinc-400'
|
|
: 'opacity-100'
|
|
}`}
|
|
style={(!s.quickCapture && isQuickCapture)
|
|
? {}
|
|
: { backgroundColor: s.accentColor + '22', color: s.accentColor }
|
|
}
|
|
>
|
|
<s.icon size={9} strokeWidth={2.5} />
|
|
{s.title}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* --- Sections --- */}
|
|
<div className="space-y-3">
|
|
<AnimatePresence mode="popLayout">
|
|
{visibleSections.map(section => (
|
|
<motion.div
|
|
key={section.id}
|
|
initial={{ opacity: 0, y: 22 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ type: 'spring', stiffness: 280, damping: 28 }}
|
|
layout
|
|
exit={{
|
|
opacity: 0,
|
|
scale: 0.97,
|
|
y: -6,
|
|
transition: { duration: 0.18, ease: 'easeIn' },
|
|
}}
|
|
>
|
|
<LeadSectionWrapper
|
|
title={section.title}
|
|
icon={section.icon}
|
|
accentColor={section.accentColor}
|
|
isOpen={openSections[section.id]}
|
|
onToggle={() => toggleSection(section.id)}
|
|
isComplete={sectionCompletion[section.id]}
|
|
>
|
|
{renderSectionContent(section)}
|
|
</LeadSectionWrapper>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ----------------------------------------------------------------
|
|
Mobile sticky save footer
|
|
---------------------------------------------------------------- */}
|
|
<div className={`fixed bottom-0 left-0 right-0 md:hidden px-4 py-3
|
|
border-t backdrop-blur-md
|
|
${isDark
|
|
? 'bg-zinc-900/90 border-white/10'
|
|
: 'bg-white/90 border-zinc-200'
|
|
}
|
|
`}>
|
|
<div className="flex items-center gap-3 max-w-3xl mx-auto">
|
|
{/* Compact mode toggle */}
|
|
<div className={`flex items-center p-0.5 rounded-lg gap-0.5 shrink-0
|
|
${isDark ? 'bg-zinc-800' : 'bg-zinc-100'}
|
|
`}>
|
|
<button
|
|
type="button"
|
|
onClick={() => modeSwitchTo(true)}
|
|
className={`p-2 rounded-md transition-all duration-200
|
|
${isQuickCapture
|
|
? 'bg-white dark:bg-zinc-700 shadow-sm text-zinc-900 dark:text-white'
|
|
: 'text-zinc-400'
|
|
}`}
|
|
>
|
|
<Zap size={14} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => modeSwitchTo(false)}
|
|
className={`p-2 rounded-md transition-all duration-200
|
|
${!isQuickCapture
|
|
? 'bg-white dark:bg-zinc-700 shadow-sm text-zinc-900 dark:text-white'
|
|
: 'text-zinc-400'
|
|
}`}
|
|
>
|
|
<FileText size={14} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Save */}
|
|
<button
|
|
type="button"
|
|
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-bold text-sm uppercase tracking-widest text-white transition-all active:scale-[0.97]"
|
|
style={{ backgroundColor: '#3B82F6' }}
|
|
>
|
|
<Save size={15} />
|
|
Save Lead
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|