[FEAT] Epic C / C5.1 — Modulo Stakeholder: registro + matrice di Mendelow (mig.051)

Registro dedicato degli stakeholder (tabella `stakeholders`, NON una colonna su
suppliers: gli interni non sono fornitori; gli esterni si COLLEGANO opzionalmente
a un supplier esistente senza duplicare il dato).

- mig.051: cfg_stakeholder_types (30 di sistema Stak.01-30 + org-added da Stak.31),
  cfg_stakeholder_quadrants (4, range corretti: Q4 potere 0-2), stakeholders,
  stakeholder_procedures (m2m -> policies). Seeder idempotente seed_stakeholders.php.
- StakeholderController: list/create/update/delete + types/addType + quadrants + pickers.
  Quadrante CALCOLATO a read-time dai range config. Anti-IDOR su org_role/supplier/
  policy/tipo; coerenza kind (interni->organigramma, esterni->fornitore); punteggi 0-5.
- api.js: metodi stk*. stakeholders.html riscritta: registro + matrice di Mendelow
  in SVG dependency-free + modali (stakeholder, nuovo tipo). Voce sidebar V2 (common-bi.js).
- Help aggiornato; cache-buster ?v=20260626 su tutte le pagine; version 1.19.0; sw cache v1.19.0.

Ancoraggio GV.SC-02 (obbligo, art. 24 D.Lgs. 138/2024). La matrice di Mendelow e'
etichettata come BUONA PRASSI, non obbligo NIS2 (regola fonti-certe). Additivo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-16 20:54:57 +02:00
co-authored by Claude Opus 4.8
parent 419162594d
commit c782aa54fa
43 changed files with 1256 additions and 278 deletions
+151
View File
@@ -0,0 +1,151 @@
<?php
/**
* NIS2 Agile — Importer idempotente del registro Stakeholder (Epic C / C5.1).
* ----------------------------------------------------------------------------
* Crea (se mancanti) le tabelle del modulo Stakeholder (mig.051) e popola i dati
* di SISTEMA: i 4 quadranti della matrice di Mendelow (range corretti, Q4 potere
* 0-2) e i 30 tipi di stakeholder Stak.01-30 (6 Interni / 24 Esterni) dal seed
* autoritativo application/data/nis2_framework_seed.json (chiave stakeholder_types,
* generata dai file docs/simon/*.xlsx — codifica NON variabile).
*
* Idempotente: CREATE TABLE IF NOT EXISTS + INSERT ... ON DUPLICATE KEY UPDATE
* sul PK `code` (nessuna trappola NULL-dedupe: il codice E' la chiave primaria).
* Rilanciabile senza duplicare ne' rompere le FK. Sostituisce i runner usa-e-getta.
*
* NB: la matrice di Mendelow (potere/interesse) e' BUONA PRASSI, NON un obbligo
* NIS2; l'obbligo e' GV.SC-02 (ruoli/responsabilita verso fornitori/clienti/partner).
*
* Uso (dentro il container app):
* docker exec nis2-app php /var/www/nis2-agile/application/cli/seed_stakeholders.php
*
* NB: solo CLI. DB API via PDO singleton (Database::getInstance()), TLS gestito
* dalla config dell'app.
*/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit("Solo da CLI.\n");
}
require_once __DIR__ . '/../config/env.php';
require_once __DIR__ . '/../config/database.php';
$pdo = Database::getInstance();
$pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
// --- DDL (idempotente, FK dentro il CREATE; allineato a docs/sql/051_*.sql) ---
$ddl = [
"CREATE TABLE IF NOT EXISTS cfg_stakeholder_types (
code VARCHAR(16) NOT NULL, organization_id INT NULL,
tipo ENUM('Interno','Esterno') NOT NULL, descr TEXT NOT NULL, ord INT NOT NULL DEFAULT 0,
created_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (code), KEY idx_cfg_stk_type_org (organization_id),
CONSTRAINT fk_cfg_stk_type_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE,
CONSTRAINT fk_cfg_stk_type_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS cfg_stakeholder_quadrants (
code VARCHAR(4) NOT NULL, organization_id INT NULL,
label VARCHAR(64) NOT NULL, strategy TEXT NULL,
power_min TINYINT NOT NULL, power_max TINYINT NOT NULL,
interest_min TINYINT NOT NULL, interest_max TINYINT NOT NULL, ord INT NOT NULL DEFAULT 0,
PRIMARY KEY (code), KEY idx_cfg_stk_quad_org (organization_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS stakeholders (
id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL,
stak_code VARCHAR(16) NOT NULL, name VARCHAR(255) NOT NULL,
org_role_id INT NULL, supplier_id INT NULL,
power TINYINT NULL, interest TINYINT NULL,
contact_name VARCHAR(255) NULL, contact_email VARCHAR(255) NULL, notes TEXT NULL,
created_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_stk_org (organization_id), KEY idx_stk_org_code (organization_id, stak_code),
KEY idx_stk_role (org_role_id), KEY idx_stk_supplier (supplier_id),
CONSTRAINT fk_stk_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE,
CONSTRAINT fk_stk_code FOREIGN KEY (stak_code) REFERENCES cfg_stakeholder_types (code),
CONSTRAINT fk_stk_role FOREIGN KEY (org_role_id) REFERENCES org_roles (id) ON DELETE SET NULL,
CONSTRAINT fk_stk_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers (id) ON DELETE SET NULL,
CONSTRAINT fk_stk_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
"CREATE TABLE IF NOT EXISTS stakeholder_procedures (
stakeholder_id INT NOT NULL, policy_id INT NOT NULL,
PRIMARY KEY (stakeholder_id, policy_id), KEY idx_stk_proc_policy (policy_id),
CONSTRAINT fk_stk_proc_stk FOREIGN KEY (stakeholder_id) REFERENCES stakeholders (id) ON DELETE CASCADE,
CONSTRAINT fk_stk_proc_policy FOREIGN KEY (policy_id) REFERENCES policies (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
];
foreach ($ddl as $stmt) {
try { $pdo->exec($stmt); }
catch (PDOException $e) { if (!in_array($e->errorInfo[1] ?? 0, [1050, 1061], true)) { throw $e; } }
}
// --- Dati di sistema ---
$seedPath = __DIR__ . '/../data/nis2_framework_seed.json';
$seed = json_decode(@file_get_contents($seedPath), true);
if (!$seed || empty($seed['stakeholder_types'])) {
fwrite(STDERR, "SEED non leggibile o privo di stakeholder_types: $seedPath\n");
exit(1);
}
// 4 quadranti di sistema — matrice di Mendelow (x=interesse, y=potere).
// REFUSO corretto: Q4 (basso-sinistra, Monitorare) ha potere 0-2 (non 3-5).
$quadrants = [
// code, label, strategy, power_min, power_max, interest_min, interest_max, ord
['Q1', 'Tenere soddisfatti (Keep Satisfied)',
'Stakeholder istituzionali: forte potere di blocco, scarso interesse quotidiano. Tenerli soddisfatti ed evitare scontenti improvvisi.',
3, 5, 0, 2, 1],
['Q2', 'Gestire attivamente (Manage Closely)',
'Stakeholder chiave: decisori principali, supporto vitale. Gestione attiva e coinvolgimento costante.',
3, 5, 3, 5, 2],
['Q3', 'Tenere informati (Keep Informed)',
'Stakeholder operativi: molto coinvolti ma scarso peso decisionale. Tenerli informati.',
0, 2, 3, 5, 3],
['Q4', 'Monitorare (Monitor)',
'Stakeholder marginali: basso potere e basso interesse. Monitoraggio costante col minimo sforzo, comunicazioni periodiche non dispendiose.',
0, 2, 0, 2, 4],
];
$pdo->beginTransaction();
try {
$stQ = $pdo->prepare(
"INSERT INTO cfg_stakeholder_quadrants
(code,label,strategy,power_min,power_max,interest_min,interest_max,ord,organization_id)
VALUES (?,?,?,?,?,?,?,?,NULL)
ON DUPLICATE KEY UPDATE label=VALUES(label),strategy=VALUES(strategy),
power_min=VALUES(power_min),power_max=VALUES(power_max),
interest_min=VALUES(interest_min),interest_max=VALUES(interest_max),ord=VALUES(ord)"
);
foreach ($quadrants as $q) { $stQ->execute($q); }
// 30 tipi di sistema: code PK, organization_id NULL, tipo Interno/Esterno, descr verbatim.
$stT = $pdo->prepare(
"INSERT INTO cfg_stakeholder_types (code,organization_id,tipo,descr,ord)
VALUES (?,NULL,?,?,?)
ON DUPLICATE KEY UPDATE tipo=VALUES(tipo),descr=VALUES(descr),ord=VALUES(ord)"
);
$ord = 0;
foreach ($seed['stakeholder_types'] as $t) {
$ord++;
$tipo = ($t['tipo'] === 'Interno') ? 'Interno' : 'Esterno';
$stT->execute([$t['code'], $tipo, $t['descr'], $ord]);
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
fwrite(STDERR, "SEED FALLITO: " . $e->getMessage() . "\n");
exit(1);
}
$counts = [
'quadranti' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_quadrants WHERE organization_id IS NULL")->fetchColumn(),
'tipi_sistema' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_types WHERE organization_id IS NULL")->fetchColumn(),
'tipi_interni' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_types WHERE organization_id IS NULL AND tipo='Interno'")->fetchColumn(),
'tipi_esterni' => (int) $pdo->query("SELECT COUNT(*) FROM cfg_stakeholder_types WHERE organization_id IS NULL AND tipo='Esterno'")->fetchColumn(),
];
echo "OK seed-stakeholders — " . json_encode($counts, JSON_UNESCAPED_UNICODE) . "\n";