[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
@@ -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');
}