[FEAT] Pagina Rischi (Simon): matrice a 4 fasce + toggle inerente/residuo; trasversali 12 col in fondo

Richieste Simon sulla finestra Rischi:
1) Matrice 5×5: eliminata la fascia 10-14; fasce = 1-2 basso (verde), 3-8 medio
   (arancione), 9-12 alto (rosso tenue), 15-25 critico (rosso). Unificate con le
   bande Alert. (matrixBand + CSS band-low/med/high/crit; rimosse level-1..10).
2) La matrice riepiloga i giudizi INERENTI o RESIDUI con un tasto di selezione
   (setMatrixView; matrixPoints legge likelihood/impact o residual_*).
3) Rischi trasversali:
   3.1 sezione spostata in FONDO (matrice dai requisiti risalita subito dopo la
       tabella derivata);
   3.2 tabella trasversali convertita alle STESSE 12 colonne (inerente+residuo+
       alert+esito), editing inline P/I via api.updateRisk (onTransversalScoreChange);
   3.3 tasti "AI Suggerisci" e "Nuovo Rischio" spostati in testa alla tabella;
   3.4 seconda matrice 5×5 dei trasversali con lo stesso toggle inerente/residuo;
   3.5 tasto Elimina per ogni rischio trasversale (confirmDeleteRisk).
