- 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>
1149 lines
56 KiB
HTML
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>
|