# Piano: Modulo Formazione — EvoAz Admin

**Data**: 5 maggio 2026  
**Stato**: Draft v1.0  
**Contesto**: Evoluzione del gestionale gix-evoaz per supportare la gestione completa del percorso formativo aziendale, dal reclutamento (assunto) all'attestazione finale.

---

## 1. Visione d'insieme

Il modulo Formazione si inserisce come **continuazione naturale del ciclo di vita del candidato**: un'applicazione con `final_outcome = 'assunto'` diventa un **discente** e accede al percorso formativo. Il modulo gestisce:

| Area | Descrizione |
|------|-------------|
| **Classi** | Gruppi di discenti per un corso/periodo |
| **Moduli formativi** | Unità didattiche (argomento, ore, docente, data) |
| **Presenze** | Registro presenze/assenze per ogni modulo |
| **Slide** | Collegamento a file Google Drive per presentazioni |
| **Test di valutazione** | Quiz inviato via mail al termine del percorso |
| **Risultati test** | Raccolta risposte, score, pass/fail per discente |

---

## 2. Stato attuale e raccordo con il sistema

### 2.1 Il candidato "assunto" oggi

```
applications (pipeline_status = 'assunto', final_outcome = 'assunto')
  ├─ id
  ├─ first_name, last_name, email, phone
  ├─ role (ruolo per cui è stato assunto)
  ├─ evaluation_total_score
  └─ anonymized_at = NULL  ← escluso da retention GDPR (⚠️ resta in questa tabella)
```

Il sistema attuale **non prevede una tabella dedicata ai dipendenti/discenti**: i dati dell'assunto restano in `applications`. Il modulo Formazione dovrà:
- Usare `applications.id` come FK per collegare i discenti alle classi
- Eventualmente (fase futura) prevedere una tabella `trainees` separata per discenti non provenienti dal recruiting

### 2.2 Tab "Formazione obbl." già esistente

Nel navItems sotto `Governance` esiste già un nodo `{ id: 'formazione', label: 'Formazione obbl.' }` che mostra solo il documento markdown del piano formativo 231. **Il nuovo modulo sarà un'area separata**, non sotto Governance.

---

## 3. Schema database — Nuove tabelle

### 3.1 `training_courses` — Corsi formativi

