Split agent workload chart on /stats into one chart per status

Previously showed a single combined bar chart across all ticket
statuses; now renders a separate sorted bar chart per status
(new/open/pending/closed), each scaled to its own max.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-08-08 12:27:54 +00:00
1 parent fcf8511ac7
commit bae653b7a9
2 files changed
+57 -32

No files matched your search

+31 -18
View File
@@ -28,7 +28,9 @@ export default async function StatsPage() {
const stats = await getStatsOverview(); const stats = await getStatsOverview();
const maxChannelCount = Math.max(...Object.values(stats.channelCounts), 1); const maxChannelCount = Math.max(...Object.values(stats.channelCounts), 1);
const maxAgentWorkload = Math.max(...stats.agentWorkload.map((a) => a.ticketCount), 1); const statusesWithWorkload = (Object.keys(STATUS_META) as (keyof typeof STATUS_META)[]).filter(
(status) => stats.agentWorkloadByStatus[status].length > 0,
);
return ( return (
<div> <div>
@@ -88,24 +90,35 @@ export default async function StatsPage() {
<DailyBarChart data={stats.ticketsPerDay} /> <DailyBarChart data={stats.ticketsPerDay} />
</div> </div>
<div className="card p-4 lg:col-span-2"> {statusesWithWorkload.length === 0 ? (
<h2 className="mb-3 text-sm font-semibold text-text-muted">Заявки по агентам (все статусы)</h2> <div className="card p-4 lg:col-span-2">
{stats.agentWorkload.length === 0 ? ( <h2 className="mb-3 text-sm font-semibold text-text-muted">Заявки по агентам</h2>
<p className="text-sm text-text-faint">Пока нет данных.</p> <p className="text-sm text-text-faint">Пока нет данных.</p>
) : ( </div>
<div className="flex flex-col gap-2"> ) : (
{stats.agentWorkload.map((agent) => ( statusesWithWorkload.map((status) => {
<HorizontalBar const workload = stats.agentWorkloadByStatus[status];
key={agent.name} const max = Math.max(...workload.map((a) => a.count), 1);
label={agent.name} return (
value={agent.ticketCount} <div key={status} className="card p-4">
max={maxAgentWorkload} <h2 className="mb-3 text-sm font-semibold text-text-muted">
colorClass="bg-chart-1" Заявки по агентам — {STATUS_META[status].label}
/> </h2>
))} <div className="flex flex-col gap-2">
</div> {workload.map((agent) => (
)} <HorizontalBar
</div> key={agent.name}
label={agent.name}
value={agent.count}
max={max}
colorClass="bg-chart-1"
/>
))}
</div>
</div>
);
})
)}
</div> </div>
</div> </div>
); );
+26 -14
View File
@@ -705,7 +705,7 @@ export interface StatsOverview {
statusCounts: Record<TicketStatus, number>; statusCounts: Record<TicketStatus, number>;
channelCounts: Record<TicketChannel, number>; channelCounts: Record<TicketChannel, number>;
ticketsPerDay: { label: string; count: number }[]; ticketsPerDay: { label: string; count: number }[];
agentWorkload: { name: string; ticketCount: number }[]; agentWorkloadByStatus: Record<TicketStatus, { name: string; count: number }[]>;
} }
/** /**
@@ -771,20 +771,32 @@ export async function getStatsOverview(): Promise<StatsOverview> {
: null; : null;
const agents = await listAgents(); const agents = await listAgents();
const ticketCountByAgent = new Map<string, number>();
let unassignedCount = 0; /** Per-agent ticket count for one status, sorted busiest-first. */
for (const t of allTickets) { function workloadForStatus(status: TicketStatus): { name: string; count: number }[] {
if (t.assigneeId) { const countByAgent = new Map<string, number>();
ticketCountByAgent.set(t.assigneeId, (ticketCountByAgent.get(t.assigneeId) ?? 0) + 1); let unassignedCount = 0;
} else { for (const t of allTickets) {
unassignedCount += 1; if (t.status !== status) continue;
if (t.assigneeId) {
countByAgent.set(t.assigneeId, (countByAgent.get(t.assigneeId) ?? 0) + 1);
} else {
unassignedCount += 1;
}
} }
const workload = agents.map((a) => ({ name: a.name, count: countByAgent.get(a.id) ?? 0 }));
if (unassignedCount > 0) {
workload.push({ name: "Не назначено", count: unassignedCount });
}
return workload.sort((a, b) => b.count - a.count);
} }
const agentWorkload = agents.map((a) => ({ name: a.name, ticketCount: ticketCountByAgent.get(a.id) ?? 0 }));
if (unassignedCount > 0) { const agentWorkloadByStatus: Record<TicketStatus, { name: string; count: number }[]> = {
agentWorkload.push({ name: "Не назначено", ticketCount: unassignedCount }); new: workloadForStatus("new"),
} open: workloadForStatus("open"),
agentWorkload.sort((a, b) => b.ticketCount - a.ticketCount); pending: workloadForStatus("pending"),
closed: workloadForStatus("closed"),
};
return { return {
totalTickets: allTickets.length, totalTickets: allTickets.length,
@@ -794,6 +806,6 @@ export async function getStatsOverview(): Promise<StatsOverview> {
statusCounts, statusCounts,
channelCounts, channelCounts,
ticketsPerDay, ticketsPerDay,
agentWorkload, agentWorkloadByStatus,
}; };
} }