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:
1 parent
fcf8511ac7
commit
bae653b7a9
2 files changed
+57
-32
No files matched your search
@@ -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
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user