317 lines
14 KiB
TypeScript
317 lines
14 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { Sparkles, Edit, Plus, Bot } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { usePrompts } from "@/contexts/PromptsContext";
|
|
import { parecerService, type ParecerListItem, type ParecerStatus } from "@/services/parecerApi";
|
|
import { toast } from "sonner";
|
|
|
|
const STATUS_OPTIONS: { value: "" | ParecerStatus; label: string }[] = [
|
|
{ value: "", label: "Todos os status" },
|
|
{ value: "processando", label: "Processando" },
|
|
{ value: "concluido", label: "Concluído" },
|
|
{ value: "erro", label: "Erro" },
|
|
];
|
|
|
|
function formatarData(createdAt: string): string {
|
|
if (!createdAt) return "—";
|
|
try {
|
|
const d = new Date(createdAt.replace(" ", "T"));
|
|
return d.toLocaleDateString("pt-BR", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
} catch {
|
|
return createdAt;
|
|
}
|
|
}
|
|
|
|
function statusEstilo(status: string): { dot: string; text: string; label: string } {
|
|
const s = (status ?? "").toLowerCase();
|
|
if (s === "concluido") return { dot: "bg-emerald-500", text: "text-emerald-600 dark:text-emerald-400", label: "Concluído" };
|
|
if (s === "processando") return { dot: "bg-amber-500", text: "text-amber-600 dark:text-amber-400", label: "Processando" };
|
|
if (s === "erro") return { dot: "bg-red-500", text: "text-red-600 dark:text-red-400", label: "Erro" };
|
|
return { dot: "bg-muted-foreground/50", text: "text-muted-foreground", label: status || "—" };
|
|
}
|
|
|
|
export const ParecerJuridicoView = () => {
|
|
const navigate = useNavigate();
|
|
const { areaItems, refreshAreas } = usePrompts();
|
|
|
|
const [data, setData] = useState<ParecerListItem[]>([]);
|
|
const [totalRegistros, setTotalRegistros] = useState(0);
|
|
const [totalPaginas, setTotalPaginas] = useState(1);
|
|
const [paginaAtual, setPaginaAtual] = useState(1);
|
|
const [perPage, setPerPage] = useState(10);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [filterTitulo, setFilterTitulo] = useState("");
|
|
const [filterAreaId, setFilterAreaId] = useState<string>("");
|
|
const [filterStatus, setFilterStatus] = useState<"" | ParecerStatus>("");
|
|
|
|
const hasActiveFilters = filterTitulo.trim() !== "" || filterAreaId !== "" || filterStatus !== "";
|
|
|
|
const clearFilters = () => {
|
|
setFilterTitulo("");
|
|
setFilterAreaId("");
|
|
setFilterStatus("");
|
|
setPaginaAtual(1);
|
|
};
|
|
|
|
const loadList = useCallback(() => {
|
|
setLoading(true);
|
|
parecerService
|
|
.listar({
|
|
titulo: filterTitulo.trim() || undefined,
|
|
area_id: filterAreaId.trim() || undefined,
|
|
status: filterStatus || undefined,
|
|
page: paginaAtual,
|
|
per_page: perPage,
|
|
})
|
|
.then((res) => {
|
|
setData(res.data ?? []);
|
|
setTotalRegistros(res.total_registros ?? 0);
|
|
setTotalPaginas(Math.max(1, res.total_paginas ?? 1));
|
|
setPaginaAtual(res.pagina_atual ?? 1);
|
|
setPerPage(res.per_page ?? 10);
|
|
})
|
|
.catch((err) => {
|
|
toast.error(err?.message ?? "Erro ao carregar pareceres.");
|
|
setData([]);
|
|
setTotalRegistros(0);
|
|
setTotalPaginas(1);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, [paginaAtual, perPage, filterTitulo, filterAreaId, filterStatus]);
|
|
|
|
useEffect(() => {
|
|
loadList();
|
|
}, [loadList]);
|
|
|
|
useEffect(() => {
|
|
refreshAreas();
|
|
}, [refreshAreas]);
|
|
|
|
const handleItemsPerPageChange = (value: string) => {
|
|
setPerPage(Number(value));
|
|
setPaginaAtual(1);
|
|
};
|
|
|
|
const handleEditar = (item: ParecerListItem) => {
|
|
navigate(`/codex/parecer-juridico/${item.id}`);
|
|
};
|
|
|
|
return (
|
|
<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 flex items-center gap-2">
|
|
<Bot className="w-5 h-5 md:w-6 md:h-6" />
|
|
Parecer Jurídico
|
|
</h1>
|
|
<div className="flex gap-2 w-full md:w-auto">
|
|
<Button onClick={() => navigate("/codex/parecer-juridico/novo")} className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
|
|
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
|
<span className="hidden sm:inline">Novo Parecer</span>
|
|
<span className="sm:hidden">Novo</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex flex-col md:flex-row items-stretch md:items-center gap-2 md:gap-4 flex-wrap">
|
|
<Input
|
|
placeholder="Buscar por título..."
|
|
value={filterTitulo}
|
|
onChange={(e) => { setFilterTitulo(e.target.value); setPaginaAtual(1); }}
|
|
className="w-full md:max-w-sm text-sm"
|
|
/>
|
|
<Select value={filterAreaId || "all"} onValueChange={(v) => { setFilterAreaId(v === "all" ? "" : v); setPaginaAtual(1); }}>
|
|
<SelectTrigger className="w-full md:w-[180px] text-sm">
|
|
<SelectValue placeholder="Todas as áreas" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Todas as áreas</SelectItem>
|
|
{areaItems.map((a) => (
|
|
<SelectItem key={a.id} value={a.id}>
|
|
{a.nome}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Select
|
|
value={filterStatus || "all"}
|
|
onValueChange={(v) => { setFilterStatus((v === "all" ? "" : v) as "" | ParecerStatus); setPaginaAtual(1); }}
|
|
>
|
|
<SelectTrigger className="w-full md:w-[160px] text-sm">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{STATUS_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value || "all"} value={opt.value || "all"}>
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{hasActiveFilters && (
|
|
<Button variant="ghost" size="sm" onClick={clearFilters}>
|
|
Limpar
|
|
</Button>
|
|
)}
|
|
<div className="flex items-center gap-2 justify-between md:justify-start">
|
|
<span className="text-xs md:text-sm text-muted-foreground whitespace-nowrap">Itens por página:</span>
|
|
<Select value={perPage.toString()} onValueChange={handleItemsPerPageChange}>
|
|
<SelectTrigger className="w-16 md:w-20">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="5">5</SelectItem>
|
|
<SelectItem value="10">10</SelectItem>
|
|
<SelectItem value="20">20</SelectItem>
|
|
<SelectItem value="50">50</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-auto p-3 md:p-6">
|
|
{loading ? (
|
|
<Card className="max-w-2xl mx-auto mt-12">
|
|
<CardContent className="py-10 text-center text-muted-foreground">
|
|
Carregando pareceres...
|
|
</CardContent>
|
|
</Card>
|
|
) : data.length === 0 && !hasActiveFilters ? (
|
|
<Card className="max-w-2xl mx-auto mt-12">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Sparkles className="w-5 h-5" />
|
|
Nenhum parecer encontrado
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Os pareceres jurídicos gerados aparecerão aqui quando disponíveis.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent />
|
|
</Card>
|
|
) : (
|
|
<>
|
|
<div className="border rounded-lg overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="min-w-[100px] font-semibold text-xs md:text-sm">Data</TableHead>
|
|
<TableHead className="min-w-[200px] font-semibold text-xs md:text-sm">Título</TableHead>
|
|
<TableHead className="min-w-[120px] font-semibold text-xs md:text-sm">Área</TableHead>
|
|
<TableHead className="min-w-[120px] font-semibold text-xs md:text-sm">Status</TableHead>
|
|
<TableHead className="min-w-[120px] font-semibold text-xs md:text-sm">Criado por</TableHead>
|
|
<TableHead className="text-center font-semibold text-xs md:text-sm w-[80px]">Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="text-center text-muted-foreground py-10 text-base">
|
|
Nenhum resultado para os filtros aplicados.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
data.map((item) => {
|
|
const status = statusEstilo(item.status);
|
|
return (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="text-xs md:text-sm">{formatarData(item.created_at)}</TableCell>
|
|
<TableCell className="font-medium text-xs md:text-sm">{item.titulo}</TableCell>
|
|
<TableCell className="text-xs md:text-sm">{item.area_nome}</TableCell>
|
|
<TableCell className="text-xs md:text-sm">
|
|
<span className={`inline-flex items-center gap-2 ${status.text}`}>
|
|
<span className={`size-2 rounded-full shrink-0 ${status.dot}`} aria-hidden />
|
|
{status.label}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-xs md:text-sm">{item.criado_por ?? "—"}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center justify-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleEditar(item)}
|
|
title="Editar"
|
|
className="h-7 w-7 md:h-9 md:w-9"
|
|
>
|
|
<Edit className="w-3 h-3 md:w-4 md:h-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
); })
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{totalRegistros > 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">
|
|
Mostrando {data.length} de {totalRegistros} {totalRegistros === 1 ? "parecer" : "pareceres"}
|
|
</p>
|
|
<div className="flex gap-1 md:gap-2 justify-center">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setPaginaAtual((p) => Math.max(1, p - 1))}
|
|
disabled={paginaAtual <= 1}
|
|
className="text-xs md:text-sm px-2 md:px-4"
|
|
>
|
|
<span className="hidden sm:inline">Anterior</span>
|
|
<span className="sm:hidden">Ant</span>
|
|
</Button>
|
|
<div className="flex items-center gap-1">
|
|
{Array.from({ length: Math.min(totalPaginas, 5) }, (_, i) => {
|
|
let page: number;
|
|
if (totalPaginas <= 5) page = i + 1;
|
|
else if (paginaAtual <= 3) page = i + 1;
|
|
else if (paginaAtual >= totalPaginas - 2) page = totalPaginas - 4 + i;
|
|
else page = paginaAtual - 2 + i;
|
|
return (
|
|
<Button
|
|
key={page}
|
|
variant={paginaAtual === page ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => setPaginaAtual(page)}
|
|
className="w-8 h-8 md:w-10 md:h-9 p-0 text-xs md:text-sm"
|
|
>
|
|
{page}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setPaginaAtual((p) => Math.min(totalPaginas, p + 1))}
|
|
disabled={paginaAtual >= totalPaginas}
|
|
className="text-xs md:text-sm px-2 md:px-4"
|
|
>
|
|
<span className="hidden sm:inline">Próxima</span>
|
|
<span className="sm:hidden">Prox</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|