Files
OPEN_CODEX_API/src/components/parecer-juridico/ParecerJuridicoView.tsx
T
2026-03-23 18:24:08 -03:00

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>
);
};