import type { FC } from 'react'; import { useMemo, useState } from 'react'; import { useSearchParams } from 'react-router'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowDownToLine, faMagnifyingGlass } from '@fortawesome/pro-duotone-svg-icons'; const Download01: FC<{ className?: string }> = ({ className }) => ; const SearchLg: FC<{ className?: string }> = ({ className }) => ; import { Button } from '@/components/base/buttons/button'; import { Input } from '@/components/base/input/input'; // Tabs removed — campaign pills handle all filtering now // import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs'; import { PaginationPageDefault } from '@/components/application/pagination/pagination'; import { PageHeader } from '@/components/layout/page-header'; import { LeadTable } from '@/components/leads/lead-table'; import { ColumnToggle, useColumnVisibility } from '@/components/application/table/column-toggle'; // Bulk actions removed — checkboxes hidden // import { BulkActionBar } from '@/components/leads/bulk-action-bar'; import { FilterPills } from '@/components/leads/filter-pills'; // Bulk action modals removed — checkboxes hidden // import { AssignModal } from '@/components/modals/assign-modal'; // import { WhatsAppSendModal } from '@/components/modals/whatsapp-send-modal'; // import { MarkSpamModal } from '@/components/modals/mark-spam-modal'; import { LeadActivitySlideout } from '@/components/leads/lead-activity-slideout'; import { useLeads } from '@/hooks/use-leads'; import { useAuth } from '@/providers/auth-provider'; import { useData } from '@/providers/data-provider'; import { cx } from '@/utils/cx'; import { rowsToCsv, downloadCsv } from '@/lib/csv-utils'; import { notify } from '@/lib/toast'; import type { Lead, LeadSource, LeadStatus } from '@/types/entities'; type TabKey = 'new' | 'my-leads' | 'all'; type ActiveFilter = { key: string; label: string; value: string; }; const PAGE_SIZE = 15; export const AllLeadsPage = () => { const { user } = useAuth(); const [searchParams] = useSearchParams(); const initialSource = searchParams.get('source') as LeadSource | null; const tab: TabKey = 'all'; // Tabs removed — show all campaign-sourced leads const [sortField, setSortField] = useState('createdAt'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [sourceFilter, setSourceFilter] = useState(initialSource); const [searchQuery, setSearchQuery] = useState(''); const [currentPage, setCurrentPage] = useState(1); const statusFilter: LeadStatus | undefined = undefined; const myLeadsOnly = false; // Exclude organic contact sources — those live on the Contacts page. // Leads page shows campaign-sourced / marketing-qualified leads only. const CONTACT_SOURCES = useMemo(() => new Set(['PHONE', 'WALK_IN', 'REFERRAL'] as const), []); const { leads: filteredLeads, total } = useLeads({ source: sourceFilter ?? undefined, excludeSources: CONTACT_SOURCES, status: statusFilter, search: searchQuery || undefined, }); const { leadActivities, campaigns } = useData(); const [campaignFilter, setCampaignFilter] = useState(null); const columnDefs = [ { id: 'phone', label: 'Phone', defaultVisible: true }, { id: 'name', label: 'Name', defaultVisible: true }, { id: 'email', label: 'Email', defaultVisible: true }, { id: 'campaign', label: 'Campaign', defaultVisible: false }, { id: 'ad', label: 'Ad', defaultVisible: false }, { id: 'source', label: 'Source', defaultVisible: true }, { id: 'firstContactedAt', label: 'First Contact', defaultVisible: false }, { id: 'lastContactedAt', label: 'Last Contact', defaultVisible: true }, { id: 'status', label: 'Status', defaultVisible: true }, { id: 'createdAt', label: 'Age', defaultVisible: true }, { id: 'spamScore', label: 'Spam', defaultVisible: false }, { id: 'dups', label: 'Dups', defaultVisible: false }, ]; const { visibleColumns, toggle: toggleColumn } = useColumnVisibility(columnDefs); // Client-side sorting const sortedLeads = useMemo(() => { const sorted = [...filteredLeads]; sorted.sort((a, b) => { let aVal: string | number | null = null; let bVal: string | number | null = null; switch (sortField) { case 'phone': aVal = a.contactPhone?.[0]?.number ?? ''; bVal = b.contactPhone?.[0]?.number ?? ''; break; case 'name': aVal = `${a.contactName?.firstName ?? ''} ${a.contactName?.lastName ?? ''}`.trim(); bVal = `${b.contactName?.firstName ?? ''} ${b.contactName?.lastName ?? ''}`.trim(); break; case 'source': aVal = a.leadSource ?? ''; bVal = b.leadSource ?? ''; break; case 'firstContactedAt': aVal = a.firstContactedAt ?? ''; bVal = b.firstContactedAt ?? ''; break; case 'lastContactedAt': aVal = a.lastContactedAt ?? ''; bVal = b.lastContactedAt ?? ''; break; case 'status': aVal = a.leadStatus ?? ''; bVal = b.leadStatus ?? ''; break; case 'createdAt': aVal = a.createdAt ?? ''; bVal = b.createdAt ?? ''; break; case 'spamScore': aVal = a.spamScore ?? 0; bVal = b.spamScore ?? 0; break; default: return 0; } if (typeof aVal === 'number' && typeof bVal === 'number') { return sortDirection === 'asc' ? aVal - bVal : bVal - aVal; } const aStr = String(aVal); const bStr = String(bVal); return sortDirection === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr); }); return sorted; }, [filteredLeads, sortField, sortDirection]); // Apply "My Leads" + campaign filter const displayLeads = useMemo(() => { let result = sortedLeads; if (myLeadsOnly) { result = result.filter((l) => l.assignedAgent === user.name); } if (campaignFilter) { result = result.filter((l) => l.campaignId === campaignFilter); } return result; }, [sortedLeads, myLeadsOnly, user.name, campaignFilter]); // Client-side pagination const totalPages = Math.max(1, Math.ceil(displayLeads.length / PAGE_SIZE)); const pagedLeads = displayLeads.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE); const handleSort = (field: string) => { if (field === sortField) { setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc')); } else { setSortField(field); setSortDirection('asc'); } setCurrentPage(1); }; const handleExportCsv = () => { // Export exactly what the user currently sees — same filters, same // sort, same tab/campaign scope. Ignores pagination so the file // contains every matching row, not just the current page. if (displayLeads.length === 0) { notify.error('Export CSV', 'No leads to export'); return; } const headers = [ 'Phone', 'First Name', 'Last Name', 'Email', 'Source', 'Status', 'Campaign', 'Assigned Agent', 'First Contact', 'Last Contact', 'Created', 'Age (days)', ]; const campaignNameById = new Map(campaigns.map((c) => [c.id, c.campaignName])); const now = Date.now(); const rows = displayLeads.map((l) => { const createdMs = l.createdAt ? new Date(l.createdAt).getTime() : null; return { 'Phone': l.contactPhone?.[0]?.number ?? '', 'First Name': l.contactName?.firstName ?? '', 'Last Name': l.contactName?.lastName ?? '', 'Email': l.contactEmail?.[0]?.address ?? '', 'Source': l.leadSource ?? '', 'Status': l.leadStatus ?? '', 'Campaign': l.campaignId ? (campaignNameById.get(l.campaignId) ?? '') : '', 'Assigned Agent': l.assignedAgent ?? '', 'First Contact': l.firstContactedAt ?? '', 'Last Contact': l.lastContactedAt ?? '', 'Created': l.createdAt ?? '', 'Age (days)': createdMs ? String(Math.floor((now - createdMs) / 86400000)) : '', }; }); const csv = rowsToCsv(headers, rows); const today = new Date().toISOString().slice(0, 10); downloadCsv(`leads-${tab}-${today}.csv`, csv); notify.success('Export CSV', `${rows.length} lead${rows.length === 1 ? '' : 's'} exported`); }; const handlePageChange = (page: number) => { setCurrentPage(page); }; // Build active filters for pills display const activeFilters: ActiveFilter[] = []; if (sourceFilter) { activeFilters.push({ key: 'source', label: 'Source', value: sourceFilter }); } if (searchQuery) { activeFilters.push({ key: 'search', label: 'Search', value: searchQuery }); } const handleRemoveFilter = (key: string) => { if (key === 'source') setSourceFilter(null); if (key === 'search') setSearchQuery(''); setCurrentPage(1); }; const handleClearAllFilters = () => { setSourceFilter(null); setSearchQuery(''); setCurrentPage(1); }; // Activity slideout state const [activityLead, setActivityLead] = useState(null); const [isActivityOpen, setIsActivityOpen] = useState(false); const handleViewActivity = (lead: Lead) => { setActivityLead(lead); setIsActivityOpen(true); }; return (
{ setSearchQuery(value); setCurrentPage(1); }} aria-label="Search leads" />
} />
{/* Active filters */} {activeFilters.length > 0 && (
)} {/* Campaign filter pills */} {campaigns.length > 0 && (
{campaigns.map(c => { const isActive = campaignFilter === c.id; const count = filteredLeads.filter(l => l.campaignId === c.id).length; return ( ); })}
)} {/* Table — fills remaining space, scrolls internally */}
{}} selectedIds={[]} selectionMode="none" sortField={sortField} sortDirection={sortDirection} onSort={handleSort} onViewActivity={handleViewActivity} visibleColumns={visibleColumns} />
{/* Pagination — pinned at bottom */} {totalPages > 1 && (
)}
{/* Activity slideout */} {activityLead && ( )}
); };