nis2-agile/public/risks.html
DevEnv nis2-agile 59fad22c0e [UX+SEC] Eccellenza pre-audit: idle timeout, loading states, i18n, UX polish
- common.js: idle session timeout 30min con avviso countdown 5min prima del logout
- common.js: checkAuth() attiva automaticamente il monitor di inattività
- api.js: messaggi errore connessione usano i18n (IT/EN) tramite I18n.t()
- risks.html: saveRisk() e aiSuggest() con setButtonLoading durante salvataggio
- risks.html: deleteRisk() ricarica la matrice se si è in matrix view
- incidents.html: createIncident() con setButtonLoading durante registrazione
- policies.html: savePolicy() e saveAIGeneratedPolicy() con setButtonLoading
- policies.html: banner AI-draft con pulsante X per dismissione

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:25:52 +01:00

1149 lines
56 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>
</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>
<!-- 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 || [];
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-detail').classList.toggle('hidden', true);
const btns = document.querySelectorAll('#view-toggle button');
btns.forEach(b => b.classList.remove('active'));
if (view === 'table') btns[0].classList.add('active');
else btns[1].classList.add('active');
if (view === 'matrix') loadMatrix();
}
// ── 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>