Nova versao codex Parecer, atualizacao completa
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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 } = 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]);
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user