feat(leads): Phase 8 — Save action, lead list page, and nav update
Save action (CreateLeadPage): - Validate required fields: first/last name, 10-digit phone, address, city, lead source - On failure: Sonner error toast + auto-opens first incomplete section - On success: Sonner success toast with name + address, redirects to /emp/fa/leads - Desktop save bar with progress % indicator and validation error message - Back button navigates to /emp/fa/leads instead of browser history Leads List page (LeadsListPage.jsx): - Route: /emp/fa/leads (FIELD_AGENT, ADMIN, OWNER) - Lead cards: name, urgency + status badges, address, phone, lead source, time ago, creator - Search bar: filters by name, address, city, source, status - Empty state with CTA when no leads saved yet - Animated card entrance with staggered spring transitions mockStore: - Add leads[] state and addLead() action — prepends new lead with id, createdAt, status: New Navigation: - Sidebar "New Lead" replaced with "Leads" (ClipboardList icon) for all roles - New Lead button lives on the list page header Author: Satyam Rastogi
This commit is contained in:
@@ -3,9 +3,11 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useMockStore } from '../data/mockStore';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
User, MapPin, Briefcase, Shield, Users,
|
||||
ChevronLeft, Zap, FileText, Save,
|
||||
ChevronLeft, Zap, FileText, Save, AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import LeadSectionWrapper from '../components/leads/LeadSectionWrapper';
|
||||
import LeadJobSection from '../components/leads/LeadJobSection';
|
||||
@@ -103,8 +105,10 @@ const INITIAL_FORM = {
|
||||
export default function CreateLeadPage() {
|
||||
const { theme } = useTheme();
|
||||
const { user } = useAuth();
|
||||
const { addLead } = useMockStore();
|
||||
const isDark = theme === 'dark';
|
||||
const navigate = useNavigate();
|
||||
const [validationError, setValidationError] = useState('');
|
||||
|
||||
const [isQuickCapture, setIsQuickCapture] = useState(true);
|
||||
const [openSections, setOpenSections] = useState({
|
||||
@@ -159,6 +163,45 @@ export default function CreateLeadPage() {
|
||||
setOpenSections(prev => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// Required field validation
|
||||
const missingFields = [];
|
||||
if (!formData.firstName) missingFields.push('First Name');
|
||||
if (!formData.lastName) missingFields.push('Last Name');
|
||||
if (!formData.phones.some(p => p.number.replace(/\D/g, '').length >= 10))
|
||||
missingFields.push('Phone Number');
|
||||
if (!formData.address) missingFields.push('Street Address');
|
||||
if (!formData.city) missingFields.push('City');
|
||||
if (!formData.leadSource) missingFields.push('Lead Source');
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
setValidationError(`Required: ${missingFields.join(', ')}`);
|
||||
// Open the first incomplete section
|
||||
if (!formData.firstName || !formData.lastName || !formData.phones.some(p => p.number.replace(/\D/g, '').length >= 10)) {
|
||||
setOpenSections(prev => ({ ...prev, contact: true }));
|
||||
} else if (!formData.address || !formData.city) {
|
||||
setOpenSections(prev => ({ ...prev, property: true }));
|
||||
} else {
|
||||
setOpenSections(prev => ({ ...prev, job: true }));
|
||||
}
|
||||
toast.error(`Missing required fields: ${missingFields.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setValidationError('');
|
||||
const savedLead = addLead({
|
||||
...formData,
|
||||
createdBy: user?.id,
|
||||
createdByName: user?.name,
|
||||
isQuickCapture,
|
||||
});
|
||||
|
||||
toast.success(`Lead saved — ${formData.firstName} ${formData.lastName}`, {
|
||||
description: formData.address || 'No address provided',
|
||||
});
|
||||
navigate('/emp/fa/leads');
|
||||
};
|
||||
|
||||
const visibleSections = isQuickCapture ? SECTIONS.filter(s => s.quickCapture) : SECTIONS;
|
||||
|
||||
const modeSwitchTo = (quick) => {
|
||||
@@ -240,7 +283,7 @@ export default function CreateLeadPage() {
|
||||
<div className="mb-7">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => navigate('/emp/fa/leads')}
|
||||
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} />
|
||||
@@ -388,6 +431,7 @@ export default function CreateLeadPage() {
|
||||
{/* Save */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
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' }}
|
||||
>
|
||||
@@ -396,6 +440,45 @@ export default function CreateLeadPage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ----------------------------------------------------------------
|
||||
Desktop save bar (hidden on mobile — mobile uses sticky footer)
|
||||
---------------------------------------------------------------- */}
|
||||
<div className={`hidden md:block fixed bottom-0 left-0 right-0 px-6 py-4
|
||||
border-t backdrop-blur-md
|
||||
${isDark ? 'bg-zinc-900/90 border-white/10' : 'bg-white/90 border-zinc-200'}
|
||||
`}>
|
||||
<div className="max-w-3xl mx-auto flex items-center justify-between gap-4">
|
||||
{/* Validation error */}
|
||||
<AnimatePresence>
|
||||
{validationError && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -8 }}
|
||||
className="flex items-center gap-2 text-sm text-red-500 dark:text-red-400"
|
||||
>
|
||||
<AlertCircle size={14} />
|
||||
{validationError}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<span className="text-xs text-zinc-400 dark:text-zinc-500 uppercase tracking-widest font-bold">
|
||||
{progress}% complete
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-2 px-6 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' }}
|
||||
>
|
||||
<Save size={14} />
|
||||
Save Lead
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user