mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: build Lead Workspace page with KPIs, source grid, lead cards, and sidebar widgets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
71
src/components/leads/aging-widget.tsx
Normal file
71
src/components/leads/aging-widget.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { cx } from '@/utils/cx';
|
||||
import { daysAgoFromNow } from '@/lib/format';
|
||||
import type { Lead } from '@/types/entities';
|
||||
|
||||
interface AgingWidgetProps {
|
||||
leads: Lead[];
|
||||
}
|
||||
|
||||
type AgingBracket = {
|
||||
label: string;
|
||||
color: string;
|
||||
barColor: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export const AgingWidget = ({ leads }: AgingWidgetProps) => {
|
||||
const leadsWithAge = leads.filter((l) => l.createdAt !== null);
|
||||
const total = leadsWithAge.length || 1;
|
||||
|
||||
const freshCount = leadsWithAge.filter((l) => daysAgoFromNow(l.createdAt!) < 2).length;
|
||||
const warmCount = leadsWithAge.filter((l) => {
|
||||
const days = daysAgoFromNow(l.createdAt!);
|
||||
return days >= 2 && days <= 5;
|
||||
}).length;
|
||||
const coldCount = leadsWithAge.filter((l) => daysAgoFromNow(l.createdAt!) > 5).length;
|
||||
|
||||
const brackets: AgingBracket[] = [
|
||||
{
|
||||
label: 'Fresh (<2 days)',
|
||||
color: 'text-success-primary',
|
||||
barColor: 'bg-success-solid',
|
||||
count: freshCount,
|
||||
},
|
||||
{
|
||||
label: 'Warm (2-5 days)',
|
||||
color: 'text-warning-primary',
|
||||
barColor: 'bg-warning-solid',
|
||||
count: warmCount,
|
||||
},
|
||||
{
|
||||
label: 'Cold (>5 days)',
|
||||
color: 'text-error-primary',
|
||||
barColor: 'bg-error-solid',
|
||||
count: coldCount,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-bold text-primary">Lead Aging</h3>
|
||||
<div className="space-y-3">
|
||||
{brackets.map((bracket) => (
|
||||
<div key={bracket.label}>
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className={cx('text-xs', bracket.color)}>{bracket.label}</span>
|
||||
<span className={cx('text-sm font-bold', bracket.color)}>
|
||||
{bracket.count}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-tertiary">
|
||||
<div
|
||||
className={cx('h-full rounded-full transition-all', bracket.barColor)}
|
||||
style={{ width: `${(bracket.count / total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
30
src/components/leads/alerts-widget.tsx
Normal file
30
src/components/leads/alerts-widget.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { daysAgoFromNow } from '@/lib/format';
|
||||
import type { Lead } from '@/types/entities';
|
||||
|
||||
interface AlertsWidgetProps {
|
||||
leads: Lead[];
|
||||
}
|
||||
|
||||
export const AlertsWidget = ({ leads }: AlertsWidgetProps) => {
|
||||
const agingCount = leads.filter(
|
||||
(l) =>
|
||||
l.leadStatus === 'NEW' &&
|
||||
l.createdAt !== null &&
|
||||
daysAgoFromNow(l.createdAt) > 5,
|
||||
).length;
|
||||
|
||||
if (agingCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-error-subtle bg-error-primary p-4">
|
||||
<p className="text-xs font-bold text-error-primary">
|
||||
{agingCount} leads aging > 5 days
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-error-primary opacity-80">
|
||||
These leads haven't been contacted and are at risk of going cold.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
64
src/components/leads/followup-widget.tsx
Normal file
64
src/components/leads/followup-widget.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { cx } from '@/utils/cx';
|
||||
import { formatShortDate } from '@/lib/format';
|
||||
import type { FollowUp } from '@/types/entities';
|
||||
|
||||
interface FollowupWidgetProps {
|
||||
overdue: FollowUp[];
|
||||
upcoming: FollowUp[];
|
||||
}
|
||||
|
||||
export const FollowupWidget = ({ overdue, upcoming }: FollowupWidgetProps) => {
|
||||
const items = [...overdue, ...upcoming].slice(0, 4);
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-bold text-primary">Upcoming Follow-ups</h3>
|
||||
<div className="space-y-2">
|
||||
{items.map((item) => {
|
||||
const isOverdue = overdue.some((o) => o.id === item.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cx(
|
||||
'rounded-lg border-l-2 p-3',
|
||||
isOverdue
|
||||
? 'border-l-error-solid bg-error-primary'
|
||||
: 'border-l-brand-solid bg-secondary',
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cx(
|
||||
'text-xs font-semibold',
|
||||
isOverdue ? 'text-error-primary' : 'text-brand-secondary',
|
||||
)}
|
||||
>
|
||||
{item.scheduledAt
|
||||
? formatShortDate(item.scheduledAt)
|
||||
: 'No date'}
|
||||
{isOverdue && ' — Overdue'}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs font-medium text-primary">
|
||||
{item.description ?? item.followUpType ?? 'Follow-up'}
|
||||
</p>
|
||||
{(item.patientName || item.patientPhone) && (
|
||||
<p className="mt-0.5 text-xs text-tertiary">
|
||||
{item.patientName}
|
||||
{item.patientPhone && ` · ${item.patientPhone}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button className="mt-3 w-full" size="sm" color="secondary">
|
||||
Open Full Schedule
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
93
src/components/leads/kpi-cards.tsx
Normal file
93
src/components/leads/kpi-cards.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead } from '@/types/entities';
|
||||
|
||||
interface KpiCardsProps {
|
||||
leads: Lead[];
|
||||
}
|
||||
|
||||
type KpiCard = {
|
||||
label: string;
|
||||
value: number;
|
||||
delta: string;
|
||||
deltaColor: string;
|
||||
isHero: boolean;
|
||||
};
|
||||
|
||||
export const KpiCards = ({ leads }: KpiCardsProps) => {
|
||||
const newCount = leads.filter((l) => l.leadStatus === 'NEW').length;
|
||||
const assignedCount = leads.filter((l) => l.assignedAgent !== null).length;
|
||||
const contactedCount = leads.filter((l) => l.leadStatus === 'CONTACTED').length;
|
||||
const convertedCount = leads.filter((l) => l.leadStatus === 'CONVERTED').length;
|
||||
|
||||
const cards: KpiCard[] = [
|
||||
{
|
||||
label: 'New Leads Today',
|
||||
value: newCount,
|
||||
delta: '+12% vs yesterday',
|
||||
deltaColor: 'text-success-primary',
|
||||
isHero: true,
|
||||
},
|
||||
{
|
||||
label: 'Assigned to CC',
|
||||
value: assignedCount,
|
||||
delta: '85% assigned',
|
||||
deltaColor: 'text-brand-secondary',
|
||||
isHero: false,
|
||||
},
|
||||
{
|
||||
label: 'Contacted',
|
||||
value: contactedCount,
|
||||
delta: '+8% vs yesterday',
|
||||
deltaColor: 'text-success-primary',
|
||||
isHero: false,
|
||||
},
|
||||
{
|
||||
label: 'Converted',
|
||||
value: convertedCount,
|
||||
delta: '+3 this week',
|
||||
deltaColor: 'text-warning-primary',
|
||||
isHero: false,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
{cards.map((card) => (
|
||||
<div
|
||||
key={card.label}
|
||||
className={cx(
|
||||
'rounded-2xl p-5 transition hover:shadow-md',
|
||||
card.isHero
|
||||
? 'flex-[1.3] bg-brand-solid text-white'
|
||||
: 'flex-1 border border-secondary bg-primary',
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cx(
|
||||
'text-xs font-medium uppercase tracking-wider',
|
||||
card.isHero ? 'text-white/70' : 'text-quaternary',
|
||||
)}
|
||||
>
|
||||
{card.label}
|
||||
</p>
|
||||
<p
|
||||
className={cx(
|
||||
'mt-1 text-display-sm font-bold',
|
||||
card.isHero ? 'text-white' : 'text-primary',
|
||||
)}
|
||||
>
|
||||
{card.value}
|
||||
</p>
|
||||
<p
|
||||
className={cx(
|
||||
'mt-1 text-xs',
|
||||
card.isHero ? 'text-white/80' : card.deltaColor,
|
||||
)}
|
||||
>
|
||||
{card.delta}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
115
src/components/leads/lead-card.tsx
Normal file
115
src/components/leads/lead-card.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Avatar } from '@/components/base/avatar/avatar';
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { LeadStatusBadge } from '@/components/shared/status-badge';
|
||||
import { SourceTag } from '@/components/shared/source-tag';
|
||||
import { AgeIndicator } from '@/components/shared/age-indicator';
|
||||
import { formatPhone, getInitials } from '@/lib/format';
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead } from '@/types/entities';
|
||||
|
||||
interface LeadCardProps {
|
||||
lead: Lead;
|
||||
onAssign: (lead: Lead) => void;
|
||||
onMessage: (lead: Lead) => void;
|
||||
onMarkSpam: (lead: Lead) => void;
|
||||
onMerge: (lead: Lead) => void;
|
||||
}
|
||||
|
||||
const sourceLabelMap: Record<string, string> = {
|
||||
FACEBOOK_AD: 'Facebook',
|
||||
INSTAGRAM: 'Instagram',
|
||||
GOOGLE_AD: 'Google',
|
||||
GOOGLE_MY_BUSINESS: 'GMB',
|
||||
WHATSAPP: 'WhatsApp',
|
||||
WEBSITE: 'Website',
|
||||
REFERRAL: 'Referral',
|
||||
WALK_IN: 'Walk-in',
|
||||
PHONE: 'Phone',
|
||||
OTHER: 'Other',
|
||||
};
|
||||
|
||||
export const LeadCard = ({ lead, onAssign, onMessage, onMarkSpam, onMerge }: LeadCardProps) => {
|
||||
const firstName = lead.contactName?.firstName ?? '';
|
||||
const lastName = lead.contactName?.lastName ?? '';
|
||||
const name = `${firstName} ${lastName}`.trim() || 'Unknown';
|
||||
const initials = firstName && lastName ? getInitials(firstName, lastName) : '??';
|
||||
const phone = lead.contactPhone?.[0] ? formatPhone(lead.contactPhone[0]) : '';
|
||||
const sourceLabel = lead.leadSource ? sourceLabelMap[lead.leadSource] ?? lead.leadSource : '';
|
||||
const isSpam = (lead.spamScore ?? 0) >= 60;
|
||||
const isDuplicate = lead.isDuplicate === true;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'flex items-center gap-4 rounded-2xl border border-secondary p-5 transition hover:shadow-md',
|
||||
isSpam ? 'bg-warning-primary' : 'bg-primary',
|
||||
)}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<Avatar size="md" initials={initials} />
|
||||
|
||||
{/* Middle content */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-semibold text-primary">{name}</span>
|
||||
{lead.leadStatus && <LeadStatusBadge status={lead.leadStatus} />}
|
||||
{isSpam && (
|
||||
<Badge size="sm" type="pill-color" color="error">
|
||||
{lead.spamScore}% Spam Risk
|
||||
</Badge>
|
||||
)}
|
||||
{isDuplicate && (
|
||||
<Badge size="sm" type="pill-color" color="warning">
|
||||
Duplicate
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 truncate text-xs text-tertiary">
|
||||
{phone}
|
||||
{lead.interestedService && ` · ${lead.interestedService}`}
|
||||
{sourceLabel && ` · via ${sourceLabel}`}
|
||||
{lead.utmCampaign && ` · ${lead.utmCampaign}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Age + Source */}
|
||||
<div className="flex items-center gap-3">
|
||||
{lead.createdAt && <AgeIndicator dateStr={lead.createdAt} />}
|
||||
{lead.leadSource && <SourceTag source={lead.leadSource} />}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{isSpam ? (
|
||||
<>
|
||||
<Button size="sm" color="secondary-destructive" onClick={() => onMarkSpam(lead)}>
|
||||
Mark Spam
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" onClick={() => onMessage(lead)}>
|
||||
Review
|
||||
</Button>
|
||||
</>
|
||||
) : isDuplicate ? (
|
||||
<>
|
||||
<Button size="sm" color="primary" onClick={() => onAssign(lead)}>
|
||||
Assign
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" onClick={() => onMerge(lead)}>
|
||||
Merge
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="sm" color="primary" onClick={() => onAssign(lead)}>
|
||||
Assign
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" onClick={() => onMessage(lead)}>
|
||||
Message
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
139
src/components/leads/source-grid.tsx
Normal file
139
src/components/leads/source-grid.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { cx } from '@/utils/cx';
|
||||
import type { Lead, LeadSource } from '@/types/entities';
|
||||
|
||||
interface SourceGridProps {
|
||||
leads: Lead[];
|
||||
onSourceFilter: (source: LeadSource | null) => void;
|
||||
activeSource: LeadSource | null;
|
||||
}
|
||||
|
||||
type SourceConfig = {
|
||||
source: LeadSource;
|
||||
label: string;
|
||||
icon: string;
|
||||
iconBg: string;
|
||||
iconText: string;
|
||||
countColor: string;
|
||||
delta: string;
|
||||
};
|
||||
|
||||
const sourceConfigs: SourceConfig[] = [
|
||||
{
|
||||
source: 'FACEBOOK_AD',
|
||||
label: 'Facebook Ads',
|
||||
icon: 'f',
|
||||
iconBg: 'bg-utility-blue-50',
|
||||
iconText: 'text-utility-blue-700',
|
||||
countColor: 'text-utility-blue-700',
|
||||
delta: '+4',
|
||||
},
|
||||
{
|
||||
source: 'GOOGLE_AD',
|
||||
label: 'Google Ads',
|
||||
icon: 'G',
|
||||
iconBg: 'bg-utility-success-50',
|
||||
iconText: 'text-utility-success-700',
|
||||
countColor: 'text-utility-success-700',
|
||||
delta: '+2',
|
||||
},
|
||||
{
|
||||
source: 'INSTAGRAM',
|
||||
label: 'Instagram',
|
||||
icon: '@',
|
||||
iconBg: 'bg-utility-pink-50',
|
||||
iconText: 'text-utility-pink-700',
|
||||
countColor: 'text-utility-pink-700',
|
||||
delta: '+1',
|
||||
},
|
||||
{
|
||||
source: 'GOOGLE_MY_BUSINESS',
|
||||
label: 'Google My Business',
|
||||
icon: 'G',
|
||||
iconBg: 'bg-utility-blue-light-50',
|
||||
iconText: 'text-utility-blue-light-700',
|
||||
countColor: 'text-utility-blue-light-700',
|
||||
delta: '+3',
|
||||
},
|
||||
{
|
||||
source: 'REFERRAL',
|
||||
label: 'Referrals',
|
||||
icon: 'R',
|
||||
iconBg: 'bg-utility-purple-50',
|
||||
iconText: 'text-utility-purple-700',
|
||||
countColor: 'text-utility-purple-700',
|
||||
delta: '+2',
|
||||
},
|
||||
{
|
||||
source: 'WALK_IN',
|
||||
label: 'Walk-ins',
|
||||
icon: 'W',
|
||||
iconBg: 'bg-utility-orange-50',
|
||||
iconText: 'text-utility-orange-700',
|
||||
countColor: 'text-utility-orange-700',
|
||||
delta: '0',
|
||||
},
|
||||
];
|
||||
|
||||
export const SourceGrid = ({ leads, onSourceFilter, activeSource }: SourceGridProps) => {
|
||||
const countBySource = (source: LeadSource): number =>
|
||||
leads.filter((l) => l.leadSource === source).length;
|
||||
|
||||
const handleClick = (source: LeadSource) => {
|
||||
if (activeSource === source) {
|
||||
onSourceFilter(null);
|
||||
} else {
|
||||
onSourceFilter(source);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{sourceConfigs.map((config) => {
|
||||
const count = countBySource(config.source);
|
||||
const isActive = activeSource === config.source;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={config.source}
|
||||
type="button"
|
||||
onClick={() => handleClick(config.source)}
|
||||
className={cx(
|
||||
'cursor-pointer rounded-xl border border-secondary bg-primary p-4 text-left transition hover:shadow-md',
|
||||
isActive && 'ring-2 ring-brand',
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span
|
||||
className={cx(
|
||||
'flex size-6 items-center justify-center rounded text-xs font-bold',
|
||||
config.iconBg,
|
||||
config.iconText,
|
||||
)}
|
||||
>
|
||||
{config.icon}
|
||||
</span>
|
||||
<span className="text-xs text-quaternary">{config.label}</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className={cx('text-xl font-bold', config.countColor)}>
|
||||
{count}
|
||||
</span>
|
||||
<span
|
||||
className={cx(
|
||||
'text-xs',
|
||||
config.delta.startsWith('+')
|
||||
? 'text-success-primary'
|
||||
: config.delta === '0'
|
||||
? 'text-quaternary'
|
||||
: 'text-error-primary',
|
||||
)}
|
||||
>
|
||||
{config.delta === '0' ? 'same' : config.delta}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,91 @@
|
||||
import { TopBar } from "@/components/layout/top-bar";
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { TopBar } from '@/components/layout/top-bar';
|
||||
import { KpiCards } from '@/components/leads/kpi-cards';
|
||||
import { SourceGrid } from '@/components/leads/source-grid';
|
||||
import { LeadCard } from '@/components/leads/lead-card';
|
||||
import { AgingWidget } from '@/components/leads/aging-widget';
|
||||
import { FollowupWidget } from '@/components/leads/followup-widget';
|
||||
import { AlertsWidget } from '@/components/leads/alerts-widget';
|
||||
import { useLeads } from '@/hooks/use-leads';
|
||||
import { useFollowUps } from '@/hooks/use-follow-ups';
|
||||
import type { LeadSource } from '@/types/entities';
|
||||
|
||||
export const LeadWorkspacePage = () => {
|
||||
const [sourceFilter, setSourceFilter] = useState<LeadSource | null>(null);
|
||||
|
||||
const { leads, total } = useLeads({ source: sourceFilter ?? undefined, status: 'NEW' });
|
||||
const { leads: allLeads } = useLeads();
|
||||
const { overdue, upcoming } = useFollowUps();
|
||||
|
||||
const displayLeads = leads.slice(0, 10);
|
||||
|
||||
const handleAssign = () => {
|
||||
// placeholder
|
||||
};
|
||||
|
||||
const handleMessage = () => {
|
||||
// placeholder
|
||||
};
|
||||
|
||||
const handleMarkSpam = () => {
|
||||
// placeholder
|
||||
};
|
||||
|
||||
const handleMerge = () => {
|
||||
// placeholder
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<TopBar title="Lead Workspace" subtitle="Ramaiah Memorial Hospital · Last 24 hours" />
|
||||
<div className="flex flex-1 items-center justify-center p-8">
|
||||
<p className="text-tertiary">Lead Workspace — coming soon</p>
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{/* Main content */}
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-7">
|
||||
<KpiCards leads={allLeads} />
|
||||
|
||||
<div>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="font-display text-md font-bold text-primary">Lead Sources</h2>
|
||||
<span className="text-xs text-quaternary">Click to filter</span>
|
||||
</div>
|
||||
<SourceGrid leads={allLeads} onSourceFilter={setSourceFilter} activeSource={sourceFilter} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3.5 flex items-center justify-between">
|
||||
<h2 className="font-display text-md font-bold text-primary">New Leads</h2>
|
||||
<Button href="/leads" color="link-color" size="sm">
|
||||
View All {total} Leads
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{displayLeads.map((lead) => (
|
||||
<LeadCard
|
||||
key={lead.id}
|
||||
lead={lead}
|
||||
onAssign={handleAssign}
|
||||
onMessage={handleMessage}
|
||||
onMarkSpam={handleMarkSpam}
|
||||
onMerge={handleMerge}
|
||||
/>
|
||||
))}
|
||||
{displayLeads.length === 0 && (
|
||||
<p className="py-8 text-center text-sm text-tertiary">
|
||||
No leads match the current filters.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right sidebar */}
|
||||
<aside className="hidden w-80 space-y-5 overflow-y-auto border-l border-secondary bg-primary p-5 xl:block">
|
||||
<AgingWidget leads={allLeads} />
|
||||
<FollowupWidget overdue={overdue} upcoming={upcoming} />
|
||||
<AlertsWidget leads={allLeads} />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user