[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>
This commit is contained in:
DevEnv nis2-agile
2026-06-15 19:01:32 +02:00
co-authored by Claude Opus 4.8
parent 47199f1e4e
commit 5368b0a61c
47 changed files with 1594 additions and 143 deletions
+8 -4
View File
@@ -1,7 +1,11 @@
# DESIGN — A4: Modello relazionale (organigramma RACI ↔ skill / formazione / inventario / procedure / rischi + scadenziario)
> Stato: **DESIGN (pre-implementazione)**. Contesto: segnalazioni Simon Fattori, punti **2, 3, 4, 5, 6, 7** (accolti in A4). Data: 2026-06-15.
> Stato: **IN CORSO**. Contesto: segnalazioni Simon Fattori, punti **2, 3, 4, 5, 6, 7** (accolti in A4). Data: 2026-06-15.
> È l'epic più grande → da realizzare a **FASI**. Ancoraggio normativo verificato (Det. ACN 164179/2025 + D.Lgs. 138/2024). Vedi `reference-dlgs-articles`, `DESIGN_A2_ONBOARDING.md`.
>
> **AVANZAMENTO**: ✅ **4.1 Organigramma** (migration `041_org_roles.sql`, `OrgRoleController`, `organigramma.html`) — LIVE. ✅ **4.2 Competenze** (migration `042_competences.sql`: `skills`/`role_skills`/`user_skills`/`skill_course_map`; `CompetenceController`; `competenze.html`) — LIVE. ⏳ 4.3 RACI · 4.4 scadenziario · 4.5 stakeholder.
>
> 🔴 **RETTIFICA NOMI REALI (verificata sul codice)**: questo design parlava di una tabella **`corrective_actions`** che **NON ESISTE**. La tabella delle azioni reale è **`capa_actions`** (mig.004), **figlia obbligatoria** di **`non_conformities`** (NCR) via `ncr_id NOT NULL`, e **non ha** una colonna `source_type`. L'alert "gap → azione" (impl. in 4.2) quindi **NON** crea `corrective_actions`/`source_type`: crea una **`non_conformities`** ancorata al gap (`source='management_review'`, `source_entity_type='competence_gap'`, `source_entity_id=role_skills.id`) + una **`capa_actions`** figlia. Le menzioni di `corrective_actions`/`source_type` qui sotto vanno lette in questo senso.
## 0. Principio
A4 trasforma i moduli oggi "a silos" in un **grafo relazionale** con al centro l'**organigramma** (ruoli/responsabilità) e la **matrice RACI** come hub che lega i ruoli agli oggetti (inventario, procedure, rischi). Obiettivo: compliance *vissuta e dimostrabile in audit* (chi è responsabile di cosa, con quali competenze, con quali scadenze).
@@ -31,7 +35,7 @@ A4 trasforma i moduli oggi "a silos" in un **grafo relazionale** con al centro l
- **`raci_assignments`** — **MATRICE RACI** (hub): `id, organization_id, role_id → org_roles, object_type ENUM('inventory','procedure','risk','supplier'), object_id, raci ENUM('R','A','C','I')`. → lega l'organigramma a inventario/procedure/rischi/fornitori (m2m via questa tabella).
- **link m2m diretti** (richiesti dalle segnalazioni 4/5/7): `procedure_inventory(procedure_id, asset_id)`, `procedure_risk(procedure_id, risk_id)`, `inventory_risk(asset_id, risk_id)`, `risk_measure(risk_id, measure_code)` (le misure ACN dell'assessment).
- **`review_schedule`** — **SCADENZIARIO centralizzato**: `id, organization_id, entity_type ENUM('role','skill','inventory','procedure','risk','supplier','measure'), entity_id, owner_role_id NULL, frequency_months, last_reviewed_at, next_review_date, status ENUM('ok','due','overdue'), notes`. Aggrega/sostituisce le scadenze sparse.
- **`actions`** (oppure riuso `corrective_actions`): azione generata da **alert** (gap competenza, scadenza, rischio) → `source_type, source_id, assigned_role_id, due_date, status`. *(Riuso consigliato di `corrective_actions` + un campo `source_type` per non duplicare il workflow CAPA.)*
- **azione da alert** → **riuso `non_conformities` + `capa_actions`** (mig.004): l'alert (gap competenza, scadenza, rischio) crea una NCR ancorata via `source_entity_type`/`source_entity_id` (es. `'competence_gap'` + `role_skills.id`) e una `capa_actions` figlia. *(NON esiste `corrective_actions`/`source_type`: si riusa l'intero workflow CAPA senza alterarne lo schema.)*
**Relazioni chiave** (tutte m2m dove indicato dalle segnalazioni):
```
@@ -41,11 +45,11 @@ skills ──< skill_course_map >── training_courses
procedures ──< procedure_inventory >── inventory ──< inventory_risk >── risks
procedures ──< procedure_risk >── risks ──< risk_measure >── misure ACN (assessment)
(role|skill|inventory|procedure|risk|supplier) ──1:N── review_schedule
gap competenze / scadenze / rischi ──trigger──> actions(corrective_actions)
gap competenze / scadenze / rischi ──trigger──> non_conformities ──< capa_actions (workflow CAPA)
```
## 3. Gap competenze (segnalazione 3)
**Gap = `role_skills.required_level` (del ruolo che l'utente ricopre) − `user_skills.level`** per ciascuna skill richiesta. Vista `competence_gap` (calcolata): per ogni utente con `org_roles.holder_user_id`, confronto richiesto vs posseduto. **Alert** quando gap > 0 → apertura **azione** (`corrective_actions`, `source_type='competence_gap'`) + voce nello **`review_schedule`**. Ancoraggio: **PR.AT-01** (tutti), **PR.AT-02** (solo essenziali), **GV.RR-04** (cyber nelle pratiche HR).
**Gap = `role_skills.required_level` (del ruolo che l'utente ricopre) − `user_skills.level`** per ciascuna skill richiesta. Vista `competence_gap` (calcolata): per ogni utente con `org_roles.holder_user_id`, confronto richiesto vs posseduto. **Alert** quando gap > 0 → apertura **azione** = NCR (`non_conformities`, `source_entity_type='competence_gap'`, `source_entity_id=role_skills.id`) + `capa_actions` figlia (+ in futuro voce nello **`review_schedule`**). Ancoraggio: **PR.AT-01** (tutti), **PR.AT-02** (solo essenziali), **GV.RR-04** (cyber nelle pratiche HR).
## 4. Ancoraggio normativo per entità (must-have)
- **Organigramma/ruoli** → **GV.RR-02** (ruoli, responsabilità e poteri stabiliti/comunicati) + **Art. 23 D.Lgs. 138/2024** (responsabilità organi di amministrazione e direttivi) → **`is_governance_body`** obbligatorio come nodo distinto con potere di approvazione.
+92
View File
@@ -0,0 +1,92 @@
-- ============================================================================
-- Migration 042 — A4 Fase 4.2: Competenze (skill / requisiti ruolo / competenze
-- possedute / mappatura corsi) + base per gap competenze e alert->azione.
-- ----------------------------------------------------------------------------
-- 4 tabelle ADDITIVE. Idempotente (CREATE TABLE IF NOT EXISTS). Applicare con la
-- STESSA connessione PDO dell'app (host MySQL servito da php-fpm), via runner.
--
-- Ancoraggio normativo (docs/DESIGN_A4_RELATIONAL.md sez.3/4): PR.AT-01 (tutti),
-- PR.AT-02 (solo soggetti essenziali), GV.RR-04 (cyber nelle pratiche HR).
--
-- NOTA architetturale: l'alert "gap competenza -> azione" RIUSA il workflow
-- NCR/CAPA gia esistente (tabelle non_conformities + capa_actions, mig.004).
-- NON esiste alcuna tabella 'corrective_actions' (nome errato nel design): le
-- azioni sono capa_actions, figlie obbligatorie di una non_conformity. Quindi
-- questa migration NON crea/altera tabelle azioni: ancora il gap alla NCR via
-- non_conformities.source_entity_type='competence_gap' + source_entity_id.
-- ============================================================================
-- Catalogo competenze. organization_id NULL = competenza globale di sistema
-- (visibile a tutte le org, come i corsi globali).
CREATE TABLE IF NOT EXISTS skills (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NULL,
name VARCHAR(150) NOT NULL,
area VARCHAR(100) NULL,
description TEXT NULL,
created_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_skills_org (organization_id),
INDEX idx_skills_area (area)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Competenze RICHIESTE da un ruolo dell'organigramma (m2m ruolo<->skill).
CREATE TABLE IF NOT EXISTS role_skills (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
role_id INT NOT NULL,
skill_id INT NOT NULL,
required_level TINYINT NOT NULL DEFAULT 3,
created_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES org_roles(id) ON DELETE CASCADE,
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
UNIQUE KEY uk_role_skill (role_id, skill_id),
INDEX idx_role_skills_org (organization_id),
INDEX idx_role_skills_skill (skill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Competenze POSSEDUTE da un utente (m2m utente<->skill). acquired_via_course_id
-- collega eventualmente la competenza al corso che l'ha erogata (registro corsi).
CREATE TABLE IF NOT EXISTS user_skills (
id INT AUTO_INCREMENT PRIMARY KEY,
organization_id INT NOT NULL,
user_id INT NOT NULL,
skill_id INT NOT NULL,
level TINYINT NOT NULL DEFAULT 1,
acquired_via_course_id INT NULL,
evidence VARCHAR(500) NULL,
assessed_at DATETIME NULL,
created_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE,
FOREIGN KEY (acquired_via_course_id) REFERENCES training_courses(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
UNIQUE KEY uk_user_skill (organization_id, user_id, skill_id),
INDEX idx_user_skills_user (user_id),
INDEX idx_user_skills_skill (skill_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Mappatura competenza <-> corso che la eroga (m2m). Il corso puo essere globale
-- (training_courses.organization_id NULL) o dell'org.
CREATE TABLE IF NOT EXISTS skill_course_map (
id INT AUTO_INCREMENT PRIMARY KEY,
skill_id INT NOT NULL,
training_course_id INT NOT NULL,
created_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE,
FOREIGN KEY (training_course_id) REFERENCES training_courses(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
UNIQUE KEY uk_skill_course (skill_id, training_course_id),
INDEX idx_skill_course_course (training_course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;