[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:
co-authored by
Claude Opus 4.8
parent
f1156a81bb
commit
f6d2076684
@@ -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)
|
||||
? ($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)
|
||||
Database::query(
|
||||
'INSERT INTO org_requisito_state
|
||||
(organization_id, requisito_id, stato, valutazione_rischio, note, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(organization_id, requisito_id, stato, valutazione_rischio, note, likelihood, impact, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
stato = VALUES(stato),
|
||||
valutazione_rischio = VALUES(valutazione_rischio),
|
||||
note = VALUES(note),
|
||||
likelihood = VALUES(likelihood),
|
||||
impact = VALUES(impact),
|
||||
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(
|
||||
'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 = ?',
|
||||
[$orgId, $reqId]
|
||||
);
|
||||
@@ -222,6 +233,8 @@ class FrameworkController extends BaseController
|
||||
'stato' => $row['stato'],
|
||||
'valutazione_rischio' => $row['valutazione_rischio'],
|
||||
'note' => $row['note'],
|
||||
'likelihood' => $row['likelihood'] !== null ? (int) $row['likelihood'] : null,
|
||||
'impact' => $row['impact'] !== null ? (int) $row['impact'] : null,
|
||||
'updated_at' => $row['updated_at'],
|
||||
], 'Valutazione salvata');
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class RiskController extends BaseController
|
||||
"SELECT q.id AS requisito_id, q.n, q.misura_code, q.requisito_code,
|
||||
q.requisito_descr, q.applies_important, q.applies_essential,
|
||||
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
|
||||
JOIN cfg_nis2_misure m ON m.misura_code = q.misura_code
|
||||
LEFT JOIN org_requisito_state s
|
||||
@@ -105,6 +105,10 @@ class RiskController extends BaseController
|
||||
'stato' => $q['stato'] ?? 'da_valutare',
|
||||
'valutazione_rischio' => $q['valutazione_rischio'] ?? 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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
/*
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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) {} } }
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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. ──
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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>.',
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
/*
|
||||
|
||||
@@ -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';
|
||||
/*
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
/*
|
||||
|
||||
+181
-17
@@ -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();
|
||||
// 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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
/*
|
||||
|
||||
@@ -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';
|
||||
/*
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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)."}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user