feat: show L2 agent pool and AI escalations on Supervisor dashboard
deploy / deploy (push) Successful in 51s

Surfaces the routing-service agent pool (/routing/agents) and live
escalation feed (/routing/escalations) on the SupervisorView used at
aimaq.konturai.kz — this is the actual frontend for the Tele2 Kazgaz
number +77476456048's call-center backend.
This commit is contained in:
Hermes Agent
2026-08-29 13:53:50 +05:00
parent 0e528b05e3
commit 03417c4f45
2 changed files with 165 additions and 1 deletions
+46
View File
@@ -227,6 +227,52 @@ export interface BackendOmnichannelInsights {
generated_at: string;
}
export type BackendAgentPoolStatus =
| 'OFFLINE'
| 'AVAILABLE'
| 'RESERVED'
| 'RINGING'
| 'TALKING'
| 'AFTER_CALL_WORK'
| 'PAUSED';
export interface BackendAgentPoolItem {
agent_id: string;
tenant_ids: string[];
extension: string;
endpoint?: string | null;
display_name: string;
level: 'L2' | 'L3';
skills: string[];
status: BackendAgentPoolStatus;
current_call_id?: string | null;
max_concurrent_calls: number;
enabled: boolean;
calls_handled_count: number;
created_at: string;
updated_at: string;
}
export type BackendEscalationStatus = 'requested' | 'ringing' | 'completed' | 'failed';
export interface BackendEscalation {
escalation_id: string;
call_id: string;
tenant_id?: string | null;
from_level: string;
to_level: string;
reason_code: string;
required_skills: string[];
priority: number;
topic?: string | null;
summary?: string | null;
status: BackendEscalationStatus;
assigned_agent_id?: string | null;
requested_at: string;
connected_at?: string | null;
completed_at?: string | null;
}
export class ApiError extends Error {
status: number;
detail: unknown;
+119 -1
View File
@@ -25,7 +25,18 @@ import {
YAxis,
} from 'recharts';
import { cn } from '../../lib/utils';
import { apiRequest, BackendAgentState, BackendKpi, BackendQueue, BackendRealtime, BackendTimeseries } from '../../lib/api';
import {
apiRequest,
BackendAgentPoolItem,
BackendAgentPoolStatus,
BackendAgentState,
BackendEscalation,
BackendEscalationStatus,
BackendKpi,
BackendQueue,
BackendRealtime,
BackendTimeseries,
} from '../../lib/api';
import { LoadingState } from '../../components/ui/FeedbackStates';
const emptyChartData = [
@@ -59,6 +70,8 @@ export const SupervisorView: React.FC = () => {
const [agents, setAgents] = React.useState<BackendAgentState[]>([]);
const [volumeTimeseries, setVolumeTimeseries] = React.useState<BackendTimeseries | null>(null);
const [abandonedTimeseries, setAbandonedTimeseries] = React.useState<BackendTimeseries | null>(null);
const [callAgents, setCallAgents] = React.useState<BackendAgentPoolItem[]>([]);
const [escalations, setEscalations] = React.useState<BackendEscalation[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
@@ -80,6 +93,8 @@ export const SupervisorView: React.FC = () => {
apiRequest<BackendAgentState[]>('supervisor', 'supervisor/agents'),
apiRequest<BackendTimeseries>('reporting', `reports/timeseries?${windowQuery.toString()}&metric=total`),
apiRequest<BackendTimeseries>('reporting', `reports/timeseries?${windowQuery.toString()}&metric=abandoned`),
apiRequest<BackendAgentPoolItem[]>('routing', 'agents'),
apiRequest<BackendEscalation[]>('routing', 'escalations?limit=30'),
]);
if (results[0].status === 'fulfilled') setRealtime(results[0].value);
@@ -88,6 +103,8 @@ export const SupervisorView: React.FC = () => {
if (results[3].status === 'fulfilled') setAgents(results[3].value);
if (results[4].status === 'fulfilled') setVolumeTimeseries(results[4].value);
if (results[5].status === 'fulfilled') setAbandonedTimeseries(results[5].value);
if (results[6].status === 'fulfilled') setCallAgents(results[6].value);
if (results[7].status === 'fulfilled') setEscalations(results[7].value);
const rejected = results.find((result) => result.status === 'rejected');
if (rejected) {
@@ -256,12 +273,113 @@ export const SupervisorView: React.FC = () => {
</table>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h3 className="text-xs font-bold text-slate-900 uppercase tracking-widest">Пул операторов L2 (звонки +7 747 645 6048)</h3>
<p className="text-[11px] text-slate-400 mt-0.5 italic">Реальный пул Routing Engine кого AI переводит на живого оператора</p>
</div>
<div className="divide-y divide-slate-50">
{callAgents.length ? (
callAgents.map((agent) => <CallAgentRow key={agent.agent_id} agent={agent} />)
) : (
<div className="px-6 py-10 text-center text-sm text-slate-400">Пул пуст операторы ещё не добавлены.</div>
)}
</div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h3 className="text-xs font-bold text-slate-900 uppercase tracking-widest">Эскалации AI оператор</h3>
<p className="text-[11px] text-slate-400 mt-0.5 italic">Живая лента передач звонка с Voice AI на L2</p>
</div>
<div className="divide-y divide-slate-50 max-h-[420px] overflow-y-auto">
{escalations.length ? (
escalations.map((escalation) => <EscalationRow key={escalation.escalation_id} escalation={escalation} />)
) : (
<div className="px-6 py-10 text-center text-sm text-slate-400">Эскалаций пока не было.</div>
)}
</div>
</div>
</div>
</>
)}
</div>
);
};
const callAgentStatusTone: Record<BackendAgentPoolStatus, string> = {
AVAILABLE: 'bg-emerald-50 text-emerald-600',
RESERVED: 'bg-amber-50 text-amber-600',
RINGING: 'bg-amber-50 text-amber-600',
TALKING: 'bg-emerald-50 text-emerald-600',
AFTER_CALL_WORK: 'bg-amber-50 text-amber-600',
PAUSED: 'bg-slate-100 text-slate-600',
OFFLINE: 'bg-rose-50 text-rose-600',
};
const callAgentStatusLabel: Record<BackendAgentPoolStatus, string> = {
AVAILABLE: 'Свободен',
RESERVED: 'Зарезервирован',
RINGING: 'Звонок идёт',
TALKING: 'На линии',
AFTER_CALL_WORK: 'Постобработка',
PAUSED: 'Пауза',
OFFLINE: 'Offline',
};
const CallAgentRow: React.FC<{ agent: BackendAgentPoolItem }> = ({ agent }) => (
<div className="px-6 py-4 flex items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 rounded-full bg-slate-100 flex items-center justify-center shrink-0">
<Users className="w-4 h-4 text-slate-400" />
</div>
<div className="min-w-0">
<div className="font-semibold text-slate-900 truncate">
{agent.display_name} <span className="text-slate-400 font-normal">· вн. {agent.extension}</span>
</div>
<div className="text-[11px] text-slate-400 italic truncate">
{agent.level} · принято звонков: {agent.calls_handled_count}
{agent.current_call_id ? ` · звонок ${agent.current_call_id}` : ''}
</div>
</div>
</div>
<span className={cn('shrink-0 text-[10px] font-bold uppercase tracking-widest px-2 py-1 rounded-full', callAgentStatusTone[agent.status])}>
{callAgentStatusLabel[agent.status]}
</span>
</div>
);
const escalationStatusTone: Record<BackendEscalationStatus, string> = {
requested: 'bg-amber-50 text-amber-600',
ringing: 'bg-amber-50 text-amber-600',
completed: 'bg-emerald-50 text-emerald-600',
failed: 'bg-rose-50 text-rose-600',
};
const escalationStatusLabel: Record<BackendEscalationStatus, string> = {
requested: 'Запрошена',
ringing: 'Дозвон',
completed: 'Завершена',
failed: 'Не удалась',
};
const EscalationRow: React.FC<{ escalation: BackendEscalation }> = ({ escalation }) => (
<div className="px-6 py-4">
<div className="flex items-center justify-between gap-3">
<div className="font-semibold text-slate-900 text-sm">звонок {escalation.call_id}</div>
<span className={cn('shrink-0 text-[10px] font-bold uppercase tracking-widest px-2 py-1 rounded-full', escalationStatusTone[escalation.status])}>
{escalationStatusLabel[escalation.status]}
</span>
</div>
<div className="text-[11px] text-slate-400 italic mt-1">
{escalation.from_level} {escalation.to_level} · причина: {escalation.reason_code}
{escalation.assigned_agent_id ? ` · оператор: ${escalation.assigned_agent_id}` : ' · оператор не назначен'}
</div>
</div>
);
const KPIItem: React.FC<{ title: string; value: string; trend: string; positive: boolean; icon: any }> = ({
title,
value,