- 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>
907 lines
41 KiB
HTML
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 & 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>
|