feat: add report printing and deletion functionality
- Implemented a new feature to print security reports with structured JSON observations, enhancing report accessibility. - Added a deletion function for reports, allowing users to remove reports with confirmation prompts. - Updated the CybersecurityWizcard component to include action buttons for printing and deleting reports, improving user interaction. - Enhanced the backend with a mutation to handle report deletions, ensuring proper cleanup of associated artifacts.
This commit is contained in:
@@ -504,6 +504,87 @@
|
||||
const data = new Date(valor);
|
||||
return Number.isNaN(data.getTime()) ? Date.now() : data.getTime();
|
||||
}
|
||||
// Imprimir conteúdo do relatório (usa campo observações JSON estruturado)
|
||||
function imprimirRelatorio(r: {
|
||||
_id: Id<'reportRequests'>;
|
||||
status: 'pendente' | 'processando' | 'concluido' | 'falhou';
|
||||
criadoEm: number;
|
||||
concluidoEm?: number;
|
||||
observacoes?: string;
|
||||
}) {
|
||||
if (typeof window === 'undefined') return;
|
||||
const win = window.open('', '_blank', 'noopener,noreferrer,width=900,height=700');
|
||||
if (!win) return;
|
||||
let conteudo: string;
|
||||
try {
|
||||
const data = r.observacoes ? JSON.parse(r.observacoes) as any : null;
|
||||
const total = data?.total ?? '—';
|
||||
const porSeveridade = data?.porSeveridade ?? {};
|
||||
const porAtaque = data?.porAtaque ?? {};
|
||||
const linhasSev = Object.entries(porSeveridade)
|
||||
.map(([k, v]) => `<tr><td>${k}</td><td style="text-align:right">${v as number}</td></tr>`)
|
||||
.join('');
|
||||
const linhasAtk = Object.entries(porAtaque)
|
||||
.map(([k, v]) => `<tr><td>${k}</td><td style="text-align:right">${v as number}</td></tr>`)
|
||||
.join('');
|
||||
const criadoStr = new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false });
|
||||
const concluidoStr = r.concluidoEm ? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false }) : '—';
|
||||
const agoraStr = new Date().toLocaleString('pt-BR', { hour12: false });
|
||||
conteudo =
|
||||
'<!doctype html>' +
|
||||
'<html><head><meta charset="utf-8"/><title>Relatório de Segurança</title>' +
|
||||
'<style>' +
|
||||
"body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; margin: 24px; color: #111;}" +
|
||||
'h1 { margin: 0 0 4px 0; font-size: 20px;}' +
|
||||
'h2 { margin: 16px 0 8px; font-size: 16px;}' +
|
||||
'.meta { color: #666; font-size: 12px; margin-bottom: 16px;}' +
|
||||
'table { border-collapse: collapse; width: 100%; margin-top: 8px;}' +
|
||||
'th, td { border: 1px solid #ddd; padding: 8px; font-size: 12px;}' +
|
||||
'th { background: #f5f5f5; text-align: left;}' +
|
||||
'.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }' +
|
||||
'@media print { .no-print { display: none; } }' +
|
||||
'.footer { margin-top: 24px; font-size: 11px; color: #666;}' +
|
||||
'</style></head><body>' +
|
||||
'<div class="no-print" style="text-align:right;margin-bottom:12px"><button onclick="window.print()">Imprimir</button></div>' +
|
||||
'<h1>Relatório de Segurança</h1>' +
|
||||
'<div class="meta">Status: <strong>' + r.status + '</strong> · Criado: ' + criadoStr + ' · Concluído: ' + concluidoStr + '</div>' +
|
||||
'<h2>Resumo</h2>' +
|
||||
'<p>Total de eventos no período: <strong>' + total + '</strong></p>' +
|
||||
'<div class="grid"><div><h2>Por Severidade</h2><table><thead><tr><th>Severidade</th><th style="text-align:right">Qtde</th></tr></thead><tbody>' +
|
||||
(linhasSev || '<tr><td colspan="2">—</td></tr>') +
|
||||
'</tbody></table></div><div><h2>Por Tipo de Ataque</h2><table><thead><tr><th>Tipo</th><th style="text-align:right">Qtde</th></tr></thead><tbody>' +
|
||||
(linhasAtk || '<tr><td colspan="2">—</td></tr>') +
|
||||
'</tbody></table></div></div>' +
|
||||
'<div class="footer">Gerado por SGSE · ' + agoraStr + '</div>' +
|
||||
'</body></html>';
|
||||
} catch {
|
||||
const obs = (r.observacoes ?? '').replace(/</g, '<');
|
||||
// Evitar literal de fechamento de script dentro deste bloco
|
||||
const scriptOpen = '<scr' + 'ipt>';
|
||||
const scriptClose = '</scr' + 'ipt>';
|
||||
conteudo =
|
||||
'<!doctype html><meta charset="utf-8"/>' +
|
||||
'<title>Relatório</title>' +
|
||||
'<pre>' + obs + '</pre>' +
|
||||
scriptOpen + 'window.print()' + scriptClose;
|
||||
}
|
||||
win.document.open();
|
||||
win.document.write(conteudo);
|
||||
win.document.close();
|
||||
}
|
||||
async function excluirRelatorio(relatorioId: Id<'reportRequests'>) {
|
||||
if (!confirm('Excluir este relatório? Esta ação não pode ser desfeita.')) return;
|
||||
try {
|
||||
const resp = await client.mutation(api.security.deletarRelatorio, { relatorioId });
|
||||
if (resp?.success) {
|
||||
feedback = { tipo: 'success', mensagem: 'Relatório excluído.' };
|
||||
} else {
|
||||
feedback = { tipo: 'error', mensagem: 'Não foi possível excluir o relatório.' };
|
||||
}
|
||||
} catch (erro: unknown) {
|
||||
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
|
||||
}
|
||||
}
|
||||
|
||||
async function gerarRelatorioAvancado(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
@@ -1396,6 +1477,7 @@
|
||||
<th>Criado</th>
|
||||
<th>Concluído</th>
|
||||
<th>Observações</th>
|
||||
<th class="w-40">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -1407,6 +1489,26 @@
|
||||
<td>{new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false })}</td>
|
||||
<td>{r.concluidoEm ? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false }) : '-'}</td>
|
||||
<td class="max-w-xs truncate">{r.observacoes ?? '-'}</td>
|
||||
<td>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-outline"
|
||||
disabled={r.status !== 'concluido'}
|
||||
title={r.status !== 'concluido' ? 'Aguarde concluir' : 'Imprimir'}
|
||||
onclick={() => imprimirRelatorio(r)}
|
||||
>
|
||||
Imprimir
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-error"
|
||||
onclick={() => excluirRelatorio(r._id)}
|
||||
>
|
||||
Excluir
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
import CybersecurityWizcard from '$lib/components/ti/CybersecurityWizcard.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
// Usar tipo amplo para evitar conflitos de tipagem do import dinâmico no build
|
||||
let Comp: any = null;
|
||||
onMount(async () => {
|
||||
const mod = await import('$lib/components/ti/CybersecurityWizcard.svelte');
|
||||
Comp = mod.default;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -23,7 +30,13 @@
|
||||
<a href={resolve('/ti')} class="btn btn-outline btn-primary">Voltar para TI</a>
|
||||
</header>
|
||||
|
||||
<CybersecurityWizcard />
|
||||
{#if browser && Comp}
|
||||
<svelte:component this={Comp} />
|
||||
{:else}
|
||||
<div class="alert">
|
||||
<span>Carregando módulo de cibersegurança…</span>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user