// Contacts page — organic inbound callers (source = PHONE, WALK_IN, // REFERRAL). Same Lead entity, filtered view. Campaign-sourced leads // live on the Leads page; contacts are people who reached out directly // without a marketing touchpoint. // // Uses the same LeadTable + column toggle + pagination pattern as // All Leads. No separate backend endpoint — filters client-side on // the DataProvider's leads array. import type { FC } from 'react'; import { useMemo, useState } from 'react'; 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'; 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'; import { LeadActivitySlideout } from '@/components/leads/lead-activity-slideout'; import { useData } from '@/providers/data-provider'; import { rowsToCsv, downloadCsv } from '@/lib/csv-utils'; import { notify } from '@/lib/toast'; import type { Lead } from '@/types/entities'; // Sources that qualify as "contacts" — direct/organic, not campaign-sourced const CONTACT_SOURCES = new Set(['PHONE', 'WALK_IN', 'REFERRAL']); const PAGE_SIZE = 15; export const ContactsPage = () => { const { leads, leadActivities } = useData(); const [searchQuery, setSearchQuery] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [sortField, setSortField] = useState('createdAt'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [activityLead, setActivityLead] = useState(null); const columnDefs = [ { id: 'phone', label: 'Phone', defaultVisible: true }, { id: 'name', label: 'Name', defaultVisible: true }, { id: 'email', label: 'Email', 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 }, ]; const { visibleColumns, toggle: toggleColumn } = useColumnVisibility(columnDefs); // Filter to contact sources only const contacts = useMemo(() => { let filtered = leads.filter((l) => CONTACT_SOURCES.has(l.leadSource ?? '')); if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); filtered = filtered.filter((l) => { const name = `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase(); const phone = l.contactPhone?.[0]?.number ?? ''; return name.includes(q) || phone.includes(q); }); } return filtered; }, [leads, searchQuery]); // Sort const sorted = useMemo(() => { const copy = [...contacts]; const dir = sortDirection === 'asc' ? 1 : -1; copy.sort((a, b) => { const av = (a as any)[sortField] ?? ''; const bv = (b as any)[sortField] ?? ''; if (av === bv) return 0; return av > bv ? dir : -dir; }); return copy; }, [contacts, sortField, sortDirection]); const totalPages = Math.max(1, Math.ceil(sorted.length / PAGE_SIZE)); const paged = sorted.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE); const handleSort = (field: string) => { if (field === sortField) { setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc')); } else { setSortField(field); setSortDirection('desc'); } setCurrentPage(1); }; const handleExportCsv = () => { if (sorted.length === 0) { notify.error('Export CSV', 'No contacts to export'); return; } const headers = ['Phone', 'First Name', 'Last Name', 'Email', 'Source', 'Status', 'Created', 'Last Contact']; const rows = sorted.map((l) => ({ '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 ?? '', 'Created': l.createdAt ?? '', 'Last Contact': l.lastContactedAt ?? '', })); const csv = rowsToCsv(headers, rows); downloadCsv(`contacts-${new Date().toISOString().slice(0, 10)}.csv`, csv); notify.success('Export CSV', `${rows.length} contact${rows.length === 1 ? '' : 's'} exported`); }; return (
{ setSearchQuery(value); setCurrentPage(1); }} aria-label="Search contacts" />
} />
{}} selectionMode="none" sortField={sortField} sortDirection={sortDirection} onSort={handleSort} onViewActivity={(lead) => setActivityLead(lead)} visibleColumns={visibleColumns} />
{totalPages > 1 && (
{ setCurrentPage(page); }} />
)}
{activityLead && ( !open && setActivityLead(null)} lead={activityLead} activities={leadActivities} /> )}
); };