Files
nis2-agile/scripts/migrate-a4.php
T
DevEnv nis2-agileandClaude Opus 4.8 5368b0a61c [FEAT] A4 Fase 4.2 — Competenze (skill/requisiti ruolo/gap) + alert→azione NCR+CAPA
Secondo modulo del modello relazionale A4 (docs/DESIGN_A4_RELATIONAL.md).

Backend:
- Migration 042 (docs/sql/042_competences.sql) + runner aggiornato (scripts/migrate-a4.php).
  4 tabelle additive/idempotenti: skills (org-owned o globali), role_skills
  (competenze richieste dal ruolo, UNIQUE role+skill), user_skills (possedute,
  UNIQUE org+user+skill), skill_course_map. APPLICATA su prod (nis2-db v8.0.45,
  TLSv1.3).
- CompetenceController (route 'competences'): catalog, skills CRUD, roleSkills,
  userSkills, skillCourses (map/unmap), gapGrid (richiesto−posseduto per ruolo
  con titolare), openAction. Multi-tenancy + anti-IDOR + livelli 1-5; competenze
  globali in sola lettura per gli utenti org.
- alert→azione: openAction RIUSA il workflow NCR/CAPA reale (la tabella azioni è
  capa_actions figlia di non_conformities, NON 'corrective_actions' che non
  esiste): crea NCR (source='management_review', source_entity_type='competence_gap',
  source_entity_id=role_skills.id) + capa_actions figlia. Anti-duplicato sul gap.
  Zero ALTER alle tabelle esistenti. Ancoraggio PR.AT-01/02, GV.RR-04, art.24 D.Lgs.

Frontend:
- competenze.html (4 schede: Catalogo, Requisiti per ruolo, Competenze persone,
  Gap & azioni) + js/competenze.js. Bootstrap Italia V2. CTA "Assegna corso"
  (riuso /training/assign) e "Apri non conformità".
- Voce sidebar "Competenze" (common.js + common-bi.js + BI_PAGES) + nav.competences
  i18n IT/EN. api.js: metodi comp* + assignTraining.

Help/KB/PWA:
- help.js: guida contestuale 'competences' (schede, calcolo gap, PR.AT-01/02,
  GV.RR-04, art.24 D.Lgs., disclaimer no-parere-legale) + mappa pagina.
- sw.js: nome cache nis2-shell-v1.17.0 (allineamento PWA).
- Design doc corretto (rettifica capa_actions vs corrective_actions) + avanzamento 4.1/4.2.

Cache-buster: ?v=20260618 dei 5 JS condivisi su 32 HTML. version.json 1.16.0 -> 1.17.0.
Smoke E2E su prod (fpm reale): catalogo, requisito, competenza, gap=2, openAction
(NCR+CAPA), anti-dup 409, mappatura corso — tutti verdi; dati di test ripuliti.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:01:32 +02:00

88 lines
3.2 KiB
PHP

<?php
/**
* NIS2 Agile - Runner migrazioni A4 (modello relazionale: organigramma RACI, skill,
* scadenziario, ...). Applica i file SQL di A4 usando la STESSA connessione PDO
* dell'app (config/database.php), cosi da colpire il DB realmente servito da
* php-fpm, NON il DB secondario del container.
*
* USO (sull'host, con la connessione dell'app):
* docker exec nis2-app php /var/www/nis2-agile/scripts/migrate-a4.php
*
* Idempotente: le migrazioni usano CREATE TABLE IF NOT EXISTS.
* Sola lettura/additivo su tabelle esistenti: NON modifica nessuna tabella
* preesistente. Stesso pattern di scripts/migrate-isms.php.
*/
require_once __DIR__ . '/../application/config/config.php';
require_once __DIR__ . '/../application/config/database.php';
$files = [
__DIR__ . '/../docs/sql/041_org_roles.sql', // Fase 4.1 — organigramma
__DIR__ . '/../docs/sql/042_competences.sql', // Fase 4.2 — competenze
// Le fasi 4.3-4.5 aggiungeranno qui i rispettivi file (043, ...).
];
$pdo = Database::getInstance();
$totalStmts = 0;
foreach ($files as $file) {
if (!is_readable($file)) {
fwrite(STDERR, "[migrate-a4] File non leggibile: $file\n");
exit(1);
}
$sql = (string) file_get_contents($file);
// Rimuove i commenti "--" (riga intera E in coda): un commento puo contenere
// ';' e spezzerebbe lo split. Le migrazioni non usano '--' dentro valori.
$lines = preg_split('/\r?\n/', $sql);
$clean = [];
foreach ($lines as $l) {
$pos = strpos($l, '--');
if ($pos !== false) {
$l = substr($l, 0, $pos);
}
if (trim($l) === '') {
continue;
}
$clean[] = $l;
}
$sql = implode("\n", $clean);
// Split semplice su ';' (nessun DELIMITER / stored procedure in questi file).
$statements = array_filter(array_map('trim', explode(';', $sql)), fn($s) => $s !== '');
echo "== " . basename($file) . " (" . count($statements) . " statement) ==\n";
foreach ($statements as $stmt) {
try {
$pdo->exec($stmt);
$totalStmts++;
} catch (PDOException $e) {
$head = substr(preg_replace('/\s+/', ' ', $stmt), 0, 80);
fwrite(STDERR, "[ERRORE] su \"$head...\": " . $e->getMessage() . "\n");
exit(1);
}
}
}
echo "\n[OK] Eseguiti $totalStmts statement.\n";
// Verifica rapida: tabella org_roles (4.1) e le 4 tabelle competenze (4.2).
$cols = $pdo->query("SHOW COLUMNS FROM org_roles")->fetchAll(PDO::FETCH_COLUMN, 0);
$expected = ['id','organization_id','role_name','parent_role_id','holder_user_id','is_governance_body','description','sort_order','created_by','created_at','updated_at'];
$missing = array_diff($expected, $cols);
if ($missing) {
fwrite(STDERR, "[ATTENZIONE] org_roles colonne mancanti: " . implode(', ', $missing) . "\n");
exit(1);
}
echo "[OK] org_roles pronta (A4 Fase 4.1).\n";
foreach (['skills','role_skills','user_skills','skill_course_map'] as $t) {
$c = $pdo->query("SHOW COLUMNS FROM `$t`")->fetchAll(PDO::FETCH_COLUMN, 0);
if (!$c) {
fwrite(STDERR, "[ATTENZIONE] tabella mancante: $t\n");
exit(1);
}
echo "[OK] $t pronta (" . count($c) . " colonne).\n";
}
echo "[OK] Competenze pronte (A4 Fase 4.2).\n";