diff --git a/src/lib/api.ts b/src/lib/api.ts index 0d8ff30..568eb0c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -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; diff --git a/src/pages/dashboards/SupervisorView.tsx b/src/pages/dashboards/SupervisorView.tsx index cfb4092..799b853 100644 --- a/src/pages/dashboards/SupervisorView.tsx +++ b/src/pages/dashboards/SupervisorView.tsx @@ -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([]); const [volumeTimeseries, setVolumeTimeseries] = React.useState(null); const [abandonedTimeseries, setAbandonedTimeseries] = React.useState(null); + const [callAgents, setCallAgents] = React.useState([]); + const [escalations, setEscalations] = React.useState([]); const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(null); @@ -80,6 +93,8 @@ export const SupervisorView: React.FC = () => { apiRequest('supervisor', 'supervisor/agents'), apiRequest('reporting', `reports/timeseries?${windowQuery.toString()}&metric=total`), apiRequest('reporting', `reports/timeseries?${windowQuery.toString()}&metric=abandoned`), + apiRequest('routing', 'agents'), + apiRequest('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 = () => { + +
+
+
+

Пул операторов L2 (звонки +7 747 645 6048)

+

Реальный пул Routing Engine — кого AI переводит на живого оператора

+
+
+ {callAgents.length ? ( + callAgents.map((agent) => ) + ) : ( +
Пул пуст — операторы ещё не добавлены.
+ )} +
+
+ +
+
+

Эскалации AI → оператор

+

Живая лента передач звонка с Voice AI на L2

+
+
+ {escalations.length ? ( + escalations.map((escalation) => ) + ) : ( +
Эскалаций пока не было.
+ )} +
+
+
)} ); }; +const callAgentStatusTone: Record = { + 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 = { + AVAILABLE: 'Свободен', + RESERVED: 'Зарезервирован', + RINGING: 'Звонок идёт', + TALKING: 'На линии', + AFTER_CALL_WORK: 'Постобработка', + PAUSED: 'Пауза', + OFFLINE: 'Offline', +}; + +const CallAgentRow: React.FC<{ agent: BackendAgentPoolItem }> = ({ agent }) => ( +
+
+
+ +
+
+
+ {agent.display_name} · вн. {agent.extension} +
+
+ {agent.level} · принято звонков: {agent.calls_handled_count} + {agent.current_call_id ? ` · звонок ${agent.current_call_id}` : ''} +
+
+
+ + {callAgentStatusLabel[agent.status]} + +
+); + +const escalationStatusTone: Record = { + 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 = { + requested: 'Запрошена', + ringing: 'Дозвон', + completed: 'Завершена', + failed: 'Не удалась', +}; + +const EscalationRow: React.FC<{ escalation: BackendEscalation }> = ({ escalation }) => ( +
+
+
звонок {escalation.call_id}
+ + {escalationStatusLabel[escalation.status]} + +
+
+ {escalation.from_level} → {escalation.to_level} · причина: {escalation.reason_code} + {escalation.assigned_agent_id ? ` · оператор: ${escalation.assigned_agent_id}` : ' · оператор не назначен'} +
+
+); + const KPIItem: React.FC<{ title: string; value: string; trend: string; positive: boolean; icon: any }> = ({ title, value,