nis2-agile/public/reports.html
DevEnv nis2-agile 0e78ec24c1 [FIX] i18n funzionante + bug audit.html + help system
- common.js: aggiunto i18nKey a navItems, data-i18n su sezioni e voci
  sidebar → toggle IT/EN ora traduce la navigazione in tempo reale
- Tutte e 10 le pagine HTML: aggiunto data-i18n="*.title" agli h2
  (dashboard, assessment, risks, incidents, policies, supply-chain,
  training, assets, reports, settings)
- FIX BUG: sidebar puntava ad audit.html (inesistente) → corretto
  in reports.html
- HelpSystem: funziona correttamente in tutte le 10 pagine
  (content-header-actions presente, init() chiamato)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 11:17:04 +01:00

907 lines
41 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audit e Report - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Tab Navigation ─────────────────────────────────────── */
.tab-nav {
display: flex;
gap: 0;
border-bottom: 2px solid var(--gray-200);
margin-bottom: 24px;
}
.tab-nav button {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
color: var(--gray-500);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.tab-nav button:hover {
color: var(--gray-700);
background: var(--gray-50);
}
.tab-nav button.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Data Table ─────────────────────────────────────────── */
.data-table {
width: 100%;
border-collapse: collapse;
background: var(--card-bg);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--card-shadow);
}
.data-table thead {
background: var(--gray-50);
}
.data-table th {
text-align: left;
padding: 12px 16px;
font-size: 0.8rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--gray-200);
}
.data-table td {
padding: 12px 16px;
font-size: 0.875rem;
color: var(--gray-700);
border-bottom: 1px solid var(--gray-100);
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover td { background: var(--gray-50); }
/* ── Status Badges ──────────────────────────────────────── */
.status-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 500;
}
.status-badge.not_started { background: var(--gray-100); color: var(--gray-600); }
.status-badge.in_progress { background: var(--info-bg); color: var(--info); }
.status-badge.implemented { background: var(--secondary-bg); color: var(--secondary); }
.status-badge.verified { background: var(--primary-bg); color: var(--primary); }
/* ── Progress Bar ───────────────────────────────────────── */
.progress-bar-container {
display: flex;
align-items: center;
gap: 8px;
}
.progress-bar {
flex-grow: 1;
height: 6px;
background: var(--gray-200);
border-radius: 3px;
overflow: hidden;
min-width: 80px;
}
.progress-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-pct {
font-size: 0.8rem;
font-weight: 600;
min-width: 36px;
text-align: right;
color: var(--gray-600);
}
/* ── Report Section ─────────────────────────────────────── */
.report-container {
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
overflow: hidden;
}
.report-header {
background: var(--gray-800);
color: white;
padding: 32px;
text-align: center;
}
.report-header h3 {
font-size: 1.3rem;
margin-bottom: 4px;
}
.report-header p {
color: var(--gray-300);
font-size: 0.85rem;
}
.report-body {
padding: 24px 32px;
}
.report-section {
margin-bottom: 24px;
}
.report-section-title {
font-size: 1rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--primary-bg);
}
.report-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
.report-summary-item {
text-align: center;
padding: 16px;
background: var(--gray-50);
border-radius: var(--border-radius);
}
.report-summary-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--gray-800);
line-height: 1.2;
}
.report-summary-label {
font-size: 0.8rem;
color: var(--gray-500);
margin-top: 4px;
}
.report-compliance-bar {
height: 20px;
background: var(--gray-200);
border-radius: 10px;
overflow: hidden;
margin: 12px 0;
}
.report-compliance-fill {
height: 100%;
border-radius: 10px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: 600;
}
/* ── ISO Mapping ────────────────────────────────────────── */
.iso-mapping-row {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid var(--gray-100);
}
.iso-mapping-row:last-child { border-bottom: none; }
.iso-nis2-badge {
background: var(--primary-bg);
color: var(--primary);
padding: 6px 14px;
border-radius: 20px;
font-size: 0.82rem;
font-weight: 600;
white-space: nowrap;
min-width: 90px;
text-align: center;
}
.iso-arrow {
color: var(--gray-300);
flex-shrink: 0;
}
.iso-arrow svg { width: 20px; height: 20px; }
.iso-controls {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex-grow: 1;
}
.iso-control-badge {
background: var(--secondary-bg);
color: var(--secondary);
padding: 4px 10px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 500;
}
.iso-title {
font-size: 0.85rem;
color: var(--gray-500);
min-width: 200px;
}
/* ── Pagination ─────────────────────────────────────────── */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-top: 20px;
padding: 12px;
}
.pagination button {
padding: 8px 16px;
border: 1px solid var(--gray-200);
background: var(--card-bg);
border-radius: var(--border-radius-sm);
color: var(--gray-600);
cursor: pointer;
font-size: 0.85rem;
transition: all var(--transition-fast);
}
.pagination button:hover:not(:disabled) {
background: var(--primary-bg);
border-color: var(--primary);
color: var(--primary);
}
.pagination button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pagination .page-info {
font-size: 0.85rem;
color: var(--gray-500);
}
/* ── Audit Log Action ───────────────────────────────────── */
.log-action {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.82rem;
}
.log-entity {
background: var(--gray-100);
padding: 2px 8px;
border-radius: var(--border-radius-sm);
font-size: 0.8rem;
color: var(--gray-600);
font-family: var(--font-mono);
}
.log-details {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.8rem;
color: var(--gray-400);
cursor: help;
}
/* ── Controls Row Clickable ─────────────────────────────── */
.data-table tbody tr.clickable-row { cursor: pointer; }
/* ── Loading/Empty ──────────────────────────────────────── */
.loading-state {
text-align: center;
padding: 48px 20px;
color: var(--gray-400);
}
.loading-state .spinner {
width: 32px;
height: 32px;
border: 3px solid var(--gray-200);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state-box {
text-align: center;
padding: 48px 20px;
color: var(--gray-400);
}
.empty-state-box svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.empty-state-box h4 {
color: var(--gray-600);
margin-bottom: 4px;
}
/* ── Print ──────────────────────────────────────────────── */
@media print {
.sidebar, .content-header, .tab-nav, .btn { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; }
.report-container { box-shadow: none; border: 1px solid #ddd; }
body { background: white; }
}
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 768px) {
.tab-nav button { padding: 10px 12px; font-size: 0.8rem; }
.iso-mapping-row { flex-direction: column; align-items: flex-start; }
.iso-title { min-width: auto; }
.report-summary-grid { grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="audit.title">Audit &amp; Report</h2>
<div class="content-header-actions">
<button class="btn btn-primary" onclick="generateReport()">Genera Report</button>
</div>
</header>
<div class="content-body">
<!-- Tab Navigation -->
<div class="tab-nav">
<button class="active" onclick="switchTab('report', this)">Report Compliance</button>
<button onclick="switchTab('controls', this)">Controlli</button>
<button onclick="switchTab('audit', this)">Audit Log</button>
<button onclick="switchTab('iso', this)">ISO 27001</button>
</div>
<!-- Tab: Report Compliance -->
<div class="tab-panel active" id="tab-report">
<div id="report-container">
<div class="empty-state-box">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>
<h4>Nessun report generato</h4>
<p>Clicca "Genera Report" per creare un report di compliance aggiornato.</p>
</div>
</div>
</div>
<!-- Tab: Controlli -->
<div class="tab-panel" id="tab-controls">
<div id="controls-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento controlli...</p>
</div>
</div>
</div>
<!-- Tab: Audit Log -->
<div class="tab-panel" id="tab-audit">
<div id="audit-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento log...</p>
</div>
</div>
</div>
<!-- Tab: ISO 27001 -->
<div class="tab-panel" id="tab-iso">
<div id="iso-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento mapping...</p>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/common.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/help.js"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
HelpSystem.init();
// ── Labels ──────────────────────────────────────────────
const controlStatusLabels = {
not_started: 'Non Iniziato',
in_progress: 'In Corso',
implemented: 'Implementato',
verified: 'Verificato'
};
const actionLabels = {
login: 'Accesso effettuato',
logout: 'Disconnessione',
user_created: 'Utente creato',
org_created: 'Organizzazione creata',
assessment_created: 'Assessment creato',
assessment_completed: 'Assessment completato',
risk_created: 'Rischio creato',
risk_updated: 'Rischio aggiornato',
risk_deleted: 'Rischio eliminato',
incident_created: 'Incidente registrato',
incident_updated: 'Incidente aggiornato',
early_warning_sent: 'Pre-allarme inviato',
notification_sent: 'Notifica inviata',
policy_created: 'Policy creata',
policy_updated: 'Policy aggiornata',
policy_approved: 'Policy approvata',
policy_ai_generated: 'Policy generata con AI',
supplier_created: 'Fornitore creato',
supplier_assessed: 'Fornitore valutato',
course_created: 'Corso creato',
training_assigned: 'Formazione assegnata',
asset_created: 'Asset registrato',
asset_updated: 'Asset aggiornato',
asset_deleted: 'Asset eliminato',
control_updated: 'Controllo aggiornato',
evidence_uploaded: 'Evidenza caricata'
};
// ── State ───────────────────────────────────────────────
let auditPage = 1;
let auditTotalPages = 1;
// ── Tab Switch ──────────────────────────────────────────
function switchTab(tabId, btn) {
document.querySelectorAll('.tab-nav button').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + tabId).classList.add('active');
if (tabId === 'controls') loadControls();
if (tabId === 'audit') { auditPage = 1; loadAuditLogs(); }
if (tabId === 'iso') loadIsoMapping();
}
// ══════════════════════════════════════════════════════════
// TAB 1: Compliance Report
// ══════════════════════════════════════════════════════════
async function generateReport() {
const container = document.getElementById('report-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Generazione report in corso...</p></div>';
// Switch to report tab
document.querySelectorAll('.tab-nav button').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelector('.tab-nav button').classList.add('active');
document.getElementById('tab-report').classList.add('active');
try {
const result = await api.generateComplianceReport();
if (result.success && result.data) {
renderReport(result.data);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nella generazione</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderReport(data) {
const container = document.getElementById('report-container');
const org = data.organization || {};
const summary = data.compliance_summary || {};
const controls = data.controls || [];
const pct = summary.compliance_percentage || 0;
const pctColor = pct >= 80 ? 'var(--secondary)' : pct >= 50 ? 'var(--warning)' : 'var(--danger)';
let controlsTableHtml = '';
controls.forEach(c => {
const st = c.status || 'not_started';
const implPct = c.implementation_percentage || 0;
const implColor = implPct >= 80 ? 'var(--secondary)' : implPct >= 50 ? 'var(--warning)' : 'var(--danger)';
controlsTableHtml += `
<tr>
<td style="font-family:var(--font-mono); font-size:0.8rem;">${escapeHtml(c.control_code || '-')}</td>
<td>${escapeHtml(c.title || '-')}</td>
<td><span class="status-badge ${st}">${controlStatusLabels[st] || st}</span></td>
<td>
<div class="progress-bar-container">
<div class="progress-bar"><div class="progress-bar-fill" style="width:${implPct}%; background:${implColor};"></div></div>
<span class="progress-pct">${implPct}%</span>
</div>
</td>
</tr>`;
});
container.innerHTML = `
<div class="report-container">
<div class="report-header">
<h3>Report di Compliance NIS2</h3>
<p>${escapeHtml(org.name || 'Organizzazione')} - ${formatDateTime(data.report_date)}</p>
</div>
<div class="report-body">
<!-- Organization Info -->
<div class="report-section">
<div class="report-section-title">Informazioni Organizzazione</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; font-size:0.9rem;">
<div><strong>Nome:</strong> ${escapeHtml(org.name || '-')}</div>
<div><strong>Settore:</strong> ${escapeHtml(org.sector || '-')}</div>
<div><strong>Tipo Entita':</strong> ${escapeHtml(org.entity_type || '-')}</div>
<div><strong>Paese:</strong> ${escapeHtml(org.country || '-')}</div>
</div>
</div>
<!-- Compliance Summary -->
<div class="report-section">
<div class="report-section-title">Riepilogo Compliance</div>
<div class="report-compliance-bar">
<div class="report-compliance-fill" style="width:${pct}%; background:${pctColor};">${pct}%</div>
</div>
<div class="report-summary-grid">
<div class="report-summary-item">
<div class="report-summary-value">${summary.total_controls || 0}</div>
<div class="report-summary-label">Controlli Totali</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value">${summary.implemented_controls || 0}</div>
<div class="report-summary-label">Implementati</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value" style="color:var(--danger);">${data.risk_count || 0}</div>
<div class="report-summary-label">Rischi Aperti</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value" style="color:var(--warning);">${data.incident_count || 0}</div>
<div class="report-summary-label">Incidenti</div>
</div>
<div class="report-summary-item">
<div class="report-summary-value" style="color:var(--secondary);">${data.policy_count || 0}</div>
<div class="report-summary-label">Policy Approvate</div>
</div>
</div>
</div>
<!-- Controls Status -->
${controls.length > 0 ? `
<div class="report-section">
<div class="report-section-title">Stato Controlli</div>
<table class="data-table" style="box-shadow:none; border:1px solid var(--gray-200);">
<thead>
<tr>
<th>Codice</th>
<th>Titolo</th>
<th>Stato</th>
<th>Implementazione</th>
</tr>
</thead>
<tbody>${controlsTableHtml}</tbody>
</table>
</div>
` : ''}
<!-- Print Button -->
<div style="text-align:center; margin-top:24px;">
<button class="btn btn-secondary" onclick="window.print()" style="gap:6px;">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clip-rule="evenodd"/></svg>
Stampa Report
</button>
</div>
</div>
</div>
`;
}
// ══════════════════════════════════════════════════════════
// TAB 2: Controls
// ══════════════════════════════════════════════════════════
async function loadControls() {
const container = document.getElementById('controls-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento controlli...</p></div>';
try {
const result = await api.listControls();
if (result.success && result.data) {
renderControls(result.data);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderControls(controls) {
const container = document.getElementById('controls-container');
if (!controls || controls.length === 0) {
container.innerHTML = `
<div class="empty-state-box">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
<h4>Nessun controllo configurato</h4>
<p>I controlli di compliance appariranno qui dopo il primo assessment.</p>
</div>`;
return;
}
let html = `
<table class="data-table">
<thead>
<tr>
<th>Codice</th>
<th>Framework</th>
<th>Titolo</th>
<th>Stato</th>
<th>% Implementazione</th>
<th>Responsabile</th>
<th>Prossima Verifica</th>
</tr>
</thead>
<tbody>`;
controls.forEach(c => {
const st = c.status || 'not_started';
const implPct = c.implementation_percentage || 0;
const implColor = implPct >= 80 ? 'var(--secondary)' : implPct >= 50 ? 'var(--warning)' : 'var(--danger)';
html += `
<tr class="clickable-row" onclick="showEditControlModal(${c.id}, this)" title="Clicca per modificare">
<td style="font-family:var(--font-mono); font-size:0.82rem; font-weight:600;">${escapeHtml(c.control_code || '-')}</td>
<td>${escapeHtml(c.framework || 'NIS2')}</td>
<td style="font-weight:500;">${escapeHtml(c.title || '-')}</td>
<td><span class="status-badge ${st}">${controlStatusLabels[st] || st}</span></td>
<td>
<div class="progress-bar-container">
<div class="progress-bar"><div class="progress-bar-fill" style="width:${implPct}%; background:${implColor};"></div></div>
<span class="progress-pct">${implPct}%</span>
</div>
</td>
<td>${escapeHtml(c.responsible_name || '-')}</td>
<td>${formatDate(c.next_review_date)}</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
function showEditControlModal(id) {
// First, find the control from the table data
api.listControls().then(result => {
if (!result.success || !result.data) return;
const control = result.data.find(c => c.id === id);
if (!control) return;
const st = control.status || 'not_started';
const implPct = control.implementation_percentage || 0;
const statusOptions = Object.entries(controlStatusLabels).map(([val, label]) =>
`<option value="${val}" ${st === val ? 'selected' : ''}>${label}</option>`
).join('');
showModal('Modifica Controllo: ' + escapeHtml(control.control_code || ''), `
<div class="form-group">
<label class="form-label">Titolo</label>
<p style="font-size:0.9rem; color:var(--gray-700); padding:8px 0;">${escapeHtml(control.title || '-')}</p>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div class="form-group">
<label class="form-label">Stato</label>
<select class="form-select" id="ctrl-status">${statusOptions}</select>
</div>
<div class="form-group">
<label class="form-label">% Implementazione</label>
<input type="range" id="ctrl-impl-pct" min="0" max="100" value="${implPct}" oninput="document.getElementById('ctrl-impl-pct-val').textContent = this.value + '%'" style="width:100%; margin-top:8px;">
<span id="ctrl-impl-pct-val" style="font-size:0.85rem; font-weight:600; color:var(--gray-700);">${implPct}%</span>
</div>
</div>
<div class="form-group">
<label class="form-label">Descrizione Evidenza</label>
<textarea class="form-input" id="ctrl-evidence" rows="3" placeholder="Descrivi le evidenze di implementazione...">${escapeHtml(control.evidence_description || '')}</textarea>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<div class="form-group">
<label class="form-label">Responsabile (ID utente)</label>
<input type="text" class="form-input" id="ctrl-responsible" value="${escapeHtml(control.responsible_user_id || '')}">
</div>
<div class="form-group">
<label class="form-label">Prossima Verifica</label>
<input type="date" class="form-input" id="ctrl-next-review" value="${control.next_review_date || ''}">
</div>
</div>
`, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveControl(${id})">Salva</button>
`
});
});
}
async function saveControl(id) {
const data = {
status: document.getElementById('ctrl-status').value,
implementation_percentage: parseInt(document.getElementById('ctrl-impl-pct').value),
evidence_description: document.getElementById('ctrl-evidence').value.trim() || null,
responsible_user_id: document.getElementById('ctrl-responsible').value.trim() || null,
next_review_date: document.getElementById('ctrl-next-review').value || null,
};
closeModal();
try {
const result = await api.updateControl(id, data);
if (result.success) {
showNotification('Controllo aggiornato con successo!', 'success');
loadControls();
} else {
showNotification(result.message || 'Errore nell\'aggiornamento.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ══════════════════════════════════════════════════════════
// TAB 3: Audit Logs
// ══════════════════════════════════════════════════════════
async function loadAuditLogs() {
const container = document.getElementById('audit-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento log...</p></div>';
try {
const result = await api.getAuditLogs({ page: auditPage, per_page: 25 });
if (result.success) {
const logs = result.data || [];
const pagination = result.pagination || {};
auditTotalPages = pagination.total_pages || 1;
renderAuditLogs(logs, pagination);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderAuditLogs(logs, pagination) {
const container = document.getElementById('audit-container');
if (!logs || logs.length === 0) {
container.innerHTML = `
<div class="empty-state-box">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
<h4>Nessun log disponibile</h4>
<p>Le attivita' di audit appariranno qui.</p>
</div>`;
return;
}
let html = `
<table class="data-table">
<thead>
<tr>
<th>Data/Ora</th>
<th>Utente</th>
<th>Azione</th>
<th>Entita'</th>
<th>Dettagli</th>
</tr>
</thead>
<tbody>`;
logs.forEach(log => {
const actionText = actionLabels[log.action] || log.action || '-';
let detailsText = '-';
if (log.details) {
try {
const d = typeof log.details === 'string' ? JSON.parse(log.details) : log.details;
detailsText = Object.entries(d).map(([k, v]) => `${k}: ${v}`).join(', ');
} catch (e) {
detailsText = String(log.details);
}
}
html += `
<tr>
<td style="white-space:nowrap;">${formatDateTime(log.created_at)}</td>
<td>${escapeHtml(log.full_name || '-')}</td>
<td><span class="log-action">${escapeHtml(actionText)}</span></td>
<td>
${log.entity_type ? `<span class="log-entity">${escapeHtml(log.entity_type)}</span>` : '-'}
${log.entity_id ? `<span style="font-size:0.8rem; color:var(--gray-400);"> #${log.entity_id}</span>` : ''}
</td>
<td><span class="log-details" title="${escapeHtml(detailsText)}">${escapeHtml(detailsText)}</span></td>
</tr>`;
});
html += '</tbody></table>';
// Pagination
const totalPages = pagination.total_pages || 1;
const currentPage = pagination.page || auditPage;
html += `
<div class="pagination">
<button onclick="goAuditPage(${currentPage - 1})" ${currentPage <= 1 ? 'disabled' : ''}>Precedente</button>
<span class="page-info">Pagina ${currentPage} di ${totalPages}</span>
<button onclick="goAuditPage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>Successiva</button>
</div>`;
container.innerHTML = html;
}
function goAuditPage(page) {
if (page < 1 || page > auditTotalPages) return;
auditPage = page;
loadAuditLogs();
}
// ══════════════════════════════════════════════════════════
// TAB 4: ISO 27001 Mapping
// ══════════════════════════════════════════════════════════
async function loadIsoMapping() {
const container = document.getElementById('iso-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento mapping ISO 27001...</p></div>';
try {
const result = await api.getIsoMapping();
if (result.success && result.data) {
renderIsoMapping(result.data);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4></div>';
}
}
function renderIsoMapping(mapping) {
const container = document.getElementById('iso-container');
if (!mapping || mapping.length === 0) {
container.innerHTML = '<div class="empty-state-box"><h4>Nessun mapping disponibile</h4></div>';
return;
}
let html = `
<div class="card">
<div class="card-header">
<h3>Mapping NIS2 - ISO 27001:2022</h3>
</div>
<div class="card-body">
<p style="font-size:0.85rem; color:var(--gray-500); margin-bottom:20px;">
Corrispondenza tra gli articoli della Direttiva NIS2 e i controlli dello standard ISO/IEC 27001:2022.
</p>`;
mapping.forEach(m => {
const isoControls = (m.iso27001 || '').split(',').map(c => c.trim()).filter(c => c);
const controlBadges = isoControls.map(c => `<span class="iso-control-badge">${escapeHtml(c)}</span>`).join('');
html += `
<div class="iso-mapping-row">
<span class="iso-nis2-badge">Art. ${escapeHtml(m.nis2)}</span>
<span class="iso-arrow">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
</span>
<div class="iso-controls">${controlBadges}</div>
<span class="iso-title">${escapeHtml(m.title || '')}</span>
</div>`;
});
html += '</div></div>';
container.innerHTML = html;
}
// ── Initial: no auto-load, user triggers report ─────────
</script>
</body>
</html>