[new/parecer juridico]
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Plus, Search, Eye, Trash2, ArrowUpDown, Download } from "lucide-react";
|
import { Plus, Search, Eye, Trash2, ArrowUpDown, Download, Loader2, CheckCircle2, XCircle } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -43,6 +44,7 @@ type SortOrder = "asc" | "desc";
|
|||||||
|
|
||||||
export const AgentView = () => {
|
export const AgentView = () => {
|
||||||
const [opinions, setOpinions] = useState<OpinionRecord[]>([]);
|
const [opinions, setOpinions] = useState<OpinionRecord[]>([]);
|
||||||
|
const [pendingOpinions, setPendingOpinions] = useState<OpinionRecord[]>([]); // Registros temporários sendo processados
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
@@ -57,7 +59,7 @@ export const AgentView = () => {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Carrega pareceres da API
|
// Carrega pareceres da API
|
||||||
const loadOpinions = async () => {
|
const loadOpinions = useCallback(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await agentService.getOpinions({
|
const data = await agentService.getOpinions({
|
||||||
@@ -65,7 +67,24 @@ export const AgentView = () => {
|
|||||||
per_page: itemsPerPage,
|
per_page: itemsPerPage,
|
||||||
search: searchTerm,
|
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) {
|
} catch (error: any) {
|
||||||
console.error('Erro ao carregar pareceres:', error);
|
console.error('Erro ao carregar pareceres:', error);
|
||||||
toast({
|
toast({
|
||||||
@@ -76,12 +95,23 @@ export const AgentView = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
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(() => {
|
useEffect(() => {
|
||||||
|
// Carrega imediatamente
|
||||||
loadOpinions();
|
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
|
// Debounce para busca
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -94,7 +124,7 @@ export const AgentView = () => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [searchTerm]);
|
}, [searchTerm, loadOpinions]);
|
||||||
|
|
||||||
const handleSort = (field: SortField) => {
|
const handleSort = (field: SortField) => {
|
||||||
if (sortField === field) {
|
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;
|
const multiplier = sortOrder === "asc" ? 1 : -1;
|
||||||
|
|
||||||
if (sortField === "created_at") {
|
if (sortField === "created_at") {
|
||||||
@@ -119,8 +152,13 @@ export const AgentView = () => {
|
|||||||
|
|
||||||
const totalPages = Math.ceil(sortedOpinions.length / itemsPerPage);
|
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 = () => {
|
const handleOpinionCreated = () => {
|
||||||
// Recarrega a lista após criar um novo parecer
|
|
||||||
loadOpinions();
|
loadOpinions();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -284,6 +322,9 @@ export const AgentView = () => {
|
|||||||
<ArrowUpDown className="w-4 h-4" />
|
<ArrowUpDown className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableHead>
|
</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">
|
<TableHead className="hidden sm:table-cell">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -300,13 +341,13 @@ export const AgentView = () => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<TableRow>
|
<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...
|
Carregando pareceres...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : sortedOpinions.length === 0 ? (
|
) : sortedOpinions.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className="text-center py-12 text-muted-foreground">
|
<TableCell colSpan={5} className="text-center py-12 text-muted-foreground">
|
||||||
{searchTerm
|
{searchTerm
|
||||||
? "Nenhum parecer encontrado"
|
? "Nenhum parecer encontrado"
|
||||||
: "Nenhum parecer criado ainda. Clique em 'Novo Parecer' para começar."}
|
: "Nenhum parecer criado ainda. Clique em 'Novo Parecer' para começar."}
|
||||||
@@ -317,6 +358,26 @@ export const AgentView = () => {
|
|||||||
<TableRow key={opinion.id}>
|
<TableRow key={opinion.id}>
|
||||||
<TableCell className="font-medium text-xs md:text-sm">{opinion.titulo}</TableCell>
|
<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 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">
|
<TableCell className="hidden sm:table-cell text-xs md:text-sm">
|
||||||
{new Date(opinion.created_at).toLocaleDateString("pt-BR")}
|
{new Date(opinion.created_at).toLocaleDateString("pt-BR")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -328,6 +389,7 @@ export const AgentView = () => {
|
|||||||
onClick={() => handleViewOpinion(opinion)}
|
onClick={() => handleViewOpinion(opinion)}
|
||||||
title="Visualizar"
|
title="Visualizar"
|
||||||
className="h-7 w-7 md:h-9 md:w-9"
|
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" />
|
<Eye className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -338,6 +400,7 @@ export const AgentView = () => {
|
|||||||
size="icon"
|
size="icon"
|
||||||
title="Baixar"
|
title="Baixar"
|
||||||
className="h-7 w-7 md:h-9 md:w-9"
|
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" />
|
<Download className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -365,6 +428,7 @@ export const AgentView = () => {
|
|||||||
onClick={() => setOpinionToDelete(opinion)}
|
onClick={() => setOpinionToDelete(opinion)}
|
||||||
title="Excluir"
|
title="Excluir"
|
||||||
className="h-7 w-7 md:h-9 md:w-9 text-destructive hover:text-destructive"
|
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" />
|
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -438,6 +502,7 @@ export const AgentView = () => {
|
|||||||
onOpenChange={setIsDialogOpen}
|
onOpenChange={setIsDialogOpen}
|
||||||
selectedOpinion={selectedOpinion}
|
selectedOpinion={selectedOpinion}
|
||||||
onOpinionCreated={handleOpinionCreated}
|
onOpinionCreated={handleOpinionCreated}
|
||||||
|
onOpinionCreating={handleOpinionCreating}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertDialog open={!!opinionToDelete} onOpenChange={(open) => !open && setOpinionToDelete(null)}>
|
<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 { Label } from "@/components/ui/label";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { ToastAction } from "@/components/ui/toast";
|
||||||
import { agentService, OpinionRecord } from "@/services/agent";
|
import { agentService, OpinionRecord } from "@/services/agent";
|
||||||
|
|
||||||
interface OpinionDialogProps {
|
interface OpinionDialogProps {
|
||||||
@@ -20,6 +21,7 @@ interface OpinionDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
selectedOpinion: OpinionRecord | null;
|
selectedOpinion: OpinionRecord | null;
|
||||||
onOpinionCreated: () => void;
|
onOpinionCreated: () => void;
|
||||||
|
onOpinionCreating?: (tempOpinion: OpinionRecord) => void; // Callback para adicionar registro temporário
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OpinionDialog = ({
|
export const OpinionDialog = ({
|
||||||
@@ -27,6 +29,7 @@ export const OpinionDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
selectedOpinion,
|
selectedOpinion,
|
||||||
onOpinionCreated,
|
onOpinionCreated,
|
||||||
|
onOpinionCreating,
|
||||||
}: OpinionDialogProps) => {
|
}: OpinionDialogProps) => {
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [category, setCategory] = useState("");
|
const [category, setCategory] = useState("");
|
||||||
@@ -68,46 +71,86 @@ export const OpinionDialog = ({
|
|||||||
return;
|
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 {
|
try {
|
||||||
const response = await agentService.createOpinion({
|
const response = await agentService.createOpinion({
|
||||||
titulo: title,
|
titulo: tempOpinion.titulo,
|
||||||
categoria: category,
|
categoria: tempOpinion.categoria,
|
||||||
instrucoes: instructions,
|
instrucoes: tempOpinion.instrucoes,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast({
|
// Recarrega a lista para pegar o parecer real da API
|
||||||
title: "Parecer gerado com sucesso!",
|
|
||||||
description: "O parecer foi criado e está disponível para download.",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Chama o callback para atualizar a lista
|
|
||||||
onOpinionCreated();
|
onOpinionCreated();
|
||||||
|
|
||||||
// Fecha o dialog após sucesso
|
// Atualizar toast para sucesso
|
||||||
setIsGenerating(false);
|
loadingToast.update({
|
||||||
onOpenChange(false);
|
id: loadingToast.id,
|
||||||
|
title: "Parecer gerado com sucesso!",
|
||||||
// Limpa os campos
|
description: "O parecer foi criado e está disponível para download.",
|
||||||
setTitle("");
|
duration: 8000, // Toast fecha após 8 segundos
|
||||||
setCategory("");
|
action: (
|
||||||
setInstructions("");
|
<ToastAction altText="Fechar notificação" onClick={() => loadingToast.dismiss()}>
|
||||||
setCreatedOpinion(null);
|
OK
|
||||||
|
</ToastAction>
|
||||||
|
),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Falha ao criar parecer');
|
throw new Error('Falha ao criar parecer');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Erro ao gerar parecer:", error);
|
console.error("Erro ao gerar parecer:", error);
|
||||||
setIsGenerating(false);
|
|
||||||
|
|
||||||
const errorMessage = error.message || "Não foi possível gerar o parecer. Tente novamente.";
|
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",
|
title: "Erro ao gerar parecer",
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
|
duration: 10000, // Toast de erro fecha após 10 segundos
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+11
-1
@@ -22,6 +22,11 @@ export interface CreateOpinionRequest {
|
|||||||
estabelecimentoId?: number;
|
estabelecimentoId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status do parecer
|
||||||
|
*/
|
||||||
|
export type OpinionStatus = 'processando' | 'concluido' | 'erro';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface para um parecer retornado pela API
|
* Interface para um parecer retornado pela API
|
||||||
*/
|
*/
|
||||||
@@ -35,6 +40,8 @@ export interface OpinionRecord {
|
|||||||
file_url: string;
|
file_url: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
file_url_melhoria: 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 {
|
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>(
|
const response = await apiService.post<CreateOpinionResponse>(
|
||||||
this.CREATE_OPINION_ENDPOINT,
|
this.CREATE_OPINION_ENDPOINT,
|
||||||
{
|
{
|
||||||
@@ -132,6 +139,9 @@ class AgentService {
|
|||||||
titulo: titulo.trim(),
|
titulo: titulo.trim(),
|
||||||
categoria: categoria.trim(),
|
categoria: categoria.trim(),
|
||||||
instrucoes: instrucoes.trim(),
|
instrucoes: instrucoes.trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: 300000, // 5 minutos para geração de parecer (processo demorado)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user