Files
nis2-agile/public/whistleblowing.html
T
DevEnv nis2-agileandClaude Opus 4.8 8799620fba [FIX] Rischi 12 col: titoli gruppo "Rischio Inerente/Residuo" + soglie alert 9-12 (feedback Simon)
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>
2026-07-31 07:36:43 +02:00

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>