diff --git a/public/risks.html b/public/risks.html index 6a395ce..495c40d 100644 --- a/public/risks.html +++ b/public/risks.html @@ -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 @@

Gestione Rischi

Registro rischi e matrice 5×5

-
- - -
+
@@ -452,7 +449,45 @@
- + +
+
+
+
+
+

Matrice di Rischio 5×5 — Rischi dai requisiti

+ Esclusi i requisiti non applicabili alla classe +
+
+ + +
+
+
+
+
+
+
Probabilita'
+
+
+
+
+
Impatto
+
+
+
+
Basso (1-2)
+
Medio (3-8)
+
Alto (9-12)
+
Critico (15-25)
+
+
+

La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024). Il tasto Inerente/Residuo mostra i giudizi prima o dopo il trattamento.

+
+
+
+ +

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.

- +
-
+
+
+ + +
+
+
-
- +
+
- - - - + + + + + + + + + + + - - - + + + + - @@ -511,53 +560,45 @@ - - -
+ +
-

Matrice di Rischio 5x5

- Rischi da requisiti (valutati) + aggiuntivi — esclusi i non applicabili +
+
+

Matrice di Rischio 5×5 — Rischi trasversali

+ Solo i rischi trasversali registrati sopra +
+
+ + +
+
Probabilita'
-
+
Impatto
-
-
- Basso (1-4) -
-
-
- Medio (5-9) -
-
-
- Alto (10-14) -
-
-
- Elevato (15-19) -
-
-
- Critico (20-25) -
+
Basso (1-2)
+
Medio (3-8)
+
Alto (9-12)
+
Critico (15-25)
-

La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).

+

La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).

+ @@ -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 = ` -
`; 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 ? `${iP}` : '—'; + const rEval = rP != null ? `${rP}` : '—'; + const ab = alertBand(iP); + const alertCell = ab + ? `` + : '—'; + const inhOutcome = iP != null ? 'Valutato' : '—'; + const ro = residualOutcome(rP); + const resOutcomeCell = ro ? `${ro.label}` : '—'; + const catLabel = CATEGORY_LABELS[r.category] || r.category || ''; + const safeTitle = (r.title || '').replace(/['\\]/g, ''); + const actBtns = editable + ? `
+ + +
` : ''; + const resCell = (rP != null ? 'Valutato' : '—') + actBtns; html += ` - - - - - - - - - + + + + + + + + + + + + `; }); 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') : '—'; + let o = ``; + for (let v = 1; v <= 5; v++) o += ``; + const rid = id.substring(id.indexOf('-') + 1); + return ``; + } + 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() }); - } - }); - - (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') }); - } - }); - - renderMatrix(points); - } - - 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'; + // ── 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) }); + }); + return pts; + } + 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(); } + // 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 => { + const key = `${r.likelihood || 0}-${r.impact || 0}`; + (cellMap[key] = cellMap[key] || []).push(r); + }); let html = ''; - // Rows from top (likelihood=5) to bottom (likelihood=1) for (let li = 5; li >= 1; li--) { - // Y-axis header html += `
${li}
`; - // 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 ? `
${risksInCell.length}
` : ''; - html += `
${dot}
`; + html += `
${dot}
`; } } - // Bottom row: corner + x-axis headers html += `
`; - for (let imp = 1; imp <= 5; imp++) { - html += `
${imp}
`; - } - + for (let imp = 1; imp <= 5; imp++) html += `
${imp}
`; 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 ─────────────────────────────────── diff --git a/public/sw.js b/public/sw.js index 699825e..0ffe899 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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 = [ diff --git a/public/version.json b/public/version.json index 35193a5..922a8aa 100644 --- a/public/version.json +++ b/public/version.json @@ -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."}
CodiceTitoloCategoriaProbabilita'Codice rischioDescrizione rischioRischio InerenteAlertRischio InerenteRischio ResiduoEsito residuoRischio Residuo
Probabilità ImpattoScoreStatoAzioniValutazioneProbabilitàImpattoValutazione
+
+
-

Nessun rischio registrato

-

Clicca "Nuovo Rischio" per aggiungere il primo rischio, oppure usa "AI Suggerisci".

+

Nessun rischio trasversale registrato

+

Usa "Nuovo Rischio" o "AI Suggerisci" qui sopra per aggiungere minacce non legate a un singolo requisito.

${escapeHtml(r.risk_code || '-')}${escapeHtml(r.title)}${escapeHtml(categoryLabel)}${r.likelihood || '-'}/5${r.impact || '-'}/5${score}${statusBadge} -
- - -
+
+ ${escapeHtml(r.risk_code || '-')} +
${escapeHtml(catLabel)}
+
${escapeHtml(r.title || '')}
+
${scoreSelT('til-' + id, iL, editable)}${scoreSelT('tii-' + id, iI, editable)}${iEval}${alertCell}${inhOutcome}${scoreSelT('trl-' + id, rL, editable)}${scoreSelT('tri-' + id, rI, editable)}${rEval}${resOutcomeCell}${resCell}