feat(fase4): add FlowTemplates frontend page
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
157
frontend/src/pages/FlowTemplates.tsx
Normal file
157
frontend/src/pages/FlowTemplates.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
Typography,
|
||||||
|
Tag,
|
||||||
|
Empty,
|
||||||
|
message,
|
||||||
|
} from 'antd';
|
||||||
|
import { CopyOutlined } from '@ant-design/icons';
|
||||||
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { apiClient } from '../api/client';
|
||||||
|
|
||||||
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
interface FlowTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
category: string;
|
||||||
|
nodes: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryColors: Record<string, string> = {
|
||||||
|
general: 'blue',
|
||||||
|
sales: 'green',
|
||||||
|
support: 'orange',
|
||||||
|
marketing: 'purple',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FlowTemplates() {
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<FlowTemplate | null>(null);
|
||||||
|
const [flowName, setFlowName] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { data: templates, isLoading } = useQuery({
|
||||||
|
queryKey: ['flow-templates'],
|
||||||
|
queryFn: () => apiClient.get<FlowTemplate[]>('/api/flow-templates'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const useTemplateMutation = useMutation({
|
||||||
|
mutationFn: ({ templateId, name }: { templateId: string; name: string }) => {
|
||||||
|
const encodedName = encodeURIComponent(name);
|
||||||
|
return apiClient.post<{ flow_id: string }>(
|
||||||
|
`/api/flow-templates/${templateId}/use?flow_name=${encodedName}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
message.success('Flujo creado desde plantilla');
|
||||||
|
setIsCreateModalOpen(false);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
setFlowName('');
|
||||||
|
navigate(`/flows/${data.flow_id}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleUseTemplate(template: FlowTemplate): void {
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
setFlowName(`${template.name} - Copia`);
|
||||||
|
setIsCreateModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalCancel(): void {
|
||||||
|
setIsCreateModalOpen(false);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleModalOk(): void {
|
||||||
|
if (selectedTemplate && flowName) {
|
||||||
|
useTemplateMutation.mutate({ templateId: selectedTemplate.id, name: flowName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderContent() {
|
||||||
|
if (isLoading) {
|
||||||
|
return <Card loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templates || templates.length === 0) {
|
||||||
|
return <Empty description="No hay plantillas disponibles" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{templates.map((template) => (
|
||||||
|
<Col key={template.id} xs={24} sm={12} md={8} lg={6}>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
actions={[
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => handleUseTemplate(template)}
|
||||||
|
>
|
||||||
|
Usar
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Card.Meta
|
||||||
|
title={template.name}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<Tag color={categoryColors[template.category] || 'default'}>
|
||||||
|
{template.category}
|
||||||
|
</Tag>
|
||||||
|
<Paragraph ellipsis={{ rows: 2 }} style={{ marginTop: 8, marginBottom: 0 }}>
|
||||||
|
{template.description || 'Sin descripcion'}
|
||||||
|
</Paragraph>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{template.nodes.length} nodos
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
|
||||||
|
<Title level={4} style={{ margin: 0 }}>Plantillas de Flujos</Title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderContent()}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="Crear flujo desde plantilla"
|
||||||
|
open={isCreateModalOpen}
|
||||||
|
onCancel={handleModalCancel}
|
||||||
|
onOk={handleModalOk}
|
||||||
|
okText="Crear"
|
||||||
|
confirmLoading={useTemplateMutation.isPending}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Text strong>Plantilla: </Text>
|
||||||
|
<Text>{selectedTemplate?.name}</Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Nombre del nuevo flujo"
|
||||||
|
value={flowName}
|
||||||
|
onChange={(e) => setFlowName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user