mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: build all data pages — worklist table, call history, patients, dashboard, reports
Worklist (call-desk): - Upgrade to Untitled UI Table with columns: Priority, Patient, Phone, Type, SLA, Actions - Filter tabs: All Tasks / Missed Calls / Callbacks / Follow-ups with counts - Search by name or phone - SLA timer color-coded: green <15m, amber <30m, red >30m Call History: - Full table: Type (direction icon), Patient (matched from leads), Phone, Duration, Outcome, Agent, Recording (play/pause), Time - Search + All/Inbound/Outbound/Missed filter - Recording playback via native <audio> Patients: - New page with table: Patient (avatar+name+age), Contact, Type, Gender, Status, Actions - Search + status filter - Call + View Details actions - Added patients to DataProvider + transforms + queries - Route /patients added, sidebar nav updated for cc-agent + executive Supervisor Dashboard: - KPI cards: Total Calls, Inbound, Outbound, Missed - Performance metrics: Avg Response Time, Callback Time, Conversion % - Agent performance table with per-agent stats - Missed Call Queue - AI Assistant section - Day/Week/Month filter Reports: - ECharts bar chart: Call Volume Trend (7-day, Inbound vs Outbound) - ECharts donut chart: Call Outcomes (Booked, Follow-up, Info, Missed) - KPI cards with trend indicators (+/-%) - Route /reports, sidebar Analytics → /reports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { FC, HTMLAttributes } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPhoneXmark, faBell, faUsers } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import {
|
||||
faPhoneArrowDown,
|
||||
faPhoneArrowUp,
|
||||
} from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { SearchLg } from '@untitledui/icons';
|
||||
import { Table, TableCard } from '@/components/application/table/table';
|
||||
import { Badge } from '@/components/base/badges/badges';
|
||||
import { Input } from '@/components/base/input/input';
|
||||
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
|
||||
import { ClickToCallButton } from './click-to-call-button';
|
||||
import { formatPhone } from '@/lib/format';
|
||||
import { cx } from '@/utils/cx';
|
||||
@@ -20,6 +28,7 @@ type WorklistLead = {
|
||||
|
||||
type WorklistFollowUp = {
|
||||
id: string;
|
||||
createdAt: string | null;
|
||||
followUpType: string | null;
|
||||
followUpStatus: string | null;
|
||||
scheduledAt: string | null;
|
||||
@@ -29,6 +38,7 @@ type WorklistFollowUp = {
|
||||
type MissedCall = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
callDirection: string | null;
|
||||
callerNumber: { number: string; callingCode: string }[] | null;
|
||||
startedAt: string | null;
|
||||
leadId: string | null;
|
||||
@@ -43,54 +53,184 @@ interface WorklistPanelProps {
|
||||
selectedLeadId: string | null;
|
||||
}
|
||||
|
||||
const IconMissed: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faPhoneXmark} className={className} />
|
||||
);
|
||||
const IconFollowUp: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faBell} className={className} />
|
||||
);
|
||||
const IconLeads: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faUsers} className={className} />
|
||||
);
|
||||
type TabKey = 'all' | 'missed' | 'callbacks' | 'follow-ups';
|
||||
|
||||
const formatAge = (dateStr: string): string => {
|
||||
const minutes = Math.max(0, Math.round((Date.now() - new Date(dateStr).getTime()) / 60000));
|
||||
if (minutes < 1) return 'Just now';
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return `${Math.floor(hours / 24)}d ago`;
|
||||
// Unified row type for the table
|
||||
type WorklistRow = {
|
||||
id: string;
|
||||
type: 'missed' | 'callback' | 'follow-up' | 'lead';
|
||||
priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';
|
||||
name: string;
|
||||
phone: string;
|
||||
phoneRaw: string;
|
||||
direction: 'inbound' | 'outbound' | null;
|
||||
typeLabel: string;
|
||||
reason: string;
|
||||
createdAt: string;
|
||||
taskState: 'PENDING' | 'ATTEMPTED' | 'SCHEDULED';
|
||||
leadId: string | null;
|
||||
originalLead: WorklistLead | null;
|
||||
};
|
||||
|
||||
const priorityConfig: Record<string, { color: 'error' | 'warning' | 'brand' | 'gray'; label: string; sort: number }> = {
|
||||
URGENT: { color: 'error', label: 'Urgent', sort: 0 },
|
||||
HIGH: { color: 'warning', label: 'High', sort: 1 },
|
||||
NORMAL: { color: 'brand', label: 'Normal', sort: 2 },
|
||||
LOW: { color: 'gray', label: 'Low', sort: 3 },
|
||||
};
|
||||
|
||||
const followUpLabel: Record<string, string> = {
|
||||
CALLBACK: 'Callback',
|
||||
APPOINTMENT_REMINDER: 'Appointment Reminder',
|
||||
POST_VISIT: 'Post-visit Follow-up',
|
||||
APPOINTMENT_REMINDER: 'Appt Reminder',
|
||||
POST_VISIT: 'Post-visit',
|
||||
MARKETING: 'Marketing',
|
||||
REVIEW_REQUEST: 'Review Request',
|
||||
REVIEW_REQUEST: 'Review',
|
||||
};
|
||||
|
||||
const priorityConfig: Record<string, { color: 'error' | 'warning' | 'brand' | 'gray'; label: string }> = {
|
||||
URGENT: { color: 'error', label: 'Urgent' },
|
||||
HIGH: { color: 'warning', label: 'High' },
|
||||
NORMAL: { color: 'brand', label: 'Normal' },
|
||||
LOW: { color: 'gray', label: 'Low' },
|
||||
// Compute SLA: minutes since created, color-coded
|
||||
const computeSla = (createdAt: string): { label: string; color: 'success' | 'warning' | 'error' } => {
|
||||
const minutes = Math.max(0, Math.round((Date.now() - new Date(createdAt).getTime()) / 60000));
|
||||
if (minutes < 1) return { label: '<1m', color: 'success' };
|
||||
if (minutes < 15) return { label: `${minutes}m`, color: 'success' };
|
||||
if (minutes < 30) return { label: `${minutes}m`, color: 'warning' };
|
||||
if (minutes < 60) return { label: `${minutes}m`, color: 'error' };
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return { label: `${hours}h ${minutes % 60}m`, color: 'error' };
|
||||
return { label: `${Math.floor(hours / 24)}d`, color: 'error' };
|
||||
};
|
||||
|
||||
const SectionHeader = ({ icon: Icon, title, count, color }: {
|
||||
icon: FC<HTMLAttributes<HTMLOrSVGElement>>;
|
||||
title: string;
|
||||
count: number;
|
||||
color: 'error' | 'blue' | 'brand';
|
||||
}) => (
|
||||
<div className="flex items-center gap-2 px-4 pt-4 pb-2">
|
||||
<Icon className="size-4 text-fg-quaternary" />
|
||||
<span className="text-xs font-bold text-tertiary uppercase tracking-wider">{title}</span>
|
||||
{count > 0 && <Badge size="sm" color={color} type="pill-color">{count}</Badge>}
|
||||
</div>
|
||||
const IconInbound: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faPhoneArrowDown} className={className} />
|
||||
);
|
||||
const IconOutbound: FC<HTMLAttributes<HTMLOrSVGElement>> = ({ className }) => (
|
||||
<FontAwesomeIcon icon={faPhoneArrowUp} className={className} />
|
||||
);
|
||||
|
||||
const buildRows = (missedCalls: MissedCall[], followUps: WorklistFollowUp[], leads: WorklistLead[]): WorklistRow[] => {
|
||||
const rows: WorklistRow[] = [];
|
||||
|
||||
for (const call of missedCalls) {
|
||||
const phone = call.callerNumber?.[0];
|
||||
rows.push({
|
||||
id: `mc-${call.id}`,
|
||||
type: 'missed',
|
||||
priority: 'HIGH',
|
||||
name: phone ? formatPhone(phone) : 'Unknown',
|
||||
phone: phone ? formatPhone(phone) : '',
|
||||
phoneRaw: phone?.number ?? '',
|
||||
direction: call.callDirection === 'OUTBOUND' ? 'outbound' : 'inbound',
|
||||
typeLabel: 'Missed Call',
|
||||
reason: call.startedAt
|
||||
? `Missed at ${new Date(call.startedAt).toLocaleTimeString('en-IN', { hour: 'numeric', minute: '2-digit', hour12: true })}`
|
||||
: 'Missed call',
|
||||
createdAt: call.createdAt,
|
||||
taskState: 'PENDING',
|
||||
leadId: call.leadId,
|
||||
originalLead: null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const fu of followUps) {
|
||||
const isOverdue = fu.followUpStatus === 'OVERDUE' || (fu.scheduledAt !== null && new Date(fu.scheduledAt) < new Date());
|
||||
const label = followUpLabel[fu.followUpType ?? ''] ?? fu.followUpType ?? 'Follow-up';
|
||||
rows.push({
|
||||
id: `fu-${fu.id}`,
|
||||
type: fu.followUpType === 'CALLBACK' ? 'callback' : 'follow-up',
|
||||
priority: (fu.priority as WorklistRow['priority']) ?? (isOverdue ? 'HIGH' : 'NORMAL'),
|
||||
name: label,
|
||||
phone: '',
|
||||
phoneRaw: '',
|
||||
direction: null,
|
||||
typeLabel: fu.followUpType === 'CALLBACK' ? 'Callback' : 'Follow-up',
|
||||
reason: fu.scheduledAt
|
||||
? `Scheduled ${new Date(fu.scheduledAt).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })}`
|
||||
: '',
|
||||
createdAt: fu.createdAt ?? fu.scheduledAt ?? new Date().toISOString(),
|
||||
taskState: isOverdue ? 'PENDING' : (fu.followUpStatus === 'COMPLETED' ? 'ATTEMPTED' : 'SCHEDULED'),
|
||||
leadId: null,
|
||||
originalLead: null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const lead of leads) {
|
||||
const firstName = lead.contactName?.firstName ?? '';
|
||||
const lastName = lead.contactName?.lastName ?? '';
|
||||
const fullName = `${firstName} ${lastName}`.trim() || 'Unknown';
|
||||
const phone = lead.contactPhone?.[0];
|
||||
rows.push({
|
||||
id: `lead-${lead.id}`,
|
||||
type: 'lead',
|
||||
priority: 'NORMAL',
|
||||
name: fullName,
|
||||
phone: phone ? formatPhone(phone) : '',
|
||||
phoneRaw: phone?.number ?? '',
|
||||
direction: null,
|
||||
typeLabel: 'Lead',
|
||||
reason: lead.interestedService ?? lead.aiSuggestedAction ?? '',
|
||||
createdAt: lead.createdAt,
|
||||
taskState: 'PENDING',
|
||||
leadId: lead.id,
|
||||
originalLead: lead,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by priority (urgent first), then by creation time (oldest first)
|
||||
rows.sort((a, b) => {
|
||||
const pa = priorityConfig[a.priority]?.sort ?? 2;
|
||||
const pb = priorityConfig[b.priority]?.sort ?? 2;
|
||||
if (pa !== pb) return pa - pb;
|
||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
});
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
const typeConfig: Record<WorklistRow['type'], { color: 'error' | 'brand' | 'blue-light' | 'gray' }> = {
|
||||
missed: { color: 'error' },
|
||||
callback: { color: 'brand' },
|
||||
'follow-up': { color: 'blue-light' },
|
||||
lead: { color: 'gray' },
|
||||
};
|
||||
|
||||
export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelectLead, selectedLeadId }: WorklistPanelProps) => {
|
||||
const [tab, setTab] = useState<TabKey>('all');
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const allRows = useMemo(
|
||||
() => buildRows(missedCalls, followUps, leads),
|
||||
[missedCalls, followUps, leads],
|
||||
);
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
let rows = allRows;
|
||||
|
||||
// Tab filter
|
||||
if (tab === 'missed') rows = rows.filter((r) => r.type === 'missed');
|
||||
else if (tab === 'callbacks') rows = rows.filter((r) => r.type === 'callback');
|
||||
else if (tab === 'follow-ups') rows = rows.filter((r) => r.type === 'follow-up');
|
||||
|
||||
// Search filter
|
||||
if (search.trim()) {
|
||||
const q = search.toLowerCase();
|
||||
rows = rows.filter(
|
||||
(r) => r.name.toLowerCase().includes(q) || r.phone.toLowerCase().includes(q) || r.phoneRaw.includes(q),
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}, [allRows, tab, search]);
|
||||
|
||||
const missedCount = allRows.filter((r) => r.type === 'missed').length;
|
||||
const callbackCount = allRows.filter((r) => r.type === 'callback').length;
|
||||
const followUpCount = allRows.filter((r) => r.type === 'follow-up').length;
|
||||
|
||||
const tabItems = [
|
||||
{ id: 'all' as const, label: 'All Tasks', badge: allRows.length > 0 ? String(allRows.length) : undefined },
|
||||
{ id: 'missed' as const, label: 'Missed Calls', badge: missedCount > 0 ? String(missedCount) : undefined },
|
||||
{ id: 'callbacks' as const, label: 'Callbacks', badge: callbackCount > 0 ? String(callbackCount) : undefined },
|
||||
{ id: 'follow-ups' as const, label: 'Follow-ups', badge: followUpCount > 0 ? String(followUpCount) : undefined },
|
||||
];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
@@ -111,112 +251,124 @@ export const WorklistPanel = ({ missedCalls, followUps, leads, loading, onSelect
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-secondary">
|
||||
{/* Missed calls */}
|
||||
{missedCalls.length > 0 && (
|
||||
<div>
|
||||
<SectionHeader icon={IconMissed} title="Missed Calls" count={missedCalls.length} color="error" />
|
||||
<div className="px-3 pb-3">
|
||||
{missedCalls.map((call) => {
|
||||
const phone = call.callerNumber?.[0];
|
||||
const phoneDisplay = phone ? formatPhone(phone) : 'Unknown number';
|
||||
const phoneNumber = phone?.number ?? '';
|
||||
return (
|
||||
<div key={call.id} className="flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 hover:bg-primary_hover transition duration-100 ease-linear">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-primary">{phoneDisplay}</span>
|
||||
<Badge size="sm" color="error" type="pill-color">
|
||||
{call.createdAt ? formatAge(call.createdAt) : 'Unknown'}
|
||||
</Badge>
|
||||
</div>
|
||||
{call.startedAt && (
|
||||
<p className="text-xs text-tertiary mt-0.5">
|
||||
{new Date(call.startedAt).toLocaleTimeString('en-IN', { hour: 'numeric', minute: '2-digit', hour12: true })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ClickToCallButton phoneNumber={phoneNumber} leadId={call.leadId ?? undefined} label="Call Back" size="sm" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<TableCard.Root size="sm">
|
||||
<TableCard.Header
|
||||
title="Worklist"
|
||||
badge={String(allRows.length)}
|
||||
contentTrailing={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-48">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
icon={SearchLg}
|
||||
size="sm"
|
||||
value={search}
|
||||
onChange={(value) => setSearch(value)}
|
||||
aria-label="Search worklist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Follow-ups */}
|
||||
{followUps.length > 0 && (
|
||||
<div>
|
||||
<SectionHeader icon={IconFollowUp} title="Follow-ups" count={followUps.length} color="blue" />
|
||||
<div className="px-3 pb-3 space-y-1">
|
||||
{followUps.map((fu) => {
|
||||
const isOverdue = fu.followUpStatus === 'OVERDUE' ||
|
||||
(fu.scheduledAt && new Date(fu.scheduledAt) < new Date());
|
||||
const label = followUpLabel[fu.followUpType ?? ''] ?? fu.followUpType ?? 'Follow-up';
|
||||
const priority = priorityConfig[fu.priority ?? 'NORMAL'] ?? priorityConfig.NORMAL;
|
||||
{/* Filter tabs */}
|
||||
<div className="border-b border-secondary px-4">
|
||||
<Tabs selectedKey={tab} onSelectionChange={(key) => setTab(key as TabKey)}>
|
||||
<TabList items={tabItems} type="underline" size="sm">
|
||||
{(item) => <Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />}
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{filteredRows.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-sm text-quaternary">
|
||||
{search ? 'No matching items' : 'No items in this category'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Head label="PRIORITY" className="w-20" />
|
||||
<Table.Head label="PATIENT" />
|
||||
<Table.Head label="PHONE" />
|
||||
<Table.Head label="TYPE" />
|
||||
<Table.Head label="SLA" className="w-20" />
|
||||
<Table.Head label="ACTIONS" className="w-24" />
|
||||
</Table.Header>
|
||||
<Table.Body items={filteredRows}>
|
||||
{(row) => {
|
||||
const priority = priorityConfig[row.priority] ?? priorityConfig.NORMAL;
|
||||
const sla = computeSla(row.createdAt);
|
||||
const typeCfg = typeConfig[row.type];
|
||||
const isSelected = row.originalLead !== null && row.originalLead.id === selectedLeadId;
|
||||
|
||||
return (
|
||||
<div key={fu.id} className={cx(
|
||||
"rounded-lg px-3 py-2.5 transition duration-100 ease-linear",
|
||||
isOverdue ? "bg-error-primary" : "hover:bg-primary_hover",
|
||||
)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-primary">{label}</span>
|
||||
{isOverdue && <Badge size="sm" color="error" type="pill-color">Overdue</Badge>}
|
||||
<Badge size="sm" color={priority.color} type="pill-color">{priority.label}</Badge>
|
||||
</div>
|
||||
{fu.scheduledAt && (
|
||||
<p className="text-xs text-tertiary mt-0.5">
|
||||
{new Date(fu.scheduledAt).toLocaleString('en-IN', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Assigned leads */}
|
||||
{leads.length > 0 && (
|
||||
<div>
|
||||
<SectionHeader icon={IconLeads} title="Assigned Leads" count={leads.length} color="brand" />
|
||||
<div className="px-3 pb-3 space-y-1">
|
||||
{leads.map((lead) => {
|
||||
const firstName = lead.contactName?.firstName ?? '';
|
||||
const lastName = lead.contactName?.lastName ?? '';
|
||||
const fullName = `${firstName} ${lastName}`.trim() || 'Unknown';
|
||||
const phone = lead.contactPhone?.[0];
|
||||
const phoneDisplay = phone ? formatPhone(phone) : '';
|
||||
const phoneNumber = phone?.number ?? '';
|
||||
const isSelected = lead.id === selectedLeadId;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={lead.id}
|
||||
onClick={() => onSelectLead(lead)}
|
||||
<Table.Row
|
||||
id={row.id}
|
||||
className={cx(
|
||||
"flex items-center justify-between gap-3 rounded-lg px-3 py-2.5 cursor-pointer transition duration-100 ease-linear",
|
||||
isSelected ? "bg-brand-primary ring-1 ring-brand" : "hover:bg-primary_hover",
|
||||
'cursor-pointer',
|
||||
isSelected && 'bg-brand-primary',
|
||||
)}
|
||||
onAction={() => {
|
||||
if (row.originalLead) {
|
||||
onSelectLead(row.originalLead);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-sm font-semibold text-primary">{fullName}</span>
|
||||
{phoneDisplay && <span className="text-xs text-tertiary">{phoneDisplay}</span>}
|
||||
<Table.Cell>
|
||||
<Badge size="sm" color={priority.color} type="pill-color">
|
||||
{priority.label}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div className="flex items-center gap-2">
|
||||
{row.direction === 'inbound' && (
|
||||
<IconInbound className="size-3.5 text-fg-success-secondary" />
|
||||
)}
|
||||
{row.direction === 'outbound' && (
|
||||
<IconOutbound className="size-3.5 text-fg-brand-secondary" />
|
||||
)}
|
||||
<span className="text-sm font-medium text-primary truncate max-w-[140px]">
|
||||
{row.name}
|
||||
</span>
|
||||
</div>
|
||||
{lead.interestedService && (
|
||||
<p className="text-xs text-quaternary mt-0.5">{lead.interestedService}</p>
|
||||
)}
|
||||
</div>
|
||||
<ClickToCallButton phoneNumber={phoneNumber} leadId={lead.id} size="sm" />
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span className="text-sm text-tertiary whitespace-nowrap">
|
||||
{row.phone || '\u2014'}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge size="sm" color={typeCfg.color} type="pill-color">
|
||||
{row.typeLabel}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge size="sm" color={sla.color} type="pill-color">
|
||||
{sla.label}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div className="flex items-center gap-1">
|
||||
{row.phoneRaw ? (
|
||||
<ClickToCallButton
|
||||
phoneNumber={row.phoneRaw}
|
||||
leadId={row.leadId ?? undefined}
|
||||
size="sm"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-quaternary">No phone</span>
|
||||
)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
}}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</TableCard.Root>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user