Clean up/simplify aircalldashboard

This commit is contained in:
2024-12-28 15:57:09 -05:00
parent 7ed6cac8f7
commit 59d4061bd0

View File

@@ -65,6 +65,7 @@ const COLORS = {
missed: "hsl(47.9 95.8% 53.1%)", // Yellow missed: "hsl(47.9 95.8% 53.1%)", // Yellow
answered: "hsl(142.1 76.2% 36.3%)", // Green answered: "hsl(142.1 76.2% 36.3%)", // Green
duration: "hsl(221.2 83.2% 53.3%)", // Blue duration: "hsl(221.2 83.2% 53.3%)", // Blue
hourly: "hsl(321.2 81.1% 41.2%)", // Pink
}; };
const TIME_RANGES = [ const TIME_RANGES = [
@@ -89,10 +90,7 @@ const formatDuration = (seconds) => {
}; };
const MetricCard = ({ title, value, subtitle, icon: Icon, iconColor }) => ( const MetricCard = ({ title, value, subtitle, icon: Icon, iconColor }) => (
<TooltipProvider> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<Tooltip>
<TooltipTrigger asChild>
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="flex flex-row items-center justify-between pb-2 p-4"> <CardHeader className="flex flex-row items-center justify-between pb-2 p-4">
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">{title}</CardTitle> <CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">{title}</CardTitle>
<Icon className={`h-4 w-4 ${iconColor}`} /> <Icon className={`h-4 w-4 ${iconColor}`} />
@@ -104,12 +102,6 @@ const MetricCard = ({ title, value, subtitle, icon: Icon, iconColor }) => (
)} )}
</CardContent> </CardContent>
</Card> </Card>
</TooltipTrigger>
<TooltipContent>
<p>{title}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
); );
const CustomTooltip = ({ active, payload, label }) => { const CustomTooltip = ({ active, payload, label }) => {
@@ -130,39 +122,7 @@ const CustomTooltip = ({ active, payload, label }) => {
return null; return null;
}; };
export const exportToCSV = (data, filename) => {
const headers = [
"Name",
"Total Calls",
"Answered",
"Missed",
"Answer Rate",
"Avg Duration",
];
const rows = data.map((agent) => [
agent.name,
agent.total,
agent.answered,
agent.missed,
`${((agent.answered / agent.total) * 100).toFixed(1)}%`,
formatDuration(agent.average_duration),
]);
const csvContent = [
headers.join(","),
...rows.map((row) => row.join(",")),
].join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `${filename}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const AgentPerformanceTable = ({ agents, onSort }) => { const AgentPerformanceTable = ({ agents, onSort }) => {
const [sortConfig, setSortConfig] = useState({ const [sortConfig, setSortConfig] = useState({
@@ -179,35 +139,15 @@ const AgentPerformanceTable = ({ agents, onSort }) => {
onSort(key, direction); onSort(key, direction);
}; };
const SortButton = ({ column }) => (
<Button
variant="ghost"
size="sm"
onClick={() => handleSort(column)}
className="flex items-center gap-1 hover:bg-transparent"
>
{column.charAt(0).toUpperCase() + column.slice(1)}
<ArrowUpDown className="h-4 w-4" />
</Button>
);
return ( return (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="hover:bg-transparent"> <TableRow className="hover:bg-transparent">
<TableHead>Agent</TableHead> <TableHead>Agent</TableHead>
<TableHead> <TableHead onClick={() => handleSort("total")}>Total Calls</TableHead>
<SortButton column="total" /> <TableHead onClick={() => handleSort("answered")}>Answered</TableHead>
</TableHead> <TableHead onClick={() => handleSort("missed")}>Missed</TableHead>
<TableHead> <TableHead onClick={() => handleSort("average_duration")}>Average Duration</TableHead>
<SortButton column="answered" />
</TableHead>
<TableHead>
<SortButton column="missed" />
</TableHead>
<TableHead>
<SortButton column="average_duration" />
</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -225,67 +165,6 @@ const AgentPerformanceTable = ({ agents, onSort }) => {
); );
}; };
const TableActions = ({ onSearch, onExport }) => {
return (
<div className="flex items-center justify-between py-4">
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Filter agents..."
onChange={(e) => onSearch(e.target.value)}
className="pl-8"
/>
</div>
</div>
<Button variant="outline" onClick={onExport}>
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
);
};
const AgentStatsCard = ({ agent, formatDuration }) => {
const answerRate = ((agent.answered / agent.total) * 100).toFixed(1);
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{agent.name}</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Answer Rate</span>
<span className="font-medium">{answerRate}%</span>
</div>
<Progress value={parseFloat(answerRate)} className="h-2" />
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Total Calls</p>
<p className="text-sm font-medium">{agent.total}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Avg Duration</p>
<p className="text-sm font-medium">
{formatDuration(agent.average_duration)}
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Answered</p>
<p className="text-sm font-medium">{agent.answered}</p>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground">Missed</p>
<p className="text-sm font-medium">{agent.missed}</p>
</div>
</div>
</CardContent>
</Card>
);
};
const AircallDashboard = () => { const AircallDashboard = () => {
const [timeRange, setTimeRange] = useState("last7days"); const [timeRange, setTimeRange] = useState("last7days");
const [metrics, setMetrics] = useState(null); const [metrics, setMetrics] = useState(null);
@@ -296,8 +175,6 @@ const AircallDashboard = () => {
key: "total", key: "total",
direction: "desc", direction: "desc",
}); });
const [searchTerm, setSearchTerm] = useState("");
const [viewType, setViewType] = useState("table"); // 'table' or 'cards'
const safeArray = (arr) => (Array.isArray(arr) ? arr : []); const safeArray = (arr) => (Array.isArray(arr) ? arr : []);
const safeObject = (obj) => (obj && typeof obj === "object" ? obj : {}); const safeObject = (obj) => (obj && typeof obj === "object" ? obj : {});
@@ -309,10 +186,6 @@ const AircallDashboard = () => {
}) })
: []; : [];
const filteredAgents = sortedAgents.filter((agent) =>
agent.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const formatDate = (dateString) => { const formatDate = (dateString) => {
try { try {
// Parse the date string (YYYY-MM-DD) // Parse the date string (YYYY-MM-DD)
@@ -348,7 +221,10 @@ const AircallDashboard = () => {
const chartData = { const chartData = {
hourly: metrics?.by_hour hourly: metrics?.by_hour
? metrics.by_hour.map((count, hour) => ({ ? metrics.by_hour.map((count, hour) => ({
hour: `${hour.toString().padStart(2, "0")}:00`, hour: new Date(2000, 0, 1, hour).toLocaleString('en-US', {
hour: 'numeric',
hour12: true
}).toUpperCase(),
calls: count || 0, calls: count || 0,
})) }))
: [], : [],
@@ -364,7 +240,10 @@ const AircallDashboard = () => {
...day, ...day,
inbound: day.inbound || 0, inbound: day.inbound || 0,
outbound: day.outbound || 0, outbound: day.outbound || 0,
date: day.date || "", date: new Date(day.date).toLocaleString('en-US', {
month: 'short',
day: 'numeric'
}),
})), })),
}; };
@@ -432,299 +311,195 @@ const AircallDashboard = () => {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-6 pt-0 space-y-6"> <CardContent className="p-6 pt-0 space-y-4">
{/* Metric Cards */} {/* Metric Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{isLoading ? ( {isLoading ? (
[...Array(6)].map((_, i) => ( [...Array(4)].map((_, i) => (
<Skeleton key={i} className="h-32 rounded-lg" /> <Skeleton key={i} className="h-32 rounded-lg" />
)) ))
) : metrics ? ( ) : metrics ? (
<> <>
<MetricCard <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
title="Total Calls" <CardHeader className="flex flex-col items-start p-4">
value={metrics.total} <CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Total Calls</CardTitle>
icon={PhoneCall} <div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">{metrics.total}</div>
iconColor="text-gray-500 dark:text-gray-400" <div className="flex gap-4 mt-2">
/> <div className="text-sm">
<MetricCard <span className="text-blue-500"> {metrics.by_direction.inbound}</span> inbound
title="Inbound" </div>
value={metrics.by_direction.inbound} <div className="text-sm">
subtitle={`${( <span className="text-emerald-500"> {metrics.by_direction.outbound}</span> outbound
(metrics.by_direction.inbound / metrics.total) * </div>
100 </div>
).toFixed(1)}%`} </CardHeader>
icon={PhoneIncoming} </Card>
iconColor="text-blue-500 dark:text-blue-400" <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
/> <CardHeader className="flex flex-col items-start p-4">
<MetricCard <CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Answer Rate</CardTitle>
title="Outbound" <div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">
value={metrics.by_direction.outbound} {`${((metrics.by_status.answered / metrics.total) * 100).toFixed(1)}%`}
subtitle={`${( </div>
(metrics.by_direction.outbound / metrics.total) * <div className="flex gap-6">
100 <div className="text-sm">
).toFixed(1)}%`} <span className="text-emerald-500">{metrics.by_status.answered}</span> answered
icon={PhoneOutgoing} </div>
iconColor="text-emerald-500 dark:text-emerald-400" <div className="text-sm">
/> <span className="text-rose-500">{metrics.by_status.missed}</span> missed
<MetricCard </div>
title="Missed" </div>
value={metrics.by_status.missed} </CardHeader>
subtitle={`${( </Card>
(metrics.by_status.missed / metrics.total) * <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
100 <CardHeader className="flex flex-col items-start p-4">
).toFixed(1)}%`} <CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Peak Hour</CardTitle>
icon={PhoneMissed} <div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">
iconColor="text-rose-500 dark:text-rose-400" {metrics?.by_hour ? new Date(2000, 0, 1, metrics.by_hour.indexOf(Math.max(...metrics.by_hour))).toLocaleString('en-US', { hour: 'numeric', hour12: true }).toUpperCase() : 'N/A'}
/> </div>
<MetricCard <div className="text-sm text-muted-foreground mt-2">
title="Avg Duration" Busiest Agent: {sortedAgents[0]?.name || "N/A"}
value={formatDuration(metrics.average_duration)} </div>
icon={Clock} </CardHeader>
iconColor="text-purple-500 dark:text-purple-400" </Card>
/> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<MetricCard <CardHeader className="flex flex-col items-start p-4">
title="Answer Rate" <CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Avg Duration</CardTitle>
value={`${( <TooltipProvider>
(metrics.by_status.answered / metrics.total) * <Tooltip>
100 <TooltipTrigger asChild>
).toFixed(1)}%`} <div>
icon={UserCheck} <div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
iconColor="text-emerald-500 dark:text-emerald-400" {formatDuration(metrics.average_duration)}
/> </div>
<div className="text-sm text-muted-foreground mt-2">
{metrics?.daily_data?.length > 0
? `${Math.round(metrics.total / metrics.daily_data.length)} calls/day`
: "N/A"}
</div>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="w-[300px]">
<div className="space-y-2">
<p className="font-medium">Duration Distribution</p>
{metrics?.duration_distribution?.map((d, i) => (
<div key={i} className="flex justify-between text-sm">
<span>{d.range}</span>
<span>{d.count} calls</span>
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</CardHeader>
</Card>
</> </>
) : null} ) : null}
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {/* Charts and Tables Section */}
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm"> <div className="space-y-4">
<CardHeader> {/* Charts Row */}
<CardTitle className="text-sm font-medium text-gray-900 dark:text-gray-100">Performance Summary</CardTitle> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
</CardHeader> {/* Daily Call Volume */}
<CardContent className="space-y-2"> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<div className="flex justify-between items-center"> <CardHeader className="p-4">
<span className="text-sm text-muted-foreground">Peak Hour</span> <CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Daily Call Volume</CardTitle>
<span className="font-medium text-gray-900 dark:text-gray-100"> </CardHeader>
{metrics?.by_hour.indexOf(Math.max(...metrics.by_hour))}:00 <CardContent className="h-[300px]">
</span> <ResponsiveContainer width="100%" height="100%">
</div> <BarChart data={chartData.daily} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
<div className="flex justify-between items-center"> <CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
<span className="text-sm text-muted-foreground">Busiest Agent</span> <XAxis
<span className="font-medium text-gray-900 dark:text-gray-100"> dataKey="date"
{sortedAgents[0]?.name || "N/A"} tick={{ fontSize: 12 }}
</span> className="text-gray-600 dark:text-gray-300"
</div> />
<div className="flex justify-between items-center"> <YAxis
<span className="text-sm text-muted-foreground">Best Answer Rate</span> tick={{ fontSize: 12 }}
<span className="font-medium text-gray-900 dark:text-gray-100"> className="text-gray-600 dark:text-gray-300"
{sortedAgents />
.filter((agent) => agent.total > 0) <RechartsTooltip content={<CustomTooltip />} />
.sort( <Legend />
(a, b) => b.answered / b.total - a.answered / a.total <Bar dataKey="inbound" fill={COLORS.inbound} name="Inbound" />
)[0]?.name || "N/A"} <Bar dataKey="outbound" fill={COLORS.outbound} name="Outbound" />
</span> </BarChart>
</div> </ResponsiveContainer>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm"> {/* Hourly Distribution */}
<CardHeader> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardTitle className="text-sm font-medium text-gray-900 dark:text-gray-100">Time Period</CardTitle> <CardHeader className="p-4">
</CardHeader> <CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Hourly Distribution</CardTitle>
<CardContent className="space-y-2"> </CardHeader>
<div className="flex justify-between items-center"> <CardContent className="h-[300px]">
<span className="text-sm text-muted-foreground">Date Range</span> <ResponsiveContainer width="100%" height="100%">
<span className="font-medium text-gray-900 dark:text-gray-100"> <BarChart data={chartData.hourly} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
{metrics?.daily_data?.length > 0 ? ( <CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
<> <XAxis
{formatDate(metrics.daily_data[0]?.date)} -{" "} dataKey="hour"
{formatDate( tick={{ fontSize: 12 }}
metrics.daily_data[metrics.daily_data.length - 1]?.date interval={2}
)} className="text-gray-600 dark:text-gray-300"
</> />
) : ( <YAxis
"No data available" tick={{ fontSize: 12 }}
)} className="text-gray-600 dark:text-gray-300"
</span> />
</div> <RechartsTooltip content={<CustomTooltip />} />
<div className="flex justify-between items-center"> <Bar dataKey="calls" fill={COLORS.hourly} name="Calls" />
<span className="text-sm text-muted-foreground">Avg Daily Calls</span> </BarChart>
<span className="font-medium text-gray-900 dark:text-gray-100"> </ResponsiveContainer>
{metrics?.daily_data?.length > 0 </CardContent>
? Math.round(metrics.total / metrics.daily_data.length) </Card>
: "N/A"} </div>
</span>
</div> {/* Tables Row */}
<div className="flex justify-between items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<span className="text-sm text-muted-foreground">Avg Duration</span> {/* Agent Performance */}
<span className="font-medium text-gray-900 dark:text-gray-100"> <Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
{metrics?.average_duration <CardHeader className="p-4">
? formatDuration(metrics.average_duration) <CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Agent Performance</CardTitle>
: "N/A"} </CardHeader>
</span> <CardContent>
</div> <AgentPerformanceTable
</CardContent> agents={sortedAgents}
</Card> onSort={(key, direction) => setAgentSort({ key, direction })}
/>
</CardContent>
</Card>
{/* Missed Call Reasons Table */}
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader className="p-4">
<CardTitle className="text-lg font-semibold text-gray-900 dark:text-gray-100">Missed Call Reasons</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead>Reason</TableHead>
<TableHead className="text-right">Count</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{chartData.missedReasons.map((reason, index) => (
<TableRow key={index} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
<TableCell className="font-medium text-gray-900 dark:text-gray-100">
{reason.reason}
</TableCell>
<TableCell className="text-right text-rose-600 dark:text-rose-400">
{reason.count}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</div> </div>
{/* Charts */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Daily Call Volume */}
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-900 dark:text-gray-100">Daily Call Volume</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData.daily}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
<XAxis
dataKey="date"
tick={{ fontSize: 12 }}
className="text-gray-600 dark:text-gray-300"
/>
<YAxis
tick={{ fontSize: 12 }}
className="text-gray-600 dark:text-gray-300"
/>
<RechartsTooltip content={<CustomTooltip />} />
<Legend />
<Line
type="monotone"
dataKey="inbound"
stroke={COLORS.inbound}
name="Inbound"
strokeWidth={2}
dot={false}
/>
<Line
type="monotone"
dataKey="outbound"
stroke={COLORS.outbound}
name="Outbound"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Duration Distribution */}
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-900 dark:text-gray-100">Call Duration Distribution</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={metrics?.duration_distribution || []}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
<XAxis
dataKey="range"
className="text-gray-600 dark:text-gray-300"
/>
<YAxis className="text-gray-600 dark:text-gray-300" />
<RechartsTooltip content={<CustomTooltip />} />
<Bar dataKey="count" fill={COLORS.duration} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Missed Call Reasons */}
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-900 dark:text-gray-100">Missed Call Reasons</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData.missedReasons} layout="vertical">
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
<XAxis type="number" className="text-gray-600 dark:text-gray-300" />
<YAxis
dataKey="reason"
type="category"
width={150}
className="text-gray-600 dark:text-gray-300"
/>
<RechartsTooltip content={<CustomTooltip />} />
<Bar dataKey="count" fill={COLORS.missed} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Hourly Distribution */}
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-900 dark:text-gray-100">Hourly Distribution</CardTitle>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={chartData.hourly}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-gray-700" />
<XAxis
dataKey="hour"
tick={{ fontSize: 12 }}
interval={2}
className="text-gray-600 dark:text-gray-300"
/>
<YAxis
tick={{ fontSize: 12 }}
className="text-gray-600 dark:text-gray-300"
/>
<RechartsTooltip content={<CustomTooltip />} />
<Bar dataKey="calls" fill={COLORS.inbound} name="Calls" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Agent Performance */}
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle className="text-sm font-medium text-gray-900 dark:text-gray-100">Agent Performance</CardTitle>
<div className="flex gap-2">
<Button
variant={viewType === "table" ? "default" : "outline"}
size="sm"
onClick={() => setViewType("table")}
>
Table
</Button>
<Button
variant={viewType === "cards" ? "default" : "outline"}
size="sm"
onClick={() => setViewType("cards")}
>
Cards
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<TableActions onSearch={setSearchTerm} onExport={handleExport} />
{viewType === "table" ? (
<AgentPerformanceTable
agents={filteredAgents}
onSort={(key, direction) => setAgentSort({ key, direction })}
/>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredAgents.map((agent) => (
<AgentStatsCard
key={agent.id}
agent={agent}
formatDuration={formatDuration}
/>
))}
</div>
)}
</CardContent>
</Card>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>