From a28b780718692e61f884db37ddfa555dc3acebb2 Mon Sep 17 00:00:00 2001 From: luisfepsale Date: Wed, 19 Nov 2025 15:19:41 -0300 Subject: [PATCH] [new/parecer juridico] --- src/components/agent/AgentView.tsx | 89 ++++++++++++++++++++++---- src/components/agent/OpinionDialog.tsx | 85 ++++++++++++++++++------ src/services/agent.ts | 12 +++- 3 files changed, 152 insertions(+), 34 deletions(-) diff --git a/src/components/agent/AgentView.tsx b/src/components/agent/AgentView.tsx index 7634aa8..3678979 100644 --- a/src/components/agent/AgentView.tsx +++ b/src/components/agent/AgentView.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from "react"; -import { Plus, Search, Eye, Trash2, ArrowUpDown, Download } from "lucide-react"; +import { useState, useEffect, useCallback } from "react"; +import { Plus, Search, Eye, Trash2, ArrowUpDown, Download, Loader2, CheckCircle2, XCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; import { Table, TableBody, @@ -43,6 +44,7 @@ type SortOrder = "asc" | "desc"; export const AgentView = () => { const [opinions, setOpinions] = useState([]); + const [pendingOpinions, setPendingOpinions] = useState([]); // Registros temporários sendo processados const [searchTerm, setSearchTerm] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); @@ -57,7 +59,7 @@ export const AgentView = () => { const { toast } = useToast(); // Carrega pareceres da API - const loadOpinions = async () => { + const loadOpinions = useCallback(async () => { setIsLoading(true); try { const data = await agentService.getOpinions({ @@ -65,7 +67,24 @@ export const AgentView = () => { per_page: itemsPerPage, search: searchTerm, }); - setOpinions(data); + + // Adiciona status aos pareceres da API + const opinionsWithStatus = data.map(opinion => ({ + ...opinion, + status: (opinion.file_url || opinion.file_url_melhoria) ? 'concluido' : 'processando' as const, + })); + + setOpinions(opinionsWithStatus); + + // Remove registros temporários que agora estão na API + setPendingOpinions(prev => + prev.filter(pending => + !opinionsWithStatus.some(opinion => + opinion.titulo === pending.titulo && + opinion.created_at.substring(0, 10) === pending.created_at.substring(0, 10) + ) + ) + ); } catch (error: any) { console.error('Erro ao carregar pareceres:', error); toast({ @@ -76,12 +95,23 @@ export const AgentView = () => { } finally { setIsLoading(false); } - }; + }, [currentPage, itemsPerPage, searchTerm, toast]); - // Carrega pareceres ao montar o componente e quando mudar página/busca + // Polling automático para atualizar status dos pareceres useEffect(() => { + // Carrega imediatamente loadOpinions(); - }, [currentPage, itemsPerPage]); + + // Configura polling a cada 10 segundos se houver pareceres pendentes + const interval = setInterval(() => { + if (pendingOpinions.length > 0) { + console.log('Polling: Atualizando lista de pareceres...'); + loadOpinions(); + } + }, 10000); // 10 segundos + + return () => clearInterval(interval); + }, [currentPage, itemsPerPage, pendingOpinions.length, loadOpinions]); // Debounce para busca useEffect(() => { @@ -94,7 +124,7 @@ export const AgentView = () => { }, 500); return () => clearTimeout(timer); - }, [searchTerm]); + }, [searchTerm, loadOpinions]); const handleSort = (field: SortField) => { if (sortField === field) { @@ -105,7 +135,10 @@ export const AgentView = () => { } }; - const sortedOpinions = [...opinions].sort((a, b) => { + // Mescla pareceres da API com registros temporários + const allOpinions = [...pendingOpinions, ...opinions]; + + const sortedOpinions = [...allOpinions].sort((a, b) => { const multiplier = sortOrder === "asc" ? 1 : -1; if (sortField === "created_at") { @@ -119,8 +152,13 @@ export const AgentView = () => { const totalPages = Math.ceil(sortedOpinions.length / itemsPerPage); + // Callback quando um parecer está sendo criado (registro temporário) + const handleOpinionCreating = (tempOpinion: OpinionRecord) => { + setPendingOpinions(prev => [tempOpinion, ...prev]); + }; + + // Callback quando um parecer foi criado (recarrega da API) const handleOpinionCreated = () => { - // Recarrega a lista após criar um novo parecer loadOpinions(); }; @@ -284,6 +322,9 @@ export const AgentView = () => { + + Status + @@ -338,6 +400,7 @@ export const AgentView = () => { size="icon" title="Baixar" className="h-7 w-7 md:h-9 md:w-9" + disabled={opinion.isLocalPending || opinion.status === 'processando'} > @@ -365,6 +428,7 @@ export const AgentView = () => { onClick={() => setOpinionToDelete(opinion)} title="Excluir" className="h-7 w-7 md:h-9 md:w-9 text-destructive hover:text-destructive" + disabled={opinion.isLocalPending} > @@ -438,6 +502,7 @@ export const AgentView = () => { onOpenChange={setIsDialogOpen} selectedOpinion={selectedOpinion} onOpinionCreated={handleOpinionCreated} + onOpinionCreating={handleOpinionCreating} /> !open && setOpinionToDelete(null)}> diff --git a/src/components/agent/OpinionDialog.tsx b/src/components/agent/OpinionDialog.tsx index 9c13608..dba8200 100644 --- a/src/components/agent/OpinionDialog.tsx +++ b/src/components/agent/OpinionDialog.tsx @@ -13,6 +13,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useToast } from "@/hooks/use-toast"; +import { ToastAction } from "@/components/ui/toast"; import { agentService, OpinionRecord } from "@/services/agent"; interface OpinionDialogProps { @@ -20,6 +21,7 @@ interface OpinionDialogProps { onOpenChange: (open: boolean) => void; selectedOpinion: OpinionRecord | null; onOpinionCreated: () => void; + onOpinionCreating?: (tempOpinion: OpinionRecord) => void; // Callback para adicionar registro temporário } export const OpinionDialog = ({ @@ -27,6 +29,7 @@ export const OpinionDialog = ({ onOpenChange, selectedOpinion, onOpinionCreated, + onOpinionCreating, }: OpinionDialogProps) => { const [title, setTitle] = useState(""); const [category, setCategory] = useState(""); @@ -68,46 +71,86 @@ export const OpinionDialog = ({ return; } - setIsGenerating(true); + // Criar registro temporário para adicionar na tabela imediatamente + const tempId = `temp-${Date.now()}`; + const tempOpinion: OpinionRecord = { + id: tempId, + estabelecimento_id: 0, + user_email: '', + titulo: title, + categoria: category, + instrucoes: instructions, + file_url: '', + file_url_melhoria: '', + created_at: new Date().toISOString(), + status: 'processando', + isLocalPending: true, + }; + // Adiciona o registro temporário na tabela + if (onOpinionCreating) { + onOpinionCreating(tempOpinion); + } + + // Criar toast de progresso que não fecha automaticamente + const loadingToast = toast({ + title: "Gerando parecer jurídico...", + description: "O parecer está sendo processado. Isso pode levar alguns minutos.", + duration: Infinity, // Toast não fecha automaticamente + }); + + // Fecha o dialog imediatamente + setIsGenerating(false); + onOpenChange(false); + + // Limpa os campos + setTitle(""); + setCategory(""); + setInstructions(""); + setCreatedOpinion(null); + + // Executar requisição em background try { const response = await agentService.createOpinion({ - titulo: title, - categoria: category, - instrucoes: instructions, + titulo: tempOpinion.titulo, + categoria: tempOpinion.categoria, + instrucoes: tempOpinion.instrucoes, }); if (response.success) { - toast({ - title: "Parecer gerado com sucesso!", - description: "O parecer foi criado e está disponível para download.", - }); - - // Chama o callback para atualizar a lista + // Recarrega a lista para pegar o parecer real da API onOpinionCreated(); - // Fecha o dialog após sucesso - setIsGenerating(false); - onOpenChange(false); - - // Limpa os campos - setTitle(""); - setCategory(""); - setInstructions(""); - setCreatedOpinion(null); + // Atualizar toast para sucesso + loadingToast.update({ + id: loadingToast.id, + title: "Parecer gerado com sucesso!", + description: "O parecer foi criado e está disponível para download.", + duration: 8000, // Toast fecha após 8 segundos + action: ( + loadingToast.dismiss()}> + OK + + ), + }); } else { throw new Error('Falha ao criar parecer'); } } catch (error: any) { console.error("Erro ao gerar parecer:", error); - setIsGenerating(false); const errorMessage = error.message || "Não foi possível gerar o parecer. Tente novamente."; - toast({ + // Recarrega a lista para remover o registro temporário e tentar pegar dados reais + onOpinionCreated(); + + // Atualizar toast para erro + loadingToast.update({ + id: loadingToast.id, title: "Erro ao gerar parecer", description: errorMessage, variant: "destructive", + duration: 10000, // Toast de erro fecha após 10 segundos }); } }; diff --git a/src/services/agent.ts b/src/services/agent.ts index cc2b244..0b3219c 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -22,6 +22,11 @@ export interface CreateOpinionRequest { estabelecimentoId?: number; } +/** + * Status do parecer + */ +export type OpinionStatus = 'processando' | 'concluido' | 'erro'; + /** * Interface para um parecer retornado pela API */ @@ -35,6 +40,8 @@ export interface OpinionRecord { file_url: string; created_at: string; file_url_melhoria: string; + status?: OpinionStatus; // Status do processamento do parecer (pode vir da API ou ser local) + isLocalPending?: boolean; // Flag para indicar se é um registro local temporário } /** @@ -123,7 +130,7 @@ class AgentService { }); try { - // Faz a requisição usando o serviço de API + // Faz a requisição usando o serviço de API com timeout de 5 minutos const response = await apiService.post( this.CREATE_OPINION_ENDPOINT, { @@ -132,6 +139,9 @@ class AgentService { titulo: titulo.trim(), categoria: categoria.trim(), instrucoes: instrucoes.trim(), + }, + { + timeout: 300000, // 5 minutos para geração de parecer (processo demorado) } );