609 lines
23 KiB
JavaScript
609 lines
23 KiB
JavaScript
// components/AircallDashboard.jsx
|
|
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardDescription,
|
|
} from "@/components/ui/card";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
PhoneCall,
|
|
PhoneMissed,
|
|
Clock,
|
|
UserCheck,
|
|
PhoneIncoming,
|
|
PhoneOutgoing,
|
|
ArrowUpDown,
|
|
Timer,
|
|
Loader2,
|
|
Download,
|
|
Search,
|
|
} from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip as RechartsTooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
BarChart,
|
|
Bar,
|
|
} from "recharts";
|
|
|
|
const COLORS = {
|
|
inbound: "hsl(262.1 83.3% 57.8%)", // Purple
|
|
outbound: "hsl(142.1 76.2% 36.3%)", // Green
|
|
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 = [
|
|
{ label: "Today", value: "today" },
|
|
{ label: "Yesterday", value: "yesterday" },
|
|
{ label: "Last 7 Days", value: "last7days" },
|
|
{ label: "Last 30 Days", value: "last30days" },
|
|
{ label: "Last 90 Days", value: "last90days" },
|
|
];
|
|
|
|
const REFRESH_INTERVAL = 5 * 60 * 1000;
|
|
|
|
const formatDuration = (seconds) => {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const remainingSeconds = seconds % 60;
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
return `${minutes}m ${remainingSeconds}s`;
|
|
};
|
|
|
|
const MetricCard = ({ title, value, subtitle, icon: Icon, iconColor }) => (
|
|
<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}`} />
|
|
</CardHeader>
|
|
<CardContent className="p-4 pt-0">
|
|
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">{value}</div>
|
|
{subtitle && (
|
|
<p className="text-sm text-muted-foreground mt-1">{subtitle}</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
const CustomTooltip = ({ active, payload, label }) => {
|
|
if (active && payload && payload.length) {
|
|
return (
|
|
<Card className="p-3 shadow-lg bg-white dark:bg-gray-900/60 backdrop-blur-sm border-none">
|
|
<CardContent className="p-0 space-y-2">
|
|
<p className="font-medium text-sm text-gray-900 dark:text-gray-100 border-b border-gray-100 dark:border-gray-800 pb-1 mb-2">{label}</p>
|
|
{payload.map((entry, index) => (
|
|
<p key={index} className="text-sm text-muted-foreground">
|
|
{`${entry.name}: ${entry.value}`}
|
|
</p>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
|
|
|
|
const AgentPerformanceTable = ({ agents, onSort }) => {
|
|
const [sortConfig, setSortConfig] = useState({
|
|
key: "total",
|
|
direction: "desc",
|
|
});
|
|
|
|
const handleSort = (key) => {
|
|
const direction =
|
|
sortConfig.key === key && sortConfig.direction === "desc"
|
|
? "asc"
|
|
: "desc";
|
|
setSortConfig({ key, direction });
|
|
onSort(key, direction);
|
|
};
|
|
|
|
return (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead>Agent</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>
|
|
{agents.map((agent) => (
|
|
<TableRow key={agent.id} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
|
<TableCell className="font-medium text-gray-900 dark:text-gray-100">{agent.name}</TableCell>
|
|
<TableCell>{agent.total}</TableCell>
|
|
<TableCell className="text-emerald-600 dark:text-emerald-400">{agent.answered}</TableCell>
|
|
<TableCell className="text-rose-600 dark:text-rose-400">{agent.missed}</TableCell>
|
|
<TableCell className="text-muted-foreground">{formatDuration(agent.average_duration)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
);
|
|
};
|
|
|
|
const SkeletonMetricCard = () => (
|
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
|
<CardHeader className="flex flex-col items-start p-4">
|
|
<Skeleton className="h-4 w-24 mb-2 bg-muted" />
|
|
<Skeleton className="h-8 w-32 mb-2 bg-muted" />
|
|
<div className="flex gap-4">
|
|
<Skeleton className="h-4 w-20 bg-muted" />
|
|
<Skeleton className="h-4 w-20 bg-muted" />
|
|
</div>
|
|
</CardHeader>
|
|
</Card>
|
|
);
|
|
|
|
const SkeletonChart = ({ type = "line" }) => (
|
|
<div className="h-[300px] w-full bg-white dark:bg-gray-900/60 backdrop-blur-sm rounded-lg p-4">
|
|
<div className="h-full flex flex-col">
|
|
<div className="flex-1 relative">
|
|
{type === "bar" ? (
|
|
<div className="h-full flex items-end justify-between gap-1">
|
|
{[...Array(24)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="w-full bg-muted rounded-t animate-pulse"
|
|
style={{ height: `${15 + Math.random() * 70}%` }}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="h-full w-full relative">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="absolute w-full h-px bg-muted"
|
|
style={{ top: `${20 + i * 20}%` }}
|
|
/>
|
|
))}
|
|
<div
|
|
className="absolute inset-0 bg-muted animate-pulse"
|
|
style={{
|
|
opacity: 0.2,
|
|
clipPath: "polygon(0 50%, 100% 20%, 100% 100%, 0 100%)",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const SkeletonTable = ({ rows = 5 }) => (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
|
<TableHead><Skeleton className="h-4 w-24 bg-muted" /></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{[...Array(rows)].map((_, i) => (
|
|
<TableRow key={i} className="hover:bg-muted/50 transition-colors">
|
|
<TableCell><Skeleton className="h-4 w-32 bg-muted" /></TableCell>
|
|
<TableCell><Skeleton className="h-4 w-16 bg-muted" /></TableCell>
|
|
<TableCell><Skeleton className="h-4 w-16 bg-muted" /></TableCell>
|
|
<TableCell><Skeleton className="h-4 w-16 bg-muted" /></TableCell>
|
|
<TableCell><Skeleton className="h-4 w-24 bg-muted" /></TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
);
|
|
|
|
const AircallDashboard = () => {
|
|
const [timeRange, setTimeRange] = useState("last7days");
|
|
const [metrics, setMetrics] = useState(null);
|
|
const [lastUpdated, setLastUpdated] = useState(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [agentSort, setAgentSort] = useState({
|
|
key: "total",
|
|
direction: "desc",
|
|
});
|
|
|
|
const safeArray = (arr) => (Array.isArray(arr) ? arr : []);
|
|
const safeObject = (obj) => (obj && typeof obj === "object" ? obj : {});
|
|
|
|
const sortedAgents = metrics?.by_users
|
|
? Object.values(metrics.by_users).sort((a, b) => {
|
|
const multiplier = agentSort.direction === "desc" ? -1 : 1;
|
|
return multiplier * (a[agentSort.key] - b[agentSort.key]);
|
|
})
|
|
: [];
|
|
|
|
const formatDate = (dateString) => {
|
|
try {
|
|
// Parse the date string (YYYY-MM-DD)
|
|
const [year, month, day] = dateString.split('-').map(Number);
|
|
|
|
// Create a date object in ET timezone
|
|
const date = new Date(Date.UTC(year, month - 1, day));
|
|
|
|
// Format the date in ET timezone
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
month: "short",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
timeZone: "America/New_York"
|
|
}).format(date);
|
|
} catch (error) {
|
|
console.error("Date formatting error:", error, { dateString });
|
|
return "Invalid Date";
|
|
}
|
|
};
|
|
|
|
|
|
const handleExport = () => {
|
|
const timestamp = new Intl.DateTimeFormat("en-US", {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
}).format(new Date());
|
|
|
|
exportToCSV(filteredAgents, `aircall-agent-metrics-${timestamp}`);
|
|
};
|
|
|
|
const chartData = {
|
|
hourly: metrics?.by_hour
|
|
? metrics.by_hour.map((count, hour) => ({
|
|
hour: new Date(2000, 0, 1, hour).toLocaleString('en-US', {
|
|
hour: 'numeric',
|
|
hour12: true
|
|
}).toUpperCase(),
|
|
calls: count || 0,
|
|
}))
|
|
: [],
|
|
|
|
missedReasons: metrics?.by_missed_reason
|
|
? Object.entries(metrics.by_missed_reason).map(([reason, count]) => ({
|
|
reason: (reason || "").replace(/_/g, " "),
|
|
count: count || 0,
|
|
}))
|
|
: [],
|
|
|
|
daily: safeArray(metrics?.daily_data).map((day) => ({
|
|
...day,
|
|
inbound: day.inbound || 0,
|
|
outbound: day.outbound || 0,
|
|
date: new Date(day.date).toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric'
|
|
}),
|
|
})),
|
|
};
|
|
|
|
const peakHour = metrics?.by_hour
|
|
? metrics.by_hour.indexOf(Math.max(...metrics.by_hour))
|
|
: null;
|
|
|
|
const busyAgent = sortedAgents?.length > 0 ? sortedAgents[0] : null;
|
|
|
|
const bestAnswerRate = sortedAgents
|
|
?.filter((agent) => agent.total > 0)
|
|
?.sort((a, b) => b.answered / b.total - a.answered / a.total)[0];
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await fetch(`/api/aircall/metrics/${timeRange}`);
|
|
if (!response.ok) throw new Error("Failed to fetch metrics");
|
|
const data = await response.json();
|
|
setMetrics(data);
|
|
setLastUpdated(data._meta?.generatedAt);
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
const interval = setInterval(fetchData, REFRESH_INTERVAL);
|
|
return () => clearInterval(interval);
|
|
}, [timeRange]);
|
|
|
|
if (error) {
|
|
return (
|
|
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
|
<CardContent className="p-4">
|
|
<div className="p-4 m-6 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-200 dark:border-red-900/20">
|
|
Error loading call data: {error}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card className="bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
|
<CardHeader className="p-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">Calls</CardTitle>
|
|
</div>
|
|
|
|
<Select value={timeRange} onValueChange={setTimeRange}>
|
|
<SelectTrigger className="w-[130px] h-9 bg-white dark:bg-gray-800">
|
|
<SelectValue placeholder="Select range" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TIME_RANGES.map((range) => (
|
|
<SelectItem key={range.value} value={range.value}>
|
|
{range.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="p-6 pt-0 space-y-4">
|
|
{/* Metric Cards */}
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{isLoading ? (
|
|
[...Array(4)].map((_, i) => (
|
|
<SkeletonMetricCard key={i} />
|
|
))
|
|
) : metrics ? (
|
|
<>
|
|
<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 text-muted-foreground">
|
|
<span className="text-blue-500">↑ {metrics.by_direction.inbound}</span> inbound
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
<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 text-muted-foreground">
|
|
<span className="text-emerald-500">{metrics.by_status.answered}</span> answered
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
<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] bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
|
<div className="space-y-2">
|
|
<p className="font-medium text-gray-900 dark:text-gray-100">Duration Distribution</p>
|
|
{metrics?.duration_distribution?.map((d, i) => (
|
|
<div key={i} className="flex justify-between text-sm text-muted-foreground">
|
|
<span>{d.range}</span>
|
|
<span>{d.count} calls</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</CardHeader>
|
|
</Card>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* 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]">
|
|
{isLoading ? (
|
|
<SkeletonChart type="bar" />
|
|
) : (
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={chartData.daily} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis
|
|
dataKey="date"
|
|
tick={{ fontSize: 12 }}
|
|
className="text-muted-foreground"
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 12 }}
|
|
className="text-muted-foreground"
|
|
/>
|
|
<RechartsTooltip content={<CustomTooltip />} />
|
|
<Legend />
|
|
<Bar dataKey="inbound" fill={COLORS.inbound} name="Inbound" />
|
|
<Bar dataKey="outbound" fill={COLORS.outbound} name="Outbound" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</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]">
|
|
{isLoading ? (
|
|
<SkeletonChart type="bar" />
|
|
) : (
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={chartData.hourly} margin={{ top: 0, right: 5, left: -35, bottom: 0 }}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis
|
|
dataKey="hour"
|
|
tick={{ fontSize: 12 }}
|
|
interval={2}
|
|
className="text-muted-foreground"
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 12 }}
|
|
className="text-muted-foreground"
|
|
/>
|
|
<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>
|
|
{isLoading ? (
|
|
<SkeletonTable rows={5} />
|
|
) : (
|
|
<div className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600">
|
|
<AgentPerformanceTable
|
|
agents={sortedAgents}
|
|
onSort={(key, direction) => setAgentSort({ key, direction })}
|
|
/>
|
|
</div>
|
|
)}
|
|
</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>
|
|
{isLoading ? (
|
|
<SkeletonTable rows={5} />
|
|
) : (
|
|
<div className="overflow-y-auto max-h-[400px] scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-300 dark:hover:scrollbar-thumb-gray-600">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead className="font-medium text-gray-900 dark:text-gray-100">Reason</TableHead>
|
|
<TableHead className="text-right font-medium text-gray-900 dark:text-gray-100">Count</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{chartData.missedReasons.map((reason, index) => (
|
|
<TableRow key={index} className="hover:bg-muted/50 transition-colors">
|
|
<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>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AircallDashboard;
|