[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
@@ -0,0 +1,36 @@
<?php
/**
* migrate_066_requisito_risk_scoring.php — Ticket #500 (opzione B, matrice).
* org_requisito_state += likelihood TINYINT NULL, impact TINYINT NULL (scala 1-5).
* Serve a posizionare i rischi-da-requisito (RSK-<misura>Com.N) sulla matrice 5x5.
* Additiva e IDEMPOTENTE. Nessun dato esistente toccato (valori NULL = non ancora valutato).
*
* Eseguire (host): docker exec nis2-app php /var/www/nis2-agile/application/cli/migrate_066_requisito_risk_scoring.php
*/
if (PHP_SAPI !== 'cli') { http_response_code(403); exit("CLI only\n"); }
require_once __DIR__ . '/../config/env.php';
require_once __DIR__ . '/../config/database.php';
$pdo = Database::getInstance();
function colExists(PDO $pdo, string $table, string $col): bool {
$st = $pdo->prepare(
"SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?"
);
$st->execute([$table, $col]);
return (bool) $st->fetchColumn();
}
$adds = [
'likelihood' => "ADD COLUMN likelihood TINYINT NULL COMMENT 'Probabilita 1-5 (rischio da non conformita del requisito)' AFTER valutazione_rischio",
'impact' => "ADD COLUMN impact TINYINT NULL COMMENT 'Impatto 1-5' AFTER likelihood",
];
foreach ($adds as $col => $clause) {
if (colExists($pdo, 'org_requisito_state', $col)) { echo " - $col gia presente, skip\n"; continue; }
$pdo->exec("ALTER TABLE org_requisito_state $clause");
echo " + aggiunta colonna org_requisito_state.$col\n";
}
echo "Migrazione 066 — org_requisito_state.likelihood/impact OK. Prossima mig=067.\n";
@@ -199,21 +199,32 @@ class FrameworkController extends BaseController
$note = array_key_exists('note', $body) $note = array_key_exists('note', $body)
? ($body['note'] !== '' ? (string) $body['note'] : null) : null; ? ($body['note'] !== '' ? (string) $body['note'] : null) : null;
// Probabilita/impatto 1-5 (opzione B ticket #500, matrice 5x5). null = non valutato.
$scale = static function ($v) {
if ($v === null || $v === '') return null;
$n = (int) $v;
return ($n >= 1 && $n <= 5) ? $n : null;
};
$likelihood = array_key_exists('likelihood', $body) ? $scale($body['likelihood']) : null;
$impact = array_key_exists('impact', $body) ? $scale($body['impact']) : null;
// upsert org-scoped (UNIQUE org+requisito) // upsert org-scoped (UNIQUE org+requisito)
Database::query( Database::query(
'INSERT INTO org_requisito_state 'INSERT INTO org_requisito_state
(organization_id, requisito_id, stato, valutazione_rischio, note, updated_by) (organization_id, requisito_id, stato, valutazione_rischio, note, likelihood, impact, updated_by)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
stato = VALUES(stato), stato = VALUES(stato),
valutazione_rischio = VALUES(valutazione_rischio), valutazione_rischio = VALUES(valutazione_rischio),
note = VALUES(note), note = VALUES(note),
likelihood = VALUES(likelihood),
impact = VALUES(impact),
updated_by = VALUES(updated_by)', updated_by = VALUES(updated_by)',
[$orgId, $reqId, $stato ?? 'da_valutare', $valutazione, $note, $this->getCurrentUserId()] [$orgId, $reqId, $stato ?? 'da_valutare', $valutazione, $note, $likelihood, $impact, $this->getCurrentUserId()]
); );
$row = Database::fetchOne( $row = Database::fetchOne(
'SELECT requisito_id, stato, valutazione_rischio, note, updated_at 'SELECT requisito_id, stato, valutazione_rischio, note, likelihood, impact, updated_at
FROM org_requisito_state WHERE organization_id = ? AND requisito_id = ?', FROM org_requisito_state WHERE organization_id = ? AND requisito_id = ?',
[$orgId, $reqId] [$orgId, $reqId]
); );
@@ -222,6 +233,8 @@ class FrameworkController extends BaseController
'stato' => $row['stato'], 'stato' => $row['stato'],
'valutazione_rischio' => $row['valutazione_rischio'], 'valutazione_rischio' => $row['valutazione_rischio'],
'note' => $row['note'], 'note' => $row['note'],
'likelihood' => $row['likelihood'] !== null ? (int) $row['likelihood'] : null,
'impact' => $row['impact'] !== null ? (int) $row['impact'] : null,
'updated_at' => $row['updated_at'], 'updated_at' => $row['updated_at'],
], 'Valutazione salvata'); ], 'Valutazione salvata');
} }
+5 -1
View File
@@ -70,7 +70,7 @@ class RiskController extends BaseController
"SELECT q.id AS requisito_id, q.n, q.misura_code, q.requisito_code, "SELECT q.id AS requisito_id, q.n, q.misura_code, q.requisito_code,
q.requisito_descr, q.applies_important, q.applies_essential, q.requisito_descr, q.applies_important, q.applies_essential,
m.misura_descr, m.nist_code, m.area_politica, m.ord, m.misura_descr, m.nist_code, m.area_politica, m.ord,
s.stato, s.valutazione_rischio, s.note s.stato, s.valutazione_rischio, s.note, s.likelihood, s.impact
FROM cfg_nis2_requisiti q FROM cfg_nis2_requisiti q
JOIN cfg_nis2_misure m ON m.misura_code = q.misura_code JOIN cfg_nis2_misure m ON m.misura_code = q.misura_code
LEFT JOIN org_requisito_state s LEFT JOIN org_requisito_state s
@@ -105,6 +105,10 @@ class RiskController extends BaseController
'stato' => $q['stato'] ?? 'da_valutare', 'stato' => $q['stato'] ?? 'da_valutare',
'valutazione_rischio' => $q['valutazione_rischio'] ?? null, 'valutazione_rischio' => $q['valutazione_rischio'] ?? null,
'note' => $q['note'] ?? null, 'note' => $q['note'] ?? null,
'likelihood' => isset($q['likelihood']) && $q['likelihood'] !== null ? (int) $q['likelihood'] : null,
'impact' => isset($q['impact']) && $q['impact'] !== null ? (int) $q['impact'] : null,
'score' => (isset($q['likelihood'], $q['impact']) && $q['likelihood'] !== null && $q['impact'] !== null)
? (int) $q['likelihood'] * (int) $q['impact'] : null,
]; ];
} }
+8
View File
@@ -0,0 +1,8 @@
-- 066_requisito_risk_scoring.sql — Ticket #500 (opzione B, matrice 5x5 dei rischi derivati).
-- Aggiunge probabilita/impatto (1-5) alla valutazione per-requisito, così i rischi
-- derivati dai requisiti (RSK-<misura>Com.N) si posizionano sulla matrice.
-- Additiva e idempotente (i valori NULL = non ancora valutato). Runner:
-- application/cli/migrate_066_requisito_risk_scoring.php
ALTER TABLE org_requisito_state
ADD COLUMN likelihood TINYINT NULL COMMENT 'Probabilita 1-5' AFTER valutazione_rischio,
ADD COLUMN impact TINYINT NULL COMMENT 'Impatto 1-5' AFTER likelihood;
+2 -2
View File
@@ -166,8 +166,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3() loadSidebar(); // = loadTopnavV3()
+2 -2
View File
@@ -274,8 +274,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+2 -2
View File
@@ -143,8 +143,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script src="/js/competenze.js?v=20260627"></script> <script src="/js/competenze.js?v=20260627"></script>
<script> <script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } } 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script src="/js/connettori-discovery.js?v=20260627"></script> <script src="/js/connettori-discovery.js?v=20260627"></script>
<script> <script>
if (typeof checkAuth === 'function' && !checkAuth()) { if (typeof checkAuth === 'function' && !checkAuth()) {
+2 -2
View File
@@ -159,8 +159,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; 'use strict';
/* Controlli periodici (control monitoring §9.1/A.8.16): controlli ricorrenti + esecuzioni (evidenza) + NC/AC. */ /* 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Icone inline (Lucide stroke) usate nelle stringhe HTML generate dal JS. // ── Icone inline (Lucide stroke) usate nelle stringhe HTML generate dal JS.
// Renderizzate immediatamente (no createIcons re-run), self-host, niente sprite BI. ── // 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script src="/js/dashboard.js?v=20260724a"></script> <script src="/js/dashboard.js?v=20260724a"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
+2 -2
View File
@@ -1181,8 +1181,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// La guida è accessibile anche da non autenticati (utile per onboarding) // La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi chrome + i18n normalmente. ZERO modifiche backend. // 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth & Init ────────────────────────────────────────────── // ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3() loadSidebar(); // = loadTopnavV3()
+2 -2
View File
@@ -164,8 +164,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+2 -2
View File
@@ -203,8 +203,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script src="/js/isms.js?v=20260627v3"></script> <script src="/js/isms.js?v=20260627v3"></script>
<script> <script>
// Icone Lucide della chrome + crumb/page-head statici. L'init dati e il wiring // 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>.', '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.', '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 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', heading: 'Matrice di Rischio',
items: [ 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.', '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\'.', '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 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>.', '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_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.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_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 ────────────────────────────────── // ── Incidenti ──────────────────────────────────
'incidents.title': { it: 'Gestione Incidenti', en: 'Incident Management' }, '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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script src="/js/kb.js"></script> <script src="/js/kb.js"></script>
<script> <script>
// Gate auth + chrome come le altre pagine V3. // 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+2 -2
View File
@@ -179,8 +179,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+2 -2
View File
@@ -141,8 +141,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth + chrome + i18n + help ────────────────────────── // ── Auth + chrome + i18n + help ──────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script src="/js/organigramma.js?v=20260627"></script> <script src="/js/organigramma.js?v=20260627"></script>
<script> <script>
// Render icone Lucide (chrome + crumb + bottoni). organigramma.js fa render albero // 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script src="/js/raci.js?v=20260627"></script> <script src="/js/raci.js?v=20260627"></script>
<script> <script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } } 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; '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_risk">Rischio</th>
<th scope="col" data-i18n="risks.col_measure">Misura</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_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" 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> </tr>
</thead> </thead>
<tbody id="derived-table-body"> <tbody id="derived-table-body">
@@ -435,7 +439,7 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3>Matrice di Rischio 5x5</h3> <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>
<div class="card-body"> <div class="card-body">
<div class="risk-matrix-container"> <div class="risk-matrix-container">
@@ -487,8 +491,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth & Init ────────────────────────────────────────────── // ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
@@ -538,6 +542,9 @@
let derivedData = []; let derivedData = [];
let orgClass = null; 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 ── // ── Gating ruolo: creazione/AI riservate ai profili amministrativi ──
const ADMIN_ROLES = ['super_admin', 'org_admin']; const ADMIN_ROLES = ['super_admin', 'org_admin'];
(function gateAdminActions() { (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 ───────────────────────────────────────────────────── // ── Load ─────────────────────────────────────────────────────
loadDerivedRisks(); // Sequenza: prima i dati (derivati + custom) poi la matrice, che li
loadRisks(); // aggrega client-side. loadMatrix() non fa più fetch (usa gli array in memoria).
loadMatrix(); (async function initRisks() {
await loadDerivedRisks();
await loadRisks();
loadMatrix();
})();
// ── Derived Risks (requisiti del framework) ────────────────── // ── Derived Risks (requisiti del framework) ──────────────────
// Badge conformità per lo stato di valutazione del requisito. // 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) { function renderDerivedTable(rows) {
const tbody = document.getElementById('derived-table-body'); const tbody = document.getElementById('derived-table-body');
const colspan = 8 + (canEvaluate ? 1 : 0);
if (!rows || rows.length === 0) { if (!rows || rows.length === 0) {
tbody.innerHTML = ` tbody.innerHTML = `
<tr><td colspan="5"> <tr><td colspan="${colspan}">
<div class="empty-state"> <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> <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> <h4>Nessun requisito applicabile</h4>
@@ -623,6 +655,27 @@
? `<span class="badge ${st.cls}">${escapeHtml(st.label)}</span>` ? `<span class="badge ${st.cls}">${escapeHtml(st.label)}</span>`
: `<span class="badge badge-neutral">Non applicabile alla tua classe</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 += ` html += `
<tr ${rowStyle}> <tr ${rowStyle}>
<td><code style="font-size:0.78rem;color:var(--primary);">${escapeHtml(r.risk_code || '-')}</code></td> <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>` : ''} ${measureDescr ? `<div style="font-size:0.78rem;color:var(--gray-500);margin-top:2px;">${escapeHtml(measureDescr)}</div>` : ''}
</td> </td>
<td>${conformityCell}</td> <td>${conformityCell}</td>
${probCell}
${impCell}
${scoreCell}
<td>${evalVal}</td> <td>${evalVal}</td>
${actionsCell}
</tr>`; </tr>`;
}); });
tbody.innerHTML = html; 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() { async function loadRisks() {
@@ -734,17 +884,31 @@
} }
// ── Matrix ─────────────────────────────────────────────────── // ── Matrix ───────────────────────────────────────────────────
async function loadMatrix() { // Opzione B: la matrice aggrega, CLIENT-SIDE, TUTTI i rischi:
try { // - rischi DERIVATI dai requisiti con probabilità+impatto valorizzati,
const result = await api.getRiskMatrix(); // ESCLUSI i "non applicabili" (stato non_applicabile o non applicabili alla classe);
if (result.success) { // - rischi AGGIUNTIVI custom (che portano già likelihood/impact).
renderMatrix(result.data.risks || []); // Legge dagli array in memoria (derivedData/risksData): niente fetch qui.
} else { function loadMatrix() {
showNotification(result.message || 'Errore caricamento matrice', 'error'); 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) { function renderMatrix(risks) {
@@ -1144,7 +1308,7 @@
if (result.success) { if (result.success) {
closeModal(); closeModal();
showNotification(riskId ? 'Rischio aggiornato' : 'Rischio creato con successo', 'success'); showNotification(riskId ? 'Rischio aggiornato' : 'Rischio creato con successo', 'success');
loadRisks(); await loadRisks();
loadMatrix(); loadMatrix();
// If detail view is visible, reload detail // If detail view is visible, reload detail
if (!document.getElementById('view-detail').classList.contains('hidden')) { if (!document.getElementById('view-detail').classList.contains('hidden')) {
@@ -1181,7 +1345,7 @@
closeModal(); closeModal();
showNotification('Rischio eliminato', 'success'); showNotification('Rischio eliminato', 'success');
backToList(); backToList();
loadRisks(); await loadRisks();
loadMatrix(); loadMatrix();
} else { } else {
showNotification(result.message || 'Errore nella eliminazione', 'error'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+2 -2
View File
@@ -214,8 +214,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
'use strict'; 'use strict';
/* /*
+2 -2
View File
@@ -546,8 +546,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth & Init (ordine V3) ───────────────────────────── // ── Auth & Init (ordine V3) ─────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); 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="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
+2 -2
View File
@@ -147,8 +147,8 @@
<script src="/js/common.js?v=20260627t"></script> <script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script> <script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260727r"></script> <script src="/js/i18n.js?v=20260727b"></script>
<script src="/js/help.js?v=20260727r"></script> <script src="/js/help.js?v=20260727b"></script>
<script> <script>
// ── Auth + chrome + i18n + help ────────────────────────────── // ── Auth + chrome + i18n + help ──────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');