[FEAT] Rischi #500 opzione B: matrice dai rischi-requisito (probabilità×impatto)

Ogni rischio derivato da un requisito puo' ora essere valutato con Probabilita' e
Impatto (1-5) e compare nella matrice 5x5.
- DB: mig 066 (org_requisito_state += likelihood/impact) + runner idempotente.
- Backend: FrameworkController::setState accetta likelihood/impact; RiskController
  ::derivedList ritorna likelihood/impact/score. Applicata su prod + reload.
- Frontend risks.html: colonne Probabilita'/Impatto/Rischio(P×I) + tasto "Valuta"
  (modale, riservato a super_admin/org_admin/compliance_manager); matrice calcolata
  client-side dai rischi derivati valutati (esclusi i non_applicabile, caveat
  nis2-expert) + rischi custom. Framing normativo invariato.
- help.js/i18n aggiornati; version 1.25.7; buster help/i18n ?v=20260727b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-07-27 15:32:59 +02:00
co-authored by Claude Opus 4.8
parent f1156a81bb
commit f6d2076684
42 changed files with 326 additions and 94 deletions
+2 -2
View File
@@ -166,8 +166,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -89,8 +89,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -96,8 +96,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -1126,8 +1126,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3()
+2 -2
View File
@@ -274,8 +274,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -381,8 +381,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -194,8 +194,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/*
+2 -2
View File
@@ -143,8 +143,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -196,8 +196,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script src="/js/competenze.js?v=20260627"></script>
<script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
+2 -2
View File
@@ -102,8 +102,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script src="/js/connettori-discovery.js?v=20260627"></script>
<script>
if (typeof checkAuth === 'function' && !checkAuth()) {
+2 -2
View File
@@ -159,8 +159,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/* Controlli periodici (control monitoring §9.1/A.8.16): controlli ricorrenti + esecuzioni (evidenza) + NC/AC. */
+2 -2
View File
@@ -226,8 +226,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Icone inline (Lucide stroke) usate nelle stringhe HTML generate dal JS.
// Renderizzate immediatamente (no createIcons re-run), self-host, niente sprite BI. ──
+2 -2
View File
@@ -116,8 +116,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script src="/js/dashboard.js?v=20260724a"></script>
<script>
// ── Auth check ───────────────────────────────────────────
+2 -2
View File
@@ -1181,8 +1181,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi chrome + i18n normalmente. ZERO modifiche backend.
+2 -2
View File
@@ -363,8 +363,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -935,8 +935,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3()
+2 -2
View File
@@ -164,8 +164,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/*
+2 -2
View File
@@ -203,8 +203,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script src="/js/isms.js?v=20260627v3"></script>
<script>
// Icone Lucide della chrome + crumb/page-head statici. L'init dati e il wiring
+3 -1
View File
@@ -607,13 +607,15 @@ const HelpSystem = (function () {
'La <strong>prima tabella</strong> elenca i rischi <strong>derivati dai requisiti</strong> del framework di sicurezza: ogni voce e\' il <strong>rischio di non conformita\'</strong> legato al mancato o parziale soddisfacimento di un requisito, <strong>non il requisito in se\'</strong>. E\' un elenco in <strong>sola lettura</strong>.',
'Il codice <strong>RSK-‹misura›Com.‹n›</strong> e\' una <strong>codifica interna di NIS2 Agile</strong>; ‹misura› (es. GV.OC-04) e\' il codice della misura del Framework Nazionale richiamato dalla Determinazione ACN 164179/2025. <strong>Non</strong> e\' un codice o numero ufficiale ACN.',
'La colonna <strong>Conformita\'</strong> mostra lo stato di valutazione del requisito (Da valutare, Non conforme, Parziale, Conforme). I requisiti <strong>non applicabili alla tua classe</strong> (importante vs essenziale) sono mostrati in grigio.',
'La seconda tabella, <strong>Rischi aggiuntivi (approccio multirischio / all-hazard)</strong>, permette ai <strong>profili amministrativi</strong> (org_admin, super_admin) di aggiungere rischi non coperti da un singolo requisito (minacce fisiche, catena di fornitura, eventi naturali): copre l\'approccio multirischio richiesto dall\'art. 24 D.Lgs. 138/2024.'
'La seconda tabella, <strong>Rischi aggiuntivi (approccio multirischio / all-hazard)</strong>, permette ai <strong>profili amministrativi</strong> (org_admin, super_admin) di aggiungere rischi non coperti da un singolo requisito (minacce fisiche, catena di fornitura, eventi naturali): copre l\'approccio multirischio richiesto dall\'art. 24 D.Lgs. 138/2024.',
'I profili <strong>Amministratore</strong>, <strong>Compliance Manager</strong> e <strong>super admin</strong> possono <strong>valutare</strong> ogni rischio-da-requisito con il pulsante <strong>Valuta</strong>: si impostano lo <strong>stato di conformità</strong>, la <strong>Probabilità</strong> e l\'<strong>Impatto</strong> (scala 1–5). La <strong>Probabilità</strong> è la probabilità che la minaccia si concretizzi <strong>sfruttando la carenza</strong> del requisito; l\'<strong>Impatto</strong> è l\'impatto della <strong>non conformità o del conseguente incidente</strong> — non la "probabilità del requisito". Il punteggio Rischio (Probabilità × Impatto) compare nella tabella e alimenta la matrice.'
]
},
{
heading: 'Matrice di Rischio',
items: [
'La matrice 5×5 e\' uno <strong>strumento di rappresentazione</strong>, non un obbligo normativo: l\'art. 24 D.Lgs. 138/2024 richiede un\'analisi dei rischi proporzionata e con approccio multirischio, senza imporre una metodologia specifica.',
'Ogni rischio-da-requisito <strong>valutato con Probabilità e Impatto</strong> compare automaticamente nella matrice 5×5, insieme ai <strong>rischi aggiuntivi</strong> (custom): la matrice rappresenta così <strong>tutti i rischi</strong>. La matrice <strong>esclude i requisiti "non applicabili"</strong> (per classe o marcati tali).',
'La <strong>matrice Probabilita\' x Impatto</strong> (5x5) classifica automaticamente i rischi in base alla loro gravita\'.',
'I livelli di probabilita\' vanno da <em>Molto Bassa</em> a <em>Molto Alta</em>.',
'I livelli di impatto vanno da <em>Trascurabile</em> a <em>Critico</em>.',
+5
View File
@@ -169,6 +169,11 @@ const I18n = (function () {
'risks.custom_heading': { it: 'Rischi aggiuntivi (approccio multirischio / all-hazard)', en: 'Additional risks (multi-risk / all-hazard approach)' },
'risks.custom_help': { it: 'Oltre ai rischi derivati dai requisiti, i profili amministrativi possono aggiungere rischi non coperti da un singolo requisito (es. minacce fisiche, catena di fornitura, eventi naturali): copre l\'approccio multirischio/all-hazard (art. 24 D.Lgs. 138/2024).', en: 'Beyond the requirement-derived risks, administrative profiles can add risks not covered by a single requirement (e.g. physical threats, supply chain, natural events): this covers the multi-risk/all-hazard approach (art. 24 Legislative Decree 138/2024).' },
'risks.matrix_caption': { it: 'La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024).', en: 'The 5×5 matrix is a representation tool, not a regulatory obligation (art. 24 Legislative Decree 138/2024).' },
'risks.matrix_subtitle': { it: 'Rischi da requisiti (valutati) + aggiuntivi — esclusi i non applicabili', en: 'Requirement-derived risks (evaluated) + additional ones — non-applicable excluded' },
'risks.col_probability': { it: 'Probabilità', en: 'Likelihood' },
'risks.col_impact': { it: 'Impatto', en: 'Impact' },
'risks.col_risk_level': { it: 'Rischio (P×I)', en: 'Risk (L×I)' },
'risks.col_actions': { it: 'Azioni', en: 'Actions' },
// ── Incidenti ──────────────────────────────────
'incidents.title': { it: 'Gestione Incidenti', en: 'Incident Management' },
+2 -2
View File
@@ -179,8 +179,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script src="/js/kb.js"></script>
<script>
// Gate auth + chrome come le altre pagine V3.
+2 -2
View File
@@ -151,8 +151,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/*
+2 -2
View File
@@ -179,8 +179,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/*
+2 -2
View File
@@ -141,8 +141,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth + chrome + i18n + help ──────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -155,8 +155,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script src="/js/organigramma.js?v=20260627"></script>
<script>
// Render icone Lucide (chrome + crumb + bottoni). organigramma.js fa render albero
+2 -2
View File
@@ -344,8 +344,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -145,8 +145,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script src="/js/raci.js?v=20260627"></script>
<script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
+2 -2
View File
@@ -236,8 +236,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -161,8 +161,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/*
+183 -19
View File
@@ -362,7 +362,11 @@
<th scope="col" data-i18n="risks.col_risk">Rischio</th>
<th scope="col" data-i18n="risks.col_measure">Misura</th>
<th scope="col" data-i18n="risks.col_conformity">Conformità</th>
<th scope="col" data-i18n="risks.col_probability">Probabilità</th>
<th scope="col" data-i18n="risks.col_impact">Impatto</th>
<th scope="col" data-i18n="risks.col_risk_level">Rischio (P×I)</th>
<th scope="col" data-i18n="risks.col_evaluation">Valutazione</th>
<th scope="col" id="derived-th-actions" data-i18n="risks.col_actions" style="display:none;">Azioni</th>
</tr>
</thead>
<tbody id="derived-table-body">
@@ -435,7 +439,7 @@
<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>
<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>
<div class="card-body">
<div class="risk-matrix-container">
@@ -487,8 +491,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
@@ -538,6 +542,9 @@
let derivedData = [];
let orgClass = null;
// Etichette impatto (per il modale Valuta).
const IMPACT_LABELS = ['', 'Trascurabile', 'Basso', 'Significativo', 'Grave', 'Catastrofico'];
// ── Gating ruolo: creazione/AI riservate ai profili amministrativi ──
const ADMIN_ROLES = ['super_admin', 'org_admin'];
(function gateAdminActions() {
@@ -550,10 +557,25 @@
}
})();
// ── Gating ruolo: valutazione rischi-da-requisito (opzione B) ──
// Chi può salvare la valutazione (stato + probabilità + impatto) di un requisito.
const EVAL_ROLES = ['super_admin', 'org_admin', 'compliance_manager'];
const canEvaluate = EVAL_ROLES.includes((api.getUserRole && api.getUserRole()) || '');
(function gateEvaluateColumn() {
if (canEvaluate) {
const th = document.getElementById('derived-th-actions');
if (th) th.style.display = '';
}
})();
// ── Load ─────────────────────────────────────────────────────
loadDerivedRisks();
loadRisks();
loadMatrix();
// Sequenza: prima i dati (derivati + custom) poi la matrice, che li
// aggrega client-side. loadMatrix() non fa più fetch (usa gli array in memoria).
(async function initRisks() {
await loadDerivedRisks();
await loadRisks();
loadMatrix();
})();
// ── Derived Risks (requisiti del framework) ──────────────────
// Badge conformità per lo stato di valutazione del requisito.
@@ -591,11 +613,21 @@
}
}
// Livello di rischio del punteggio derivato (P×I 1-25): opzione B.
// 1-4 basso/verde, 5-9 medio/giallo, 10-14 alto/arancio, 15-25 critico/rosso.
function getDerivedScoreClass(score) {
if (score >= 15) return 'score-critical';
if (score >= 10) return 'score-high';
if (score >= 5) return 'score-medium';
return 'score-low';
}
function renderDerivedTable(rows) {
const tbody = document.getElementById('derived-table-body');
const colspan = 8 + (canEvaluate ? 1 : 0);
if (!rows || rows.length === 0) {
tbody.innerHTML = `
<tr><td colspan="5">
<tr><td colspan="${colspan}">
<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.944z" clip-rule="evenodd"/></svg>
<h4>Nessun requisito applicabile</h4>
@@ -623,6 +655,27 @@
? `<span class="badge ${st.cls}">${escapeHtml(st.label)}</span>`
: `<span class="badge badge-neutral">Non applicabile alla tua classe</span>`;
// Probabilità / Impatto / Rischio: solo per righe applicabili e non "non applicabili".
let probCell = '<td>—</td>', impCell = '<td>—</td>', scoreCell = '<td>—</td>';
if (applicable && r.stato !== 'non_applicabile') {
const l = (r.likelihood >= 1 && r.likelihood <= 5) ? parseInt(r.likelihood) : null;
const i = (r.impact >= 1 && r.impact <= 5) ? parseInt(r.impact) : null;
probCell = `<td>${l != null ? l + '/5' : '—'}</td>`;
impCell = `<td>${i != null ? i + '/5' : '—'}</td>`;
if (r.score != null && r.score !== '' && !isNaN(r.score)) {
const sc = parseInt(r.score);
scoreCell = `<td><span class="risk-score ${getDerivedScoreClass(sc)}" title="P${l != null ? l : '?'} × I${i != null ? i : '?'} = ${sc}">${sc}</span></td>`;
}
}
// Colonna Azioni (solo per i ruoli che possono valutare).
let actionsCell = '';
if (canEvaluate) {
actionsCell = applicable
? `<td><button class="btn btn-ghost btn-sm" onclick="openEvaluateModal(${r.requisito_id})" title="Valuta rischio"><i data-lucide="sliders-horizontal" class="ic"></i> Valuta</button></td>`
: '<td><span class="text-muted">—</span></td>';
}
html += `
<tr ${rowStyle}>
<td><code style="font-size:0.78rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></td>
@@ -635,10 +688,107 @@
${measureDescr ? `<div style="font-size:0.78rem;color:var(--gray-500);margin-top:2px;">${escapeHtml(measureDescr)}</div>` : ''}
</td>
<td>${conformityCell}</td>
${probCell}
${impCell}
${scoreCell}
<td>${evalVal}</td>
${actionsCell}
</tr>`;
});
tbody.innerHTML = html;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
}
// ── Modale "Valuta" (opzione B) — valutazione rischio-da-requisito ──
const DERIVED_STATUS_OPTIONS = [
['da_valutare', 'Da valutare'],
['non_applicabile', 'Non applicabile'],
['non_conforme', 'Non conforme'],
['parziale', 'Parziale'],
['conforme', 'Conforme'],
];
function scaleOptions(labels, cur) {
let o = '';
for (let v = 1; v <= 5; v++) {
o += `<option value="${v}" ${v == cur ? 'selected' : ''}>${v} - ${labels[v]}</option>`;
}
return o;
}
function openEvaluateModal(requisitoId) {
if (!canEvaluate) return;
const r = (derivedData || []).find(x => String(x.requisito_id) === String(requisitoId));
if (!r) { showNotification('Rischio non trovato', 'error'); return; }
const curL = (r.likelihood >= 1 && r.likelihood <= 5) ? parseInt(r.likelihood) : 3;
const curI = (r.impact >= 1 && r.impact <= 5) ? parseInt(r.impact) : 3;
const curStato = r.stato || 'da_valutare';
const curNote = r.valutazione_rischio || '';
const statoOpts = DERIVED_STATUS_OPTIONS.map(([v, l]) =>
`<option value="${v}" ${v === curStato ? 'selected' : ''}>${escapeHtml(l)}</option>`).join('');
const content = `
<div style="padding:10px 12px;background:var(--gray-50);border:1px solid var(--gray-200);border-radius:var(--border-radius);margin-bottom:16px;">
<code style="font-size:0.78rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code>
<div style="font-size:0.68rem;text-transform:uppercase;letter-spacing:0.04em;font-weight:600;color:var(--gray-500);margin-top:4px;">Rischio di non conformità</div>
<div style="font-size:0.86rem;color:var(--gray-800);">${escapeHtml(r.requisito_descr || '')}</div>
</div>
<div class="form-group">
<label for="eval-stato" class="form-label">Conformità (stato del requisito)</label>
<select class="form-select" id="eval-stato">${statoOpts}</select>
</div>
<div class="form-row">
<div class="form-group">
<label for="eval-likelihood" class="form-label">Probabilità</label>
<select class="form-select" id="eval-likelihood">${scaleOptions(LIKELIHOOD_LABELS, curL)}</select>
<div style="font-size:0.72rem;color:var(--gray-500);margin-top:4px;">Probabilità che la minaccia si concretizzi sfruttando la carenza del requisito.</div>
</div>
<div class="form-group">
<label for="eval-impact" class="form-label">Impatto</label>
<select class="form-select" id="eval-impact">${scaleOptions(IMPACT_LABELS, curI)}</select>
<div style="font-size:0.72rem;color:var(--gray-500);margin-top:4px;">Impatto della non conformità o del conseguente incidente.</div>
</div>
</div>
<div class="form-group">
<label for="eval-note" class="form-label">Valutazione del rischio <span style="color:var(--gray-400);font-size:0.75rem;font-weight:400;">(opzionale)</span></label>
<textarea class="form-textarea" id="eval-note" rows="3" placeholder="Note sulla valutazione del rischio">${escapeHtml(curNote)}</textarea>
</div>
<p class="text-muted" style="font-size:0.75rem;margin:0;line-height:1.5;">La matrice 5×5 è uno strumento di rappresentazione, non un obbligo normativo (art. 24 D.Lgs. 138/2024). I requisiti "non applicabili" sono esclusi dalla matrice.</p>
`;
showModal('Valuta rischio', content, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveEvaluation(${r.requisito_id})">Salva valutazione</button>
`
});
}
async function saveEvaluation(requisitoId) {
if (!canEvaluate) return;
const btn = document.querySelector('#modal-overlay .btn-primary');
const payload = {
requisito_id: parseInt(requisitoId),
stato: document.getElementById('eval-stato').value,
valutazione_rischio: document.getElementById('eval-note').value.trim(),
likelihood: parseInt(document.getElementById('eval-likelihood').value),
impact: parseInt(document.getElementById('eval-impact').value),
};
setButtonLoading(btn, true);
try {
// frameworkSetState è _acn: ritorna data e lancia su success=false.
// Inviamo SEMPRE stato+valutazione+probabilità+impatto insieme (UPSERT che sovrascrive).
await api.frameworkSetState(payload);
closeModal();
showNotification('Valutazione salvata', 'success');
await loadDerivedRisks();
loadMatrix();
} catch (e) {
setButtonLoading(btn, false);
showNotification(e.message || 'Errore nel salvataggio', 'error');
}
}
async function loadRisks() {
@@ -734,17 +884,31 @@
}
// ── 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');
// Opzione B: la matrice aggrega, CLIENT-SIDE, TUTTI i rischi:
// - rischi DERIVATI dai requisiti con probabilità+impatto valorizzati,
// 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() });
}
} catch (e) {
showNotification('Errore di connessione', 'error');
}
});
(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) {
@@ -1144,7 +1308,7 @@
if (result.success) {
closeModal();
showNotification(riskId ? 'Rischio aggiornato' : 'Rischio creato con successo', 'success');
loadRisks();
await loadRisks();
loadMatrix();
// If detail view is visible, reload detail
if (!document.getElementById('view-detail').classList.contains('hidden')) {
@@ -1181,7 +1345,7 @@
closeModal();
showNotification('Rischio eliminato', 'success');
backToList();
loadRisks();
await loadRisks();
loadMatrix();
} else {
showNotification(result.message || 'Errore nella eliminazione', 'error');
+2 -2
View File
@@ -739,8 +739,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -219,8 +219,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/*
+2 -2
View File
@@ -214,8 +214,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
'use strict';
/*
+2 -2
View File
@@ -546,8 +546,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -194,8 +194,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth & Init (ordine V3) ─────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -1 +1 @@
{"version": "1.25.6", "build": "2026-07-27-v1.25.6", "date": "2026-07-27", "changelog": "Gestione Rischi (ticket #500): i tasti 'Nuovo Rischio' e 'AI Suggerisci' restano disponibili SOLO ai profili amministrativi (super_admin/org_admin) e servono ad aggiungere rischi non legati a un singolo requisito (approccio multirischio/all-hazard, art. 24 D.Lgs. 138/2024); per gli altri utenti non compaiono. L'elenco principale dei rischi resta l'insieme dei requisiti Lex&ISO (codice RSK-<misura>Com.N)."}
{"version": "1.25.7", "build": "2026-07-27-v1.25.7", "date": "2026-07-27", "changelog": "Gestione Rischi (ticket #500, opzione B): ogni rischio-requisito puo' ora essere valutato con Probabilita' e Impatto (1-5) tramite il tasto 'Valuta' (riservato ai profili che valutano); i rischi valutati compaiono nella matrice 5x5 insieme ai 'Rischi aggiuntivi'. La matrice esclude i requisiti non applicabili. Probabilita' = probabilita' che la minaccia sfrutti la carenza; impatto = conseguenze della non conformita' (art. 24 D.Lgs. 138/2024; la matrice e' uno strumento di rappresentazione, non un metodo imposto)."}
+2 -2
View File
@@ -262,8 +262,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
+2 -2
View File
@@ -147,8 +147,8 @@
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script>
<script src="/js/help.js?v=20260727r"></script>
<script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727b"></script>
<script>
// ── Auth + chrome + i18n + help ──────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');