```sql
CREATE TABLE training_courses (
  id              INT UNSIGNED    AUTO_INCREMENT PRIMARY KEY,
  title           VARCHAR(200)    NOT NULL,
  description     TEXT            DEFAULT NULL,
  year            YEAR            NOT NULL,
  status          ENUM('planned','active','completed','archived') NOT NULL DEFAULT 'planned',
  created_by      INT UNSIGNED    DEFAULT NULL,  -- FK users.id
  created_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 3.2 `training_modules` — Moduli didattici

```sql
CREATE TABLE training_modules (
  id              INT UNSIGNED    AUTO_INCREMENT PRIMARY KEY,
  course_id       INT UNSIGNED    NOT NULL,
  title           VARCHAR(200)    NOT NULL,
  description     TEXT            DEFAULT NULL,
  trainer         VARCHAR(150)    DEFAULT NULL,     -- nome docente/relatore
  hours           DECIMAL(4,2)    DEFAULT NULL,     -- durata in ore
  scheduled_at    DATETIME        DEFAULT NULL,     -- data/ora prevista
  done_at         DATETIME        DEFAULT NULL,     -- data/ora effettiva
  slides_url      VARCHAR(500)    DEFAULT NULL,     -- link Google Drive
  sort_order      INT             NOT NULL DEFAULT 0,
  created_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_tm_course FOREIGN KEY (course_id) REFERENCES training_courses(id) ON DELETE CASCADE,
  KEY idx_tm_course (course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 3.3 `training_enrollments` — Iscrizioni discenti

```sql
CREATE TABLE training_enrollments (
  id              INT UNSIGNED    AUTO_INCREMENT PRIMARY KEY,
  course_id       INT UNSIGNED    NOT NULL,
  application_id  INT UNSIGNED    NOT NULL,         -- FK applications.id
  notes           TEXT            DEFAULT NULL,
  enrolled_at     DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_te_course  FOREIGN KEY (course_id)     REFERENCES training_courses(id)  ON DELETE CASCADE,
  CONSTRAINT fk_te_app     FOREIGN KEY (application_id) REFERENCES applications(id)      ON DELETE CASCADE,
  UNIQUE KEY uq_enrollment (course_id, application_id),
  KEY idx_te_course (course_id),
  KEY idx_te_app (application_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 3.4 `training_attendance` — Presenze per modulo

```sql
CREATE TABLE training_attendance (
  id              INT UNSIGNED    AUTO_INCREMENT PRIMARY KEY,
  module_id       INT UNSIGNED    NOT NULL,
  enrollment_id   INT UNSIGNED    NOT NULL,         -- FK training_enrollments.id
  status          ENUM('present','absent','justified') NOT NULL DEFAULT 'present',
  notes           TEXT            DEFAULT NULL,
  recorded_at     DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_ta_module     FOREIGN KEY (module_id)     REFERENCES training_modules(id)     ON DELETE CASCADE,
  CONSTRAINT fk_ta_enrollment FOREIGN KEY (enrollment_id) REFERENCES training_enrollments(id) ON DELETE CASCADE,
  UNIQUE KEY uq_attendance (module_id, enrollment_id),
  KEY idx_ta_module (module_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 3.5 `training_tests` — Test di valutazione

```sql
CREATE TABLE training_tests (
  id              INT UNSIGNED    AUTO_INCREMENT PRIMARY KEY,
  course_id       INT UNSIGNED    NOT NULL,
  title           VARCHAR(200)    NOT NULL,
  description     TEXT            DEFAULT NULL,
  time_limit_min  INT             DEFAULT NULL,     -- tempo massimo in minuti (NULL = illimitato)
  pass_score      INT             NOT NULL DEFAULT 70,  -- punteggio minimo per superare (0-100)
  questions_json  LONGTEXT        NOT NULL,          -- JSON con struttura domande
  active          TINYINT(1)      NOT NULL DEFAULT 0,
  created_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at      DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  CONSTRAINT fk_tt_course FOREIGN KEY (course_id) REFERENCES training_courses(id) ON DELETE CASCADE,
  KEY idx_tt_course (course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

**Struttura `questions_json`**:
```json
[
  {
    "id": 1,
    "text": "Qual è il principale obiettivo del Modello 231?",
    "type": "single",
    "options": [
      { "id": "a", "text": "Aumentare i profitti aziendali" },
      { "id": "b", "text": "Prevenire i reati d'impresa e la responsabilità amministrativa" },
      { "id": "c", "text": "Gestire le risorse umane" }
    ],
    "correct": "b",
    "points": 10
  }
]
```

Tipi domanda supportati: `single` (risposta singola), `multi` (risposta multipla — fase 2).

### 3.6 `training_test_tokens` — Token per accesso al test

```sql
CREATE TABLE training_test_tokens (
  id              INT UNSIGNED    AUTO_INCREMENT PRIMARY KEY,
  test_id         INT UNSIGNED    NOT NULL,
  enrollment_id   INT UNSIGNED    NOT NULL,
  token           VARCHAR(64)     NOT NULL UNIQUE,  -- UUID sicuro
  sent_at         DATETIME        DEFAULT NULL,
  expires_at      DATETIME        NOT NULL,
  used_at         DATETIME        DEFAULT NULL,     -- NULL = non ancora usato
  CONSTRAINT fk_ttt_test       FOREIGN KEY (test_id)       REFERENCES training_tests(id)       ON DELETE CASCADE,
  CONSTRAINT fk_ttt_enrollment FOREIGN KEY (enrollment_id) REFERENCES training_enrollments(id) ON DELETE CASCADE,
  KEY idx_ttt_token (token),
  KEY idx_ttt_test (test_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 3.7 `training_test_results` — Risultati test

```sql
CREATE TABLE training_test_results (
  id              INT UNSIGNED    AUTO_INCREMENT PRIMARY KEY,
  token_id        INT UNSIGNED    NOT NULL UNIQUE,  -- 1 risultato per token
  test_id         INT UNSIGNED    NOT NULL,
  enrollment_id   INT UNSIGNED    NOT NULL,
  answers_json    LONGTEXT        NOT NULL,          -- JSON risposte date
  score           INT             NOT NULL,          -- 0-100
  passed          TINYINT(1)      NOT NULL,
  completed_at    DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT fk_ttr_token      FOREIGN KEY (token_id)      REFERENCES training_test_tokens(id)  ON DELETE CASCADE,
  CONSTRAINT fk_ttr_test       FOREIGN KEY (test_id)        REFERENCES training_tests(id)        ON DELETE CASCADE,
  CONSTRAINT fk_ttr_enrollment FOREIGN KEY (enrollment_id)  REFERENCES training_enrollments(id)  ON DELETE CASCADE,
  KEY idx_ttr_enrollment (enrollment_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

**Struttura `answers_json`**:
```json
[
  { "question_id": 1, "answer": "b" },
  { "question_id": 2, "answer": "a" }
]
```

---

## 4. API Backend — Nuovi endpoint

Tutti protetti da `authMiddleware` (token `full`) salvo dove indicato.

### 4.1 Corsi

| Metodo | Endpoint | Descrizione |
|--------|----------|-------------|
| GET | `/api/admin/training/courses` | Lista corsi |
| POST | `/api/admin/training/courses` | Crea corso |
| PUT | `/api/admin/training/courses/:id` | Aggiorna corso |
| PATCH | `/api/admin/training/courses/:id/status` | Cambia stato |
| DELETE | `/api/admin/training/courses/:id` | Elimina corso |

### 4.2 Moduli

| Metodo | Endpoint | Descrizione |
|--------|----------|-------------|
| GET | `/api/admin/training/courses/:courseId/modules` | Lista moduli del corso |
| POST | `/api/admin/training/courses/:courseId/modules` | Crea modulo |
| PUT | `/api/admin/training/modules/:id` | Aggiorna modulo |
| DELETE | `/api/admin/training/modules/:id` | Elimina modulo |

### 4.3 Iscrizioni (classe)

| Metodo | Endpoint | Descrizione |
|--------|----------|-------------|
| GET | `/api/admin/training/courses/:courseId/enrollments` | Lista discenti iscritti (con dati da applications) |
| POST | `/api/admin/training/courses/:courseId/enrollments` | Iscrive discente (application_id) |
| DELETE | `/api/admin/training/enrollments/:id` | Rimuovi iscrizione |
| GET | `/api/admin/training/eligible` | Lista assunti iscrivibili (applications con final_outcome='assunto') |

### 4.4 Presenze

| Metodo | Endpoint | Descrizione |
|--------|----------|-------------|
| GET | `/api/admin/training/modules/:moduleId/attendance` | Presenze del modulo |
| PUT | `/api/admin/training/modules/:moduleId/attendance` | Salva presenze (bulk) — body: `[{enrollment_id, status, notes}]` |

### 4.5 Test

| Metodo | Endpoint | Descrizione |
|--------|----------|-------------|
| GET | `/api/admin/training/courses/:courseId/tests` | Lista test del corso |
| POST | `/api/admin/training/courses/:courseId/tests` | Crea test |
| PUT | `/api/admin/training/tests/:id` | Aggiorna test |
| DELETE | `/api/admin/training/tests/:id` | Elimina test |
| POST | `/api/admin/training/tests/:id/send` | Invia mail con link test a discenti selezionati |
| GET | `/api/admin/training/tests/:id/results` | Lista risultati test |

### 4.6 Endpoint pubblico test (NO AUTH)

| Metodo | Endpoint | Descrizione |
|--------|----------|-------------|
| GET | `/api/training/test/:token` | Carica test da token (valida scadenza, usato) |
| POST | `/api/training/test/:token/submit` | Invia risposte e calcola score |

> **Sicurezza**: il token è un UUID v4 di 64 caratteri, one-time use. Dopo `submit` viene segnato `used_at`. Nessun dato personale viene esposto nell'endpoint GET — solo le domande (senza `correct`).

---

## 5. Struttura frontend — Nuova area "Formazione"

### 5.1 NavItem da aggiungere

```javascript
{
  id: 'training',
  label: 'Formazione',
  icon: 'fa-chalkboard-user',
  children: [
    { id: 'training-courses',   label: 'Corsi',            icon: 'fa-layer-group' },
    { id: 'training-class',     label: 'Classe / Presenze', icon: 'fa-users' },
    { id: 'training-tests',     label: 'Test di valutazione', icon: 'fa-circle-question' },
    { id: 'training-results',   label: 'Risultati',         icon: 'fa-chart-bar' },
  ]
}
```

### 5.2 Tab: Corsi (`training-courses`)

**Vista lista corsi**:
- Card per ogni corso: titolo, anno, stato (badge colore), n. moduli, n. discenti
- Filtri: anno, stato
- Pulsante "Nuovo corso"
- Click su corso → apre dettaglio inline (o navigazione a sotto-tab)

**Dettaglio corso (inline expandable)**:
- Modifica titolo, descrizione, anno, stato
- **Sezione Moduli**: lista moduli in ordine (`sort_order`), drag-reorder (fase 2), form aggiungi modulo
  - Per ogni modulo: titolo, docente, ore, data/ora prevista, data/ora effettiva, link slide (icona Google Drive)
  - Espandi modulo → slide iframe embed (o link apri in tab)
- Pulsante "Salva"

### 5.3 Tab: Classe / Presenze (`training-class`)

**Selettore corso** (select in cima)

**Sezione Classe (discenti iscritti)**:
- Tabella: Nome, Cognome, Email, Ruolo, Data iscrizione, Azioni (rimuovi)
- Pulsante "+ Aggiungi discente" → modale con ricerca tra gli assunti (GET `/api/admin/training/eligible`)
- Nel modale: autocomplete nome/email, mostra ruolo, score, data assunzione → Conferma iscrizione

**Sezione Presenze**:
- Select modulo (tra i moduli del corso selezionato)
- Tabella registro: Nome Discente | Modulo X — data | Presenza (presente/assente/giustificato) | Note
- Salvataggio bulk con un unico "Salva presenze"
- Indicatore % presenze per discente (totale moduli fatti / presenti)

### 5.4 Tab: Test di valutazione (`training-tests`)

**Selettore corso**

**Lista test del corso**:
- Per ogni test: titolo, n. domande, punteggio minimo, stato (attivo/inattivo), n. invii, n. completati
- Pulsante "Nuovo test"
- Azioni: modifica, elimina, attiva/disattiva, **invia**, visualizza risultati

**Editor test (inline form)**:
- Titolo, descrizione, tempo limite, punteggio minimo per superare
- **Builder domande**:
  - Lista domande riordinabili
  - Per ogni domanda: testo, tipo (`single`), lista opzioni (+ aggiungi opzione), risposta corretta (radio)
  - Punti per domanda (default: uguale per tutte, `100 / n_domande`)
  - Pulsante "+ Aggiungi domanda"

**Pannello invio mail**:
- Selezione discenti da inviare (checkbox lista classe)
- Preview mail
- Pulsante "Invia test"
- Dopo invio: log {discente, inviato_at, scadenza}

### 5.5 Tab: Risultati (`training-results`)

**Selettore corso + test**

**Tabella risultati**:
- Nome discente, Email, Data completamento, Score (%), Superato (badge), Azioni (dettaglio risposte)

**Dettaglio risposte** (modale):
- Per ogni domanda: testo domanda, risposta data, risposta corretta, ✅/❌
- Score finale e outcome

**Export CSV** (pulsante) — scarica risultati del test selezionato

---

## 6. Pagina pubblica test (No Auth)

### 6.1 Nuova route frontend

```javascript
{ path: '/test/:token', component: TestView }
```

### 6.2 `TestView.vue` — Flusso utente

**Step 1 — Landing / identificazione**:
- Logo Evoluzione Azienda
- Titolo corso + titolo test
- Nome discente (caricato dal token — solo nome, no dati sensibili)
- Istruzioni: n. domande, tempo limite, una sola opportunità
- Pulsante **"Inizia il test"** (che avvia il timer lato frontend)

**Step 2 — Domande**:
- Barra progresso (domanda X di N)
- Timer countdown (se `time_limit_min` impostato)
- Testo domanda + opzioni (radio button)
- Navigazione Avanti / Indietro (non obbligatoria rispondere subito)
- Pulsante **"Invia risposte"** (visibile dall'ultima domanda o da sempre)
- Conferma prima di invio ("Hai risposto a X/N domande, confermi?")

**Step 3 — Risultato**:
- Score: `N%`
- Esito: ✅ **Superato** / ❌ **Non superato** (con punteggio minimo richiesto)
- "Grazie per aver completato il test. Il tuo esito è stato registrato."
- **Nessun dettaglio sulle risposte esposte** (anti-copia)

**Casi errore**:
- Token non trovato → 404 con messaggio
- Token scaduto → messaggio "Il link è scaduto, contatta il referente formazione"
- Token già usato → "Hai già completato questo test"

---

## 7. Email: template invito test

Nuova funzione `sendTestInviteEmail({name, email, testTitle, courseTitle, testUrl, expiresAt})`:

```html
Oggetto: 📋 Test di valutazione: {testTitle} — Evoluzione Azienda

Corpo:
  Ciao {name},
  
  Hai ricevuto questo messaggio perché sei iscritto al corso di formazione
  "{courseTitle}".
  
  È il momento di completare il test di valutazione:
  
  ┌─────────────────────────────────────────┐
  │  [Accedi al test →]  (link con token)   │
  └─────────────────────────────────────────┘
  
  Il link è personale e scade il {expiresAt}.
  Non condividerlo con nessuno.
  
  Per supporto: formazione@evoluzioneazienda.it
```

---

## 8. Fasi di implementazione

### Fase 1 — Infrastruttura e Corsi (priorità alta)
- [ ] Migrazioni DB: 7 nuove tabelle in `index.js`
- [ ] `trainingController.js` — CRUD corsi + moduli
- [ ] `trainingEnrollmentController.js` — iscrizioni + lista assunti eligible
- [ ] Aggiunta endpoint in `allRoutes.js`
- [ ] Frontend: navItem + tab `training-courses`
- [ ] Frontend: tab `training-class` (iscrizioni + presenze)

### Fase 2 — Test e invio mail
- [ ] `trainingTestController.js` — CRUD test + builder domande + invio mail
- [ ] `training_test_tokens` generazione UUID crypto-safe
- [ ] `sendTestInviteEmail` in `emailService.js`
- [ ] Endpoint pubblici `/api/training/test/:token` + `/submit`
- [ ] Nuova route `/test/:token` in `router/index.js`
- [ ] Nuovo componente `TestView.vue`
- [ ] Frontend: tab `training-tests` + editor + pannello invio

### Fase 3 — Risultati e reportistica
- [ ] Tab `training-results` con tabella + modale dettaglio risposte
- [ ] Export CSV risultati
- [ ] Dashboard riepilogativa: % presenze per discente, score medio, superati/non superati
- [ ] (Opzionale) PDF attestato di partecipazione

### Fase 4 — Slide e miglioramenti UX
- [ ] Embed Google Drive iframe con validazione URL
- [ ] Proiezione slide: link fullscreen per presentare in aula
- [ ] Drag & drop riordinamento moduli
- [ ] Tipo domanda `multi` (risposta multipla) nel builder

---

## 9. Note tecniche e vincoli

### 9.1 Slide da Google Drive

Google Drive consente l'embed tramite:
```
https://docs.google.com/presentation/d/{FILE_ID}/embed?start=false&loop=false&delayms=3000
```
Il campo `slides_url` accetta sia l'URL di condivisione normale che l'URL embed. Il backend valida che sia un dominio `docs.google.com` o `drive.google.com`.

### 9.2 Sicurezza token test

- Token generato con `crypto.randomBytes(32).toString('hex')` (64 char hex, 256 bit di entropia)
- Scadenza default: 72 ore dall'invio
- One-time use: dopo `submit`, `used_at` viene impostato e il token viene rifiutato
- Endpoint pubblico rate-limited: max 20 req/ora per IP

### 9.3 Discenti non provenienti dal recruiting

Nella fase 1 i discenti sono esclusivamente assunti via pipeline recruiting (`applications.final_outcome = 'assunto'`). Se in futuro si devono aggiungere discenti esterni (es. dipendenti pre-esistenti), si valuterà una tabella `trainees` separata con FK opzionale su `applications.id`.

### 9.4 GDPR e retention

I dati di `training_enrollments`, `training_attendance` e `training_test_results` legati a un'applicazione **non vengono anonimizzati** finché il dipendente è attivo. La policy di retention dovrà essere aggiornata per gestire i discenti separatamente dai candidati scartati.

### 9.5 Integrazione con il tab "Formazione obbl." esistente

Il tab `formazione` sotto `Governance` continua ad esistere come documentazione normativa (piano formazione 231). Il nuovo modulo ha ID `training` nell'albero di navigazione ed è collocato come voce di primo livello nella sidebar, **separato dalla Governance**.

---

## 10. Schema ER semplificato

```
applications (assunto)
    │ 1
    │
    ▼ N
training_enrollments ◄──── training_courses ◄──── training_modules
    │                           │                      │
    │                           │                      │
    │ 1                         │ 1                    │ 1
    │                           │                      │
    ▼ N                         ▼ N                    ▼ N
training_attendance         training_tests         training_attendance
                                │
                                │ 1
                                ▼ N
                        training_test_tokens
                                │
                                │ 1
                                ▼ 1
                        training_test_results
```
