- dashboard: complianceScore ora ritorna 'score' (overall_score ultimo assessment); la gauge usa avg_implementation se >0, altrimenti il punteggio assessment. Prima mostrava 0% per org con gap analysis ma senza modulo controlli (H2). - risks.html backToList(): ripristina la vista corrente tra le 4 (table/matrix/fair/kri), prima cadeva sempre su table/matrix (H1); renderDetail nasconde tutte e 4. - risks.html loadFair(): legge risksRes.data.items (endpoint paginato), prima risksRes.data.risks era undefined e il dropdown FAIR restava vuoto (M1). php -l + node --check OK. version 1.10.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1360 lines
73 KiB
HTML
1360 lines
73 KiB
HTML
<!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 && risksRes.data.items) || risksRes.data.risks || [];
|
||
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');
|
||
document.getElementById('view-fair').classList.add('hidden');
|
||
document.getElementById('view-kri').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');
|
||
// Ripristina la vista corrente tra le 4 (table/matrix/fair/kri), non solo table/matrix.
|
||
const view = ['table', 'matrix', 'fair', 'kri'].includes(currentView) ? currentView : 'table';
|
||
document.getElementById('view-' + view).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>
|