[FEAT] Gestione Rischi: tabella 12 colonne (inerente+residuo) — integrazione prototipo "Modulo Azioni" (Simon)

Integra la spec di Simon per la pagina Rischi con le correzioni normative
(parere nis2-expert + iso-27001-expert) applicate come eccezioni motivate:
- 12 colonne: Codice (link → Misure e Requisiti), Descrizione (RISCHIO DEFAULT =
  cfg_nis2_rischi.risk_descr), VALUTAZIONE RISCHIO INERENTE (P/I/PxI), Alert,
  Rischio Inerente, VALUTAZIONE RISCHIO RESIDUO (P/I/PxI), Esito residuo, Rischio Residuo.
- Editing INLINE di Probabilità/Impatto (1-5), salvataggio automatico.
- Alert a bande (15-25 Prioritaria / 9-14 Critica / 3-8 Necessaria / 1-2 Suggerita)
  → popup azioni + "crea azione" (tabella requisito_actions, seme Modulo Azioni).
- Eccezione #1: il rischio RESIDUO NON aggiorna in automatico lo STATO DI CONFORMITÀ del
  requisito (piani distinti). Col.11 "Esito residuo" = flag Rivalutare/Adeguato; conformità esplicita.
- Eccezione #3: banda 13-14 inclusa in "Critica" (9-14). "Danno €" → "Impatto" (1-5).
  Soglie = metodo interno (ISO 27001 §6.1.2), non obbligo (help/UI lo dichiarano).

Backend: mig 067; derivedList (+risk_descr +residuo), setState (+residuo), requisitoActions list/create.
help.js + i18n IT/EN. Cache-buster api/i18n/help 20260730a. Prossima mig=068.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-07-30 10:49:52 +02:00
co-authored by Claude Opus 4.8
parent cf224480cc
commit 8a0bc5ca39
49 changed files with 535 additions and 180 deletions
@@ -207,24 +207,29 @@ class FrameworkController extends BaseController
};
$likelihood = array_key_exists('likelihood', $body) ? $scale($body['likelihood']) : null;
$impact = array_key_exists('impact', $body) ? $scale($body['impact']) : null;
// Rischio RESIDUO (mig 067) — dopo il trattamento. Il client invia sempre la riga intera.
$resLikelihood = array_key_exists('residual_likelihood', $body) ? $scale($body['residual_likelihood']) : null;
$resImpact = array_key_exists('residual_impact', $body) ? $scale($body['residual_impact']) : null;
// upsert org-scoped (UNIQUE org+requisito)
Database::query(
'INSERT INTO org_requisito_state
(organization_id, requisito_id, stato, valutazione_rischio, note, likelihood, impact, updated_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
(organization_id, requisito_id, stato, valutazione_rischio, note, likelihood, impact, residual_likelihood, residual_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),
residual_likelihood = VALUES(residual_likelihood),
residual_impact = VALUES(residual_impact),
updated_by = VALUES(updated_by)',
[$orgId, $reqId, $stato ?? 'da_valutare', $valutazione, $note, $likelihood, $impact, $this->getCurrentUserId()]
[$orgId, $reqId, $stato ?? 'da_valutare', $valutazione, $note, $likelihood, $impact, $resLikelihood, $resImpact, $this->getCurrentUserId()]
);
$row = Database::fetchOne(
'SELECT requisito_id, stato, valutazione_rischio, note, likelihood, impact, updated_at
'SELECT requisito_id, stato, valutazione_rischio, note, likelihood, impact, residual_likelihood, residual_impact, updated_at
FROM org_requisito_state WHERE organization_id = ? AND requisito_id = ?',
[$orgId, $reqId]
);
@@ -235,6 +240,8 @@ class FrameworkController extends BaseController
'note' => $row['note'],
'likelihood' => $row['likelihood'] !== null ? (int) $row['likelihood'] : null,
'impact' => $row['impact'] !== null ? (int) $row['impact'] : null,
'residual_likelihood' => $row['residual_likelihood'] !== null ? (int) $row['residual_likelihood'] : null,
'residual_impact' => $row['residual_impact'] !== null ? (int) $row['residual_impact'] : null,
'updated_at' => $row['updated_at'],
], 'Valutazione salvata');
}