feat(fase3): add Queue management frontend page

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

View File

@@ -0,0 +1,248 @@
import { useState } from 'react';
import {
Button,
Card,
Form,
Input,
InputNumber,
message,
Modal,
Popconfirm,
Select,
Space,
Table,
Tag,
Typography,
} from 'antd';
import {
DeleteOutlined,
EditOutlined,
PlusOutlined,
TeamOutlined,
} from '@ant-design/icons';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../api/client';
const { Title } = Typography;
interface Queue {
id: string;
name: string;
description: string | null;
assignment_method: string;
max_per_agent: number;
sla_first_response: number;
sla_resolution: number;
is_active: boolean;
agent_count: number;
}
const ASSIGNMENT_METHOD_LABELS: Record<string, string> = {
round_robin: 'Round Robin',
least_busy: 'Menos Ocupado',
skill_based: 'Por Habilidades',
};
export default function Queues() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAgentsModalOpen, setIsAgentsModalOpen] = useState(false);
const [selectedQueue, setSelectedQueue] = useState<Queue | null>(null);
const [form] = Form.useForm();
const queryClient = useQueryClient();
const { data: queues, isLoading } = useQuery({
queryKey: ['queues'],
queryFn: () => apiClient.get<Queue[]>('/api/queues'),
});
const createMutation = useMutation({
mutationFn: (data: Partial<Queue>) => apiClient.post('/api/queues', data),
onSuccess: () => {
message.success('Cola creada');
queryClient.invalidateQueries({ queryKey: ['queues'] });
closeModal();
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: Partial<Queue> }) =>
apiClient.put(`/api/queues/${id}`, data),
onSuccess: () => {
message.success('Cola actualizada');
queryClient.invalidateQueries({ queryKey: ['queues'] });
closeModal();
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiClient.delete(`/api/queues/${id}`),
onSuccess: () => {
message.success('Cola eliminada');
queryClient.invalidateQueries({ queryKey: ['queues'] });
},
});
const closeModal = () => {
setIsModalOpen(false);
setSelectedQueue(null);
form.resetFields();
};
const handleSubmit = (values: Partial<Queue>) => {
if (selectedQueue) {
updateMutation.mutate({ id: selectedQueue.id, data: values });
} else {
createMutation.mutate(values);
}
};
const handleEdit = (queue: Queue) => {
setSelectedQueue(queue);
form.setFieldsValue(queue);
setIsModalOpen(true);
};
const handleCreate = () => {
setSelectedQueue(null);
form.resetFields();
setIsModalOpen(true);
};
const handleManageAgents = (queue: Queue) => {
setSelectedQueue(queue);
setIsAgentsModalOpen(true);
};
const columns = [
{ title: 'Nombre', dataIndex: 'name', key: 'name' },
{
title: 'Método Asignación',
dataIndex: 'assignment_method',
key: 'assignment_method',
render: (method: string) => ASSIGNMENT_METHOD_LABELS[method] || method,
},
{
title: 'Agentes',
dataIndex: 'agent_count',
key: 'agent_count',
render: (count: number) => <Tag icon={<TeamOutlined />}>{count}</Tag>,
},
{ title: 'Max/Agente', dataIndex: 'max_per_agent', key: 'max_per_agent' },
{
title: 'SLA Respuesta',
dataIndex: 'sla_first_response',
key: 'sla_first_response',
render: (seconds: number) => `${Math.round(seconds / 60)} min`,
},
{
title: 'Acciones',
key: 'actions',
render: (_: unknown, record: Queue) => (
<Space>
<Button
size="small"
icon={<TeamOutlined />}
onClick={() => handleManageAgents(record)}
>
Agentes
</Button>
<Button
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
/>
<Popconfirm
title="¿Eliminar cola?"
onConfirm={() => deleteMutation.mutate(record.id)}
>
<Button size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
];
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Title level={4} style={{ margin: 0 }}>Colas de Atención</Title>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
Nueva Cola
</Button>
</div>
<Card>
<Table
dataSource={queues}
columns={columns}
rowKey="id"
loading={isLoading}
pagination={false}
/>
</Card>
<Modal
title={selectedQueue ? 'Editar Cola' : 'Nueva Cola'}
open={isModalOpen}
onCancel={closeModal}
footer={null}
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
name="name"
label="Nombre"
rules={[{ required: true, message: 'Ingresa un nombre' }]}
>
<Input />
</Form.Item>
<Form.Item name="description" label="Descripción">
<Input.TextArea rows={2} />
</Form.Item>
<Form.Item
name="assignment_method"
label="Método de Asignación"
initialValue="round_robin"
>
<Select>
<Select.Option value="round_robin">Round Robin</Select.Option>
<Select.Option value="least_busy">Menos Ocupado</Select.Option>
<Select.Option value="skill_based">Por Habilidades</Select.Option>
</Select>
</Form.Item>
<Form.Item name="max_per_agent" label="Máximo por Agente" initialValue={10}>
<InputNumber min={1} max={50} />
</Form.Item>
<Form.Item
name="sla_first_response"
label="SLA Primera Respuesta (segundos)"
initialValue={300}
>
<InputNumber min={60} step={60} />
</Form.Item>
<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
loading={createMutation.isPending || updateMutation.isPending}
>
{selectedQueue ? 'Actualizar' : 'Crear'}
</Button>
<Button onClick={closeModal}>Cancelar</Button>
</Space>
</Form.Item>
</Form>
</Modal>
<Modal
title={`Agentes - ${selectedQueue?.name}`}
open={isAgentsModalOpen}
onCancel={() => setIsAgentsModalOpen(false)}
footer={null}
width={600}
>
<p>Gestión de agentes en cola (próximamente)</p>
</Modal>
</div>
);
}