[new/parecer juridico]
This commit is contained in:
@@ -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<OpinionRecord[]>([]);
|
||||
const [pendingOpinions, setPendingOpinions] = useState<OpinionRecord[]>([]); // 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 = () => {
|
||||
<ArrowUpDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead className="hidden lg:table-cell text-center">
|
||||
<span className="font-semibold text-xs md:text-sm">Status</span>
|
||||
</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -300,13 +341,13 @@ export const AgentView = () => {
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-12 text-muted-foreground">
|
||||
<TableCell colSpan={5} className="text-center py-12 text-muted-foreground">
|
||||
Carregando pareceres...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : sortedOpinions.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-12 text-muted-foreground">
|
||||
<TableCell colSpan={5} className="text-center py-12 text-muted-foreground">
|
||||
{searchTerm
|
||||
? "Nenhum parecer encontrado"
|
||||
: "Nenhum parecer criado ainda. Clique em 'Novo Parecer' para começar."}
|
||||
@@ -317,6 +358,26 @@ export const AgentView = () => {
|
||||
<TableRow key={opinion.id}>
|
||||
<TableCell className="font-medium text-xs md:text-sm">{opinion.titulo}</TableCell>
|
||||
<TableCell className="hidden md:table-cell text-sm">{opinion.categoria || "-"}</TableCell>
|
||||
<TableCell className="hidden lg:table-cell text-center">
|
||||
{opinion.status === 'processando' && (
|
||||
<Badge variant="secondary" className="gap-1 bg-yellow-100 text-yellow-800 hover:bg-yellow-100">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
Gerando...
|
||||
</Badge>
|
||||
)}
|
||||
{opinion.status === 'concluido' && (
|
||||
<Badge variant="secondary" className="gap-1 bg-green-100 text-green-800 hover:bg-green-100">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
Concluído
|
||||
</Badge>
|
||||
)}
|
||||
{opinion.status === 'erro' && (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<XCircle className="w-3 h-3" />
|
||||
Erro
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell text-xs md:text-sm">
|
||||
{new Date(opinion.created_at).toLocaleDateString("pt-BR")}
|
||||
</TableCell>
|
||||
@@ -328,6 +389,7 @@ export const AgentView = () => {
|
||||
onClick={() => handleViewOpinion(opinion)}
|
||||
title="Visualizar"
|
||||
className="h-7 w-7 md:h-9 md:w-9"
|
||||
disabled={opinion.isLocalPending || opinion.status === 'processando'}
|
||||
>
|
||||
<Eye className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</Button>
|
||||
@@ -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'}
|
||||
>
|
||||
<Download className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</Button>
|
||||
@@ -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}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</Button>
|
||||
@@ -438,6 +502,7 @@ export const AgentView = () => {
|
||||
onOpenChange={setIsDialogOpen}
|
||||
selectedOpinion={selectedOpinion}
|
||||
onOpinionCreated={handleOpinionCreated}
|
||||
onOpinionCreating={handleOpinionCreating}
|
||||
/>
|
||||
|
||||
<AlertDialog open={!!opinionToDelete} onOpenChange={(open) => !open && setOpinionToDelete(null)}>
|
||||
|
||||
@@ -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: (
|
||||
<ToastAction altText="Fechar notificação" onClick={() => loadingToast.dismiss()}>
|
||||
OK
|
||||
</ToastAction>
|
||||
),
|
||||
});
|
||||
} 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+11
-1
@@ -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<CreateOpinionResponse>(
|
||||
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)
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user