Files
helix-engage/src/components/dashboard/agent-table.tsx
2026-03-23 16:41:58 +05:30

131 lines
6.0 KiB
TypeScript

import { useMemo } from "react";
import { faUserHeadset } from "@fortawesome/pro-duotone-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "react-router";
import { Table, TableCard } from "@/components/application/table/table";
import { Avatar } from "@/components/base/avatar/avatar";
import { Badge } from "@/components/base/badges/badges";
import { getInitials } from "@/lib/format";
import type { Call } from "@/types/entities";
const formatDuration = (seconds: number): string => {
if (seconds < 60) return `${seconds}s`;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
};
const formatPercent = (value: number): string => {
if (isNaN(value) || !isFinite(value)) return "0%";
return `${Math.round(value)}%`;
};
interface AgentTableProps {
calls: Call[];
}
export const AgentTable = ({ calls }: AgentTableProps) => {
const agents = useMemo(() => {
const agentMap = new Map<string, Call[]>();
for (const call of calls) {
const agent = call.agentName ?? "Unknown";
if (!agentMap.has(agent)) agentMap.set(agent, []);
agentMap.get(agent)!.push(call);
}
return Array.from(agentMap.entries())
.map(([name, agentCalls]) => {
const inbound = agentCalls.filter((c) => c.callDirection === "INBOUND").length;
const outbound = agentCalls.filter((c) => c.callDirection === "OUTBOUND").length;
const missed = agentCalls.filter((c) => c.callStatus === "MISSED").length;
const total = agentCalls.length;
const completedCalls = agentCalls.filter((c) => (c.durationSeconds ?? 0) > 0);
const totalDuration = completedCalls.reduce((sum, c) => sum + (c.durationSeconds ?? 0), 0);
const avgHandle = completedCalls.length > 0 ? Math.round(totalDuration / completedCalls.length) : 0;
const booked = agentCalls.filter((c) => c.disposition === "APPOINTMENT_BOOKED").length;
const conversion = total > 0 ? (booked / total) * 100 : 0;
const nameParts = name.split(" ");
return {
id: name,
name,
initials: getInitials(nameParts[0] ?? "", nameParts[1] ?? ""),
inbound,
outbound,
missed,
total,
avgHandle,
conversion,
};
})
.sort((a, b) => b.total - a.total);
}, [calls]);
if (agents.length === 0) {
return (
<TableCard.Root size="sm">
<TableCard.Header title="Agent Performance" description="Call metrics by agent" />
<div className="flex flex-col items-center justify-center gap-2 py-12">
<FontAwesomeIcon icon={faUserHeadset} className="size-8 text-fg-quaternary" />
<p className="text-sm text-tertiary">No agent data available</p>
</div>
</TableCard.Root>
);
}
return (
<TableCard.Root size="sm">
<TableCard.Header title="Agent Performance" badge={agents.length} description="Call metrics by agent" />
<Table>
<Table.Header>
<Table.Head label="AGENT" isRowHeader />
<Table.Head label="IN" />
<Table.Head label="OUT" />
<Table.Head label="MISSED" />
<Table.Head label="AVG HANDLE" />
<Table.Head label="CONVERSION" />
</Table.Header>
<Table.Body items={agents}>
{(agent) => (
<Table.Row id={agent.id}>
<Table.Cell>
<Link to={`/agent/${encodeURIComponent(agent.name)}`} className="no-underline">
<div className="flex items-center gap-2">
<Avatar size="xs" initials={agent.initials} />
<span className="text-sm font-medium text-brand-secondary transition duration-100 ease-linear hover:text-brand-secondary_hover">
{agent.name}
</span>
</div>
</Link>
</Table.Cell>
<Table.Cell>
<span className="text-sm text-success-primary">{agent.inbound}</span>
</Table.Cell>
<Table.Cell>
<span className="text-sm text-brand-secondary">{agent.outbound}</span>
</Table.Cell>
<Table.Cell>
{agent.missed > 0 ? (
<Badge size="sm" color="error">
{agent.missed}
</Badge>
) : (
<span className="text-sm text-tertiary">0</span>
)}
</Table.Cell>
<Table.Cell>
<span className="text-sm text-secondary">{formatDuration(agent.avgHandle)}</span>
</Table.Cell>
<Table.Cell>
<Badge size="sm" color={agent.conversion >= 30 ? "success" : agent.conversion >= 15 ? "warning" : "gray"}>
{formatPercent(agent.conversion)}
</Badge>
</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table>
</TableCard.Root>
);
};