feat(leads): Lead Creation Phase 2 — Job Details section with live fields
- UrgencyPillSelector: 3-pill selector (Standard/High/Emergency) with Framer Motion layoutId sliding background, spring physics, color transition on text - LeadJobSection: full Job Details fields — Lead Source (always), Lead Type / Work Type / Trade Type (Full Form only, each row animates in/out with spring height collapse), Urgency pill (always), Notes textarea (Full Form only) - Full Form fields use overflow-hidden + height:auto spring animation — no phantom gaps since spacing is managed via pt-4 inside each animated block - CreateLeadPage: formData state tracking all sections (scaffold for Phase 3-6 fields), updateField handler, sectionCompletion map, GSAP progress bar now driven by real field completion percentage, Job Details section wired up with live data
This commit is contained in:
+104
-27
@@ -8,6 +8,7 @@ import {
|
||||
ChevronLeft, Zap, FileText, Save,
|
||||
} from 'lucide-react';
|
||||
import LeadSectionWrapper from '../components/leads/LeadSectionWrapper';
|
||||
import LeadJobSection from '../components/leads/LeadJobSection';
|
||||
import gsap from 'gsap';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -73,6 +74,41 @@ const sectionVariants = {
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Initial form state
|
||||
// ---------------------------------------------------------------------------
|
||||
const INITIAL_FORM = {
|
||||
// Job Details
|
||||
leadSource: '',
|
||||
leadType: '',
|
||||
workType: '',
|
||||
tradeType: '',
|
||||
urgency: 'standard',
|
||||
notes: '',
|
||||
// Contact (Phase 3)
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phones: [],
|
||||
emails: [],
|
||||
// Property (Phase 4)
|
||||
address: '',
|
||||
city: '',
|
||||
state: 'TX',
|
||||
zip: '',
|
||||
propertyType: '',
|
||||
// Insurance (Phase 5)
|
||||
insuranceCompany: '',
|
||||
claimNumber: '',
|
||||
claimStatus: '',
|
||||
adjusterName: '',
|
||||
adjusterPhone: '',
|
||||
policyNumber: '',
|
||||
// Assignment (Phase 6)
|
||||
assignedTo: '',
|
||||
priority: 'medium',
|
||||
followUpDate: '',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -90,19 +126,45 @@ export default function CreateLeadPage() {
|
||||
insurance: false,
|
||||
assignment: false,
|
||||
});
|
||||
const [formData, setFormData] = useState(INITIAL_FORM);
|
||||
|
||||
const progressBarRef = useRef(null);
|
||||
|
||||
// Animate progress bar on mount (Phase 1: stays at 0)
|
||||
// Derived: which fields must be filled for this mode
|
||||
const requiredFields = isQuickCapture
|
||||
? ['leadSource']
|
||||
: ['leadSource', 'leadType', 'workType'];
|
||||
|
||||
const filledCount = requiredFields.filter(f => formData[f]).length;
|
||||
const progress = requiredFields.length > 0
|
||||
? Math.round((filledCount / requiredFields.length) * 100)
|
||||
: 0;
|
||||
|
||||
// GSAP animates the progress bar whenever progress changes
|
||||
useEffect(() => {
|
||||
if (progressBarRef.current) {
|
||||
gsap.fromTo(
|
||||
progressBarRef.current,
|
||||
{ scaleX: 0, transformOrigin: 'left' },
|
||||
{ scaleX: 0, duration: 0.6, ease: 'power2.out' }
|
||||
);
|
||||
gsap.to(progressBarRef.current, {
|
||||
width: `${progress}%`,
|
||||
duration: 0.45,
|
||||
ease: 'power2.out',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}, [progress]);
|
||||
|
||||
// Section completion map — grows as phases add fields
|
||||
const sectionCompletion = {
|
||||
contact: false, // Phase 3
|
||||
property: false, // Phase 4
|
||||
job: isQuickCapture
|
||||
? !!formData.leadSource
|
||||
: !!(formData.leadSource && formData.leadType && formData.workType),
|
||||
insurance: false, // Phase 5
|
||||
assignment: false, // Phase 6
|
||||
};
|
||||
|
||||
const updateField = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const toggleSection = (id) => {
|
||||
setOpenSections(prev => ({ ...prev, [id]: !prev[id] }));
|
||||
@@ -115,21 +177,47 @@ export default function CreateLeadPage() {
|
||||
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 }));
|
||||
}
|
||||
};
|
||||
|
||||
// Render the content for a given section (placeholder until its phase is built)
|
||||
const renderSectionContent = (section) => {
|
||||
if (section.id === 'job') {
|
||||
return (
|
||||
<LeadJobSection
|
||||
formData={formData}
|
||||
updateField={updateField}
|
||||
isQuickCapture={isQuickCapture}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Placeholder for sections not yet built
|
||||
return (
|
||||
<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.35 }}
|
||||
/>
|
||||
<p className="text-xs font-semibold uppercase tracking-widest text-zinc-400">
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen transition-colors duration-300
|
||||
${isDark ? 'bg-[#09090b]' : 'bg-zinc-100/80'}
|
||||
`}>
|
||||
{/* Progress bar — thin accent line at top */}
|
||||
{/* --- Progress bar --- */}
|
||||
<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"
|
||||
className="h-full bg-blue-500"
|
||||
style={{ width: '0%' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -141,7 +229,6 @@ export default function CreateLeadPage() {
|
||||
|
||||
{/* --- Page Header --- */}
|
||||
<div className="mb-7">
|
||||
{/* Back breadcrumb */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
@@ -164,7 +251,7 @@ export default function CreateLeadPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Capture / Full Form toggle */}
|
||||
{/* 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'}
|
||||
`}>
|
||||
@@ -195,7 +282,7 @@ export default function CreateLeadPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode indicator pills */}
|
||||
{/* Section indicator pills */}
|
||||
<div className="flex items-center gap-2 mt-4 flex-wrap">
|
||||
{SECTIONS.map(s => (
|
||||
<div
|
||||
@@ -243,19 +330,9 @@ export default function CreateLeadPage() {
|
||||
accentColor={section.accentColor}
|
||||
isOpen={openSections[section.id]}
|
||||
onToggle={() => toggleSection(section.id)}
|
||||
isComplete={sectionCompletion[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>
|
||||
{renderSectionContent(section)}
|
||||
</LeadSectionWrapper>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -274,7 +351,7 @@ export default function CreateLeadPage() {
|
||||
}
|
||||
`}>
|
||||
<div className="flex items-center gap-3 max-w-3xl mx-auto">
|
||||
{/* Mode toggle — compact repeat for mobile */}
|
||||
{/* 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'}
|
||||
`}>
|
||||
@@ -302,7 +379,7 @@ export default function CreateLeadPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Save button */}
|
||||
{/* 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]"
|
||||
|
||||
Reference in New Issue
Block a user