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