import { useEffect, useState, useMemo, useCallback } from 'react'; import { Link, useLocation, useSearchParams } from 'react-router-dom'; import { useClientsStore } from '@/stores/clients'; import type { ClientCreate } from '@/types'; import { Search, Plus, Users, X, Upload, LayoutGrid, Kanban, ChevronLeft, ChevronRight } from 'lucide-react'; import { cn, getRelativeTime, getInitials } from '@/lib/utils'; import Badge, { StageBadge } from '@/components/Badge'; import EmptyState from '@/components/EmptyState'; import { PageLoader } from '@/components/LoadingSpinner'; import Modal from '@/components/Modal'; import ClientForm from '@/components/ClientForm'; import CSVImportModal from '@/components/CSVImportModal'; const PAGE_SIZES = [25, 50, 100]; export default function ClientsPage() { const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const { clients, isLoading, searchQuery, selectedTag, setSearchQuery, setSelectedTag, fetchClients, createClient } = useClientsStore(); const [showCreate, setShowCreate] = useState(false); const [showImport, setShowImport] = useState(false); const [creating, setCreating] = useState(false); const [viewMode, setViewMode] = useState<'grid' | 'pipeline'>(() => (localStorage.getItem('clients-view') as 'grid' | 'pipeline') || 'grid' ); // Pagination state from URL const currentPage = parseInt(searchParams.get('page') || '1', 10) || 1; const pageSize = parseInt(searchParams.get('pageSize') || '50', 10) || 50; const setPage = useCallback((page: number) => { const params = new URLSearchParams(searchParams); params.set('page', String(page)); setSearchParams(params, { replace: true }); }, [searchParams, setSearchParams]); const setPageSize = useCallback((size: number) => { const params = new URLSearchParams(searchParams); params.set('pageSize', String(size)); params.set('page', '1'); // reset to first page setSearchParams(params, { replace: true }); }, [searchParams, setSearchParams]); // Initialize selected tag from URL useEffect(() => { const urlTag = searchParams.get('tag'); if (urlTag && urlTag !== selectedTag) { setSelectedTag(urlTag); } }, []); useEffect(() => { fetchClients(); }, [fetchClients]); useEffect(() => { if (location.state?.openCreate) { setShowCreate(true); window.history.replaceState({}, ''); } }, [location.state]); // Client-side filtering for immediate feedback const filteredClients = useMemo(() => { let result = clients; if (searchQuery) { const q = searchQuery.toLowerCase(); result = result.filter( (c) => `${c.firstName} ${c.lastName}`.toLowerCase().includes(q) || c.email?.toLowerCase().includes(q) || c.company?.toLowerCase().includes(q) ); } if (selectedTag) { result = result.filter((c) => c.tags?.includes(selectedTag)); } return result; }, [clients, searchQuery, selectedTag]); // Pagination for grid view const totalClients = filteredClients.length; const totalPages = Math.ceil(totalClients / pageSize); const paginatedClients = useMemo(() => { if (viewMode === 'pipeline') return filteredClients; // pipeline shows all const start = (currentPage - 1) * pageSize; return filteredClients.slice(start, start + pageSize); }, [filteredClients, currentPage, pageSize, viewMode]); // Reset page when filters change useEffect(() => { if (currentPage > 1) { setPage(1); } }, [searchQuery, selectedTag]); // Pipeline columns const pipelineStages = ['lead', 'prospect', 'onboarding', 'active', 'inactive'] as const; const pipelineColumns = useMemo(() => { const cols: Record = {}; pipelineStages.forEach(s => { cols[s] = []; }); filteredClients.forEach(c => { const stage = c.stage || 'lead'; if (cols[stage]) cols[stage].push(c); }); return cols; }, [filteredClients]); const stageCounts = useMemo(() => { const counts: Record = {}; clients.forEach(c => { const s = c.stage || 'lead'; counts[s] = (counts[s] || 0) + 1; }); return counts; }, [clients]); // All unique tags const allTags = useMemo(() => { const tags = new Set(); clients.forEach((c) => c.tags?.forEach((t) => tags.add(t))); return Array.from(tags).sort(); }, [clients]); const handleCreate = async (data: ClientCreate) => { setCreating(true); try { await createClient(data); setShowCreate(false); } catch (err) { console.error(err); } finally { setCreating(false); } }; if (isLoading && clients.length === 0) return ; return (

Clients{clients.length > 0 && ({clients.length})}

{clients.length} contacts in your network

{/* Search + Tags */}
setSearchQuery(e.target.value)} placeholder="Search clients..." className="w-full pl-10 pr-4 py-2.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-900 dark:text-slate-100 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" /> {searchQuery && ( )}
{allTags.length > 0 && (
{allTags.map((tag) => ( setSelectedTag(selectedTag === tag ? null : tag)} > {tag} ))} {selectedTag && ( )}
)}
{/* Pipeline Summary Bar */} {viewMode === 'grid' && clients.length > 0 && (
{pipelineStages.map((stage) => { const count = stageCounts[stage] || 0; const total = clients.length; const pct = total > 0 ? Math.max((count / total) * 100, count > 0 ? 4 : 0) : 0; const colors: Record = { lead: 'bg-slate-300 dark:bg-slate-600', prospect: 'bg-blue-400 dark:bg-blue-500', onboarding: 'bg-amber-400 dark:bg-amber-500', active: 'bg-emerald-400 dark:bg-emerald-500', inactive: 'bg-red-400 dark:bg-red-500', }; return (
{stage} ({count})
); })}
)} {/* Client Grid / Pipeline View */} {filteredClients.length === 0 ? ( setShowCreate(true) } : undefined} /> ) : viewMode === 'pipeline' ? ( /* Pipeline / Kanban View */
{pipelineStages.map((stage) => { const stageClients = pipelineColumns[stage] || []; const headerColors: Record = { lead: 'border-t-slate-400', prospect: 'border-t-blue-500', onboarding: 'border-t-amber-500', active: 'border-t-emerald-500', inactive: 'border-t-red-500', }; return (

{stage}

{stageClients.length}
{stageClients.map((client) => (
{getInitials(client.firstName, client.lastName)}

{client.firstName} {client.lastName}

{client.company && (

{client.company}

)}
{client.tags && client.tags.length > 0 && (
{client.tags.slice(0, 2).map(tag => ( {tag} ))}
)}

{getRelativeTime(client.lastContacted)}

))} {stageClients.length === 0 && (

No clients

)}
); })}
) : ( /* Grid View */
{paginatedClients.map((client) => (
{getInitials(client.firstName, client.lastName)}

{client.firstName} {client.lastName}

{client.company && (

{client.role ? `${client.role} at ` : ''}{client.company}

)}
{client.tags && client.tags.slice(0, 2).map((tag) => ( {tag} ))} {client.tags && client.tags.length > 2 && ( +{client.tags.length - 2} )}
Last contacted: {getRelativeTime(client.lastContacted)}
))}
)} {/* Pagination Controls (grid view only) */} {viewMode === 'grid' && totalPages > 1 && (
Show per page
Page {currentPage} of {totalPages}
)} {/* Create Modal */} setShowCreate(false)} title="Add Client" size="lg"> {/* Import CSV Modal */} setShowImport(false)} onComplete={() => fetchClients()} />
); }