Clean up/simplify aircalldashboard
This commit is contained in:
@@ -65,6 +65,7 @@ const COLORS = {
|
||||
missed: "hsl(47.9 95.8% 53.1%)", // Yellow
|
||||
answered: "hsl(142.1 76.2% 36.3%)", // Green
|
||||
duration: "hsl(221.2 83.2% 53.3%)", // Blue
|
||||
hourly: "hsl(321.2 81.1% 41.2%)", // Pink
|
||||
};
|
||||
|
||||
const TIME_RANGES = [
|
||||
@@ -89,10 +90,7 @@ const formatDuration = (seconds) => {
|
||||
};
|
||||
|
||||
const MetricCard = ({ title, value, subtitle, icon: Icon, iconColor }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
<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">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">{title}</CardTitle>
|
||||
<Icon className={`h-4 w-4 ${iconColor}`} />
|
||||
@@ -104,12 +102,6 @@ const MetricCard = ({ title, value, subtitle, icon: Icon, iconColor }) => (
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{title}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }) => {
|
||||
@@ -130,39 +122,7 @@ const CustomTooltip = ({ active, payload, label }) => {
|
||||
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 [sortConfig, setSortConfig] = useState({
|
||||
@@ -179,35 +139,15 @@ const AgentPerformanceTable = ({ agents, onSort }) => {
|
||||
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 (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead>Agent</TableHead>
|
||||
<TableHead>
|
||||
<SortButton column="total" />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<SortButton column="answered" />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<SortButton column="missed" />
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<SortButton column="average_duration" />
|
||||
</TableHead>
|
||||
<TableHead onClick={() => handleSort("total")}>Total Calls</TableHead>
|
||||
<TableHead onClick={() => handleSort("answered")}>Answered</TableHead>
|
||||
<TableHead onClick={() => handleSort("missed")}>Missed</TableHead>
|
||||
<TableHead onClick={() => handleSort("average_duration")}>Average Duration</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<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 [timeRange, setTimeRange] = useState("last7days");
|
||||
const [metrics, setMetrics] = useState(null);
|
||||
@@ -296,8 +175,6 @@ const AircallDashboard = () => {
|
||||
key: "total",
|
||||
direction: "desc",
|
||||
});
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [viewType, setViewType] = useState("table"); // 'table' or 'cards'
|
||||
|
||||
const safeArray = (arr) => (Array.isArray(arr) ? arr : []);
|
||||
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) => {
|
||||
try {
|
||||
// Parse the date string (YYYY-MM-DD)
|
||||
@@ -348,7 +221,10 @@ const AircallDashboard = () => {
|
||||
const chartData = {
|
||||
hourly: metrics?.by_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,
|
||||
}))
|
||||
: [],
|
||||
@@ -364,7 +240,10 @@ const AircallDashboard = () => {
|
||||
...day,
|
||||
inbound: day.inbound || 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>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-6 pt-0 space-y-6">
|
||||
<CardContent className="p-6 pt-0 space-y-4">
|
||||
{/* 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 ? (
|
||||
[...Array(6)].map((_, i) => (
|
||||
[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-32 rounded-lg" />
|
||||
))
|
||||
) : metrics ? (
|
||||
<>
|
||||
<MetricCard
|
||||
title="Total Calls"
|
||||
value={metrics.total}
|
||||
icon={PhoneCall}
|
||||
iconColor="text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Inbound"
|
||||
value={metrics.by_direction.inbound}
|
||||
subtitle={`${(
|
||||
(metrics.by_direction.inbound / metrics.total) *
|
||||
100
|
||||
).toFixed(1)}%`}
|
||||
icon={PhoneIncoming}
|
||||
iconColor="text-blue-500 dark:text-blue-400"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Outbound"
|
||||
value={metrics.by_direction.outbound}
|
||||
subtitle={`${(
|
||||
(metrics.by_direction.outbound / metrics.total) *
|
||||
100
|
||||
).toFixed(1)}%`}
|
||||
icon={PhoneOutgoing}
|
||||
iconColor="text-emerald-500 dark:text-emerald-400"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Missed"
|
||||
value={metrics.by_status.missed}
|
||||
subtitle={`${(
|
||||
(metrics.by_status.missed / metrics.total) *
|
||||
100
|
||||
).toFixed(1)}%`}
|
||||
icon={PhoneMissed}
|
||||
iconColor="text-rose-500 dark:text-rose-400"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Avg Duration"
|
||||
value={formatDuration(metrics.average_duration)}
|
||||
icon={Clock}
|
||||
iconColor="text-purple-500 dark:text-purple-400"
|
||||
/>
|
||||
<MetricCard
|
||||
title="Answer Rate"
|
||||
value={`${(
|
||||
(metrics.by_status.answered / metrics.total) *
|
||||
100
|
||||
).toFixed(1)}%`}
|
||||
icon={UserCheck}
|
||||
iconColor="text-emerald-500 dark:text-emerald-400"
|
||||
/>
|
||||
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
<CardHeader className="flex flex-col items-start p-4">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Total Calls</CardTitle>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">{metrics.total}</div>
|
||||
<div className="flex gap-4 mt-2">
|
||||
<div className="text-sm">
|
||||
<span className="text-blue-500">↑ {metrics.by_direction.inbound}</span> inbound
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-emerald-500">↓ {metrics.by_direction.outbound}</span> outbound
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
<CardHeader className="flex flex-col items-start p-4">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Answer Rate</CardTitle>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">
|
||||
{`${((metrics.by_status.answered / metrics.total) * 100).toFixed(1)}%`}
|
||||
</div>
|
||||
<div className="flex gap-6">
|
||||
<div className="text-sm">
|
||||
<span className="text-emerald-500">{metrics.by_status.answered}</span> answered
|
||||
</div>
|
||||
<div className="text-sm">
|
||||
<span className="text-rose-500">{metrics.by_status.missed}</span> missed
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
<CardHeader className="flex flex-col items-start p-4">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Peak Hour</CardTitle>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2">
|
||||
{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>
|
||||
<div className="text-sm text-muted-foreground mt-2">
|
||||
Busiest Agent: {sortedAgents[0]?.name || "N/A"}
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
<CardHeader className="flex flex-col items-start p-4">
|
||||
<CardTitle className="text-sm font-medium text-gray-600 dark:text-gray-300">Avg Duration</CardTitle>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{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}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<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">Performance Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Peak Hour</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{metrics?.by_hour.indexOf(Math.max(...metrics.by_hour))}:00
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Busiest Agent</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{sortedAgents[0]?.name || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Best Answer Rate</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{sortedAgents
|
||||
.filter((agent) => agent.total > 0)
|
||||
.sort(
|
||||
(a, b) => b.answered / b.total - a.answered / a.total
|
||||
)[0]?.name || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Charts and Tables Section */}
|
||||
<div className="space-y-4">
|
||||
{/* Charts Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Daily Call Volume */}
|
||||
<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">Daily Call Volume</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData.daily} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
||||
<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 />
|
||||
<Bar dataKey="inbound" fill={COLORS.inbound} name="Inbound" />
|
||||
<Bar dataKey="outbound" fill={COLORS.outbound} name="Outbound" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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">Time Period</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Date Range</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{metrics?.daily_data?.length > 0 ? (
|
||||
<>
|
||||
{formatDate(metrics.daily_data[0]?.date)} -{" "}
|
||||
{formatDate(
|
||||
metrics.daily_data[metrics.daily_data.length - 1]?.date
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
"No data available"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Avg Daily Calls</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{metrics?.daily_data?.length > 0
|
||||
? Math.round(metrics.total / metrics.daily_data.length)
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Avg Duration</span>
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{metrics?.average_duration
|
||||
? formatDuration(metrics.average_duration)
|
||||
: "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Hourly Distribution */}
|
||||
<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">Hourly Distribution</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData.hourly} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
||||
<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.hourly} name="Calls" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tables Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Agent Performance */}
|
||||
<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">Agent Performance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AgentPerformanceTable
|
||||
agents={sortedAgents}
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user