feat(fase3): add Queue management frontend page
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
248
frontend/src/pages/Queues.tsx
Normal file
248
frontend/src/pages/Queues.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user