nis2-agile/public/risks.html
DevEnv nis2-agile ff2b5eaeeb [FIX] risks.html: apostrofi non escapati in LIKELIHOOD_DETAILS rompevano TUTTO lo script inline
Bug PRE-ESISTENTE (commit 7823898 del 2026-02-20, presente anche in 94d7867): gli apostrofi in
"dell'attivita" (riga 512) e "l'anno" (riga 513) chiudevano le stringhe a singolo apice ->
SyntaxError che azzerava l'INTERO blocco <script> della pagina Rischi (tabella, matrice, FAIR,
KRI, dettaglio: tutti gli onclick davano ReferenceError). La pagina era servita 200 ma JS morto.
Trovato dalla review multi-agente (agente frontend). node --check ora OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:34:52 +02:00

1360 lines
73 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestione Rischi - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── View Toggle ─────────────────────────────────────── */
.view-toggle {
display: flex;
background: var(--gray-100);
border-radius: var(--border-radius);
padding: 3px;
gap: 2px;
}
.view-toggle button {
padding: 6px 16px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
color: var(--gray-500);
font-family: inherit;
transition: all var(--transition-fast);
}
.view-toggle button.active {
background: var(--card-bg);
color: var(--gray-800);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* ── Filters Bar ─────────────────────────────────────── */
.filters-bar {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
}
.filters-bar .form-select {
width: auto;
min-width: 160px;
padding: 8px 36px 8px 12px;
font-size: 0.8rem;
}
/* ── Risk Score Badge ────────────────────────────────── */
.risk-score {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
font-weight: 700;
font-size: 0.85rem;
color: #fff;
}
.risk-score.score-low { background: var(--secondary); }
.risk-score.score-medium { background: var(--warning); color: var(--gray-900); }
.risk-score.score-high { background: #f97316; }
.risk-score.score-critical { background: var(--danger); }
/* ── Risk Matrix ─────────────────────────────────────── */
.risk-matrix-container {
max-width: 680px;
margin: 0 auto;
}
.risk-matrix-wrapper {
display: flex;
gap: 8px;
}
.risk-matrix-y-label {
writing-mode: vertical-lr;
transform: rotate(180deg);
text-align: center;
font-weight: 700;
font-size: 0.8rem;
color: var(--gray-600);
display: flex;
align-items: center;
justify-content: center;
padding-right: 4px;
}
.risk-matrix-grid-wrapper {
flex: 1;
}
.risk-matrix {
display: grid;
grid-template-columns: 40px repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr) 40px;
gap: 3px;
width: 100%;
aspect-ratio: 1.15;
}
.matrix-y-header {
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.8rem;
color: var(--gray-600);
}
.matrix-x-header {
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.8rem;
color: var(--gray-600);
}
.matrix-corner {
/* empty corner cell */
}
.matrix-cell {
border-radius: var(--border-radius-sm);
display: flex;
align-items: center;
justify-content: center;
cursor: default;
position: relative;
min-height: 64px;
transition: transform var(--transition-fast);
}
.matrix-cell:hover {
transform: scale(1.04);
z-index: 2;
}
.matrix-cell.level-1 { background: #dcfce7; }
.matrix-cell.level-2 { background: #bbf7d0; }
.matrix-cell.level-3 { background: #fef9c3; }
.matrix-cell.level-4 { background: #fde68a; }
.matrix-cell.level-5 { background: #fed7aa; }
.matrix-cell.level-6 { background: #fdba74; }
.matrix-cell.level-7 { background: #fb923c; }
.matrix-cell.level-8 { background: #fca5a5; }
.matrix-cell.level-9 { background: #f87171; }
.matrix-cell.level-10 { background: #ef4444; }
.matrix-dot {
width: 28px;
height: 28px;
border-radius: 50%;
background: rgba(255,255,255,0.9);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.75rem;
color: var(--gray-800);
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.risk-matrix-x-label {
text-align: center;
font-weight: 700;
font-size: 0.8rem;
color: var(--gray-600);
margin-top: 8px;
}
.matrix-legend {
display: flex;
gap: 16px;
justify-content: center;
margin-top: 20px;
flex-wrap: wrap;
}
.matrix-legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: var(--gray-600);
}
.matrix-legend-color {
width: 16px;
height: 16px;
border-radius: 3px;
}
/* ── Detail View ─────────────────────────────────────── */
.detail-back {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 600;
color: var(--gray-500);
cursor: pointer;
margin-bottom: 16px;
border: none;
background: none;
font-family: inherit;
padding: 0;
}
.detail-back:hover { color: var(--primary); }
.detail-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 24px;
}
.detail-field {
margin-bottom: 16px;
}
.detail-field-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 600;
color: var(--gray-500);
margin-bottom: 4px;
}
.detail-field-value {
font-size: 0.9rem;
color: var(--gray-800);
}
.score-display {
display: flex;
gap: 24px;
padding: 16px;
background: var(--gray-50);
border-radius: var(--border-radius);
margin-bottom: 16px;
}
.score-item {
text-align: center;
}
.score-item-value {
font-size: 1.75rem;
font-weight: 800;
line-height: 1.2;
}
.score-item-label {
font-size: 0.7rem;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.treatment-item {
padding: 16px;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
margin-bottom: 12px;
}
.treatment-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
/* ── Table clickable rows ───────────────────────────── */
.table-clickable tbody tr { cursor: pointer; }
/* ── Range value display ─────────────────────────────── */
.range-value-display {
text-align: center;
font-weight: 700;
font-size: 1.1rem;
color: var(--primary);
margin: 4px 0;
}
.range-label-text {
text-align: center;
font-size: 0.8rem;
color: var(--gray-600);
font-weight: 600;
}
.score-preview {
text-align: center;
padding: 12px;
background: var(--gray-50);
border-radius: var(--border-radius);
margin-top: 8px;
}
.score-preview-value {
font-size: 2rem;
font-weight: 800;
}
.score-preview-label {
font-size: 0.75rem;
color: var(--gray-500);
text-transform: uppercase;
}
@media (max-width: 768px) {
.detail-grid { grid-template-columns: 1fr; }
.filters-bar { flex-direction: column; align-items: stretch; }
.filters-bar .form-select { min-width: 100%; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="risks.title">Gestione Rischi</h2>
<div class="content-header-actions">
<div class="view-toggle" id="view-toggle">
<button class="active" onclick="switchView('table')">Tabella</button>
<button onclick="switchView('matrix')">Matrice</button>
<button onclick="switchView('fair')" data-i18n="risks.fair_tab">Quantitativo (FAIR)</button>
<button onclick="switchView('kri')" data-i18n="risks.kri_tab">KRI</button>
</div>
<button class="btn btn-secondary" id="btn-ai-suggest" onclick="aiSuggest()">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
AI Suggerisci
</button>
<button class="btn btn-primary" onclick="showRiskModal()">+ Nuovo Rischio</button>
</div>
</header>
<div class="content-body">
<!-- Table View -->
<div id="view-table">
<div class="filters-bar">
<select class="form-select" id="filter-category" onchange="loadRisks()">
<option value="">Tutte le categorie</option>
<option value="cyber">Cyber</option>
<option value="operational">Operativo</option>
<option value="compliance">Compliance</option>
<option value="supply_chain">Supply Chain</option>
<option value="physical">Fisico</option>
<option value="human">Umano</option>
</select>
<select class="form-select" id="filter-status" onchange="loadRisks()">
<option value="">Tutti gli stati</option>
<option value="identified">Identificato</option>
<option value="analyzing">In Analisi</option>
<option value="treating">In Trattamento</option>
<option value="monitored">Monitorato</option>
<option value="closed">Chiuso</option>
</select>
</div>
<div class="card">
<div class="table-container">
<table class="table-clickable">
<thead>
<tr>
<th>Codice</th>
<th>Titolo</th>
<th>Categoria</th>
<th>Probabilita'</th>
<th>Impatto</th>
<th>Score</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody id="risks-table-body">
<tr>
<td colspan="8">
<div class="spinner" style="margin:40px auto;"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Matrix View -->
<div id="view-matrix" class="hidden">
<div class="card">
<div class="card-header">
<h3>Matrice di Rischio 5x5</h3>
<span class="text-muted" style="font-size:0.8rem;">Rischi inerenti (esclusi chiusi)</span>
</div>
<div class="card-body">
<div class="risk-matrix-container">
<div class="risk-matrix-wrapper">
<div class="risk-matrix-y-label">Probabilita'</div>
<div class="risk-matrix-grid-wrapper">
<div class="risk-matrix" id="risk-matrix-grid">
<div class="spinner" style="margin:60px auto; grid-column: span 6;"></div>
</div>
<div class="risk-matrix-x-label">Impatto</div>
</div>
</div>
<div class="matrix-legend">
<div class="matrix-legend-item">
<div class="matrix-legend-color" style="background:#bbf7d0;"></div>
Basso (1-4)
</div>
<div class="matrix-legend-item">
<div class="matrix-legend-color" style="background:#fde68a;"></div>
Medio (5-9)
</div>
<div class="matrix-legend-item">
<div class="matrix-legend-color" style="background:#fdba74;"></div>
Alto (10-14)
</div>
<div class="matrix-legend-item">
<div class="matrix-legend-color" style="background:#fb923c;"></div>
Elevato (15-19)
</div>
<div class="matrix-legend-item">
<div class="matrix-legend-color" style="background:#ef4444;"></div>
Critico (20-25)
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FAIR Quantitative View -->
<div id="view-fair" class="hidden">
<div class="card" style="margin-bottom:16px;">
<div class="card-header">
<h3>Analisi Quantitativa del Rischio (FAIR)</h3>
<span class="text-muted" style="font-size:0.8rem;">Perdita Annua Attesa (ALE) in EUR — simulazione Monte Carlo</span>
</div>
<div class="card-body">
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:12px; margin-bottom:16px;">
<div><label class="form-label">Rischio</label>
<select class="form-select" id="fair-risk-select"><option value="">— seleziona —</option></select></div>
<div><label class="form-label">TEF min <small>(ev/anno)</small></label><input type="number" step="0.1" class="form-input" id="fair-tef-min" value="0.5"></div>
<div><label class="form-label">TEF probabile</label><input type="number" step="0.1" class="form-input" id="fair-tef-ml" value="1"></div>
<div><label class="form-label">TEF max</label><input type="number" step="0.1" class="form-input" id="fair-tef-max" value="2"></div>
<div><label class="form-label">Vulnerabilita <small>(0-1)</small></label><input type="number" step="0.05" min="0" max="1" class="form-input" id="fair-vuln" value="0.3"></div>
<div><label class="form-label">Perdita min <small>(EUR)</small></label><input type="number" step="1000" class="form-input" id="fair-lm-min" value="50000"></div>
<div><label class="form-label">Perdita probabile</label><input type="number" step="1000" class="form-input" id="fair-lm-ml" value="300000"></div>
<div><label class="form-label">Perdita max</label><input type="number" step="1000" class="form-input" id="fair-lm-max" value="2000000"></div>
</div>
<button class="btn btn-primary" onclick="runFair()" id="btn-run-fair">Calcola ALE</button>
<div id="fair-result" class="hidden" style="margin-top:20px;">
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:16px;">
<div class="stat-box"><div class="stat-label">ALE P10 (ottimistico)</div><div class="stat-value" id="fair-p10"></div></div>
<div class="stat-box" style="border-color:var(--primary);"><div class="stat-label">ALE Mediana (P50)</div><div class="stat-value" id="fair-p50"></div></div>
<div class="stat-box"><div class="stat-label">ALE P90 (pessimistico)</div><div class="stat-value" id="fair-p90"></div></div>
<div class="stat-box"><div class="stat-label">ALE Media</div><div class="stat-value" id="fair-mean"></div></div>
</div>
<div style="font-size:0.8rem; color:var(--gray-600); margin-bottom:8px;">Distribuzione delle perdite annue simulate (LEF medio <span id="fair-lef"></span> eventi/anno):</div>
<div id="fair-histogram" style="display:flex; align-items:flex-end; gap:2px; height:160px; border-bottom:2px solid var(--gray-300); padding:4px;"></div>
<div id="fair-histogram-labels" style="display:flex; gap:2px; font-size:0.65rem; color:var(--gray-500); margin-top:4px;"></div>
</div>
</div>
</div>
<div class="card">
<div class="card-header"><h3>Registro Quantitativo</h3>
<span class="text-muted" style="font-size:0.8rem;">ALE di portafoglio: <strong id="fair-portfolio"></strong></span></div>
<div class="table-container">
<table class="table-clickable"><thead><tr>
<th>Codice</th><th>Titolo</th><th>Categoria</th><th>ALE Min</th><th>ALE Mediana</th><th>ALE Max</th><th>ALE Media</th>
</tr></thead><tbody id="fair-register-body"><tr><td colspan="7" class="text-muted" style="text-align:center;padding:20px;">Nessun rischio quantificato</td></tr></tbody></table>
</div>
</div>
</div>
<!-- KRI Dashboard View -->
<div id="view-kri" class="hidden">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<div id="kri-summary" style="display:flex; gap:12px;"></div>
<button class="btn btn-primary" onclick="showKriModal()">+ Nuovo KRI</button>
</div>
<div class="card">
<div class="table-container">
<table class="table-clickable"><thead><tr>
<th>Stato</th><th>Indicatore</th><th>Categoria</th><th>Valore</th><th>Soglia Warning</th><th>Soglia Critica</th><th>Azioni</th>
</tr></thead><tbody id="kri-table-body"><tr><td colspan="7"><div class="spinner" style="margin:30px auto;"></div></td></tr></tbody></table>
</div>
</div>
</div>
<!-- Detail View -->
<div id="view-detail" class="hidden"></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 CATEGORY_LABELS = {
cyber: 'Cyber', operational: 'Operativo', compliance: 'Compliance',
supply_chain: 'Supply Chain', physical: 'Fisico', human: 'Umano'
};
const STATUS_LABELS = {
identified: 'Identificato', analyzing: 'In Analisi', treating: 'In Trattamento',
monitored: 'Monitorato', closed: 'Chiuso'
};
const TREATMENT_LABELS = {
mitigate: 'Mitigare', accept: 'Accettare', transfer: 'Trasferire', avoid: 'Evitare'
};
const TREATMENT_STATUS_LABELS = {
planned: 'Pianificato', in_progress: 'In Corso', completed: 'Completato', cancelled: 'Annullato'
};
// Scala Likelihood (probabilità annua)
const LIKELIHOOD_LABELS = ['', 'Molto Basso', 'Basso', 'Medio', 'Alto', 'Molto Alto'];
const LIKELIHOOD_DETAILS = [
'',
'1 - Remota: <1% probabilità annua (evento eccezionale)',
'2 - Improbabile: 1-10% (evento raro ma documentato nel settore)',
'3 - Possibile: 10-30% (può verificarsi nel corso dell\'attività)',
'4 - Probabile: 30-70% (ci si aspetta che avvenga almeno una volta l\'anno)',
'5 - Quasi Certa: >70% (si verificherà con elevata probabilità)',
];
// Scala Impact (danno economico / utenti impattati)
const IMPACT_DETAILS = [
'',
'1 - Minimo: <€10k danno, <100 utenti, servizio ripristinato in <1h',
'2 - Basso: €10k-50k, 100-500 utenti, ripristino <8h',
'3 - Significativo: €50k-500k, 500-5000 utenti, ripristino <24h',
'4 - Grave: €500k-10M, 5000-50000 utenti, ripristino <7 giorni',
'5 - Catastrofico: >€10M, >50000 utenti, servizi critici nazionali impattati',
];
// ── State ────────────────────────────────────────────────────
let currentView = 'table';
let risksData = [];
// ── Load ─────────────────────────────────────────────────────
loadRisks();
async function loadRisks() {
const params = {};
const cat = document.getElementById('filter-category').value;
const status = document.getElementById('filter-status').value;
if (cat) params.category = cat;
if (status) params.status = status;
try {
const result = await api.listRisks(params);
if (result.success) {
risksData = (result.data && result.data.items) || [];
renderRisksTable(risksData);
} else {
showNotification(result.message || 'Errore nel caricamento rischi', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
// ── Table Render ─────────────────────────────────────────────
function renderRisksTable(risks) {
const tbody = document.getElementById('risks-table-body');
if (!risks || risks.length === 0) {
tbody.innerHTML = `
<tr><td colspan="8">
<div class="empty-state">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
<h4>Nessun rischio registrato</h4>
<p>Clicca "Nuovo Rischio" per aggiungere il primo rischio, oppure usa "AI Suggerisci".</p>
</div>
</td></tr>`;
return;
}
let html = '';
risks.forEach(r => {
const score = r.inherent_risk_score || 0;
const scoreClass = getScoreClassForRisk(score);
const statusBadge = getStatusBadge(r.status);
const categoryLabel = CATEGORY_LABELS[r.category] || r.category;
html += `
<tr onclick="viewRiskDetail(${r.id})">
<td><code style="font-size:0.8rem; color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></td>
<td><strong>${escapeHtml(r.title)}</strong></td>
<td><span class="tag">${escapeHtml(categoryLabel)}</span></td>
<td>${r.likelihood || '-'}/5</td>
<td>${r.impact || '-'}/5</td>
<td><span class="risk-score ${scoreClass}">${score}</span></td>
<td>${statusBadge}</td>
<td>
<div class="btn-group" onclick="event.stopPropagation()">
<button class="btn btn-ghost btn-sm btn-icon" onclick="showRiskModal(${r.id})" title="Modifica">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
</button>
<button class="btn btn-ghost btn-sm btn-icon" onclick="confirmDeleteRisk(${r.id}, '${escapeHtml(r.title)}')" title="Elimina">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
</button>
</div>
</td>
</tr>`;
});
tbody.innerHTML = html;
}
function getScoreClassForRisk(score) {
if (score >= 20) return 'score-critical';
if (score >= 15) return 'score-high';
if (score >= 10) return 'score-medium';
return 'score-low';
}
function getScoreColorForRisk(score) {
if (score >= 20) return 'var(--danger)';
if (score >= 15) return '#f97316';
if (score >= 10) return 'var(--warning)';
return 'var(--secondary)';
}
function getStatusBadge(status) {
const map = {
identified: 'badge-info', analyzing: 'badge-warning',
treating: 'badge-primary', monitored: 'badge-success', closed: 'badge-neutral'
};
const cls = map[status] || 'badge-neutral';
const label = STATUS_LABELS[status] || status;
return `<span class="badge ${cls}">${escapeHtml(label)}</span>`;
}
// ── View Switching ───────────────────────────────────────────
function switchView(view) {
currentView = view;
document.getElementById('view-table').classList.toggle('hidden', view !== 'table');
document.getElementById('view-matrix').classList.toggle('hidden', view !== 'matrix');
document.getElementById('view-fair').classList.toggle('hidden', view !== 'fair');
document.getElementById('view-kri').classList.toggle('hidden', view !== 'kri');
document.getElementById('view-detail').classList.toggle('hidden', true);
const btns = document.querySelectorAll('#view-toggle button');
btns.forEach(b => b.classList.remove('active'));
const idx = { table: 0, matrix: 1, fair: 2, kri: 3 }[view] ?? 0;
if (btns[idx]) btns[idx].classList.add('active');
if (view === 'matrix') loadMatrix();
if (view === 'fair') loadFair();
if (view === 'kri') loadKri();
}
// ── FAIR quantitativo ────────────────────────────────────────
const fmtEur = n => '€ ' + Number(n || 0).toLocaleString('it-IT', { maximumFractionDigits: 0 });
async function loadFair() {
// popola select rischi + registro
try {
const [risksRes, regRes] = await Promise.all([api.listRisks({ per_page: 200 }), api.getFairRegister()]);
const sel = document.getElementById('fair-risk-select');
if (risksRes.success) {
const risks = risksRes.data.risks || risksRes.data || [];
sel.innerHTML = '<option value="">— seleziona rischio —</option>' +
risks.map(r => `<option value="${r.id}">${escapeHtml((r.risk_code ? r.risk_code + ' · ' : '') + r.title)}</option>`).join('');
}
if (regRes.success) renderFairRegister(regRes.data);
} catch (e) { showNotification('Errore caricamento FAIR', 'error'); }
}
function renderFairRegister(data) {
document.getElementById('fair-portfolio').textContent = fmtEur(data.portfolio_ale_mean);
const body = document.getElementById('fair-register-body');
const risks = data.risks || [];
if (!risks.length) { body.innerHTML = '<tr><td colspan="7" class="text-muted" style="text-align:center;padding:20px;">Nessun rischio quantificato. Calcola l\'ALE di un rischio qui sopra.</td></tr>'; return; }
body.innerHTML = risks.map(r => `<tr>
<td>${escapeHtml(r.risk_code || '—')}</td><td>${escapeHtml(r.title)}</td>
<td>${escapeHtml(CATEGORY_LABELS[r.category] || r.category || '')}</td>
<td>${fmtEur(r.ale_min)}</td><td><strong>${fmtEur(r.ale_ml)}</strong></td>
<td>${fmtEur(r.ale_max)}</td><td>${fmtEur(r.ale_mean)}</td></tr>`).join('');
}
async function runFair() {
const riskId = document.getElementById('fair-risk-select').value;
if (!riskId) { showNotification('Seleziona un rischio', 'warning'); return; }
const btn = document.getElementById('btn-run-fair');
setButtonLoading(btn, true);
const payload = {
tef_min: +document.getElementById('fair-tef-min').value,
tef_ml: +document.getElementById('fair-tef-ml').value,
tef_max: +document.getElementById('fair-tef-max').value,
vuln: +document.getElementById('fair-vuln').value,
lm_min: +document.getElementById('fair-lm-min').value,
lm_ml: +document.getElementById('fair-lm-ml').value,
lm_max: +document.getElementById('fair-lm-max').value,
};
try {
const res = await api.computeFair(riskId, payload);
if (res.success) {
renderFairResult(res.data.result);
showNotification('Analisi FAIR calcolata', 'success');
api.getFairRegister().then(r => { if (r.success) renderFairRegister(r.data); });
} else { showNotification(res.message || 'Errore calcolo FAIR', 'error'); }
} catch (e) { showNotification('Errore di connessione', 'error'); }
finally { setButtonLoading(btn, false); }
}
function renderFairResult(r) {
document.getElementById('fair-result').classList.remove('hidden');
document.getElementById('fair-p10').textContent = fmtEur(r.ale_min);
document.getElementById('fair-p50').textContent = fmtEur(r.ale_ml);
document.getElementById('fair-p90').textContent = fmtEur(r.ale_max);
document.getElementById('fair-mean').textContent = fmtEur(r.ale_mean);
document.getElementById('fair-lef').textContent = (r.lef_mean ?? 0).toFixed(3);
const bars = document.getElementById('fair-histogram');
const labels = document.getElementById('fair-histogram-labels');
const h = r.histogram || [];
const maxCount = Math.max(1, ...h.map(b => b.count));
bars.innerHTML = h.map(b => {
const pct = Math.round(b.count / maxCount * 100);
return `<div title="${fmtEur(b.from)} ${fmtEur(b.to)}: ${b.count}" style="flex:1; background:var(--primary); opacity:.8; height:${pct}%; min-height:2px; border-radius:2px 2px 0 0;"></div>`;
}).join('');
labels.innerHTML = h.map((b, i) => i % 3 === 0 ? `<div style="flex:3; text-align:left;">${fmtEur(b.from)}</div>` : '').join('');
}
// ── KRI dashboard ────────────────────────────────────────────
const KRI_STATUS = { green: ['#22c55e', 'OK'], amber: ['#eab308', 'Attenzione'], red: ['#ef4444', 'Critico'], unknown: ['#9ca3af', 'N/D'] };
async function loadKri() {
try {
const res = await api.listKri();
if (res.success) renderKri(res.data);
else showNotification(res.message || 'Errore KRI', 'error');
} catch (e) { showNotification('Errore di connessione', 'error'); }
}
function renderKri(data) {
const sum = data.summary || {};
document.getElementById('kri-summary').innerHTML = ['red', 'amber', 'green', 'unknown'].map(s =>
`<div class="stat-box" style="border-left:4px solid ${KRI_STATUS[s][0]};"><div class="stat-value">${sum[s] || 0}</div><div class="stat-label">${KRI_STATUS[s][1]}</div></div>`).join('');
const body = document.getElementById('kri-table-body');
const kris = data.kris || [];
if (!kris.length) { body.innerHTML = '<tr><td colspan="7" class="text-muted" style="text-align:center;padding:20px;">Nessun KRI definito. Creane uno per monitorare gli indicatori di rischio.</td></tr>'; return; }
body.innerHTML = kris.map(k => {
const st = KRI_STATUS[k.status] || KRI_STATUS.unknown;
const val = k.current_value !== null ? `${(+k.current_value).toLocaleString('it-IT')} ${k.unit || ''}` : '—';
return `<tr>
<td><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:${st[0]};" title="${st[1]}"></span></td>
<td><strong>${escapeHtml(k.name)}</strong>${k.description ? `<br><small class="text-muted">${escapeHtml(k.description)}</small>` : ''}</td>
<td>${escapeHtml(CATEGORY_LABELS[k.category] || k.category || '')}</td>
<td><strong>${escapeHtml(val)}</strong></td>
<td>${k.threshold_warning ?? '—'}</td><td>${k.threshold_critical ?? '—'}</td>
<td><button class="btn btn-sm btn-secondary" onclick='showKriModal(${JSON.stringify(k)})'>Modifica</button></td></tr>`;
}).join('');
}
function showKriModal(kri = null) {
const k = kri || {};
const body = `
<div class="form-group"><label class="form-label">Nome indicatore *</label><input class="form-input" id="kri-name" value="${escapeHtml(k.name || '')}"></div>
<div class="form-group"><label class="form-label">Descrizione</label><input class="form-input" id="kri-desc" value="${escapeHtml(k.description || '')}"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Categoria</label><select class="form-select" id="kri-cat">
${['cyber', 'operational', 'compliance', 'supply_chain', 'physical', 'human'].map(c => `<option value="${c}" ${k.category === c ? 'selected' : ''}>${CATEGORY_LABELS[c] || c}</option>`).join('')}</select></div>
<div class="form-group"><label class="form-label">Unita (%, count, EUR...)</label><input class="form-input" id="kri-unit" value="${escapeHtml(k.unit || '')}"></div>
<div class="form-group"><label class="form-label">Valore corrente</label><input type="number" step="any" class="form-input" id="kri-cur" value="${k.current_value ?? ''}"></div>
<div class="form-group"><label class="form-label">Obiettivo</label><input type="number" step="any" class="form-input" id="kri-target" value="${k.target_value ?? ''}"></div>
<div class="form-group"><label class="form-label">Soglia Warning</label><input type="number" step="any" class="form-input" id="kri-warn" value="${k.threshold_warning ?? ''}"></div>
<div class="form-group"><label class="form-label">Soglia Critica</label><input type="number" step="any" class="form-input" id="kri-crit" value="${k.threshold_critical ?? ''}"></div>
<div class="form-group" style="grid-column:span 2;"><label class="form-label">Direzione</label><select class="form-select" id="kri-dir">
<option value="higher_worse" ${k.direction === 'higher_worse' ? 'selected' : ''}>Valori alti = peggio</option>
<option value="lower_worse" ${k.direction === 'lower_worse' ? 'selected' : ''}>Valori bassi = peggio</option></select></div>
</div>`;
showModal(k.id ? 'Modifica KRI' : 'Nuovo KRI', body, {
footer: `<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveKri(${k.id || 'null'})">Salva</button>`
});
}
async function saveKri(id) {
const data = {
name: document.getElementById('kri-name').value.trim(),
description: document.getElementById('kri-desc').value.trim(),
category: document.getElementById('kri-cat').value,
unit: document.getElementById('kri-unit').value.trim(),
current_value: document.getElementById('kri-cur').value,
target_value: document.getElementById('kri-target').value,
threshold_warning: document.getElementById('kri-warn').value,
threshold_critical: document.getElementById('kri-crit').value,
direction: document.getElementById('kri-dir').value,
};
if (!data.name) { showNotification('Nome obbligatorio', 'warning'); return; }
try {
const res = id ? await api.updateKri(id, data) : await api.createKri(data);
if (res.success) { closeModal(); showNotification('KRI salvato', 'success'); loadKri(); }
else showNotification(res.message || 'Errore salvataggio', 'error');
} catch (e) { showNotification('Errore di connessione', 'error'); }
}
// ── Matrix ───────────────────────────────────────────────────
async function loadMatrix() {
try {
const result = await api.getRiskMatrix();
if (result.success) {
renderMatrix(result.data.risks || []);
} else {
showNotification(result.message || 'Errore caricamento matrice', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
function renderMatrix(risks) {
const grid = document.getElementById('risk-matrix-grid');
// Build a map: `${likelihood}-${impact}` -> count
const cellMap = {};
risks.forEach(r => {
const key = `${r.likelihood || 0}-${r.impact || 0}`;
if (!cellMap[key]) cellMap[key] = [];
cellMap[key].push(r);
});
// Color level mapping for cell score (likelihood * impact)
function getCellLevel(score) {
if (score <= 1) return 'level-1';
if (score <= 2) return 'level-2';
if (score <= 4) return 'level-3';
if (score <= 6) return 'level-4';
if (score <= 8) return 'level-5';
if (score <= 10) return 'level-6';
if (score <= 14) return 'level-7';
if (score <= 16) return 'level-8';
if (score <= 19) return 'level-9';
return 'level-10';
}
let html = '';
// Rows from top (likelihood=5) to bottom (likelihood=1)
for (let li = 5; li >= 1; li--) {
// Y-axis header
html += `<div class="matrix-y-header">${li}</div>`;
// 5 cells per row (impact 1-5)
for (let imp = 1; imp <= 5; imp++) {
const score = li * imp;
const level = getCellLevel(score);
const key = `${li}-${imp}`;
const risksInCell = cellMap[key] || [];
const dot = risksInCell.length > 0
? `<div class="matrix-dot" title="${risksInCell.map(r => escapeHtml(r.title)).join(', ')}">${risksInCell.length}</div>`
: '';
html += `<div class="matrix-cell ${level}" title="P:${li} x I:${imp} = ${score}">${dot}</div>`;
}
}
// Bottom row: corner + x-axis headers
html += `<div class="matrix-corner"></div>`;
for (let imp = 1; imp <= 5; imp++) {
html += `<div class="matrix-x-header">${imp}</div>`;
}
grid.innerHTML = html;
}
// ── Detail View ──────────────────────────────────────────────
async function viewRiskDetail(id) {
try {
const result = await api.getRisk(id);
if (!result.success) {
showNotification(result.message || 'Errore nel caricamento', 'error');
return;
}
renderDetail(result.data);
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
function renderDetail(risk) {
document.getElementById('view-table').classList.add('hidden');
document.getElementById('view-matrix').classList.add('hidden');
const container = document.getElementById('view-detail');
container.classList.remove('hidden');
const score = risk.inherent_risk_score || 0;
const residualScore = risk.residual_risk_score || 0;
const categoryLabel = CATEGORY_LABELS[risk.category] || risk.category;
const treatmentLabel = TREATMENT_LABELS[risk.treatment] || risk.treatment;
let treatmentsHtml = '';
if (risk.treatments && risk.treatments.length > 0) {
risk.treatments.forEach(t => {
const tStatusBadge = getTreatmentStatusBadge(t.status);
treatmentsHtml += `
<div class="treatment-item">
<div class="treatment-item-header">
<strong>${escapeHtml(t.action_description)}</strong>
${tStatusBadge}
</div>
<div style="font-size:0.8rem; color:var(--gray-500);">
${t.responsible_name ? 'Responsabile: ' + escapeHtml(t.responsible_name) : ''}
${t.due_date ? ' | Scadenza: ' + formatDate(t.due_date) : ''}
</div>
${t.notes ? '<p style="font-size:0.85rem; margin-top:8px;">' + escapeHtml(t.notes) + '</p>' : ''}
</div>`;
});
} else {
treatmentsHtml = '<p class="text-muted" style="font-size:0.85rem;">Nessun trattamento registrato.</p>';
}
container.innerHTML = `
<button class="detail-back" onclick="backToList()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd"/></svg>
Torna alla lista
</button>
<div class="detail-grid">
<div>
<div class="card mb-24">
<div class="card-header">
<h3>
<code style="color:var(--primary); margin-right:8px;">${escapeHtml(risk.risk_code || '')}</code>
${escapeHtml(risk.title)}
</h3>
<div class="btn-group">
<button class="btn btn-secondary btn-sm" onclick="showRiskModal(${risk.id})">Modifica</button>
<button class="btn btn-danger btn-sm" onclick="confirmDeleteRisk(${risk.id}, '${escapeHtml(risk.title)}')">Elimina</button>
</div>
</div>
<div class="card-body">
<div class="score-display">
<div class="score-item">
<div class="score-item-value">${risk.likelihood || '-'}</div>
<div class="score-item-label">Probabilita'</div>
</div>
<div class="score-item" style="font-size:1.5rem; color:var(--gray-400); padding-top:4px;">x</div>
<div class="score-item">
<div class="score-item-value">${risk.impact || '-'}</div>
<div class="score-item-label">Impatto</div>
</div>
<div class="score-item" style="font-size:1.5rem; color:var(--gray-400); padding-top:4px;">=</div>
<div class="score-item">
<div class="score-item-value" style="color:${getScoreColorForRisk(score)}">${score}</div>
<div class="score-item-label">Rischio Inerente</div>
</div>
${residualScore > 0 ? `
<div class="score-item" style="border-left:2px solid var(--gray-200); padding-left:24px;">
<div class="score-item-value" style="color:${getScoreColorForRisk(residualScore)}">${residualScore}</div>
<div class="score-item-label">Rischio Residuo</div>
</div>` : ''}
</div>
${risk.description ? `
<div class="detail-field">
<div class="detail-field-label">Descrizione</div>
<div class="detail-field-value">${escapeHtml(risk.description)}</div>
</div>` : ''}
<div class="form-row">
<div class="detail-field">
<div class="detail-field-label">Categoria</div>
<div class="detail-field-value"><span class="tag">${escapeHtml(categoryLabel)}</span></div>
</div>
<div class="detail-field">
<div class="detail-field-label">Stato</div>
<div class="detail-field-value">${getStatusBadge(risk.status)}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Trattamento</div>
<div class="detail-field-value"><span class="badge badge-primary">${escapeHtml(treatmentLabel)}</span></div>
</div>
</div>
<div class="form-row">
${risk.threat_source ? `
<div class="detail-field">
<div class="detail-field-label">Sorgente Minaccia</div>
<div class="detail-field-value">${escapeHtml(risk.threat_source)}</div>
</div>` : ''}
${risk.vulnerability ? `
<div class="detail-field">
<div class="detail-field-label">Vulnerabilita'</div>
<div class="detail-field-value">${escapeHtml(risk.vulnerability)}</div>
</div>` : ''}
</div>
<div class="form-row">
${risk.owner_name ? `
<div class="detail-field">
<div class="detail-field-label">Responsabile</div>
<div class="detail-field-value">${escapeHtml(risk.owner_name)}</div>
</div>` : ''}
${risk.review_date ? `
<div class="detail-field">
<div class="detail-field-label">Data Revisione</div>
<div class="detail-field-value">${formatDate(risk.review_date)}</div>
</div>` : ''}
${risk.nis2_article ? `
<div class="detail-field">
<div class="detail-field-label">Articolo NIS2</div>
<div class="detail-field-value">${escapeHtml(risk.nis2_article)}</div>
</div>` : ''}
</div>
</div>
</div>
<!-- Treatments -->
<div class="card">
<div class="card-header">
<h3>Trattamenti</h3>
<button class="btn btn-sm btn-primary" onclick="showAddTreatmentModal(${risk.id})">+ Aggiungi</button>
</div>
<div class="card-body">
${treatmentsHtml}
</div>
</div>
</div>
<div>
<div class="card mb-24">
<div class="card-header"><h3>Riepilogo</h3></div>
<div class="card-body">
<div class="detail-field">
<div class="detail-field-label">Codice</div>
<div class="detail-field-value"><code>${escapeHtml(risk.risk_code || '-')}</code></div>
</div>
<div class="detail-field">
<div class="detail-field-label">Creato il</div>
<div class="detail-field-value">${formatDateTime(risk.created_at)}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Aggiornato il</div>
<div class="detail-field-value">${formatDateTime(risk.updated_at)}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Trattamenti</div>
<div class="detail-field-value">${risk.treatments ? risk.treatments.length : 0}</div>
</div>
</div>
</div>
</div>
</div>
`;
}
function getTreatmentStatusBadge(status) {
const map = { planned: 'badge-info', in_progress: 'badge-warning', completed: 'badge-success', cancelled: 'badge-neutral' };
return `<span class="badge ${map[status] || 'badge-neutral'}">${escapeHtml(TREATMENT_STATUS_LABELS[status] || status)}</span>`;
}
function backToList() {
document.getElementById('view-detail').classList.add('hidden');
if (currentView === 'table') {
document.getElementById('view-table').classList.remove('hidden');
} else {
document.getElementById('view-matrix').classList.remove('hidden');
}
}
// ── Create/Edit Risk Modal ───────────────────────────────────
async function showRiskModal(riskId = null) {
let risk = {};
if (riskId) {
try {
const result = await api.getRisk(riskId);
if (result.success) risk = result.data;
} catch (e) { /* use defaults */ }
}
const isEdit = !!riskId;
const title = isEdit ? 'Modifica Rischio' : 'Nuovo Rischio';
const likelihood = risk.likelihood || 3;
const impact = risk.impact || 3;
const initialScore = likelihood * impact;
const content = `
<div class="form-group">
<label class="form-label">Titolo <span class="required">*</span></label>
<input type="text" class="form-input" id="risk-title" value="${escapeHtml(risk.title || '')}" placeholder="Titolo del rischio">
</div>
<div class="form-group">
<label class="form-label">Descrizione</label>
<textarea class="form-textarea" id="risk-description" rows="3" placeholder="Descrizione dettagliata">${escapeHtml(risk.description || '')}</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Categoria</label>
<select class="form-select" id="risk-category">
<option value="cyber" ${risk.category==='cyber'?'selected':''}>Cyber</option>
<option value="operational" ${risk.category==='operational'?'selected':''}>Operativo</option>
<option value="compliance" ${risk.category==='compliance'?'selected':''}>Compliance</option>
<option value="supply_chain" ${risk.category==='supply_chain'?'selected':''}>Supply Chain</option>
<option value="physical" ${risk.category==='physical'?'selected':''}>Fisico</option>
<option value="human" ${risk.category==='human'?'selected':''}>Umano</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Trattamento</label>
<select class="form-select" id="risk-treatment">
<option value="mitigate" ${risk.treatment==='mitigate'?'selected':''}>Mitigare</option>
<option value="accept" ${risk.treatment==='accept'?'selected':''}>Accettare</option>
<option value="transfer" ${risk.treatment==='transfer'?'selected':''}>Trasferire</option>
<option value="avoid" ${risk.treatment==='avoid'?'selected':''}>Evitare</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Sorgente Minaccia</label>
<input type="text" class="form-input" id="risk-threat-source" value="${escapeHtml(risk.threat_source || '')}" placeholder="Es. attaccante esterno">
</div>
<div class="form-group">
<label class="form-label">Vulnerabilita'</label>
<input type="text" class="form-input" id="risk-vulnerability" value="${escapeHtml(risk.vulnerability || '')}" placeholder="Es. software non aggiornato">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Probabilita' <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(frequenza stimata annua)</span></label>
<div class="range-value-display" id="likelihood-value">${likelihood}</div>
<input type="range" class="form-range" id="risk-likelihood" min="1" max="5" value="${likelihood}" oninput="updateRiskScore()">
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="likelihood-detail">${LIKELIHOOD_DETAILS[likelihood]}</div>
</div>
<div class="form-group">
<label class="form-label">Impatto <span style="color:var(--gray-400); font-size:0.75rem; font-weight:400;">(danno economico / utenti)</span></label>
<div class="range-value-display" id="impact-value">${impact}</div>
<input type="range" class="form-range" id="risk-impact" min="1" max="5" value="${impact}" oninput="updateRiskScore()">
<div style="font-size:0.75rem; color:var(--gray-500); margin-top:4px; padding:6px 8px; background:var(--gray-50); border-radius:4px;" id="impact-detail">${IMPACT_DETAILS[impact]}</div>
</div>
</div>
<div class="score-preview" id="score-preview">
<div class="score-preview-label">Rischio Inerente</div>
<div class="score-preview-value" id="score-preview-value" style="color:${getScoreColorForRisk(initialScore)}">${initialScore}</div>
</div>
<div class="form-row" style="margin-top:16px;">
<div class="form-group">
<label class="form-label">Responsabile (ID utente)</label>
<input type="text" class="form-input" id="risk-owner" value="${escapeHtml(risk.owner_user_id || '')}" placeholder="ID utente responsabile">
</div>
<div class="form-group">
<label class="form-label">Data Revisione</label>
<input type="date" class="form-input" id="risk-review-date" value="${risk.review_date ? risk.review_date.split(' ')[0] : ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Articolo NIS2 di riferimento</label>
<input type="text" class="form-input" id="risk-nis2-article" value="${escapeHtml(risk.nis2_article || '')}" placeholder="Es. Art. 21, par. 2">
</div>
`;
showModal(title, content, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveRisk(${riskId || 'null'})">${isEdit ? 'Salva Modifiche' : 'Crea Rischio'}</button>
`
});
}
function updateRiskScore() {
const l = parseInt(document.getElementById('risk-likelihood').value);
const i = parseInt(document.getElementById('risk-impact').value);
const score = l * i;
document.getElementById('likelihood-value').textContent = l;
document.getElementById('impact-value').textContent = i;
const lDetail = document.getElementById('likelihood-detail');
const iDetail = document.getElementById('impact-detail');
if (lDetail) lDetail.textContent = LIKELIHOOD_DETAILS[l];
if (iDetail) iDetail.textContent = IMPACT_DETAILS[i];
document.getElementById('score-preview-value').textContent = score;
document.getElementById('score-preview-value').style.color = getScoreColorForRisk(score);
}
async function saveRisk(riskId) {
const btn = document.querySelector('#modal-overlay .btn-primary');
const data = {
title: document.getElementById('risk-title').value.trim(),
description: document.getElementById('risk-description').value.trim(),
category: document.getElementById('risk-category').value,
treatment: document.getElementById('risk-treatment').value,
threat_source: document.getElementById('risk-threat-source').value.trim(),
vulnerability: document.getElementById('risk-vulnerability').value.trim(),
likelihood: parseInt(document.getElementById('risk-likelihood').value),
impact: parseInt(document.getElementById('risk-impact').value),
owner_user_id: document.getElementById('risk-owner').value.trim() || null,
review_date: document.getElementById('risk-review-date').value || null,
nis2_article: document.getElementById('risk-nis2-article').value.trim(),
};
if (!data.title) {
showNotification('Il titolo e\' obbligatorio', 'warning');
return;
}
setButtonLoading(btn, true);
try {
let result;
if (riskId) {
result = await api.updateRisk(riskId, data);
} else {
result = await api.createRisk(data);
}
if (result.success) {
closeModal();
showNotification(riskId ? 'Rischio aggiornato' : 'Rischio creato con successo', 'success');
loadRisks();
// If detail view is visible, reload detail
if (!document.getElementById('view-detail').classList.contains('hidden')) {
viewRiskDetail(riskId || result.data.id);
}
} else {
setButtonLoading(btn, false);
showNotification(result.message || 'Errore nel salvataggio', 'error');
}
} catch (e) {
setButtonLoading(btn, false);
showNotification('Errore di connessione', 'error');
}
}
// ── Delete Risk ──────────────────────────────────────────────
function confirmDeleteRisk(id, title) {
showModal('Conferma Eliminazione', `
<p>Sei sicuro di voler eliminare il rischio <strong>${escapeHtml(title)}</strong>?</p>
<p class="text-muted" style="margin-top:8px; font-size:0.85rem;">Questa azione non puo' essere annullata.</p>
`, {
size: 'sm',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-danger" onclick="deleteRisk(${id})">Elimina</button>
`
});
}
async function deleteRisk(id) {
try {
const result = await api.deleteRisk(id);
if (result.success) {
closeModal();
showNotification('Rischio eliminato', 'success');
const wasMatrix = currentView === 'matrix';
backToList();
loadRisks();
if (wasMatrix) loadMatrix();
} else {
showNotification(result.message || 'Errore nella eliminazione', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
// ── Add Treatment Modal ──────────────────────────────────────
function showAddTreatmentModal(riskId) {
const content = `
<div class="form-group">
<label class="form-label">Descrizione Azione <span class="required">*</span></label>
<textarea class="form-textarea" id="treatment-description" rows="3" placeholder="Descrizione dell'azione di trattamento"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Responsabile (ID utente)</label>
<input type="text" class="form-input" id="treatment-responsible" placeholder="ID utente">
</div>
<div class="form-group">
<label class="form-label">Data Scadenza</label>
<input type="date" class="form-input" id="treatment-due-date">
</div>
</div>
<div class="form-group">
<label class="form-label">Stato</label>
<select class="form-select" id="treatment-status">
<option value="planned">Pianificato</option>
<option value="in_progress">In Corso</option>
<option value="completed">Completato</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Note</label>
<textarea class="form-textarea" id="treatment-notes" rows="2" placeholder="Note aggiuntive"></textarea>
</div>
`;
showModal('Aggiungi Trattamento', content, {
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveTreatment(${riskId})">Aggiungi</button>
`
});
}
async function saveTreatment(riskId) {
const data = {
action_description: document.getElementById('treatment-description').value.trim(),
responsible_user_id: document.getElementById('treatment-responsible').value.trim() || null,
due_date: document.getElementById('treatment-due-date').value || null,
status: document.getElementById('treatment-status').value,
notes: document.getElementById('treatment-notes').value.trim(),
};
if (!data.action_description) {
showNotification('La descrizione dell\'azione e\' obbligatoria', 'warning');
return;
}
try {
const result = await api.post(`/risks/${riskId}/treatments`, data);
if (result.success) {
closeModal();
showNotification('Trattamento aggiunto', 'success');
viewRiskDetail(riskId);
} else {
showNotification(result.message || 'Errore nel salvataggio', 'error');
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
}
// ── AI Suggest ───────────────────────────────────────────────
async function aiSuggest() {
const btn = document.getElementById('btn-ai-suggest');
setButtonLoading(btn, true);
showNotification('Generazione suggerimenti AI in corso...', 'info');
try {
const result = await api.aiSuggestRisks();
if (result.success && result.data) {
const suggestions = Array.isArray(result.data) ? result.data : (result.data.suggestions || []);
if (suggestions.length === 0) {
showNotification('Nessun suggerimento disponibile', 'info');
return;
}
let listHtml = '<div style="max-height:400px; overflow-y:auto;">';
suggestions.forEach((s, idx) => {
listHtml += `
<div style="padding:12px; border:1px solid var(--gray-200); border-radius:var(--border-radius); margin-bottom:8px;">
<strong>${escapeHtml(s.title || 'Rischio ' + (idx + 1))}</strong>
<p style="font-size:0.85rem; color:var(--gray-600); margin:4px 0;">${escapeHtml(s.description || '')}</p>
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-top:6px;">
${s.category ? `<span class="tag">${escapeHtml(CATEGORY_LABELS[s.category] || s.category)}</span>` : ''}
${s.likelihood ? `<span class="badge badge-info">P:${s.likelihood}</span>` : ''}
${s.impact ? `<span class="badge badge-warning">I:${s.impact}</span>` : ''}
</div>
</div>`;
});
listHtml += '</div>';
showModal('Rischi Suggeriti dall\'AI', listHtml, {
size: 'lg',
footer: `<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>`
});
} else {
showNotification(result.message || 'Errore AI', 'error');
}
} catch (e) {
showNotification('Errore di connessione al servizio AI', 'error');
} finally {
setButtonLoading(btn, false);
}
}
</script>
</body>
</html>