[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:
co-authored by
Claude Opus 4.8
parent
419162594d
commit
c782aa54fa
@@ -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";
|
||||
Reference in New Issue
Block a user