feat(fase4): add FlowTemplates frontend page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude AI
2026-01-29 11:13:10 +00:00
parent af061b1a07
commit 76c48ff78f

View 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>
);
}