[Parecer Juridico]
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Plus, Search, Eye, Trash2, ArrowUpDown } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus, Search, Eye, Trash2, ArrowUpDown, Download } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -17,30 +17,84 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { OpinionDialog } from "./OpinionDialog";
|
||||
import { AgentSearch } from "./AgentSearch";
|
||||
import { agentService, OpinionRecord } from "@/services/agent";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
export interface LegalOpinion {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
createdAt: Date;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
type SortField = "title" | "createdAt" | "category";
|
||||
type SortField = "titulo" | "created_at" | "categoria";
|
||||
type SortOrder = "asc" | "desc";
|
||||
|
||||
export const AgentView = () => {
|
||||
const [opinions, setOpinions] = useState<LegalOpinion[]>([]);
|
||||
const [opinions, setOpinions] = useState<OpinionRecord[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
const [sortField, setSortField] = useState<SortField>("createdAt");
|
||||
const [sortField, setSortField] = useState<SortField>("created_at");
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [selectedOpinion, setSelectedOpinion] = useState<LegalOpinion | null>(null);
|
||||
const [selectedOpinion, setSelectedOpinion] = useState<OpinionRecord | null>(null);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [opinionToDelete, setOpinionToDelete] = useState<OpinionRecord | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Carrega pareceres da API
|
||||
const loadOpinions = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await agentService.getOpinions({
|
||||
page: currentPage,
|
||||
per_page: itemsPerPage,
|
||||
search: searchTerm,
|
||||
});
|
||||
setOpinions(data);
|
||||
} catch (error: any) {
|
||||
console.error('Erro ao carregar pareceres:', error);
|
||||
toast({
|
||||
title: "Erro ao carregar pareceres",
|
||||
description: error.message || "Não foi possível carregar a lista de pareceres.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Carrega pareceres ao montar o componente e quando mudar página/busca
|
||||
useEffect(() => {
|
||||
loadOpinions();
|
||||
}, [currentPage, itemsPerPage]);
|
||||
|
||||
// Debounce para busca
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (currentPage === 1) {
|
||||
loadOpinions();
|
||||
} else {
|
||||
setCurrentPage(1); // Volta para a primeira página ao buscar
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
@@ -51,39 +105,26 @@ export const AgentView = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const filteredOpinions = opinions.filter(
|
||||
(op) =>
|
||||
op.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
op.category?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const sortedOpinions = [...filteredOpinions].sort((a, b) => {
|
||||
const sortedOpinions = [...opinions].sort((a, b) => {
|
||||
const multiplier = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
if (sortField === "createdAt") {
|
||||
return multiplier * (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||
|
||||
if (sortField === "created_at") {
|
||||
return multiplier * (new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
||||
}
|
||||
|
||||
|
||||
const aValue = a[sortField] || "";
|
||||
const bValue = b[sortField] || "";
|
||||
return multiplier * aValue.toString().localeCompare(bValue.toString());
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(sortedOpinions.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const paginatedOpinions = sortedOpinions.slice(startIndex, startIndex + itemsPerPage);
|
||||
|
||||
const handleOpinionCreated = (newOpinion: LegalOpinion) => {
|
||||
setOpinions([newOpinion, ...opinions]);
|
||||
setIsDialogOpen(false);
|
||||
setSelectedOpinion(null);
|
||||
const handleOpinionCreated = () => {
|
||||
// Recarrega a lista após criar um novo parecer
|
||||
loadOpinions();
|
||||
};
|
||||
|
||||
const handleDeleteOpinion = (id: string) => {
|
||||
setOpinions(opinions.filter(op => op.id !== id));
|
||||
};
|
||||
|
||||
const handleViewOpinion = (opinion: LegalOpinion) => {
|
||||
const handleViewOpinion = (opinion: OpinionRecord) => {
|
||||
setSelectedOpinion(opinion);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
@@ -93,12 +134,69 @@ export const AgentView = () => {
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSelectFromSearch = (opinion: LegalOpinion) => {
|
||||
const handleSelectFromSearch = (opinion: OpinionRecord) => {
|
||||
setSelectedOpinion(opinion);
|
||||
setShowSearch(false);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDownloadVersion = async (opinion: OpinionRecord, version: 'v1' | 'v2') => {
|
||||
const fileUrl = version === 'v1' ? opinion.file_url : opinion.file_url_melhoria;
|
||||
|
||||
if (!fileUrl) {
|
||||
toast({
|
||||
title: "Arquivo não disponível",
|
||||
description: `A ${version === 'v1' ? 'versão 1' : 'versão melhorada'} ainda não está disponível.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileName = `${opinion.titulo}_${version === 'v1' ? 'v1' : 'melhorada'}.docx`;
|
||||
await agentService.downloadOpinion(fileUrl, fileName);
|
||||
|
||||
toast({
|
||||
title: "Download iniciado",
|
||||
description: "O parecer está sendo baixado.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Erro ao fazer download",
|
||||
description: error.message || "Não foi possível baixar o arquivo.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteOpinion = async () => {
|
||||
if (!opinionToDelete) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await agentService.deleteOpinion(opinionToDelete.id);
|
||||
|
||||
toast({
|
||||
title: "Parecer excluído",
|
||||
description: "O parecer foi excluído com sucesso.",
|
||||
});
|
||||
|
||||
// Recarrega a lista de pareceres
|
||||
await loadOpinions();
|
||||
|
||||
// Fecha o diálogo de confirmação
|
||||
setOpinionToDelete(null);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "Erro ao excluir parecer",
|
||||
description: error.message || "Não foi possível excluir o parecer.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (showSearch) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -116,7 +214,7 @@ export const AgentView = () => {
|
||||
<div className="flex flex-col h-full bg-background pb-16 md:pb-0">
|
||||
<div className="p-3 md:p-6 border-b border-border">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 mb-4">
|
||||
<h1 className="text-xl md:text-3xl font-bold text-foreground">Agente de Parecer Jurídico</h1>
|
||||
<h1 className="text-xl md:text-3xl font-bold text-foreground">Gerador de Modelos de Parecer Jurídico</h1>
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Button onClick={() => setShowSearch(true)} variant="outline" className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
|
||||
<Search className="w-3 h-3 md:w-4 md:h-4" />
|
||||
@@ -169,7 +267,7 @@ export const AgentView = () => {
|
||||
<TableHead className="min-w-[200px]">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handleSort("title")}
|
||||
onClick={() => handleSort("titulo")}
|
||||
className="flex items-center gap-1 font-semibold text-xs md:text-sm p-1 md:p-2"
|
||||
>
|
||||
Título
|
||||
@@ -179,7 +277,7 @@ export const AgentView = () => {
|
||||
<TableHead className="hidden md:table-cell">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handleSort("category")}
|
||||
onClick={() => handleSort("categoria")}
|
||||
className="flex items-center gap-1 font-semibold text-sm"
|
||||
>
|
||||
Categoria
|
||||
@@ -189,7 +287,7 @@ export const AgentView = () => {
|
||||
<TableHead className="hidden sm:table-cell">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handleSort("createdAt")}
|
||||
onClick={() => handleSort("created_at")}
|
||||
className="flex items-center gap-1 font-semibold text-xs md:text-sm p-1 md:p-2"
|
||||
>
|
||||
Data
|
||||
@@ -200,7 +298,13 @@ export const AgentView = () => {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedOpinions.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} 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">
|
||||
{searchTerm
|
||||
@@ -209,12 +313,12 @@ export const AgentView = () => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
paginatedOpinions.map((opinion) => (
|
||||
sortedOpinions.map((opinion) => (
|
||||
<TableRow key={opinion.id}>
|
||||
<TableCell className="font-medium text-xs md:text-sm">{opinion.title}</TableCell>
|
||||
<TableCell className="hidden md:table-cell text-sm">{opinion.category || "-"}</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 sm:table-cell text-xs md:text-sm">
|
||||
{new Date(opinion.createdAt).toLocaleDateString("pt-BR")}
|
||||
{new Date(opinion.created_at).toLocaleDateString("pt-BR")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
@@ -227,12 +331,40 @@ export const AgentView = () => {
|
||||
>
|
||||
<Eye className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Baixar"
|
||||
className="h-7 w-7 md:h-9 md:w-9"
|
||||
>
|
||||
<Download className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDownloadVersion(opinion, 'v1')}
|
||||
disabled={!opinion.file_url}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Versão 1
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDownloadVersion(opinion, 'v2')}
|
||||
disabled={!opinion.file_url_melhoria}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Versão Melhorada
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteOpinion(opinion.id)}
|
||||
className="text-destructive hover:text-destructive h-7 w-7 md:h-9 md:w-9"
|
||||
onClick={() => setOpinionToDelete(opinion)}
|
||||
title="Excluir"
|
||||
className="h-7 w-7 md:h-9 md:w-9 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</Button>
|
||||
@@ -245,10 +377,10 @@ export const AgentView = () => {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
{sortedOpinions.length > 0 && (
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 mt-4">
|
||||
<p className="text-xs md:text-sm text-muted-foreground text-center sm:text-left">
|
||||
{startIndex + 1}-{Math.min(startIndex + itemsPerPage, sortedOpinions.length)} de {sortedOpinions.length}
|
||||
Mostrando {sortedOpinions.length} {sortedOpinions.length === 1 ? 'parecer' : 'pareceres'}
|
||||
</p>
|
||||
<div className="flex gap-1 md:gap-2 justify-center">
|
||||
<Button
|
||||
@@ -307,6 +439,27 @@ export const AgentView = () => {
|
||||
selectedOpinion={selectedOpinion}
|
||||
onOpinionCreated={handleOpinionCreated}
|
||||
/>
|
||||
|
||||
<AlertDialog open={!!opinionToDelete} onOpenChange={(open) => !open && setOpinionToDelete(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tem certeza que deseja excluir o parecer "{opinionToDelete?.titulo}"? Esta ação não pode ser desfeita.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isDeleting}>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteOpinion}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{isDeleting ? "Excluindo..." : "Excluir"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user