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
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>