sgmp/frontend/app/dashboard/page.tsx

429 lines
17 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
type Usuario = {
matricula: string;
nome: string;
perfil: string;
perfil_display: string;
};
type Solicitacao = {
id: string;
tipo: string;
tipo_display: string;
colaborador: string | null;
status: string;
status_display: string;
criado_em: string | null;
enviada_em: string | null;
solicitante_nome: string | null;
pode_aprovar: boolean;
pode_dar_parecer: boolean;
};
type DashboardData = {
usuario: Usuario;
total: number;
pendentes: number;
solicitacoes: Solicitacao[];
pagination: {
page: number;
per_page: number;
total_pages: number;
total_count: number;
};
};
const STATUS_CLASSES: Record<string, string> = {
RASCUNHO: "bg-amber-50 text-amber-800 border-amber-200",
AGUARDANDO_HEAD: "bg-amber-50 text-amber-800 border-amber-200",
ENVIADA: "bg-blue-50 text-blue-800 border-blue-200",
APROVADA_GG: "bg-emerald-50 text-emerald-800 border-emerald-200",
APROVADA_CONTROLADORIA: "bg-emerald-50 text-emerald-800 border-emerald-200",
APROVADA_DIRETORIA: "bg-emerald-50 text-emerald-800 border-emerald-200",
AGUARDANDO_DIRETORIA: "bg-amber-50 text-amber-800 border-amber-200",
FINALIZADA: "bg-slate-100 text-slate-700 border-slate-200",
REPROVADA: "bg-red-50 text-red-800 border-red-200",
};
const DJANGO_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8888";
function formatarData(iso: string | null) {
if (!iso) return "—";
try {
const d = new Date(iso);
return d.toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return iso;
}
}
export default function DashboardPage() {
const router = useRouter();
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [page, setPage] = useState(1);
useEffect(() => {
async function carregar() {
setLoading(true);
setError("");
try {
const res = await fetch(`/api/dashboard/?page=${page}`, {
credentials: "include",
});
// #region agent log
fetch("http://localhost:7701/ingest/5073259c-ddcc-441a-a087-e13a2cf7ac9e", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Debug-Session-Id": "ca90b5",
},
body: JSON.stringify({
sessionId: "ca90b5",
runId: "login-debug-01",
hypothesisId: "H1",
location: "frontend/app/dashboard/page.tsx:fetch",
message: "dashboard_fetch_status",
data: { status: res.status, page },
timestamp: Date.now(),
}),
}).catch(() => {});
// #endregion
if (res.status === 401) {
router.push(`/tela_login?next=${encodeURIComponent("/dashboard")}`);
return;
}
if (!res.ok) {
setError("Erro ao carregar o dashboard.");
return;
}
const json = await res.json();
setData(json);
} catch {
setError("Erro de conexão.");
} finally {
setLoading(false);
}
}
carregar();
}, [page, router]);
async function handleLogout() {
try {
await fetch("/api/auth/logout/", {
method: "POST",
credentials: "include",
});
} catch {
/* ignore */
}
router.push("/tela_login");
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-slate-100">
<p className="text-slate-500">Carregando</p>
</div>
);
}
if (error || !data) {
return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4 bg-slate-100">
<p className="text-red-600">{error || "Erro ao carregar."}</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Tentar novamente
</button>
</div>
);
}
const { usuario, total, pendentes, solicitacoes, pagination } = data;
const isGestor = usuario.perfil === "GESTOR";
return (
<div className="min-h-screen bg-slate-100">
{/* Header */}
<header className="bg-slate-900 text-white border-b border-slate-800">
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
<h1 className="text-lg font-bold tracking-tight">SGMP CORP</h1>
<nav className="flex flex-wrap items-center gap-x-4 gap-y-1">
<Link
href="/dashboard"
className="text-sm font-medium text-slate-300 hover:text-white"
>
Dashboard
</Link>
<button
onClick={handleLogout}
className="text-sm text-red-400 hover:text-red-300 flex items-center gap-1"
>
Sair
</button>
</nav>
</div>
</header>
<main className="max-w-6xl mx-auto py-6 md:py-8 px-4 sm:px-6">
{/* Saudação */}
<div className="mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-slate-800 tracking-tight">
SGMP - Movimentação de Pessoas
</h2>
<p className="text-slate-500 text-sm md:text-base mt-2">
Olá, <strong>{usuario.nome}</strong>!
<span className="inline-block bg-slate-200 py-0.5 px-2 rounded text-xs ml-2 text-slate-600">
{usuario.perfil_display}
</span>
<br />
<small className="text-slate-400">Matrícula: {usuario.matricula}</small>
</p>
{isGestor && (
<div className="mt-4">
<Link
href="/nova-solicitacao"
className="inline-flex items-center gap-2 py-2.5 px-4 rounded-md text-sm font-semibold bg-blue-600 text-white hover:bg-blue-700"
>
+ Nova Solicitação
</Link>
</div>
)}
</div>
{isGestor && (
<div className="mb-8 bg-amber-50 border border-amber-300 rounded-xl p-4 flex gap-3 items-start">
<span className="text-2xl">💡</span>
<div>
<h4 className="m-0 mb-1 text-amber-800 font-semibold">
Lembrete Rápido
</h4>
<p className="m-0 text-amber-700 text-sm">
Solicitações em <strong>Rascunho</strong> são visíveis para
você. Lembre-se de clicar em{" "}
<strong>&quot;Enviar para Aprovação&quot;</strong> na página de
detalhes para iniciar o fluxo.
</p>
</div>
</div>
)}
{/* Métricas */}
<div className="grid grid-cols-2 md:grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-4 md:gap-6 mb-10">
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm border-l-4 border-l-blue-500">
<div className="text-xs uppercase tracking-wider text-slate-500 font-bold mb-2">
Total
</div>
<div className="text-3xl font-extrabold leading-none text-blue-600">
{total}
</div>
</div>
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm border-l-4 border-l-amber-500">
<div className="text-xs uppercase tracking-wider text-slate-500 font-bold mb-2">
Pendentes
</div>
<div className="text-3xl font-extrabold leading-none text-amber-600">
{pendentes}
</div>
</div>
</div>
{/* Tabela de solicitações */}
<div className="mb-8">
<h3 className="text-slate-800 text-xl font-semibold mb-4 flex items-center gap-2">
{isGestor ? "📋 Minhas Solicitações" : "⏳ Pendentes de Aprovação"}
</h3>
{!isGestor && (
<div className="bg-blue-50 border border-blue-200 rounded-lg py-3 px-4 mb-5 text-blue-800 text-sm flex items-center gap-2">
Você está vendo solicitações com status{" "}
<strong>Enviada</strong> aguardando sua análise.
</div>
)}
{solicitacoes.length > 0 ? (
<>
<div className="md:hidden space-y-3">
{solicitacoes.map((s) => (
<div
key={s.id}
className="bg-white rounded-xl shadow border border-slate-200 p-4"
>
<div className="flex items-start justify-between gap-3 mb-2 min-w-0">
<p className="font-semibold text-slate-900 leading-tight min-w-0 flex-1 text-sm">
{s.tipo_display}
</p>
<span
className={`inline-flex max-w-[min(100%,14rem)] text-left break-words leading-snug py-1 px-2.5 rounded-full text-xs font-bold border ${
STATUS_CLASSES[s.status] ||
"bg-slate-100 text-slate-700 border-slate-200"
}`}
>
{s.status_display}
</span>
</div>
<p className="text-sm text-slate-700 font-medium">
{s.colaborador || "N/A"}
</p>
<p className="text-sm text-slate-600 mt-1">
{formatarData(s.criado_em)}
</p>
<div className="flex items-center gap-2 flex-wrap mt-3">
<Link
href={`/solicitacoes/${s.id}`}
className="text-blue-600 font-semibold text-sm hover:underline"
>
Detalhes
</Link>
{s.pode_dar_parecer && (
<a
href={`${DJANGO_BASE_URL}/solicitacao/${s.id}/`}
className="inline-flex items-center gap-1.5 py-2 px-3 rounded-md text-sm font-semibold bg-blue-600 text-white hover:bg-blue-700 no-underline"
>
📝 Parecer
</a>
)}
</div>
{s.pode_aprovar && (
<p className="text-slate-600 text-sm font-medium mt-2">
Aprovar/Reprovar na página de detalhes.
</p>
)}
</div>
))}
</div>
<div className="hidden md:block overflow-x-auto bg-white rounded-xl shadow border border-slate-200">
<table className="w-full min-w-[640px] border-collapse text-slate-900">
<thead>
<tr>
<th className="p-4 text-left border-b border-slate-200 bg-slate-50 font-semibold text-slate-600 text-xs uppercase tracking-wide">
Tipo
</th>
<th className="p-4 text-left border-b border-slate-200 bg-slate-50 font-semibold text-slate-600 text-xs uppercase tracking-wide">
Colaborador
</th>
<th className="p-4 text-left border-b border-slate-200 bg-slate-50 font-semibold text-slate-600 text-xs uppercase tracking-wide">
Status
</th>
<th className="p-4 text-left border-b border-slate-200 bg-slate-50 font-semibold text-slate-600 text-xs uppercase tracking-wide">
Data
</th>
<th className="p-4 text-left border-b border-slate-200 bg-slate-50 font-semibold text-slate-600 text-xs uppercase tracking-wide">
Ações
</th>
</tr>
</thead>
<tbody>
{solicitacoes.map((s) => (
<tr
key={s.id}
className="border-b border-slate-200 hover:bg-slate-50"
>
<td className="p-4 font-semibold min-w-0 max-w-[11rem] break-words">
{s.tipo_display}
</td>
<td className="p-4 text-slate-900 font-medium min-w-0 max-w-[12rem] break-words">
{s.colaborador || (
<span className="text-slate-700 font-medium">N/A</span>
)}
</td>
<td className="p-4 min-w-0 max-w-[14rem]">
<span
className={`inline-flex max-w-full text-left break-words leading-snug py-1 px-2.5 rounded-full text-xs font-bold border ${
STATUS_CLASSES[s.status] ||
"bg-slate-100 text-slate-700 border-slate-200"
}`}
>
{s.status_display}
</span>
</td>
<td className="p-4 text-slate-900 font-medium whitespace-nowrap">
{formatarData(s.criado_em)}
</td>
<td className="p-4">
<div className="flex gap-3 items-center flex-wrap">
<Link
href={`/solicitacoes/${s.id}`}
className="text-blue-600 font-semibold text-sm hover:underline"
>
Detalhes
</Link>
{s.pode_dar_parecer && (
<a
href={`${DJANGO_BASE_URL}/solicitacao/${s.id}/`}
className="inline-flex items-center gap-1.5 py-2 px-4 rounded-md text-sm font-semibold bg-blue-600 text-white hover:bg-blue-700 no-underline"
>
📝 Parecer
</a>
)}
{s.pode_aprovar && (
<span className="text-slate-600 text-sm font-medium">
(Aprovar/Reprovar na página de detalhes)
</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{pagination.total_pages > 1 && (
<div className="mt-6 flex items-center justify-center gap-2 flex-wrap">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={pagination.page <= 1}
className="py-2 px-3.5 border border-slate-200 rounded-md text-slate-600 bg-white text-sm hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Anterior
</button>
<span className="py-2 px-3.5 bg-blue-600 text-white rounded-md font-medium text-sm">
{pagination.page} / {pagination.total_pages}
</span>
<button
onClick={() =>
setPage((p) =>
Math.min(pagination.total_pages, p + 1)
)
}
disabled={pagination.page >= pagination.total_pages}
className="py-2 px-3.5 border border-slate-200 rounded-md text-slate-600 bg-white text-sm hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Próxima
</button>
</div>
)}
</>
) : (
<div className="text-center py-16 px-5 text-slate-500 bg-white rounded-xl border-2 border-dashed border-slate-300">
<p className="m-0 text-lg">🎉 Nenhuma solicitação encontrada.</p>
{isGestor && (
<p className="text-sm mt-2">
Clique em <strong>Nova Solicitação</strong> para iniciar um novo fluxo.
</p>
)}
</div>
)}
</div>
</main>
</div>
);
}