Riscontro Simon sulla tabella Gestione Rischi: - Titoli di gruppo sui tripletti Probabilità/Impatto/Valutazione: da "VALUTAZIONE RISCHIO INERENTE/RESIDUO" a "Rischio Inerente"/"Rischio Residuo" (risks.html thead + i18n.js grp_inherent/grp_residual IT+EN). - Ritirata eccezione #3: banda Alert riportata a spec Simon 15-25/9-12/3-8/1-2. Simon ha ragione: con P,I in 1-5 i prodotti 7/11/13/14 non esistono → nessun buco. alertBand resta >=9 (=9-12 in pratica); aggiornati commento, help.js, manuale, KB (9-14 -> 9-12). - Cache-buster help.js/i18n.js -> 20260730b (i18n va bustato o riscrive il titolo). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
518 lines
35 KiB
HTML
518 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>Segnalazioni Sicurezza · NIS2 Agile</title>
|
|
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
|
|
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
|
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
|
<style>
|
|
/* ── Compat V3: token + classi emesse dal JS (ex style.css, niente CDN) ── */
|
|
:root{
|
|
--primary:#1F5FA8;--primary-bg:#E7F0FB;--secondary:#1E8A5A;--secondary-bg:#E6F4EC;
|
|
--success:#1E8A5A;--success-bg:#E6F4EC;--warning:#D98324;--warning-bg:#FBF1E2;
|
|
--danger:#C62828;--danger-bg:#FBE9E9;--info:#1F5FA8;--info-bg:#E7F0FB;
|
|
--gray-50:#F8FAFC;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-300:#CBD5E1;
|
|
--gray-400:#94A3B8;--gray-500:#64748B;--gray-600:#475569;--gray-700:#334155;--gray-800:#15263C;
|
|
--card-bg:#fff;--card-shadow:var(--shadow);--border-radius:8px;--border-radius-lg:12px;--border-radius-sm:6px;
|
|
--font-mono:'SF Mono','Fira Code','Consolas',monospace;--transition-fast:150ms ease;
|
|
}
|
|
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
.mb-16{margin-bottom:16px}.mb-24{margin-bottom:24px}
|
|
/* Buttons (parità con .btn-primary/.btn-ghost di v3.css) */
|
|
.btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 14px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer;line-height:1.2}
|
|
.btn:disabled{opacity:.55;cursor:not-allowed}
|
|
.btn-primary{background:var(--c-blue);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
|
|
.btn-secondary,.btn-outline{background:#fff;color:var(--ink);border-color:var(--line)}
|
|
.btn-secondary:hover:not(:disabled),.btn-outline:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
|
|
.btn-sm{padding:5px 10px;font-size:11.5px}
|
|
.icon-inline{width:16px;height:16px;fill:currentColor;vertical-align:text-bottom}
|
|
/* Forms */
|
|
.form-group{margin-bottom:16px}
|
|
.form-label{display:block;font-size:12px;font-weight:600;color:var(--ink);margin-bottom:6px}
|
|
.form-control,.form-input,.form-select{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}
|
|
.form-control:focus,.form-input:focus,.form-select:focus{outline:none;border-color:var(--c-blue);box-shadow:0 0 0 3px rgba(31,95,168,.12)}
|
|
textarea.form-control{resize:vertical;min-height:80px}
|
|
/* Cards (override v3 .card already exists; add header/body) */
|
|
.card-header{padding:16px 20px;border-bottom:1px solid var(--line)}
|
|
.card-header h3{margin:0;font-size:15px;font-weight:700}
|
|
.card-body{padding:20px}
|
|
/* Stats grid (KPI) */
|
|
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px}
|
|
.stat-card{background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);padding:16px 18px;border-left:4px solid var(--c-blue)}
|
|
.stat-label{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#7a8aa0}
|
|
.stat-value{font-size:25px;font-weight:800;line-height:1.1;margin-top:4px;color:var(--ink)}
|
|
/* Table */
|
|
.table-container{overflow-x:auto;border:1px solid var(--line);border-radius:12px}
|
|
.table-container table{width:100%;border-collapse:collapse;font-size:13px;background:#fff}
|
|
.table-container thead{background:#F5F8FC}
|
|
.table-container th{text-align:left;padding:11px 14px;font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#7a8aa0;border-bottom:1px solid var(--line)}
|
|
.table-container td{padding:11px 14px;border-bottom:1px solid var(--line);vertical-align:middle}
|
|
.table-container tbody tr:last-child td{border-bottom:0}
|
|
.table-container tbody tr:hover td{background:#FAFCFE}
|
|
/* Badge / empty / spinner */
|
|
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
|
|
.badge-neutral{background:#EEF2F7;color:#5b6b80}
|
|
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:40px 20px;text-align:center;color:var(--muted)}
|
|
.empty-state h4{margin:0;font-size:14px;font-weight:700;color:var(--ink)}
|
|
.empty-state p{margin:0;font-size:12.5px}
|
|
.spinner{width:30px;height:30px;border:3px solid var(--line);border-top-color:var(--c-blue);border-radius:50%;animation:v3spin .7s linear infinite}
|
|
@media (prefers-reduced-motion:reduce){.spinner{animation:none}}
|
|
/* Page-specific */
|
|
.priority-badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:6px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
|
|
.priority-critical{background:var(--danger-bg);color:var(--danger)}
|
|
.priority-high{background:#FBF1E2;color:#9A6418}
|
|
.priority-medium{background:var(--warning-bg);color:#9A6418}
|
|
.priority-low{background:var(--gray-100);color:var(--gray-600)}
|
|
.status-badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:6px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
|
|
.status-received{background:#E7F0FB;color:#1F5FA8}
|
|
.status-under_review{background:var(--warning-bg);color:#9A6418}
|
|
.status-investigating{background:#f3e8ff;color:#7c3aed}
|
|
.status-resolved,.status-closed{background:var(--success-bg);color:var(--success)}
|
|
.status-rejected{background:var(--danger-bg);color:var(--danger)}
|
|
.anonymous-shield{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;background:var(--success-bg);border-radius:6px;color:var(--success);font-size:11px;font-weight:600}
|
|
.art32-banner{background:linear-gradient(135deg,rgba(31,95,168,.07),rgba(30,138,90,.07));border:1px solid var(--line);border-radius:12px;padding:18px 22px;margin-bottom:18px}
|
|
.art32-banner h3{font-size:14px;font-weight:700;color:var(--ink);margin:0 0 6px}
|
|
.art32-banner p{font-size:12.5px;color:var(--muted);margin:0}
|
|
</style>
|
|
<!-- PWA:start -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#0066CC">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
|
<meta name="application-name" content="NIS2 Agile">
|
|
<script src="/js/pwa.js?v=20260614" defer></script>
|
|
<!-- PWA:end -->
|
|
</head>
|
|
<body>
|
|
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
|
<div id="topnav"></div>
|
|
|
|
<main class="wrap">
|
|
<nav class="crumb" aria-label="breadcrumb">
|
|
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
|
<i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
|
|
<i data-lucide="chevron-right" class="ic"></i><span>Segnalazioni</span>
|
|
</nav>
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>Segnalazioni Sicurezza</h1>
|
|
<p class="sub">Canale interno Art.32 NIS2 — Segnala violazioni e anomalie di sicurezza</p>
|
|
</div>
|
|
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
<button class="btn-ghost" onclick="switchView('track')">
|
|
<i data-lucide="search" class="ic"></i>Traccia segnalazione anonima
|
|
</button>
|
|
<button class="btn-primary" onclick="switchView('submit')">
|
|
<i data-lucide="plus" class="ic"></i>Nuova segnalazione
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Art.32 Banner -->
|
|
<div class="art32-banner">
|
|
<h3>Art. 32 D.Lgs. 138/2024 — Canale Segnalazioni Interno</h3>
|
|
<p>Le entità NIS2 devono predisporre canali interni per la segnalazione di violazioni alla sicurezza informatica. Le segnalazioni anonime sono protette e il segnalante non può essere identificato. Ogni segnalazione viene gestita con priorità e tracciabilità.</p>
|
|
</div>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="stats-grid mb-24" id="stats-grid">
|
|
<div class="stat-card"><div class="spinner"></div></div>
|
|
</div>
|
|
|
|
<!-- Views -->
|
|
<div id="view-list">
|
|
<!-- Filters -->
|
|
<div class="card mb-16">
|
|
<div class="card-body" style="padding:16px 20px;">
|
|
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
|
|
<label for="filter-status" class="sr-only">Filtra per stato</label>
|
|
<select id="filter-status" class="form-control" style="width:auto;" onchange="loadReports()" aria-label="Filtra per stato">
|
|
<option value="">Tutti gli stati</option>
|
|
<option value="received">Ricevute</option>
|
|
<option value="under_review">In revisione</option>
|
|
<option value="investigating">In indagine</option>
|
|
<option value="resolved">Risolte</option>
|
|
<option value="closed">Chiuse</option>
|
|
</select>
|
|
<label for="filter-priority" class="sr-only">Filtra per priorità</label>
|
|
<select id="filter-priority" class="form-control" style="width:auto;" onchange="loadReports()" aria-label="Filtra per priorità">
|
|
<option value="">Tutte le priorità</option>
|
|
<option value="critical">Critica</option>
|
|
<option value="high">Alta</option>
|
|
<option value="medium">Media</option>
|
|
<option value="low">Bassa</option>
|
|
</select>
|
|
<label for="filter-category" class="sr-only">Filtra per categoria</label>
|
|
<select id="filter-category" class="form-control" style="width:auto;" onchange="loadReports()" aria-label="Filtra per categoria">
|
|
<option value="">Tutte le categorie</option>
|
|
<option value="security_incident">Incidente sicurezza</option>
|
|
<option value="data_breach">Data breach</option>
|
|
<option value="unauthorized_access">Accesso non autorizzato</option>
|
|
<option value="policy_violation">Violazione policy</option>
|
|
<option value="supply_chain_risk">Rischio supply chain</option>
|
|
<option value="nis2_non_compliance">Non conformità NIS2</option>
|
|
<option value="other">Altro</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="reports-container"><div class="spinner" style="margin:60px auto;"></div></div>
|
|
</div>
|
|
|
|
<!-- Submit Form -->
|
|
<div id="view-submit" style="display:none;">
|
|
<div class="card" style="max-width:720px;">
|
|
<div class="card-header">
|
|
<h3>Nuova Segnalazione</h3>
|
|
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Segnala anonimamente o con il tuo nome. L'anonimato è garantito dalla piattaforma.</p>
|
|
</div>
|
|
<div class="card-body">
|
|
<div style="margin-bottom:20px; padding:12px 16px; background:var(--success-bg); border:1px solid var(--line); border-radius:var(--border-radius); display:flex; align-items:center; gap:10px;">
|
|
<i data-lucide="lock" class="ic" style="width:18px; height:18px; color:var(--success); flex-shrink:0;"></i>
|
|
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
|
|
<input type="checkbox" id="is-anonymous" checked style="accent-color:var(--success);">
|
|
<span><strong>Segnalazione anonima</strong> — La tua identità non verrà registrata</span>
|
|
</label>
|
|
</div>
|
|
|
|
<form id="submit-form" onsubmit="submitReport(event)">
|
|
<div class="form-group">
|
|
<label class="form-label" for="rep-category">Categoria *</label>
|
|
<select id="rep-category" class="form-control" required>
|
|
<option value="">Seleziona categoria...</option>
|
|
<option value="security_incident">Incidente di sicurezza</option>
|
|
<option value="data_breach">Data breach / Violazione dati</option>
|
|
<option value="unauthorized_access">Accesso non autorizzato</option>
|
|
<option value="policy_violation">Violazione policy interna</option>
|
|
<option value="supply_chain_risk">Rischio supply chain</option>
|
|
<option value="nis2_non_compliance">Non conformità NIS2</option>
|
|
<option value="corruption">Corruzione / Frode</option>
|
|
<option value="other">Altro</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="rep-title">Titolo *</label>
|
|
<input type="text" id="rep-title" class="form-control" placeholder="Breve descrizione dell'anomalia..." required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="rep-description">Descrizione dettagliata *</label>
|
|
<textarea id="rep-description" class="form-control" rows="6" placeholder="Descrivi cosa hai osservato, quando è successo, chi è coinvolto (se noto), e qualsiasi altra informazione utile..." required></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="rep-priority">Priorità stimata</label>
|
|
<select id="rep-priority" class="form-control">
|
|
<option value="medium">Media</option>
|
|
<option value="high">Alta</option>
|
|
<option value="critical">Critica — Azione immediata richiesta</option>
|
|
<option value="low">Bassa</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="rep-article">Articolo NIS2 violato (opzionale)</label>
|
|
<input type="text" id="rep-article" class="form-control" placeholder="Es. Art.21, Art.23...">
|
|
</div>
|
|
<div class="form-group" id="contact-email-group">
|
|
<label class="form-label" for="rep-email">Email per follow-up (opzionale)</label>
|
|
<input type="email" id="rep-email" class="form-control" placeholder="Email solo per ricevere aggiornamenti">
|
|
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:4px;">Lascia vuoto per mantenere l'anonimato completo. Oppure fornisci un'email temporanea.</p>
|
|
</div>
|
|
<div style="display:flex; gap:12px; margin-top:24px;">
|
|
<button type="submit" class="btn btn-primary">Invia Segnalazione</button>
|
|
<button type="button" class="btn btn-secondary" onclick="switchView('list')">Annulla</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Track Anonymous -->
|
|
<div id="view-track" style="display:none;">
|
|
<div class="card" style="max-width:600px;">
|
|
<div class="card-header">
|
|
<h3>Traccia Segnalazione Anonima</h3>
|
|
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Inserisci il token ricevuto al momento della segnalazione per verificarne lo stato.</p>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="form-group">
|
|
<label class="form-label" for="track-token">Token Segnalazione</label>
|
|
<input type="text" id="track-token" class="form-control" placeholder="Inserisci il token...">
|
|
</div>
|
|
<div style="display:flex; gap:12px;">
|
|
<button class="btn btn-primary" onclick="trackReport()">Verifica Stato</button>
|
|
<button class="btn btn-secondary" onclick="switchView('list')">Annulla</button>
|
|
</div>
|
|
<div id="track-result" style="margin-top:24px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Report Detail Modal handled by common.js showModal -->
|
|
|
|
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
|
|
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
|
|
<script src="/js/api.js?v=20260730a"></script>
|
|
<script src="/js/common.js?v=20260729b"></script>
|
|
<script src="/vendor/lucide/lucide.min.js"></script>
|
|
<script src="/js/topnav-v3.js?v=20260729"></script>
|
|
<script src="/js/i18n.js?v=20260730b"></script>
|
|
<script src="/js/help.js?v=20260730b"></script>
|
|
<script>
|
|
if (!checkAuth()) throw new Error('Not authenticated');
|
|
loadSidebar();
|
|
if (typeof I18n !== 'undefined') I18n.init();
|
|
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
|
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
|
|
|
let currentView = 'list';
|
|
|
|
const categoryLabels = {
|
|
security_incident: 'Incidente sicurezza', data_breach: 'Data breach',
|
|
unauthorized_access: 'Accesso non autorizzato', policy_violation: 'Violazione policy',
|
|
supply_chain_risk: 'Rischio supply chain', nis2_non_compliance: 'Non conformità NIS2',
|
|
corruption: 'Corruzione/Frode', fraud: 'Frode', other: 'Altro'
|
|
};
|
|
|
|
const priorityLabels = { critical: 'Critica', high: 'Alta', medium: 'Media', low: 'Bassa' };
|
|
const statusLabels = {
|
|
received: 'Ricevuta', under_review: 'In revisione', investigating: 'In indagine',
|
|
resolved: 'Risolta', closed: 'Chiusa', rejected: 'Respinta'
|
|
};
|
|
|
|
function switchView(view) {
|
|
currentView = view;
|
|
['list','submit','track'].forEach(v => {
|
|
document.getElementById('view-' + v).style.display = v === view ? 'block' : 'none';
|
|
});
|
|
if (view === 'list') { loadStats(); loadReports(); }
|
|
}
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const result = await api.request('GET', '/whistleblowing/stats');
|
|
if (result.success) renderStats(result.data.stats);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function renderStats(s) {
|
|
const grid = document.getElementById('stats-grid');
|
|
grid.innerHTML = `
|
|
<div class="stat-card">
|
|
<div class="stat-label">Totale Segnalazioni</div>
|
|
<div class="stat-value">${s.total || 0}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Da Gestire</div>
|
|
<div class="stat-value" style="color:var(--warning);">${(s.received || 0) + (s.under_review || 0)}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">In Indagine</div>
|
|
<div class="stat-value" style="color:var(--primary);">${s.investigating || 0}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Critiche/Alte</div>
|
|
<div class="stat-value" style="color:var(--danger);">${(s.critical || 0) + (s.high || 0)}</div>
|
|
</div>`;
|
|
}
|
|
|
|
async function loadReports() {
|
|
const container = document.getElementById('reports-container');
|
|
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
|
|
try {
|
|
const params = new URLSearchParams();
|
|
const status = document.getElementById('filter-status').value;
|
|
const priority = document.getElementById('filter-priority').value;
|
|
const category = document.getElementById('filter-category').value;
|
|
if (status) params.append('status', status);
|
|
if (priority) params.append('priority', priority);
|
|
if (category) params.append('category', category);
|
|
|
|
const result = await api.request('GET', '/whistleblowing/list?' + params);
|
|
if (result.success) renderReports(result.data.reports || []);
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><h4>Errore caricamento segnalazioni</h4></div>';
|
|
}
|
|
}
|
|
|
|
function renderReports(reports) {
|
|
const container = document.getElementById('reports-container');
|
|
if (!reports.length) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<i data-lucide="megaphone" style="width:34px;height:34px;color:var(--gray-300);" aria-hidden="true"></i>
|
|
<h4>Nessuna segnalazione trovata</h4>
|
|
<p>Il canale di segnalazione è attivo. Le segnalazioni appariranno qui.</p>
|
|
</div>`;
|
|
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch(e){} }
|
|
return;
|
|
}
|
|
|
|
let html = `<div class="table-container"><table>
|
|
<thead><tr><th scope="col">Codice</th><th scope="col">Categoria</th><th scope="col">Titolo</th><th scope="col">Priorità</th><th scope="col">Stato</th><th scope="col">Anonima</th><th scope="col">Assegnata</th><th scope="col">Data</th><th scope="col"></th></tr></thead><tbody>`;
|
|
|
|
reports.forEach(r => {
|
|
html += `<tr>
|
|
<td><code style="font-size:0.8rem;">${escapeHtml(r.report_code)}</code></td>
|
|
<td><span class="badge badge-neutral">${escapeHtml(categoryLabels[r.category] || r.category)}</span></td>
|
|
<td><strong style="font-size:0.875rem;">${escapeHtml(r.title)}</strong></td>
|
|
<td><span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span></td>
|
|
<td><span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span></td>
|
|
<td>${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>Anonima</span>' : '<span style="color:var(--gray-400); font-size:0.75rem;">Firmata</span>'}</td>
|
|
<td>${r.assigned_to_name ? escapeHtml(r.assigned_to_name) : '<span style="color:var(--gray-400);">—</span>'}</td>
|
|
<td style="font-size:0.8rem;">${formatDate(r.created_at)}</td>
|
|
<td><button class="btn btn-sm btn-secondary" onclick="viewReport(${r.id})">Gestisci</button></td>
|
|
</tr>`;
|
|
});
|
|
html += '</tbody></table></div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
async function viewReport(id) {
|
|
try {
|
|
const result = await api.request('GET', `/whistleblowing/${id}`);
|
|
if (!result.success) { showNotification('Errore caricamento.', 'error'); return; }
|
|
const r = result.data;
|
|
|
|
const timelineHtml = (r.timeline || []).map(t => `
|
|
<div style="padding:10px 0; border-bottom:1px solid var(--gray-100); display:flex; gap:12px;">
|
|
<div style="width:8px; height:8px; border-radius:50%; background:var(--primary); margin-top:6px; flex-shrink:0;"></div>
|
|
<div>
|
|
<p style="font-size:0.8125rem; color:var(--gray-700);">${escapeHtml(t.description)}</p>
|
|
<p style="font-size:0.75rem; color:var(--gray-400);">${formatDateTime(t.created_at)} ${t.created_by_name ? '— ' + escapeHtml(t.created_by_name) : ''}</p>
|
|
</div>
|
|
</div>`).join('');
|
|
|
|
showModal(`${escapeHtml(r.report_code)} — ${escapeHtml(r.title)}`, `
|
|
<div style="display:flex; gap:10px; flex-wrap:wrap; margin-bottom:16px;">
|
|
<span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span>
|
|
<span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span>
|
|
${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>Anonima</span>' : ''}
|
|
</div>
|
|
<div style="padding:12px 16px; background:var(--gray-50); border-radius:var(--border-radius); margin-bottom:16px; font-size:0.875rem; line-height:1.6;">
|
|
${escapeHtml(r.description)}
|
|
</div>
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; font-size:0.8125rem;">
|
|
<div><strong>Categoria:</strong> ${escapeHtml(categoryLabels[r.category] || r.category)}</div>
|
|
<div><strong>Art. NIS2:</strong> ${r.nis2_article ? escapeHtml(r.nis2_article) : '—'}</div>
|
|
<div><strong>Assegnata a:</strong> ${r.assigned_to_name ? escapeHtml(r.assigned_to_name) : '—'}</div>
|
|
<div><strong>Data:</strong> ${formatDateTime(r.created_at)}</div>
|
|
</div>
|
|
<h4 style="font-size:0.8125rem; font-weight:700; margin-bottom:8px;">Timeline</h4>
|
|
<div>${timelineHtml || '<p style="color:var(--gray-400); font-size:0.8rem;">Nessun evento in timeline.</p>'}</div>
|
|
<div style="margin-top:20px; display:flex; flex-direction:column; gap:10px;">
|
|
<label for="modal-status" class="sr-only">Cambia stato</label>
|
|
<select id="modal-status" class="form-control">
|
|
<option value="">Cambia stato...</option>
|
|
${['under_review','investigating','resolved','closed','rejected'].map(s =>
|
|
`<option value="${s}" ${r.status === s ? 'selected' : ''}>${escapeHtml(statusLabels[s])}</option>`
|
|
).join('')}
|
|
</select>
|
|
<label for="modal-notes" class="sr-only">Note di risoluzione</label>
|
|
<textarea id="modal-notes" class="form-control" rows="3" placeholder="Note di risoluzione...">${escapeHtml(r.resolution_notes || '')}</textarea>
|
|
</div>
|
|
`, `<button class="btn btn-primary" onclick="updateReport(${id})">Salva</button>
|
|
<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>`);
|
|
|
|
} catch (e) { showNotification('Errore di connessione.', 'error'); }
|
|
}
|
|
|
|
async function updateReport(id) {
|
|
const status = document.getElementById('modal-status').value;
|
|
const notes = document.getElementById('modal-notes').value;
|
|
const body = {};
|
|
if (status) body.status = status;
|
|
if (notes) body.resolution_notes = notes;
|
|
if (!Object.keys(body).length) { closeModal(); return; }
|
|
|
|
try {
|
|
const result = await api.request('PUT', `/whistleblowing/${id}`, body);
|
|
if (result.success) {
|
|
closeModal();
|
|
showNotification('Segnalazione aggiornata.', 'success');
|
|
loadReports(); loadStats();
|
|
} else showNotification(result.message || 'Errore.', 'error');
|
|
} catch (e) { showNotification('Errore di connessione.', 'error'); }
|
|
}
|
|
|
|
async function submitReport(e) {
|
|
e.preventDefault();
|
|
const btn = e.target.querySelector('[type=submit]');
|
|
btn.disabled = true; btn.textContent = 'Invio...';
|
|
|
|
try {
|
|
const body = {
|
|
category: document.getElementById('rep-category').value,
|
|
title: document.getElementById('rep-title').value,
|
|
description: document.getElementById('rep-description').value,
|
|
priority: document.getElementById('rep-priority').value,
|
|
nis2_article: document.getElementById('rep-article').value,
|
|
is_anonymous: document.getElementById('is-anonymous').checked ? 1 : 0,
|
|
contact_email: document.getElementById('rep-email').value,
|
|
organization_id: localStorage.getItem('nis2_org_id'),
|
|
};
|
|
|
|
const result = await api.request('POST', '/whistleblowing/submit', body);
|
|
if (result.success) {
|
|
const token = result.data.anonymous_token;
|
|
showModal('Segnalazione Inviata', `
|
|
<div style="padding:16px; background:rgba(16,185,129,0.1); border-radius:var(--border-radius); margin-bottom:16px;">
|
|
<p style="font-weight:700; color:var(--success);">Segnalazione ricevuta con codice: <code>${escapeHtml(result.data.report_code)}</code></p>
|
|
</div>
|
|
${token ? `<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:8px;">Token per tracking anonimo (conservalo):</p>
|
|
<div style="padding:12px; background:var(--gray-100); border-radius:var(--border-radius); font-family:monospace; font-size:0.8rem; word-break:break-all; user-select:all;">${escapeHtml(token)}</div>` : ''}
|
|
`, `<button class="btn btn-primary" onclick="closeModal(); switchView('list');">OK</button>`);
|
|
e.target.reset();
|
|
} else {
|
|
showNotification(result.message || 'Errore.', 'error');
|
|
}
|
|
} catch (err) { showNotification('Errore di connessione.', 'error'); }
|
|
finally { btn.disabled = false; btn.textContent = 'Invia Segnalazione'; }
|
|
}
|
|
|
|
async function trackReport() {
|
|
const token = document.getElementById('track-token').value.trim();
|
|
if (!token) { showNotification('Inserisci il token.', 'error'); return; }
|
|
|
|
const container = document.getElementById('track-result');
|
|
container.innerHTML = '<div class="spinner"></div>';
|
|
try {
|
|
const result = await api.request('GET', '/whistleblowing/track-anonymous?token=' + encodeURIComponent(token));
|
|
if (result.success) {
|
|
const r = result.data;
|
|
const tl = (r.timeline || []).map(t => `
|
|
<div style="display:flex; gap:10px; padding:8px 0; border-bottom:1px solid var(--gray-100);">
|
|
<div style="width:6px; height:6px; border-radius:50%; background:var(--primary); margin-top:7px; flex-shrink:0;"></div>
|
|
<div><p style="font-size:0.8125rem;">${escapeHtml(t.description)}</p>
|
|
<p style="font-size:0.75rem; color:var(--gray-400);">${formatDateTime(t.created_at)}</p></div>
|
|
</div>`).join('');
|
|
container.innerHTML = `
|
|
<div style="padding:16px; border:1px solid var(--gray-200); border-radius:var(--border-radius);">
|
|
<div style="display:flex; gap:10px; margin-bottom:12px;">
|
|
<code style="font-size:0.875rem;">${escapeHtml(r.report_code)}</code>
|
|
<span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span>
|
|
</div>
|
|
<p style="font-size:0.8125rem; color:var(--gray-600); margin-bottom:12px;">Categoria: ${escapeHtml(categoryLabels[r.category] || r.category)}</p>
|
|
<div>${tl}</div>
|
|
</div>`;
|
|
} else {
|
|
container.innerHTML = '<div class="empty-state"><h4>Token non valido o segnalazione non trovata</h4></div>';
|
|
}
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
|
|
}
|
|
}
|
|
|
|
// Init
|
|
loadStats();
|
|
loadReports();
|
|
</script>
|
|
</body>
|
|
</html>
|