mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
131 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
};
|