Files
SeConectai_Vendas/frontend/app/master/evolution-config/page.tsx
T
tiagoandClaude Sonnet 5 323912748f Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)
Adiciona o frontend Next.js em frontend/ (setup, design system,
autenticação/tema, layout/navegação, área master, área tenant completa:
clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas,
equipe e perfil). Estende o backend de forma aditiva e autorizada:
cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe +
edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo
e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:41:13 -03:00

224 lines
8.0 KiB
TypeScript

'use client';
import * as React from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plug, Plus } from 'lucide-react';
import {
createEvolutionConfig,
deleteEvolutionConfig,
listEvolutionConfigs,
testarConexaoEvolution,
updateEvolutionConfig,
type EvolutionConfig,
} from '@/lib/api/evolution-config';
import {
evolutionConfigSchema,
type EvolutionConfigFormValues,
} from '@/lib/validators/evolution-config';
import { ApiError } from '@/lib/api/client';
import { formatDate } from '@/lib/format';
import { toast } from '@/lib/toast';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { DataTable, type DataTableColumn } from '@/components/ui/data-table';
import { FormField } from '@/components/ui/form-field';
import { Input } from '@/components/ui/input';
import { ResponsiveDialog } from '@/components/ui/responsive-dialog';
import { ConfirmDialog } from '@/components/domain/confirm-dialog';
export default function EvolutionConfigPage() {
const queryClient = useQueryClient();
const { data: configs, isLoading } = useQuery({
queryKey: ['master', 'evolution-config'],
queryFn: listEvolutionConfigs,
});
const [editing, setEditing] = React.useState<EvolutionConfig | null>(null);
const [formOpen, setFormOpen] = React.useState(false);
const [deleting, setDeleting] = React.useState<EvolutionConfig | null>(null);
const [testingId, setTestingId] = React.useState<string | null>(null);
const form = useForm<EvolutionConfigFormValues>({ resolver: zodResolver(evolutionConfigSchema) });
function openCreate() {
setEditing(null);
form.reset({ nome: '', base_url: '', api_key: '', versao: '' });
setFormOpen(true);
}
function openEdit(config: EvolutionConfig) {
setEditing(config);
form.reset({ nome: config.nome, base_url: config.base_url, api_key: '', versao: config.versao });
setFormOpen(true);
}
const saveMutation = useMutation({
mutationFn: (values: EvolutionConfigFormValues) => {
if (editing) {
return updateEvolutionConfig(editing.id, {
nome: values.nome,
base_url: values.base_url,
versao: values.versao || undefined,
...(values.api_key ? { api_key: values.api_key } : {}),
});
}
return createEvolutionConfig({
nome: values.nome,
base_url: values.base_url,
api_key: values.api_key,
versao: values.versao || undefined,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['master', 'evolution-config'] });
toast.success(editing ? 'Configuração atualizada.' : 'Configuração criada.');
setFormOpen(false);
},
onError: (err) => {
toast.error(err instanceof ApiError ? err.message : 'Não foi possível salvar a configuração.');
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteEvolutionConfig(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['master', 'evolution-config'] });
toast.success('Configuração excluída.');
setDeleting(null);
},
onError: (err) => {
toast.error(err instanceof ApiError ? err.message : 'Não foi possível excluir a configuração.');
setDeleting(null);
},
});
const testMutation = useMutation({
mutationFn: (id: string) => testarConexaoEvolution(id),
onMutate: (id) => setTestingId(id),
onSuccess: (result) => {
toast.success(`Conexão OK (status ${result.status_http}${result.versao ? `, versão ${result.versao}` : ''}).`);
},
onError: (err) => {
toast.error(err instanceof ApiError ? err.message : 'Falha ao testar a conexão.');
},
onSettled: () => setTestingId(null),
});
const columns: DataTableColumn<EvolutionConfig>[] = [
{ id: 'nome', header: 'Nome', primary: true, cell: (row) => row.nome },
{ id: 'base_url', header: 'URL base', cell: (row) => row.base_url },
{ id: 'api_key', header: 'API key', cell: (row) => <code className="text-xs">{row.api_key}</code> },
{ id: 'versao', header: 'Versão', cell: (row) => row.versao },
{
id: 'status',
header: 'Status',
primary: true,
cell: (row) => <Badge variant={row.ativo ? 'default' : 'outline'}>{row.ativo ? 'Ativo' : 'Inativo'}</Badge>,
},
{ id: 'criado_em', header: 'Criado em', hideOnMobileCard: true, cell: (row) => formatDate(row.criado_em) },
];
return (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-xl font-semibold text-foreground">Evolution API</h1>
<p className="text-sm text-muted-foreground">Conexão usada para disparar mensagens de WhatsApp</p>
</div>
<Button onClick={openCreate}>
<Plus /> Nova configuração
</Button>
</div>
<DataTable
columns={columns}
data={configs ?? []}
keyField={(row) => row.id}
loading={isLoading}
emptyTitle="Nenhuma configuração cadastrada"
emptyDescription="Cadastre a conexão com a Evolution API para habilitar o envio de WhatsApp."
emptyAction={
<Button size="sm" onClick={openCreate}>
Nova configuração
</Button>
}
rowActions={(row) => (
<>
<Button
variant="ghost"
size="sm"
loading={testingId === row.id}
onClick={() => testMutation.mutate(row.id)}
>
<Plug /> Testar conexão
</Button>
<Button variant="ghost" size="sm" onClick={() => openEdit(row)}>
Editar
</Button>
<Button variant="ghost" size="sm" onClick={() => setDeleting(row)}>
Excluir
</Button>
</>
)}
/>
<ResponsiveDialog
open={formOpen}
onOpenChange={setFormOpen}
title={editing ? 'Editar configuração' : 'Nova configuração'}
footer={
<>
<Button variant="outline" onClick={() => setFormOpen(false)}>
Cancelar
</Button>
<Button
loading={saveMutation.isPending}
onClick={form.handleSubmit((values) => saveMutation.mutate(values))}
>
Salvar
</Button>
</>
}
>
<form
className="flex flex-col gap-4 px-4 pb-4 sm:px-0 sm:pb-0"
onSubmit={form.handleSubmit((values) => saveMutation.mutate(values))}
>
<FormField label="Nome" error={form.formState.errors.nome?.message} required>
<Input {...form.register('nome')} />
</FormField>
<FormField label="URL base" error={form.formState.errors.base_url?.message} required>
<Input placeholder="https://evolution.exemplo.com" {...form.register('base_url')} />
</FormField>
<FormField
label="API key"
error={form.formState.errors.api_key?.message}
helpText={editing ? 'Deixe em branco para manter a chave atual' : undefined}
required={!editing}
>
<Input type="password" autoComplete="off" {...form.register('api_key')} />
</FormField>
<FormField label="Versão" error={form.formState.errors.versao?.message}>
<Input placeholder="2.3" {...form.register('versao')} />
</FormField>
</form>
</ResponsiveDialog>
<ConfirmDialog
open={deleting !== null}
onOpenChange={(open) => !open && setDeleting(null)}
title={`Excluir "${deleting?.nome}"?`}
description="Só é possível excluir se nenhuma instância de WhatsApp estiver usando esta configuração."
confirmLabel="Excluir"
destructive
loading={deleteMutation.isPending}
onConfirm={() => deleting && deleteMutation.mutate(deleting.id)}
/>
</div>
);
}