From 9a640b71368310e7b8b500adb8a4ff23aede5d31 Mon Sep 17 00:00:00 2001 From: Claude AI Date: Thu, 29 Jan 2026 10:59:26 +0000 Subject: [PATCH] feat(fase3): add Supervisor Dashboard frontend Co-Authored-By: Claude Opus 4.5 --- frontend/src/pages/SupervisorDashboard.tsx | 384 +++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 frontend/src/pages/SupervisorDashboard.tsx diff --git a/frontend/src/pages/SupervisorDashboard.tsx b/frontend/src/pages/SupervisorDashboard.tsx new file mode 100644 index 0000000..95a428e --- /dev/null +++ b/frontend/src/pages/SupervisorDashboard.tsx @@ -0,0 +1,384 @@ +import { + Alert, + Avatar, + Badge, + Button, + Card, + Col, + List, + Row, + Space, + Statistic, + Table, + Tabs, + Tag, + Typography, +} from 'antd'; +import { + CheckCircleOutlined, + ClockCircleOutlined, + ExclamationCircleOutlined, + MessageOutlined, + ReloadOutlined, + TeamOutlined, + UserOutlined, +} from '@ant-design/icons'; +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; + +import { apiClient } from '../api/client'; + +const { Title, Text } = Typography; + +interface DashboardData { + conversations: { + by_status: Record; + waiting: number; + resolved_today: number; + }; + agents: { + online: number; + total: number; + }; + messages_today: number; + avg_csat: number | null; +} + +interface AgentStatus { + id: string; + name: string; + email: string; + role: string; + status: string; + active_conversations: number; + waiting_conversations: number; + resolved_today: number; +} + +interface QueueStatus { + id: string; + name: string; + waiting_conversations: number; + active_conversations: number; + online_agents: number; + total_agents: number; +} + +interface CriticalConversation { + id: string; + contact_name: string; + contact_phone: string; + status: string; + priority: string; + last_message_at: string | null; + reason: string; +} + +const STATUS_COLORS: Record = { + online: 'green', + offline: 'default', + away: 'orange', + busy: 'red', +}; + +function getReasonLabel(reason: string): string { + if (reason === 'long_wait') { + return 'Espera prolongada'; + } + return 'Alta prioridad'; +} + +function getReasonColor(reason: string): string { + if (reason === 'long_wait') { + return 'orange'; + } + return 'red'; +} + +export default function SupervisorDashboard(): JSX.Element { + const { data: dashboard, refetch: refetchDashboard } = useQuery({ + queryKey: ['supervisor-dashboard'], + queryFn: () => apiClient.get('/api/supervisor/dashboard'), + refetchInterval: 10000, + }); + + const { data: agents, refetch: refetchAgents } = useQuery({ + queryKey: ['supervisor-agents'], + queryFn: () => apiClient.get('/api/supervisor/agents'), + refetchInterval: 10000, + }); + + const { data: queues, refetch: refetchQueues } = useQuery({ + queryKey: ['supervisor-queues'], + queryFn: () => apiClient.get('/api/supervisor/queues'), + refetchInterval: 10000, + }); + + const { data: criticalConversations } = useQuery({ + queryKey: ['critical-conversations'], + queryFn: () => apiClient.get('/api/supervisor/conversations/critical'), + refetchInterval: 15000, + }); + + function handleRefresh(): void { + refetchDashboard(); + refetchAgents(); + refetchQueues(); + } + + const agentColumns = [ + { + title: 'Agente', + dataIndex: 'name', + key: 'name', + render: (name: string, record: AgentStatus) => ( + + + {name} + + ), + }, + { + title: 'Estado', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + {status} + ), + }, + { + title: 'Activas', + dataIndex: 'active_conversations', + key: 'active', + render: (count: number) => ( + 0 ? 'blue' : 'default'} /> + ), + }, + { + title: 'En Espera', + dataIndex: 'waiting_conversations', + key: 'waiting', + render: (count: number) => ( + 0 ? 'orange' : 'default'} /> + ), + }, + { + title: 'Resueltas Hoy', + dataIndex: 'resolved_today', + key: 'resolved', + }, + ]; + + const queueColumns = [ + { + title: 'Cola', + dataIndex: 'name', + key: 'name', + }, + { + title: 'En Espera', + dataIndex: 'waiting_conversations', + key: 'waiting', + render: (count: number) => ( + 0 ? 'orange' : 'default'} /> + ), + }, + { + title: 'Activas', + dataIndex: 'active_conversations', + key: 'active', + render: (count: number) => ( + 0 ? 'blue' : 'default'} /> + ), + }, + { + title: 'Agentes Online', + key: 'agents', + render: (_: unknown, record: QueueStatus) => ( + {record.online_agents}/{record.total_agents} + ), + }, + ]; + + const hasCriticalConversations = criticalConversations && criticalConversations.length > 0; + const conversationsByStatus = dashboard?.conversations.by_status; + const waitingCount = dashboard?.conversations.waiting || 0; + + const tabItems = [ + { + key: 'agents', + label: 'Estado de Agentes', + children: ( + + + + ), + }, + { + key: 'queues', + label: 'Estado de Colas', + children: ( + +
+ + ), + }, + { + key: 'critical', + label: ( + + Criticas {hasCriticalConversations && } + + ), + children: ( + + {hasCriticalConversations ? ( + ( + + } />} + title={conv.contact_name || conv.contact_phone} + description={ + + + {getReasonLabel(conv.reason)} + + + {conv.last_message_at + ? dayjs(conv.last_message_at).fromNow() + : 'Sin mensajes'} + + + } + /> + + )} + /> + ) : ( + No hay conversaciones criticas + )} + + ), + }, + ]; + + return ( +
+
+ Panel de Supervisor + +
+ + {hasCriticalConversations && ( + } + style={{ marginBottom: 16 }} + /> + )} + + +
+ + } + valueStyle={{ color: '#52c41a' }} + /> + + + + + } + valueStyle={{ color: waitingCount > 0 ? '#faad14' : undefined }} + /> + + + + + } + valueStyle={{ color: '#52c41a' }} + /> + + + + + } + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}