mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
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:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user