feat: Lead Master — campaign filter pills + fixed-height table layout

- Campaign filter pills: clickable badges for each campaign + "No Campaign", toggle filtering
- Fixed-height layout: header/tabs/pills pinned, table fills viewport with internal scroll, pagination pinned at bottom

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 13:18:31 +05:30
parent 7af1ccb713
commit c36802864c

View File

@@ -24,6 +24,7 @@ import { LeadActivitySlideout } from '@/components/leads/lead-activity-slideout'
import { useLeads } from '@/hooks/use-leads'; import { useLeads } from '@/hooks/use-leads';
import { useAuth } from '@/providers/auth-provider'; import { useAuth } from '@/providers/auth-provider';
import { useData } from '@/providers/data-provider'; import { useData } from '@/providers/data-provider';
import { cx } from '@/utils/cx';
import type { Lead, LeadSource, LeadStatus } from '@/types/entities'; import type { Lead, LeadSource, LeadStatus } from '@/types/entities';
type TabKey = 'new' | 'my-leads' | 'all'; type TabKey = 'new' | 'my-leads' | 'all';
@@ -57,7 +58,8 @@ export const AllLeadsPage = () => {
search: searchQuery || undefined, search: searchQuery || undefined,
}); });
const { agents, templates, leadActivities } = useData(); const { agents, templates, leadActivities, campaigns } = useData();
const [campaignFilter, setCampaignFilter] = useState<string | null>(null);
// Client-side sorting // Client-side sorting
const sortedLeads = useMemo(() => { const sortedLeads = useMemo(() => {
@@ -114,13 +116,19 @@ export const AllLeadsPage = () => {
return sorted; return sorted;
}, [filteredLeads, sortField, sortDirection]); }, [filteredLeads, sortField, sortDirection]);
// Apply "My Leads" filter when on that tab // Apply "My Leads" + campaign filter
const displayLeads = useMemo(() => { const displayLeads = useMemo(() => {
let result = sortedLeads;
if (myLeadsOnly) { if (myLeadsOnly) {
return sortedLeads.filter((l) => l.assignedAgent === user.name); result = result.filter((l) => l.assignedAgent === user.name);
} }
return sortedLeads; if (campaignFilter) {
}, [sortedLeads, myLeadsOnly, user.name]); result = campaignFilter === '__none__'
? result.filter((l) => !l.campaignId)
: result.filter((l) => l.campaignId === campaignFilter);
}
return result;
}, [sortedLeads, myLeadsOnly, user.name, campaignFilter]);
// Client-side pagination // Client-side pagination
const totalPages = Math.max(1, Math.ceil(displayLeads.length / PAGE_SIZE)); const totalPages = Math.max(1, Math.ceil(displayLeads.length / PAGE_SIZE));
@@ -203,9 +211,9 @@ export const AllLeadsPage = () => {
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<TopBar title="All Leads" subtitle={`${total} total`} /> <TopBar title="All Leads" subtitle={`${total} total`} />
<div className="flex flex-1 flex-col overflow-y-auto p-7"> <div className="flex flex-1 flex-col overflow-hidden">
{/* Tabs + Controls row */} {/* Tabs + Controls row */}
<div className="flex items-center justify-between"> <div className="flex shrink-0 items-center justify-between px-6 py-3 border-b border-secondary">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
href="/" href="/"
@@ -264,7 +272,7 @@ export const AllLeadsPage = () => {
{/* Active filters */} {/* Active filters */}
{activeFilters.length > 0 && ( {activeFilters.length > 0 && (
<div className="mt-3"> <div className="shrink-0 px-6 pt-2">
<FilterPills <FilterPills
filters={activeFilters} filters={activeFilters}
onRemove={handleRemoveFilter} onRemove={handleRemoveFilter}
@@ -273,9 +281,55 @@ export const AllLeadsPage = () => {
</div> </div>
)} )}
{/* Campaign filter pills */}
{campaigns.length > 0 && (
<div className="flex shrink-0 items-center gap-1.5 px-6 py-2 border-b border-secondary overflow-x-auto">
<button
onClick={() => { setCampaignFilter(null); setCurrentPage(1); }}
className={cx(
'shrink-0 rounded-full px-3 py-1 text-xs font-medium transition duration-100 ease-linear',
!campaignFilter
? 'bg-brand-solid text-white'
: 'bg-secondary text-tertiary hover:text-secondary hover:bg-secondary_hover',
)}
>
All
</button>
{campaigns.map(c => {
const isActive = campaignFilter === c.id;
const count = filteredLeads.filter(l => l.campaignId === c.id).length;
return (
<button
key={c.id}
onClick={() => { setCampaignFilter(isActive ? null : c.id); setCurrentPage(1); }}
className={cx(
'shrink-0 rounded-full px-3 py-1 text-xs font-medium transition duration-100 ease-linear',
isActive
? 'bg-brand-solid text-white'
: 'bg-secondary text-tertiary hover:text-secondary hover:bg-secondary_hover',
)}
>
{c.campaignName ?? 'Untitled'} ({count})
</button>
);
})}
<button
onClick={() => { setCampaignFilter(campaignFilter === '__none__' ? null : '__none__'); setCurrentPage(1); }}
className={cx(
'shrink-0 rounded-full px-3 py-1 text-xs font-medium transition duration-100 ease-linear',
campaignFilter === '__none__'
? 'bg-brand-solid text-white'
: 'bg-secondary text-tertiary hover:text-secondary hover:bg-secondary_hover',
)}
>
No Campaign ({filteredLeads.filter(l => !l.campaignId).length})
</button>
</div>
)}
{/* Bulk action bar */} {/* Bulk action bar */}
{selectedIds.length > 0 && ( {selectedIds.length > 0 && (
<div className="mt-3"> <div className="shrink-0 px-6 pt-2">
<BulkActionBar <BulkActionBar
selectedCount={selectedIds.length} selectedCount={selectedIds.length}
onAssign={handleBulkAssign} onAssign={handleBulkAssign}
@@ -286,8 +340,8 @@ export const AllLeadsPage = () => {
</div> </div>
)} )}
{/* Table */} {/* Table — fills remaining space, scrolls internally */}
<div className="mt-3"> <div className="flex flex-1 flex-col min-h-0 overflow-hidden px-4 pt-2">
<LeadTable <LeadTable
leads={pagedLeads} leads={pagedLeads}
onSelectionChange={setSelectedIds} onSelectionChange={setSelectedIds}
@@ -299,9 +353,9 @@ export const AllLeadsPage = () => {
/> />
</div> </div>
{/* Pagination */} {/* Pagination — pinned at bottom */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="mt-3"> <div className="shrink-0 border-t border-secondary px-6 py-3">
<PaginationPageDefault <PaginationPageDefault
page={currentPage} page={currentPage}
total={totalPages} total={totalPages}