- Solo frontend/statico. version 1.27.0 + SW cache bump.
- Verificato puppeteer (996002): 4 bande, 2 toggle funzionanti, 12 col, 6 delete, 0 errori.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-08-03 10:39:05 +02:00
co-authored by Claude Opus 4.8
parent b8c7d3c9b9
commit 4809d8e895
3 changed files with 237 additions and 141 deletions
+229 -133
View File
@@ -134,16 +134,16 @@
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; }
/* Bande di rischio (spec Simon): 1-2 basso verde · 3-8 medio arancione · 9-12 alto rosso tenue · 15-25 critico rosso */
.matrix-cell.band-low { background: #bbf7d0; }
.matrix-cell.band-med { background: #fdba74; }
.matrix-cell.band-high { background: #f87171; }
.matrix-cell.band-crit { background: #dc2626; }
/* Toggle Inerente/Residuo sopra ciascuna matrice */
.mx-toggle { display:inline-flex; border:1px solid var(--gray-300); border-radius:8px; overflow:hidden; }
.mx-toggle button { border:0; background:#fff; padding:6px 14px; font-size:0.8rem; font-weight:700; color:var(--gray-600); cursor:pointer; }
.mx-toggle button.on { background:var(--primary); color:#fff; }
.mx-head { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:10px; }
.matrix-dot {
width: 28px;
@@ -300,11 +300,13 @@
}
/* Requisiti del framework: riquadro a scorrimento verticale (ticket #501/1) */
#view-derived .table-container.scroll-box {
#view-derived .table-container.scroll-box,
#view-table .table-container.scroll-box {
max-height: 60vh;
overflow-y: auto;
}
#view-derived .table-container.scroll-box thead th {
#view-derived .table-container.scroll-box thead th,
#view-table .table-container.scroll-box thead th {
position: sticky;
background: var(--gray-50);
z-index: 2;
@@ -391,12 +393,7 @@
<h1 data-i18n="risks.title">Gestione Rischi</h1>
<p class="sub">Registro rischi e matrice 5×5</p>
</div>
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<button class="btn-ghost" id="btn-ai-suggest" onclick="aiSuggest()">
<i data-lucide="sparkles" class="ic"></i>AI Suggerisci
</button>
<button class="btn-primary" id="btn-new-risk" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
</div>
<!-- I tasti AI Suggerisci / Nuovo Rischio sono in testa alla tabella "Rischi trasversali" (3.3) -->
</div>
<div class="content-body">
@@ -452,7 +449,45 @@
</div>
</div>
<!-- ── SEZIONE 2: Rischi trasversali (multirischio / all-hazards) — accordion (#501 p3) ── -->
<!-- ── Matrice 5×5 dei RISCHI DAI REQUISITI (toggle Inerente/Residuo — punto 2) ── -->
<div id="view-matrix" style="margin-top:16px;">
<div class="card">
<div class="card-header">
<div class="mx-head" style="width:100%;">
<div>
<h3 style="margin:0;">Matrice di Rischio 5×5 — Rischi dai requisiti</h3>
<span class="text-muted" style="font-size:0.8rem;">Esclusi i requisiti non applicabili alla classe</span>
</div>
<div class="mx-toggle" role="group" aria-label="Vista matrice rischi dai requisiti">
<button type="button" id="mxd-inh" class="on" onclick="setMatrixView('derived','inherent')">Inerente</button>
<button type="button" id="mxd-res" onclick="setMatrixView('derived','residual')">Residuo</button>
</div>
</div>
</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-derived">
<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-2)</div>
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#fdba74;"></div>Medio (3-8)</div>
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#f87171;"></div>Alto (9-12)</div>
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#dc2626;"></div>Critico (15-25)</div>
</div>
</div>
<p class="text-muted" style="font-size:0.78rem;text-align:center;margin:16px auto 0;max-width:640px;line-height:1.5;">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024). Il tasto <b>Inerente/Residuo</b> mostra i giudizi prima o dopo il trattamento.</p>
</div>
</div>
</div>
<!-- ── SEZIONE 2 (in fondo): Rischi trasversali (multirischio / all-hazards) — accordion (#501 p3) ── -->
<details id="allhazard-section" class="allhazard" open>
<summary class="allhazard-summary">
<span class="ah-caret" aria-hidden="true">
@@ -463,9 +498,14 @@
</summary>
<p class="text-muted" style="font-size:0.82rem;margin:10px 0 16px;line-height:1.5;" data-i18n="risks.custom_help">Qui si registrano le minacce non riconducibili a un singolo requisito (ambiente fisico, catena di fornitura, eventi naturali, errore umano), come richiesto dall'approccio multirischio/all-hazards (art. 24 D.Lgs. 138/2024, che recepisce l'art. 21 §2 della Direttiva (UE) 2022/2555). Si distinguono dai «rischi di non conformità» della tabella qui sopra, che misurano il mancato o parziale soddisfacimento dei singoli requisiti.</p>
<!-- Table View (rischi trasversali) -->
<!-- Table View (rischi trasversali) — tasti in testa (3.3) + tabella 12 colonne (3.2) -->
<div id="view-table">
<div class="filters-bar">
<div class="mx-head" style="margin-bottom:12px;">
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<button class="btn-ghost" id="btn-ai-suggest" onclick="aiSuggest()"><i data-lucide="sparkles" class="ic"></i>AI Suggerisci</button>
<button class="btn-primary" id="btn-new-risk" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
</div>
<div class="filters-bar" style="margin:0;">
<select class="form-select" id="filter-category" onchange="loadRisks()" aria-label="Filtra per categoria">
<option value="">Tutte le categorie</option>
<option value="cyber">Cyber</option>
@@ -484,25 +524,34 @@
<option value="closed">Chiuso</option>
</select>
</div>
</div>
<div class="card">
<div class="table-container">
<table class="table-clickable">
<div class="table-container scroll-box">
<table class="derived-12">
<thead>
<tr>
<th scope="col">Codice</th>
<th scope="col">Titolo</th>
<th scope="col">Categoria</th>
<th scope="col">Probabilita'</th>
<th scope="col" rowspan="2">Codice rischio</th>
<th scope="col" rowspan="2">Descrizione rischio</th>
<th scope="colgroup" colspan="3" class="grp grp-inh">Rischio Inerente</th>
<th scope="col" rowspan="2">Alert</th>
<th scope="col" rowspan="2">Rischio Inerente</th>
<th scope="colgroup" colspan="3" class="grp grp-res">Rischio Residuo</th>
<th scope="col" rowspan="2">Esito residuo</th>
<th scope="col" rowspan="2">Rischio Residuo</th>
</tr>
<tr>
<th scope="col">Probabilità</th>
<th scope="col">Impatto</th>
<th scope="col">Score</th>
<th scope="col">Stato</th>
<th scope="col">Azioni</th>
<th scope="col">Valutazione</th>
<th scope="col">Probabilità</th>
<th scope="col">Impatto</th>
<th scope="col">Valutazione</th>
</tr>
</thead>
<tbody id="risks-table-body">
<tr>
<td colspan="8">
<td colspan="12">
<div class="spinner" style="margin:40px auto;"></div>
</td>
</tr>
@@ -511,53 +560,45 @@
</div>
</div>
</div>
</details>
<!-- Matrix View (sempre visibile) -->
<div id="view-matrix" style="margin-top:16px;">
<!-- ── Matrice 5×5 dei RISCHI TRASVERSALI (toggle Inerente/Residuo — punto 3.4) ── -->
<div id="view-matrix-trans" style="margin-top:18px;">
<div class="card">
<div class="card-header">
<h3>Matrice di Rischio 5x5</h3>
<span class="text-muted" style="font-size:0.8rem;" data-i18n="risks.matrix_subtitle">Rischi da requisiti (valutati) + aggiuntivi — esclusi i non applicabili</span>
<div class="mx-head" style="width:100%;">
<div>
<h3 style="margin:0;">Matrice di Rischio 5×5 — Rischi trasversali</h3>
<span class="text-muted" style="font-size:0.8rem;">Solo i rischi trasversali registrati sopra</span>
</div>
<div class="mx-toggle" role="group" aria-label="Vista matrice rischi trasversali">
<button type="button" id="mxt-inh" class="on" onclick="setMatrixView('trans','inherent')">Inerente</button>
<button type="button" id="mxt-res" onclick="setMatrixView('trans','residual')">Residuo</button>
</div>
</div>
</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="risk-matrix" id="risk-matrix-grid-transversal">
<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 class="matrix-legend-item"><div class="matrix-legend-color" style="background:#bbf7d0;"></div>Basso (1-2)</div>
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#fdba74;"></div>Medio (3-8)</div>
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#f87171;"></div>Alto (9-12)</div>
<div class="matrix-legend-item"><div class="matrix-legend-color" style="background:#dc2626;"></div>Critico (15-25)</div>
</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>
<p class="text-muted" style="font-size:0.78rem;text-align:center;margin:16px auto 0;max-width:640px;line-height:1.5;" data-i18n="risks.matrix_caption">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).</p>
<p class="text-muted" style="font-size:0.78rem;text-align:center;margin:16px auto 0;max-width:640px;line-height:1.5;">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).</p>
</div>
</div>
</div>
</details>
<!-- Detail View -->
<div id="view-detail" class="hidden"></div>
@@ -1045,6 +1086,7 @@
risksData = (result.data && result.data.items) || [];
renderRisksTable(risksData);
updateAllhazardHeader(risksData.length);
loadTransversalMatrix();
} else {
showNotification(result.message || 'Errore nel caricamento rischi', 'error');
}
@@ -1076,46 +1118,99 @@
if (!risks || risks.length === 0) {
tbody.innerHTML = `
<tr><td colspan="8">
<tr><td colspan="12">
<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>
<h4>Nessun rischio trasversale registrato</h4>
<p>Usa "Nuovo Rischio" o "AI Suggerisci" qui sopra per aggiungere minacce non legate a un singolo requisito.</p>
</div>
</td></tr>`;
return;
}
const editable = canEvaluate;
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;
const id = r.id;
const iL = (r.likelihood >= 1 && r.likelihood <= 5) ? parseInt(r.likelihood) : null;
const iI = (r.impact >= 1 && r.impact <= 5) ? parseInt(r.impact) : null;
const iP = (iL != null && iI != null) ? iL * iI : null;
const rL = (r.residual_likelihood >= 1 && r.residual_likelihood <= 5) ? parseInt(r.residual_likelihood) : null;
const rI = (r.residual_impact >= 1 && r.residual_impact <= 5) ? parseInt(r.residual_impact) : null;
const rP = (rL != null && rI != null) ? rL * rI : null;
const iEval = iP != null ? `<span class="rk-eval ${evalClass(iP)}">${iP}</span>` : '<span class="rk-empty">—</span>';
const rEval = rP != null ? `<span class="rk-eval ${evalClass(rP)}">${rP}</span>` : '<span class="rk-empty">—</span>';
const ab = alertBand(iP);
const alertCell = ab
? `<button class="alert-btn ${ab.cls}" onclick="viewRiskDetail(${id})" title="Apri il rischio (trattamenti/azioni)">${ab.label}</button>`
: '<span class="rk-empty">—</span>';
const inhOutcome = iP != null ? '<span class="rk-valutato">Valutato</span>' : '<span class="rk-empty">—</span>';
const ro = residualOutcome(rP);
const resOutcomeCell = ro ? `<span class="${ro.cls}">${ro.label}</span>` : '<span class="rk-empty">—</span>';
const catLabel = CATEGORY_LABELS[r.category] || r.category || '';
const safeTitle = (r.title || '').replace(/['\\]/g, '');
const actBtns = editable
? `<div style="display:flex;gap:5px;margin-top:5px;">
<button class="btn btn-ghost btn-sm btn-icon" onclick="showRiskModal(${id})" title="Modifica"><svg viewBox="0 0 20 20" width="14" height="14" 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(${id}, '${escapeHtml(safeTitle)}')" title="Elimina rischio trasversale"><svg viewBox="0 0 20 20" width="14" height="14" 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>` : '';
const resCell = (rP != null ? '<span class="rk-valutato">Valutato</span>' : '<span class="rk-empty">—</span>') + actBtns;
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>
<tr>
<td class="cell-code">
<a href="javascript:void(0)" onclick="viewRiskDetail(${id})" title="Apri il rischio"><code style="font-size:0.75rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></a>
</td>
<td class="cell-desc">
<div style="font-size:0.64rem;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;color:var(--gray-500);">${escapeHtml(catLabel)}</div>
<div style="font-size:0.82rem;color:var(--gray-800);">${escapeHtml(r.title || '')}</div>
</td>
<td>${scoreSelT('til-' + id, iL, editable)}</td>
<td>${scoreSelT('tii-' + id, iI, editable)}</td>
<td>${iEval}</td>
<td>${alertCell}</td>
<td>${inhOutcome}</td>
<td>${scoreSelT('trl-' + id, rL, editable)}</td>
<td>${scoreSelT('tri-' + id, rI, editable)}</td>
<td>${rEval}</td>
<td>${resOutcomeCell}</td>
<td>${resCell}</td>
</tr>`;
});
tbody.innerHTML = html;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
}
// Select 1-5 inline per i rischi trasversali (salva su tabella risks via api.updateRisk).
function scoreSelT(id, val, editable) {
if (!editable) return (val >= 1 && val <= 5) ? (val + '/5') : '<span class="rk-empty">—</span>';
let o = `<option value="">—</option>`;
for (let v = 1; v <= 5; v++) o += `<option value="${v}" ${v == val ? 'selected' : ''}>${v}</option>`;
const rid = id.substring(id.indexOf('-') + 1);
return `<select class="rk-sel" id="${id}" onchange="onTransversalScoreChange(${rid})">${o}</select>`;
}
async function onTransversalScoreChange(riskId) {
const r = (risksData || []).find(x => String(x.id) === String(riskId));
if (!r) return;
const rd = elId => { const el = document.getElementById(elId); if (!el) return null; const v = el.value; return v === '' ? null : parseInt(v); };
r.likelihood = rd('til-' + riskId);
r.impact = rd('tii-' + riskId);
r.inherent_risk_score = (r.likelihood && r.impact) ? r.likelihood * r.impact : 0;
r.residual_likelihood = rd('trl-' + riskId);
r.residual_impact = rd('tri-' + riskId);
r.residual_risk_score = (r.residual_likelihood && r.residual_impact) ? r.residual_likelihood * r.residual_impact : 0;
renderRisksTable(risksData);
loadTransversalMatrix();
try {
await api.updateRisk(riskId, {
likelihood: r.likelihood || 0,
impact: r.impact || 0,
residual_likelihood: r.residual_likelihood,
residual_impact: r.residual_impact,
});
} catch (e) { showNotification(e.message || 'Errore nel salvataggio', 'error'); }
}
function getScoreClassForRisk(score) {
@@ -1148,76 +1243,77 @@
// ESCLUSI i "non applicabili" (stato non_applicabile o non applicabili alla classe);
// - rischi AGGIUNTIVI custom (che portano già likelihood/impact).
// Legge dagli array in memoria (derivedData/risksData): niente fetch qui.
function loadMatrix() {
const points = [];
(derivedData || []).forEach(r => {
if (!isDerivedApplicable(r)) return; // esclude i non applicabili alla classe
if (r.stato === 'non_applicabile') return; // esclude i non applicabili
const l = parseInt(r.likelihood), i = parseInt(r.impact);
if (l >= 1 && l <= 5 && i >= 1 && i <= 5) {
points.push({ likelihood: l, impact: i, title: ((r.risk_code || '') + ' ' + (r.requisito_code || '')).trim() });
// ── Matrici: vista (inerente/residuo) indipendente per ciascuna matrice (punti 2 e 3.4) ──
let matrixViewDerived = 'inherent';
let matrixViewTrans = 'inherent';
function setMatrixView(scope, view) {
if (scope === 'derived') {
matrixViewDerived = view;
const a = document.getElementById('mxd-inh'), b = document.getElementById('mxd-res');
if (a) a.classList.toggle('on', view === 'inherent');
if (b) b.classList.toggle('on', view === 'residual');
loadDerivedMatrix();
} else {
matrixViewTrans = view;
const a = document.getElementById('mxt-inh'), b = document.getElementById('mxt-res');
if (a) a.classList.toggle('on', view === 'inherent');
if (b) b.classList.toggle('on', view === 'residual');
loadTransversalMatrix();
}
}
function matrixPoints(list, view, titleFn, filterFn) {
const pts = [];
(list || []).forEach(r => {
if (filterFn && !filterFn(r)) return;
const l = view === 'residual' ? parseInt(r.residual_likelihood) : parseInt(r.likelihood);
const i = view === 'residual' ? parseInt(r.residual_impact) : parseInt(r.impact);
if (l >= 1 && l <= 5 && i >= 1 && i <= 5) pts.push({ likelihood: l, impact: i, title: titleFn(r) });
});
(risksData || []).forEach(r => {
const l = parseInt(r.likelihood), i = parseInt(r.impact);
if (l >= 1 && l <= 5 && i >= 1 && i <= 5) {
points.push({ likelihood: l, impact: i, title: r.title || (r.risk_code || 'Rischio') });
return pts;
}
});
renderMatrix(points);
function loadDerivedMatrix() {
const pts = matrixPoints(derivedData, matrixViewDerived,
r => ((r.risk_code || '') + ' ' + (r.requisito_code || '')).trim(),
r => isDerivedApplicable(r) && r.stato !== 'non_applicabile');
renderMatrix(pts, 'risk-matrix-grid-derived');
}
function loadTransversalMatrix() {
const pts = matrixPoints(risksData, matrixViewTrans, r => r.title || (r.risk_code || 'Rischio'));
renderMatrix(pts, 'risk-matrix-grid-transversal');
}
// Compat: le vecchie chiamate loadMatrix() aggiornano entrambe le matrici
function loadMatrix() { loadDerivedMatrix(); loadTransversalMatrix(); }
function renderMatrix(risks) {
const grid = document.getElementById('risk-matrix-grid');
// Build a map: `${likelihood}-${impact}` -> count
// Bande (spec Simon): 1-2 basso, 3-8 medio, 9-12 alto, 15-25 critico
function matrixBand(score) {
if (score >= 15) return 'band-crit';
if (score >= 9) return 'band-high';
if (score >= 3) return 'band-med';
return 'band-low';
}
function renderMatrix(risks, gridId) {
const grid = document.getElementById(gridId);
if (!grid) return;
const cellMap = {};
risks.forEach(r => {
(risks || []).forEach(r => {
const key = `${r.likelihood || 0}-${r.impact || 0}`;
if (!cellMap[key]) cellMap[key] = [];
cellMap[key].push(r);
(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>`;
html += `<div class="matrix-cell ${matrixBand(score)}" 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>`;
}
for (let imp = 1; imp <= 5; imp++) html += `<div class="matrix-x-header">${imp}</div>`;
grid.innerHTML = html;
}
@@ -1237,8 +1333,8 @@
function renderDetail(risk) {
document.getElementById('view-derived').classList.add('hidden');
document.getElementById('view-table').classList.add('hidden');
document.getElementById('view-matrix').classList.add('hidden');
['allhazard-section', 'view-matrix-trans'].forEach(id => { const el = document.getElementById(id); if (el) el.classList.add('hidden'); });
const container = document.getElementById('view-detail');
container.classList.remove('hidden');
@@ -1412,8 +1508,8 @@
document.getElementById('view-detail').classList.add('hidden');
// Sezione derivati, tabella custom e matrice sono sempre visibili.
document.getElementById('view-derived').classList.remove('hidden');
document.getElementById('view-table').classList.remove('hidden');
document.getElementById('view-matrix').classList.remove('hidden');
['allhazard-section', 'view-matrix-trans'].forEach(id => { const el = document.getElementById(id); if (el) el.classList.remove('hidden'); });
}
// ── Create/Edit Risk Modal ───────────────────────────────────
+1 -1
View File
@@ -13,7 +13,7 @@
* Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json),
* cosi' activate elimina automaticamente la shell vecchia.
*/
const CACHE = 'nis2-shell-v1.26.0';
const CACHE = 'nis2-shell-v1.27.0';
// Shell V3 (top-nav): niente più style.css/common-bi.js/bootstrap-italia (UI V2).
const PRECACHE = [
+1 -1
View File
@@ -1 +1 @@
{"version": "1.26.0", "build": "2026-07-30-v1.26.0", "date": "2026-07-30", "changelog": "Gestione Rischi rinnovata: la tabella dei rischi dai requisiti ora ha 12 colonne con DOPPIA valutazione — rischio INERENTE (prima del trattamento) e RESIDUO (dopo). Probabilita e Impatto si compilano direttamente in tabella (1-5). La colonna Alert propone un'azione in base al punteggio (Prioritaria/Critica/Necessaria/Suggerita) e da li puoi creare azioni sul rischio. Codice cliccabile verso Misure e Requisiti. NB: il punteggio di rischio residuo NON cambia da solo la conformita del requisito (che dipende da attuazione ed evidenze): la aggiorni in modo esplicito."}
{"version": "1.27.0", "build": "2026-08-01-v1.27.0", "date": "2026-08-01", "changelog": "Pagina Rischi rinnovata (richieste Simon): la matrice 5x5 ora ha 4 fasce di rischio (1-2 basso verde, 3-8 medio arancione, 9-12 alto rosso tenue, 15-25 critico rosso) e un tasto per passare tra i giudizi INERENTI e RESIDUI. La sezione 'Rischi trasversali' e' stata spostata in fondo, ora ha le stesse 12 colonne dei rischi dai requisiti (valutazione inerente e residua, alert, esito), i tasti 'AI Suggerisci' e 'Nuovo Rischio' in testa, una propria matrice 5x5 con lo stesso tasto Inerente/Residuo, e un tasto per eliminare ogni rischio trasversale."}