diff --git a/src/components/leads/bulk-action-bar.tsx b/src/components/leads/bulk-action-bar.tsx
new file mode 100644
index 0000000..b45ce4d
--- /dev/null
+++ b/src/components/leads/bulk-action-bar.tsx
@@ -0,0 +1,50 @@
+interface BulkActionBarProps {
+ selectedCount: number;
+ onAssign: () => void;
+ onWhatsApp: () => void;
+ onMarkSpam: () => void;
+ onDeselect: () => void;
+}
+
+export const BulkActionBar = ({ selectedCount, onAssign, onWhatsApp, onMarkSpam, onDeselect }: BulkActionBarProps) => {
+ if (selectedCount === 0) return null;
+
+ const buttonBase = 'rounded-lg px-3 py-1.5 text-xs font-semibold border-none cursor-pointer transition duration-100 ease-linear';
+
+ return (
+
+
{selectedCount} leads selected
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/leads/filter-pills.tsx b/src/components/leads/filter-pills.tsx
new file mode 100644
index 0000000..4d5856a
--- /dev/null
+++ b/src/components/leads/filter-pills.tsx
@@ -0,0 +1,48 @@
+import { X } from '@untitledui/icons';
+import { cx } from '@/utils/cx';
+
+type FilterPill = {
+ key: string;
+ label: string;
+ value: string;
+};
+
+interface FilterPillsProps {
+ filters: FilterPill[];
+ onRemove: (key: string) => void;
+ onClearAll: () => void;
+}
+
+export const FilterPills = ({ filters, onRemove, onClearAll }: FilterPillsProps) => {
+ if (filters.length === 0) return null;
+
+ return (
+
+ {filters.map((filter) => (
+
+ {filter.label}: {filter.value}
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/leads/lead-table.tsx b/src/components/leads/lead-table.tsx
new file mode 100644
index 0000000..b5a07a7
--- /dev/null
+++ b/src/components/leads/lead-table.tsx
@@ -0,0 +1,310 @@
+import { useMemo, useState } from 'react';
+import { TableBody as AriaTableBody } from 'react-aria-components';
+import type { SortDescriptor, Selection } from 'react-aria-components';
+import { DotsVertical } from '@untitledui/icons';
+import { Badge } from '@/components/base/badges/badges';
+import { Button } from '@/components/base/buttons/button';
+import { Table } from '@/components/application/table/table';
+import { LeadStatusBadge } from '@/components/shared/status-badge';
+import { SourceTag } from '@/components/shared/source-tag';
+import { AgeIndicator } from '@/components/shared/age-indicator';
+import { formatPhone, formatShortDate } from '@/lib/format';
+import { cx } from '@/utils/cx';
+import type { Lead } from '@/types/entities';
+
+type LeadTableProps = {
+ leads: Lead[];
+ onSelectionChange: (selectedIds: string[]) => void;
+ selectedIds: string[];
+ sortField: string;
+ sortDirection: 'asc' | 'desc';
+ onSort: (field: string) => void;
+};
+
+type TableRow = {
+ id: string;
+ type: 'lead' | 'dup-sub';
+ lead: Lead;
+};
+
+const SpamDisplay = ({ score }: { score: number }) => {
+ const colorClass =
+ score < 30
+ ? 'text-success-primary'
+ : score < 60
+ ? 'text-warning-primary'
+ : 'text-error-primary';
+
+ const bgClass = score >= 60 ? 'rounded px-1.5 py-0.5 bg-error-primary' : '';
+
+ return {score}%;
+};
+
+export const LeadTable = ({
+ leads,
+ onSelectionChange,
+ selectedIds,
+ sortField,
+ sortDirection,
+ onSort,
+}: LeadTableProps) => {
+ const [expandedDupId, setExpandedDupId] = useState(null);
+
+ const selectedKeys: Selection = new Set(selectedIds);
+
+ const handleSelectionChange = (keys: Selection) => {
+ if (keys === 'all') {
+ // Only select actual lead rows, not dup sub-rows
+ onSelectionChange(leads.map((l) => l.id));
+ } else {
+ // Filter out dup sub-row IDs
+ const leadOnlyIds = [...keys].filter((k) => !String(k).endsWith('-dup')) as string[];
+ onSelectionChange(leadOnlyIds);
+ }
+ };
+
+ const sortDescriptor: SortDescriptor = {
+ column: sortField,
+ direction: sortDirection === 'asc' ? 'ascending' : 'descending',
+ };
+
+ const handleSortChange = (descriptor: SortDescriptor) => {
+ if (descriptor.column) {
+ onSort(String(descriptor.column));
+ }
+ };
+
+ // Flatten leads + expanded dup sub-rows into a single list
+ const tableRows = useMemo(() => {
+ const rows: TableRow[] = [];
+ for (const lead of leads) {
+ rows.push({ id: lead.id, type: 'lead', lead });
+ if (lead.isDuplicate === true && expandedDupId === lead.id) {
+ rows.push({ id: `${lead.id}-dup`, type: 'dup-sub', lead });
+ }
+ }
+ return rows;
+ }, [leads, expandedDupId]);
+
+ const columns = [
+ { id: 'phone', label: 'Phone', allowsSorting: true },
+ { id: 'name', label: 'Name', allowsSorting: true },
+ { id: 'email', label: 'Email', allowsSorting: false },
+ { id: 'campaign', label: 'Campaign', allowsSorting: false },
+ { id: 'ad', label: 'Ad', allowsSorting: false },
+ { id: 'source', label: 'Source', allowsSorting: true },
+ { id: 'firstContactedAt', label: 'First Contact', allowsSorting: true },
+ { id: 'lastContactedAt', label: 'Last Contact', allowsSorting: true },
+ { id: 'status', label: 'Status', allowsSorting: true },
+ { id: 'createdAt', label: 'Age', allowsSorting: true },
+ { id: 'spamScore', label: 'Spam', allowsSorting: true },
+ { id: 'dups', label: 'Dups', allowsSorting: false },
+ { id: 'actions', label: '', allowsSorting: false },
+ ];
+
+ return (
+
+
+
+ {(column) => (
+
+ )}
+
+
+
+ {(row) => {
+ const { lead } = row;
+ const firstName = lead.contactName?.firstName ?? '';
+ const lastName = lead.contactName?.lastName ?? '';
+ const name = `${firstName} ${lastName}`.trim() || '\u2014';
+ const phone = lead.contactPhone?.[0]
+ ? formatPhone(lead.contactPhone[0])
+ : '\u2014';
+ const email = lead.contactEmail?.[0]?.address ?? '\u2014';
+
+ // Render duplicate sub-row
+ if (row.type === 'dup-sub') {
+ return (
+
+
+ {phone}
+
+
+ {name}
+
+
+ {email}
+
+
+ {lead.leadSource ? (
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+
+ Same phone number
+
+
+
+
+ {lead.createdAt
+ ? formatShortDate(lead.createdAt)
+ : '\u2014'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ // Render normal lead row
+ const isSpamRow = (lead.spamScore ?? 0) >= 60;
+ const isSelected = selectedIds.includes(lead.id);
+ const isDup = lead.isDuplicate === true;
+ const isExpanded = expandedDupId === lead.id;
+
+ return (
+
+
+ {phone}
+
+
+ {name}
+
+
+ {email}
+
+
+ {lead.utmCampaign ? (
+
+ {lead.utmCampaign}
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+ {lead.adId ? (
+
+ Ad
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+ {lead.leadSource ? (
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+
+ {lead.firstContactedAt
+ ? formatShortDate(lead.firstContactedAt)
+ : '\u2014'}
+
+
+
+
+ {lead.lastContactedAt
+ ? formatShortDate(lead.lastContactedAt)
+ : '\u2014'}
+
+
+
+ {lead.leadStatus ? (
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+ {lead.createdAt ? (
+
+ ) : (
+ {'\u2014'}
+ )}
+
+
+ {lead.spamScore != null ? (
+
+ ) : (
+ 0%
+ )}
+
+
+ {isDup ? (
+
+ ) : (
+ 0
+ )}
+
+
+
+
+
+ );
+ }}
+
+
+
+ );
+};
diff --git a/src/pages/all-leads.tsx b/src/pages/all-leads.tsx
index 018d4f0..f92d81a 100644
--- a/src/pages/all-leads.tsx
+++ b/src/pages/all-leads.tsx
@@ -1,11 +1,258 @@
-import { TopBar } from "@/components/layout/top-bar";
+import { useMemo, useState } from 'react';
+import { ArrowLeft, Download01, FilterLines, SearchLg, SwitchVertical01 } from '@untitledui/icons';
+import { Button } from '@/components/base/buttons/button';
+import { Input } from '@/components/base/input/input';
+import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
+import { PaginationPageDefault } from '@/components/application/pagination/pagination';
+import { TopBar } from '@/components/layout/top-bar';
+import { LeadTable } from '@/components/leads/lead-table';
+import { BulkActionBar } from '@/components/leads/bulk-action-bar';
+import { FilterPills } from '@/components/leads/filter-pills';
+import { useLeads } from '@/hooks/use-leads';
+import type { LeadSource, LeadStatus } from '@/types/entities';
+
+type TabKey = 'new' | 'all';
+
+type ActiveFilter = {
+ key: string;
+ label: string;
+ value: string;
+};
+
+const PAGE_SIZE = 25;
export const AllLeadsPage = () => {
+ const [tab, setTab] = useState('new');
+ const [selectedIds, setSelectedIds] = useState([]);
+ const [sortField, setSortField] = useState('createdAt');
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
+ const [sourceFilter, setSourceFilter] = useState(null);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+
+ const statusFilter: LeadStatus | undefined = tab === 'new' ? 'NEW' : undefined;
+
+ const { leads: filteredLeads, total } = useLeads({
+ source: sourceFilter ?? undefined,
+ status: statusFilter,
+ search: searchQuery || undefined,
+ });
+
+ // 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]);
+
+ // Client-side pagination
+ const totalPages = Math.max(1, Math.ceil(sortedLeads.length / PAGE_SIZE));
+ const pagedLeads = sortedLeads.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 handleTabChange = (key: string | number) => {
+ setTab(key as TabKey);
+ setCurrentPage(1);
+ setSelectedIds([]);
+ };
+
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page);
+ setSelectedIds([]);
+ };
+
+ // 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);
+ };
+
+ const tabItems = [
+ { id: 'new', label: 'New', badge: tab === 'new' ? total : undefined },
+ { id: 'all', label: 'All Leads', badge: tab === 'all' ? total : undefined },
+ ];
+
return (
-
-
-
All Leads — coming soon
+
+
+
+ {/* Tabs + Controls row */}
+
+
+
+
+
+
+ {(item) => (
+
+ )}
+
+
+
+
+
+
+
+
+ {
+ setSearchQuery(value);
+ setCurrentPage(1);
+ }}
+ aria-label="Search leads"
+ />
+
+
+
+
+
+ {/* Active filters */}
+ {activeFilters.length > 0 && (
+
+
+
+ )}
+
+ {/* Bulk action bar */}
+ {selectedIds.length > 0 && (
+
+ {}}
+ onWhatsApp={() => {}}
+ onMarkSpam={() => {}}
+ onDeselect={() => setSelectedIds([])}
+ />
+
+ )}
+
+ {/* Table */}
+
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+ )}
);