feat(leads): Lead Creation Phase 1 — page shell, section framework, mock data

- CreateLeadPage.jsx: full page shell with Quick Capture / Full Form toggle, Barlow Condensed header, breadcrumb nav, GSAP progress bar stub, mobile sticky save footer
- LeadSectionWrapper.jsx: collapsible section component with Framer Motion height animation, spring chevron rotation, completion dot, dual light/dark header design matching GlassPanel language
- 5 sections wired up: Contact (blue), Property (emerald), Job Details (amber), Insurance (violet), Assignment (cyan) — Quick Capture shows first 3, Full Form shows all 5
- Section indicator pills in header dim gracefully when hidden in Quick Capture mode
- LEAD_FORM_OPTIONS added to mockStore: lead types, sources, work types, trade types, urgency/priority levels, insurance companies, claim statuses, phone/email types, US states
- Route /emp/fa/leads/new added (FIELD_AGENT, ADMIN, OWNER)
- New Lead nav item added to sidebar for all 3 roles
This commit is contained in:
Satyam
2026-03-13 16:07:29 +05:30
parent dc117e8180
commit 8bb321e1f7
5 changed files with 523 additions and 2 deletions
+318
View File
@@ -0,0 +1,318 @@
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 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',
},
];
// ---------------------------------------------------------------------------
// Animation variants
// ---------------------------------------------------------------------------
const containerVariants = {
hidden: {},
visible: { transition: { staggerChildren: 0.07 } },
};
const sectionVariants = {
hidden: { opacity: 0, y: 22 },
visible: {
opacity: 1,
y: 0,
transition: { type: 'spring', stiffness: 280, damping: 28 },
},
};
// ---------------------------------------------------------------------------
// 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 progressBarRef = useRef(null);
// Animate progress bar on mount (Phase 1: stays at 0)
useEffect(() => {
if (progressBarRef.current) {
gsap.fromTo(
progressBarRef.current,
{ scaleX: 0, transformOrigin: 'left' },
{ scaleX: 0, duration: 0.6, ease: 'power2.out' }
);
}
}, []);
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);
// Close insurance + assignment when switching to quick capture
if (quick) {
setOpenSections(prev => ({ ...prev, insurance: false, assignment: false }));
}
};
return (
<div className={`min-h-screen transition-colors duration-300
${isDark ? 'bg-[#09090b]' : 'bg-zinc-100/80'}
`}>
{/* Progress bar — thin accent line at top */}
<div className={`h-[3px] w-full ${isDark ? 'bg-zinc-800' : 'bg-zinc-200'}`}>
<div
ref={progressBarRef}
className="h-full bg-blue-500 transition-all duration-500 ease-out"
style={{ width: '0%' }}
/>
</div>
{/* ----------------------------------------------------------------
Page content
---------------------------------------------------------------- */}
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-6 lg:py-10 pb-28 md:pb-10">
{/* --- Page Header --- */}
<div className="mb-7">
{/* Back breadcrumb */}
<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 Capture / 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>
{/* Mode 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 --- */}
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-3"
>
<AnimatePresence mode="popLayout">
{visibleSections.map(section => (
<motion.div
key={section.id}
variants={sectionVariants}
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)}
>
{/* Placeholder — fields added in Phase 2+ */}
<div className={`rounded-xl border-2 border-dashed px-6 py-8 flex flex-col items-center justify-center gap-2 text-center
${isDark ? 'border-zinc-700' : 'border-zinc-200'}
`}>
<section.icon
size={28}
style={{ color: section.accentColor, opacity: 0.4 }}
/>
<p className="text-xs font-semibold uppercase tracking-widest text-zinc-400">
{section.description}
</p>
</div>
</LeadSectionWrapper>
</motion.div>
))}
</AnimatePresence>
</motion.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">
{/* Mode toggle — compact repeat for mobile */}
<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 */}
<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>
);
}