Files
helix-engage/src/hooks/use-leads.ts
saridsa2 ca482e731e feat: Contacts page + P360 for all tabs + dynamic column toggle + slot flicker fix
Contacts page:
 - New /contacts route — shows leads with source=PHONE/WALK_IN/REFERRAL
 - Leads page now excludes those sources (campaign-sourced only)
 - Sidebar: Contacts nav item added for all roles; Leads added for cc-agent
 - Same LeadTable + pagination + CSV export pattern as All Leads

P360 context panel for all worklist tabs (#6-10):
 - WorklistPanel: onSelectLead → onSelectItem (generic WorklistSelection)
 - call-desk: handleSelectItem builds ContextPanelSubject for any row type
 - ContextPanelSubject type replaces (lead as any).patientId casts
 - Highlight tracks row.id (mc-*/fu-*/lead-*) not lead.id

Dynamic column toggle (blank-screen fix):
 - missed-calls + call-recordings refactored to React Aria dynamic
   collections API (Table.Header columns={} + Table.Row columns={})
 - Fixes "Cell count must match column count" crash on column hide
 - Row-header column metadata in columnDefs instead of hardcoded JSX

Slot flickering fix (#2):
 - Removed clinic + timeSlot from slot-fetch useEffect deps (circular
   loop: effect sets clinic → clinic in deps → re-fires)
 - Memoized timeSlotSelectItems

Other:
 - GlobalSearch hidden (stale appointment state on navigation)
 - Branch column: shows campaign name from relation, falls back to DID
 - formatSource maps PHONE → "Phone"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:55:57 +05:30

62 lines
2.0 KiB
TypeScript

import { useMemo } from 'react';
import type { Lead, LeadSource, LeadStatus } from '@/types/entities';
import { useData } from '@/providers/data-provider';
type UseLeadsFilters = {
source?: LeadSource;
excludeSources?: Set<LeadSource>;
status?: LeadStatus;
search?: string;
};
type UseLeadsResult = {
leads: Lead[];
total: number;
updateLead: (id: string, updates: Partial<Lead>) => void;
};
export const useLeads = (filters: UseLeadsFilters = {}): UseLeadsResult => {
const { leads, updateLead } = useData();
const { source, excludeSources, status, search } = filters;
const filteredLeads = useMemo(() => {
return leads.filter((lead) => {
if (source !== undefined && lead.leadSource !== source) {
return false;
}
if (excludeSources && lead.leadSource && excludeSources.has(lead.leadSource)) {
return false;
}
if (status !== undefined && lead.leadStatus !== status) {
return false;
}
if (search !== undefined && search.trim() !== '') {
const query = search.trim().toLowerCase();
const firstName = lead.contactName?.firstName?.toLowerCase() ?? '';
const lastName = lead.contactName?.lastName?.toLowerCase() ?? '';
const fullName = `${firstName} ${lastName}`.trim();
const phones = (lead.contactPhone ?? []).map((p) => p.number.toLowerCase());
const matchesName = firstName.includes(query) || lastName.includes(query) || fullName.includes(query);
const matchesPhone = phones.some((phone) => phone.includes(query));
if (!matchesName && !matchesPhone) {
return false;
}
}
return true;
});
}, [leads, source, excludeSources, status, search]);
return {
leads: filteredLeads,
total: filteredLeads.length,
updateLead,
};
};