feat(fase3): add Supervisor Dashboard frontend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude AI
2026-01-29 10:59:26 +00:00
parent aadb98571c
commit 9a640b7136

View File

@@ -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<string, number>;
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<string, string> = {
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<DashboardData>('/api/supervisor/dashboard'),
refetchInterval: 10000,
});
const { data: agents, refetch: refetchAgents } = useQuery({
queryKey: ['supervisor-agents'],
queryFn: () => apiClient.get<AgentStatus[]>('/api/supervisor/agents'),
refetchInterval: 10000,
});
const { data: queues, refetch: refetchQueues } = useQuery({
queryKey: ['supervisor-queues'],
queryFn: () => apiClient.get<QueueStatus[]>('/api/supervisor/queues'),
refetchInterval: 10000,
});
const { data: criticalConversations } = useQuery({
queryKey: ['critical-conversations'],
queryFn: () => apiClient.get<CriticalConversation[]>('/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) => (
<Space>
<Badge status={record.status === 'online' ? 'success' : 'default'} />
<Text>{name}</Text>
</Space>
),
},
{
title: 'Estado',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={STATUS_COLORS[status]}>{status}</Tag>
),
},
{
title: 'Activas',
dataIndex: 'active_conversations',
key: 'active',
render: (count: number) => (
<Badge count={count} showZero color={count > 0 ? 'blue' : 'default'} />
),
},
{
title: 'En Espera',
dataIndex: 'waiting_conversations',
key: 'waiting',
render: (count: number) => (
<Badge count={count} showZero color={count > 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) => (
<Badge count={count} showZero color={count > 0 ? 'orange' : 'default'} />
),
},
{
title: 'Activas',
dataIndex: 'active_conversations',
key: 'active',
render: (count: number) => (
<Badge count={count} showZero color={count > 0 ? 'blue' : 'default'} />
),
},
{
title: 'Agentes Online',
key: 'agents',
render: (_: unknown, record: QueueStatus) => (
<Text>{record.online_agents}/{record.total_agents}</Text>
),
},
];
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: (
<Card>
<Table
dataSource={agents}
columns={agentColumns}
rowKey="id"
pagination={false}
size="small"
/>
</Card>
),
},
{
key: 'queues',
label: 'Estado de Colas',
children: (
<Card>
<Table
dataSource={queues}
columns={queueColumns}
rowKey="id"
pagination={false}
size="small"
/>
</Card>
),
},
{
key: 'critical',
label: (
<span>
Criticas {hasCriticalConversations && <Badge count={criticalConversations.length} />}
</span>
),
children: (
<Card>
{hasCriticalConversations ? (
<List
dataSource={criticalConversations}
renderItem={(conv) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar icon={<UserOutlined />} />}
title={conv.contact_name || conv.contact_phone}
description={
<Space>
<Tag color={getReasonColor(conv.reason)}>
{getReasonLabel(conv.reason)}
</Tag>
<Text type="secondary">
{conv.last_message_at
? dayjs(conv.last_message_at).fromNow()
: 'Sin mensajes'}
</Text>
</Space>
}
/>
</List.Item>
)}
/>
) : (
<Text type="secondary">No hay conversaciones criticas</Text>
)}
</Card>
),
},
];
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Title level={4}>Panel de Supervisor</Title>
<Button icon={<ReloadOutlined />} onClick={handleRefresh}>
Actualizar
</Button>
</div>
{hasCriticalConversations && (
<Alert
message={`${criticalConversations.length} conversaciones requieren atencion`}
type="warning"
showIcon
icon={<ExclamationCircleOutlined />}
style={{ marginBottom: 16 }}
/>
)}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="Agentes Online"
value={dashboard?.agents.online || 0}
suffix={`/ ${dashboard?.agents.total || 0}`}
prefix={<TeamOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="En Espera"
value={waitingCount}
prefix={<ClockCircleOutlined />}
valueStyle={{ color: waitingCount > 0 ? '#faad14' : undefined }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Resueltas Hoy"
value={dashboard?.conversations.resolved_today || 0}
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Mensajes Hoy"
value={dashboard?.messages_today || 0}
prefix={<MessageOutlined />}
/>
</Card>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card size="small">
<Statistic
title="Bot"
value={conversationsByStatus?.bot || 0}
valueStyle={{ fontSize: 20, color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="Esperando"
value={conversationsByStatus?.waiting || 0}
valueStyle={{ fontSize: 20, color: '#faad14' }}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="Activas"
value={conversationsByStatus?.active || 0}
valueStyle={{ fontSize: 20, color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="CSAT Promedio"
value={dashboard?.avg_csat || '-'}
suffix={dashboard?.avg_csat ? '/ 5' : ''}
valueStyle={{ fontSize: 20 }}
/>
</Card>
</Col>
</Row>
<Tabs items={tabItems} />
</div>
);
}