feat: enhance audit page by adding user information retrieval and improving CSV export format, providing better insights and clarity in reports

This commit is contained in:
2025-11-30 08:42:21 -03:00
parent 334676b860
commit f1c2ae0e6b
2 changed files with 133 additions and 41 deletions

View File

@@ -7,6 +7,7 @@
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
import Papa from "papaparse";
import logoGovPE from "$lib/assets/logo_governo_PE.png";
let abaAtiva = $state<"atividades" | "logins">("atividades");
let limite = $state(50);
@@ -96,9 +97,12 @@
}
if (filtroUsuario) {
const usuarioLower = filtroUsuario.toLowerCase();
filtrados = filtrados.filter(l =>
l.matriculaOuEmail.toLowerCase().includes(usuarioLower)
);
filtrados = filtrados.filter(l => {
const nomeMatch = l.usuarioNome?.toLowerCase().includes(usuarioLower);
const emailMatch = l.usuarioEmail?.toLowerCase().includes(usuarioLower);
const matriculaMatch = l.matriculaOuEmail?.toLowerCase().includes(usuarioLower);
return nomeMatch || emailMatch || matriculaMatch;
});
}
if (filtroStatus !== "todos") {
filtrados = filtrados.filter(l =>
@@ -125,21 +129,27 @@
});
}
function formatarLocalizacao(login: { cidade?: string; estado?: string; pais?: string; endereco?: string } | undefined): string {
if (!login) return "-";
function formatarLocalizacao(login: any): string {
if (!login || typeof login !== 'object') return "-";
const partes: string[] = [];
if (login.cidade) partes.push(login.cidade);
if (login.estado) partes.push(login.estado);
if (login.pais) partes.push(login.pais);
if (login.cidade && typeof login.cidade === 'string' && login.cidade.trim()) {
partes.push(login.cidade.trim());
}
if (login.estado && typeof login.estado === 'string' && login.estado.trim()) {
partes.push(login.estado.trim());
}
if (login.pais && typeof login.pais === 'string' && login.pais.trim()) {
partes.push(login.pais.trim());
}
if (partes.length > 0) {
return partes.join(", ");
}
// Se não tiver cidade/estado/pais, mas tiver endereco, mostrar endereco
if (login.endereco) {
return login.endereco;
if (login.endereco && typeof login.endereco === 'string' && login.endereco.trim()) {
return login.endereco.trim();
}
return "-";
@@ -198,17 +208,27 @@
"Detalhes": atividade.detalhes || "-"
}));
} else {
csvData = dadosParaExportar.map((login: any) => ({
"Data/Hora": formatarData(login.timestamp),
"Usuário/Email": login.matriculaOuEmail,
"Status": login.sucesso ? "Sucesso" : "Falhou",
"Motivo Falha": login.motivoFalha || "-",
"IP": login.ipAddress || "-",
"Localização": formatarLocalizacao(login),
"Dispositivo": login.device || "-",
"Navegador": login.browser || "-",
"Sistema": login.sistema || "-"
}));
csvData = dadosParaExportar.map((login: any) => {
const usuarioInfo = [];
if (login.usuarioNome) usuarioInfo.push(login.usuarioNome);
if (login.usuarioEmail) usuarioInfo.push(login.usuarioEmail);
if (!login.usuarioNome && !login.usuarioEmail && login.matriculaOuEmail) {
usuarioInfo.push(login.matriculaOuEmail);
}
return {
"Data/Hora": formatarData(login.timestamp),
"Usuário": login.usuarioNome || "-",
"Email": login.usuarioEmail || login.matriculaOuEmail || "-",
"Status": login.sucesso ? "Sucesso" : "Falhou",
"Motivo Falha": login.motivoFalha || "-",
"IP": login.ipAddress || "-",
"Localização": formatarLocalizacao(login),
"Dispositivo": login.device || "-",
"Navegador": login.browser || "-",
"Sistema": login.sistema || "-"
};
});
}
const csv = Papa.unparse(csvData);
@@ -245,25 +265,49 @@
const doc = new jsPDF();
// Adicionar logo no canto superior esquerdo
let yPos = 20;
try {
const logoImg = await new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
setTimeout(() => reject(new Error('Timeout loading logo')), 3000);
img.src = logoGovPE;
});
const logoWidth = 25;
const aspectRatio = logoImg.height / logoImg.width;
const logoHeight = logoWidth * aspectRatio;
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
yPos = 10 + logoHeight + 10;
} catch (err) {
console.warn('Erro ao carregar logo:', err);
yPos = 20;
}
// Título
doc.setFontSize(20);
doc.setTextColor(102, 126, 234);
const titulo = abaAtiva === "atividades"
? "Relatório de Atividades do Sistema"
: "Relatório de Histórico de Logins";
doc.text(titulo, 14, 20);
doc.text(titulo, 14, yPos);
// Informações gerais
doc.setFontSize(12);
doc.setTextColor(0, 0, 0);
yPos += 8;
doc.text(
`Gerado em: ${format(new Date(), "dd/MM/yyyy HH:mm", { locale: ptBR })}`,
14,
30
yPos
);
doc.text(`Total de registros: ${dadosParaExportar.length}`, 14, 36);
let yPos = 50;
yPos += 6;
doc.text(`Total de registros: ${dadosParaExportar.length}`, 14, yPos);
yPos += 10;
if (abaAtiva === "atividades") {
// Tabela de atividades
@@ -285,15 +329,25 @@
});
} else {
// Tabela de logins
const loginsData = dadosParaExportar.map((login: any) => [
formatarData(login.timestamp),
login.matriculaOuEmail,
login.sucesso ? "Sucesso" : "Falhou",
login.ipAddress || "-",
formatarLocalizacao(login).substring(0, 30),
login.device || "-",
login.browser || "-"
]);
const loginsData = dadosParaExportar.map((login: any) => {
const usuarioInfo = [];
if (login.usuarioNome) usuarioInfo.push(login.usuarioNome);
if (login.usuarioEmail) usuarioInfo.push(login.usuarioEmail);
if (!login.usuarioNome && !login.usuarioEmail && login.matriculaOuEmail) {
usuarioInfo.push(login.matriculaOuEmail);
}
const usuarioStr = usuarioInfo.length > 0 ? usuarioInfo.join(" / ") : "-";
return [
formatarData(login.timestamp),
usuarioStr,
login.sucesso ? "Sucesso" : "Falhou",
login.ipAddress || "-",
formatarLocalizacao(login).substring(0, 30),
login.device || "-",
login.browser || "-"
];
});
autoTable(doc, {
startY: yPos,
@@ -700,11 +754,27 @@
</div>
</td>
<td>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-sm font-medium">{login.matriculaOuEmail}</span>
<div class="flex flex-col gap-1">
{#if login.usuarioNome}
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-sm font-semibold">{login.usuarioNome}</span>
</div>
{/if}
{#if login.usuarioEmail || login.matriculaOuEmail}
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-base-content/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="text-xs text-base-content/70 font-mono">
{login.usuarioEmail || (login.matriculaOuEmail && typeof login.matriculaOuEmail === 'string' ? login.matriculaOuEmail : "-")}
</span>
</div>
{:else}
<span class="text-xs text-base-content/40">-</span>
{/if}
</div>
</td>
<td>