Newer
Older
itcca-allievi / .cursor / plans / itcca_allievi_wp_plugin.plan.md
---
name: itcca allievi wp plugin
overview: "Plugin WordPress per gestire le iscrizioni ai corsi di Tai Chi: estende l'utente WP con ~40 campi (mappati sul CSV INSARRI), form pubblico via shortcode, gestione completa nel backoffice e sincronizzazione bidirezionale con un Google Sheet privato via OAuth 2.0. Tutto sviluppato e testato in stack Docker locale persistente."
todos:
  - id: docker
    content: Scrivere docker-compose.yml + .env + .gitignore + README con istruzioni Google Cloud Console (OAuth client)
    status: completed
  - id: plugin-bootstrap
    content: Creare scheletro plugin (itcca-allievi.php, class-plugin.php, composer.json con google/apiclient) e attivazione/disattivazione
    status: completed
  - id: fields-roles
    content: Implementare class-fields.php (registro centrale dei 41 meta + tipi/validatori/sezioni) e class-roles.php (ruolo allievo)
    status: completed
  - id: validators
    content: "Validatori: codice fiscale con checksum, CAP, telefono, sesso M/F, date"
    status: completed
  - id: user-admin
    content: "Estensione pagina utente WP: sezioni a fisarmonica per ogni gruppo di campi + età/anni calcolati read-only"
    status: completed
  - id: allievi-list
    content: Pagina admin Allievi ITCCA con WP_List_Table (tutte le colonne CSV, sort, filtri, quick-edit flag)
    status: completed
  - id: form-public
    content: "Shortcode [itcca_iscrizione]: template, CSS con variabili che ereditano dal tema, JS di validazione client, handler POST con creazione utente e nonce/honeypot"
    status: completed
  - id: settings
    content: "Pagina impostazioni: Google client ID/secret, lista centri (per dropdown admin), testo privacy form, redirect post-iscrizione, colore primario"
    status: completed
  - id: google-oauth
    content: "Implementare flow OAuth 2.0 (scope drive.file + spreadsheets): connect, callback, salvataggio token cifrati, refresh automatico"
    status: completed
  - id: google-picker
    content: "Integrare Google Picker API: bottone 'Seleziona foglio da Drive', salva spreadsheet_id+nome, select tab, storico fogli connessi"
    status: completed
  - id: phase-a-align
    content: "Fase A (foglio → WP): importer con match per CF, report diff, preview con checkbox, apply su user_meta + creazione nuovi allievi"
    status: completed
  - id: phase-b-push
    content: "Fase B (WP → foglio): hook on save, mappatura user_id ↔ riga, retry via WP-Cron, log errori in admin, bottone manuale 'Pusha tutti'"
    status: completed
  - id: export-csv
    content: Export CSV nel formato originale INSARRI + WP-CLI import comando per popolazione iniziale
    status: completed
  - id: gdpr
    content: Hook GDPR exporter/eraser e rate limiting form pubblico
    status: completed
  - id: test-docker
    content: Avviare lo stack, completare setup WP, configurare OAuth, fare end-to-end test (iscrizione → utente → riga Sheet)
    status: completed
  - id: schema-reconcile
    content: "Riconciliazione schema da foglio: refactor Fields (baseline + overrides), nuova SchemaReconcile (diff + fuzzy match + UI), routing Picker→pagina schema, sezione 'Campi rimossi (storico)' nell'edit utente, esclusione dei deleted dall'export CSV e dal push Fase B"
    status: completed
  - id: zodiac-elements
    content: "Dropdown Animale (zodiaco cinese auto-calcolato + override) ed Elemento/Elemento sx/Elemento dx con palette colori; visibilità in lista; Dashboard con 4 grafici a torta inline SVG"
    status: completed
  - id: oauth-wizard
    content: "Wizard di setup Google OAuth in 5 step con link diretti a Cloud Console, validazione live delle credenziali e copia redirect URI"
    status: completed
  - id: dashboard-kpi-age
    content: "KPI età sulla Dashboard: card Età media, Più giovane (nome+anni), Più anziano (nome+anni) e bar chart nascite per mese"
    status: completed
  - id: sedi-crud
    content: "CRUD Sedi di insegnamento in Settings (nome + indirizzo + lat/lng), migrazione da itcca_centri, integrazione con dropdown Centro"
    status: completed
  - id: geocoder
    content: "Servizio Geocoder via OSM Nominatim (rate-limit 1 req/sec, cache hash indirizzo, User-Agent dedicato, retry tramite endpoint AJAX a singolo step)"
    status: completed
  - id: dashboard-map
    content: "Mappa Leaflet sotto le torte con marker sedi (icona ★) + marker residenze allievi (●), bottone batch-geocode con progress, layer toggle"
    status: completed
  - id: sedi-abandoned
    content: "Flag 'Abbandonata' sulle Sedi: checkbox in tabella Settings + riga visiva 'disattivata' + marker grigio in scala di grigi sulla mappa + layer dedicato 'Sedi abbandonate'"
    status: completed
  - id: active-flag
    content: "Stato attivo/inattivo derivato dai tab del foglio (INSARRI = attivi, ZZZ auto-rilevato = inattivi): meta itcca_active + itcca_sheet_tab, Fase A dual-tab con anteprima e log esteso, Fase B routing sul tab giusto senza spostare righe, views Attivi/Inattivi/Tutti nella lista admin e nella dashboard, marker inattivi differenziati sulla mappa"
    status: completed
isProject: false
---

## Stack e cartella di lavoro

Tutto in `/Users/fabio.arrigoni/Studio/Itcca-allievi/`:

```
Itcca-allievi/
├── .cursor/
│   └── plans/
│       └── itcca_allievi_wp_plugin.plan.md   # questo file (fonte di verità del piano)
├── docker-compose.yml
├── .env                           # secrets DB
├── .gitignore
├── README.md                      # setup & istruzioni Google Cloud
└── plugin/
    └── itcca-allievi/             # montato in wp-content/plugins
```

### Docker stack

- `wordpress:6.7-php8.3-apache` su porta `8080`
- `mariadb:11` con volume nominato `db_data` (persistenza DB)
- `phpmyadmin` su porta `8081` (debug)
- Volume nominato `wp_data` per tutto `/var/www/html` (persistenza media/temi/altri plugin)
- Bind-mount di `./plugin/itcca-allievi` su `/var/www/html/wp-content/plugins/itcca-allievi` (hot-reload del codice)
- `.env` con `DB_ROOT_PASSWORD`, `DB_PASSWORD`, `WP_DEBUG=1`
- `WP_HOME`/`WP_SITEURL` su `http://localhost:8080`

## Plugin `itcca-allievi`

### Struttura file

```
itcca-allievi/
├── itcca-allievi.php              # main file + bootstrap
├── composer.json                  # google/apiclient ^2.18
├── vendor/                        # via composer install
├── includes/
│   ├── class-plugin.php           # singleton, registrazione hook
│   ├── class-fields.php           # registro centrale dei meta + tipi
│   ├── class-roles.php            # ruolo "allievo"
│   ├── class-validators.php       # CF (regex+checksum), CAP, telefono, email
│   ├── class-form-shortcode.php   # [itcca_iscrizione] + handler POST
│   ├── class-user-admin.php       # sezioni edit utente + colonne list table
│   ├── class-allievi-list.php     # pagina "Allievi" con tutte le colonne CSV
│   ├── class-settings.php         # pagina impostazioni
│   ├── class-google-oauth.php     # OAuth flow + token refresh
│   ├── class-google-picker.php    # Google Picker UI per selezione foglio da Drive
│   ├── class-google-sheets.php    # read/append/update righe via Sheets API
│   ├── class-sheet-history.php    # storico fogli usati negli anni
│   ├── class-schema-reconcile.php # riconciliazione schema (diff colonne foglio vs WP)
│   ├── class-phase-a-align.php    # Fase A: importer foglio → WP con diff/preview
│   ├── class-phase-b-push.php     # Fase B: push WP → foglio on save + retry
│   └── class-export-csv.php       # export formato INSARRI
├── assets/
│   ├── css/form.css               # variabili CSS che ereditano dal tema
│   ├── css/admin.css
│   └── js/form.js                 # validazione client + calcolo età live
└── templates/
    └── form-iscrizione.php
```

### Mappatura campi CSV → user meta

Tutti i campi salvati come `user_meta` con prefisso `itcca_`. Riepilogo delle 41 colonne del CSV `INSARRI`:

- **Anagrafica**: `itcca_ins` (default `INSARRI` dalle impostazioni), `itcca_cognome`, `itcca_nome`, `itcca_sesso` (M/F), `itcca_nascita_data` (Y-m-d), `itcca_nascita_luogo`, `itcca_cf`
- **Residenza**: `itcca_indirizzo`, `itcca_civico`, `itcca_comune`, `itcca_cap`
- **Contatti**: usa `user_email` nativo + `itcca_cellulare`
- **Calcolati (non salvati)**: `X` = età da `nascita_data`, `Anno` = anni di pratica da `inizio`
- **Tesseramento**: `itcca_sede_u`, `itcca_centro`, `itcca_ruolo`, `itcca_grado`, `itcca_inizio` (anno)
- **Quote**: `itcca_quota_uisp`, `itcca_quota_itcca`, `itcca_quota_ado` (decimal)
- **Ricevuta/pagamento**: `itcca_pag_chi`, `itcca_pag_il`, `itcca_ric_il`, `itcca_ric_n`, `itcca_att`, `itcca_pag`, `itcca_pro`, `itcca_ric`, `itcca_onl` (i 5 flag come booleani)
- **Stato**: `itcca_r` (R/N), `itcca_a` (A/D)
- **UISP card**: `itcca_uisp_n`, `itcca_uisp_d`
- **Altro**: `itcca_doc`, `itcca_animale`, `itcca_elem`, `itcca_el_sx`, `itcca_el_dx`, `itcca_note`

Il registro è centralizzato in `class-fields.php` in modo che list table, edit page, form e Google Sheets condividano la stessa fonte di verità (label, tipo, validatore, sezione, visibilità nel form pubblico).

### Form pubblico (shortcode `[itcca_iscrizione]`)

**Tutti i campi sono obbligatori** (asterisco rosso in UI, attributo HTML `required`, ricontrollo server-side). La sede del corso e tutti gli altri campi CSV non elencati qui li compili tu manualmente nel backoffice.

Campi visibili al pubblico:

- **Cognome** *(obbligatorio, testo)*
- **Nome** *(obbligatorio, testo)*
- **Sesso** *(obbligatorio, radio M / F)*
- **Data di nascita** *(obbligatorio, date picker, deve essere data valida nel passato)*
- **Comune di nascita** *(obbligatorio, testo libero, es. "Osio Sotto-BG")*
- **Codice fiscale** *(obbligatorio, validato lato client e server)*
- **Indirizzo di residenza** *(obbligatorio, testo)*
- **Numero civico** *(obbligatorio, testo breve)*
- **Comune di residenza** *(obbligatorio, testo)*
- **CAP** *(obbligatorio, esattamente 5 cifre)*
- **Email** *(obbligatorio, validato lato client e server)*
- **Cellulare** *(obbligatorio, solo cifre + opzionale prefisso `+`, lunghezza 9-15)*
- **Checkbox consenso privacy + liberatoria** *(obbligatorio, testo configurabile dalle impostazioni)*
- Honeypot anti-bot nascosto + nonce WordPress (non visibili all'utente)

**Validazioni**:

- **Codice fiscale**: client-side regex `^[A-Z]{6}[0-9]{2}[A-Z][0-9]{2}[A-Z][0-9]{3}[A-Z]$` + server-side checksum algoritmo ufficiale (carattere 16 calcolato dai primi 15 secondo tabella ministeriale). Inoltre controllo unicità: due iscritti non possono avere lo stesso CF.
- **Email**: client-side regex HTML5 + server-side `is_email()` di WordPress. Controllo unicità su `wp_users.user_email`.
- **CAP**: regex `^[0-9]{5}$`.
- **Cellulare**: regex `^\+?[0-9]{9,15}$`, dopo strip degli spazi.
- **Data di nascita**: parsing in `Y-m-d`, deve essere nel passato e non oltre 120 anni fa.
- **Coerenza CF ↔ altri campi**: se il sesso/data nascita/comune nascita estraibili dal CF non corrispondono ai dati inseriti, mostro un warning ma NON blocco (in alcuni casi limite il CF storico può divergere). Il blocco lo facciamo solo sui dati malformati, non sulle incongruenze.

Errori di validazione mostrati inline sotto ogni campo (sia da JS che da PHP al re-render del form dopo submit).

Flusso submit:
1. Validazione client-side immediata (JS) per CF, email, CAP, cellulare, campi required
2. Validazione server-side al POST (tutta, indipendentemente dal client)
3. Crea utente WP con ruolo `allievo`, username = slug di `cognome.nome`, password random + email "imposta password"
4. Salva tutti i meta
5. Email notifica all'admin
6. Push riga su Google Sheet (vedi sotto), con retry differito via WP-Cron se la chiamata fallisce
7. Redirect a pagina di conferma (configurabile)

**Tema grafico**: il CSS usa `currentColor`, variabili CSS (`--itcca-primary`) e classi WP standard (`.wp-block-button`, ecc.) per ereditare dal tema attivo. Una sezione "Aspetto" nelle impostazioni permette di sovrascrivere il colore primario. Se vuoi una resa pixel-perfect del tema vivo, dopo la prima iterazione mi passi URL o screenshot e raffino.

### Backoffice WordPress

Due punti di accesso:

1. **Pagina utente standard** (`/wp-admin/user-edit.php`): sezioni a fisarmonica
   - Anagrafica, Residenza, Tesseramento, Quote, Ricevuta/Pagamento, Stato, UISP card, Note
   - Età e anni di pratica mostrati come read-only calcolati

2. **Nuova pagina "Allievi ITCCA"** (`/wp-admin/admin.php?page=itcca-allievi`)
   - `WP_List_Table` con tutte le 41 colonne del CSV, ordinabili
   - Filtri rapidi: per `Centro`, per `R` (rinnovi/nuovi), per `A` (attivi/disattivi), per anno `Inizio`
   - Ricerca per cognome/nome/CF
   - Quick-edit inline per i flag (`Att`, `Pag`, `Pro`, `Ric`, `Onl`, `R`, `A`)
   - Azione bulk: "Sincronizza con Google Sheet"
   - Bottone "Esporta CSV" che produce un file identico per schema a `INSARRI.csv`

### Integrazione Google Sheets (OAuth 2.0 + Picker)

**Pagina impostazioni** `Allievi ITCCA → Impostazioni`:

- Sezione "Google":
  - Campi `OAuth Client ID`, `OAuth Client Secret`, `Picker API Key` (li ricavi da Google Cloud Console, istruzioni nel README)
  - Bottone "Connetti Google" → avvia OAuth, callback su `?page=itcca-allievi-oauth`, salva access/refresh token
- Sezione "Foglio Google attivo":
  - Bottone "Seleziona foglio da Drive" → apre il **Google Picker** (esploratore Drive nativo, con ricerca/recenti/Drive condivisi) filtrato sui soli Google Sheets
  - Dopo selezione mostra: nome foglio, ID, link, data ultima Fase A
  - Select del tab (popolato via Sheets API)
  - Mapping colonne ↔ campi (precompilato dal CSV INSARRI, editabile)
  - Bottoni azione: "Riallinea da foglio (Fase A)", "Pusha tutti su foglio (Fase B forzata)", "Test connessione"
- Sezione "Storico fogli usati":
  - Tabella sola lettura: anno, nome foglio, ID, link Drive, data connessione, data disconnessione, ultima Fase A
  - Bottone per riconnettere un foglio dello storico se necessario

**Implementazione OAuth**:

- Composer dipendenza: `google/apiclient`
- Tu crei su Google Cloud Console (istruzioni nel README):
  1. Un **OAuth Client ID** di tipo "Web application"
  2. Una **API Key** (per il Picker, frontend-only, ristretta per referrer)
- Scope richiesti:
  - `https://www.googleapis.com/auth/spreadsheets` (lettura/scrittura del foglio scelto)
  - `https://www.googleapis.com/auth/drive.file` (scope minimo: accesso ai soli file aperti tramite Picker — più privacy-friendly del `drive.readonly`)
- Redirect URI: `http://localhost:8080/wp-admin/admin.php?page=itcca-allievi-oauth` (in produzione cambierai con il dominio)
- Access + refresh token cifrati con `AUTH_KEY` e salvati in `wp_options`
- Refresh automatico alla scadenza

**Modello di sincronizzazione a due fasi**

Il cambio anno è un'operazione esplicita: ogni anno selezioni un foglio nuovo dal Picker. Il foglio del nuovo anno è già pre-popolato manualmente da te, con alcune celle modificate rispetto al precedente. Per gestire questo workflow il plugin opera in due fasi distinte e sequenziali.

**Fase A — Allineamento iniziale (foglio → WordPress, one-shot per ogni foglio)**

Trigger: bottone "Riallinea da foglio" oppure suggerito automaticamente con avviso subito dopo la selezione di un nuovo foglio dal Picker.

1. Il plugin scarica tutte le righe del tab attivo via `spreadsheets.values.get`
2. Per ogni riga del foglio, cerca un match con un utente WP confrontando il **codice fiscale** (chiave unica)
3. Produce un report di **diff** raggruppato in tre categorie, tutte gestite con scelta **caso per caso** nell'UI di preview:
   - **Da aggiornare in WP** (match per CF + celle diverse): tabella `campo: valore_WP → valore_foglio` per ogni differenza. Checkbox per riga "applica modifiche", più "seleziona/deseleziona tutto" per categoria
   - **Solo sul foglio** (CF presente in foglio, mancante in WP): anteprima del nuovo utente che verrebbe creato. Checkbox "crea utente WP" per riga (default: deselezionato, lo abiliti tu)
   - **Solo in WP** (CF presente in WP, mancante sul foglio): tipicamente allievi non rinnovati o rimossi a mano dal foglio. Per ogni riga radio button con 3 azioni:
     - "Lascia stare" (default): WP non viene toccato e l'allievo non viene scritto sul foglio finché tu non salvi modifiche dalla sua scheda
     - "Imposta `A=D` (disattivo)": utile per archiviazione rapida dei non rinnovi
     - "Pusha sul foglio comunque" (append a fine foglio): se è stato cancellato per errore dal foglio
4. **Pagina di preview** con le tre tabelle sopra + pulsante "Applica selezionati" in fondo
5. Apply: aggiorna `user_meta`, crea i nuovi utenti (solo quelli flaggati), applica le azioni "Solo in WP" scelte, popola `itcca_sheet_row` con il numero di riga del foglio per ogni allievo presente sul foglio
6. Log completo dell'operazione (`wp_options` → `itcca_phase_a_log`): data, n. righe lette, n. aggiornate, n. importate, n. impostate inattive, eventuali errori (es. CF duplicati o non validi)
7. Marca il foglio come "allineato": da questo momento la Fase B è attiva
8. Il bottone "Riallinea da foglio (Fase A)" resta **sempre disponibile** nelle impostazioni: utile quando edito qualche cella a mano sul foglio già connesso e voglio risincronizzare WP senza dover ricollegare il foglio

**Fase B — Push continuo (WordPress → foglio, attiva dopo Fase A)**

- Hook su `profile_update` / `user_register` / salvataggio dalla pagina admin "Allievi ITCCA" → enqueue task di push
- `class-phase-b-push.php` usa la mappatura `user_id ↔ riga` salvata in `user_meta` (`itcca_sheet_row`)
- Riga esistente → `spreadsheets.values.update` di tutta la riga
- Riga mancante (nuovo iscritto dal form pubblico o creato manualmente) → `spreadsheets.values.append` + aggiorna `itcca_sheet_row`
- Match di fallback per `CF` se `itcca_sheet_row` manca
- Errori loggati in `wp_options` (`itcca_sync_errors`) e visibili in admin con bottone "Riprova" per riga
- Bottone manuale "Pusha tutti su foglio" per forzare il re-push completo (utile dopo modifiche bulk)

**Comportamento al cambio anno (selezione di un foglio diverso dal Picker)**

1. Picker → memorizza nuovo `spreadsheet_id` + nome
2. Sposta il foglio precedente nello "Storico fogli" con timestamp di disconnessione
3. **Reset di tutti gli `itcca_sheet_row`**: la mappatura riga ↔ user_id del foglio precedente non è più valida
4. Disabilita temporaneamente la Fase B
5. Selezione del tab → il plugin legge l'header e confronta con lo schema WP attuale. Se diverge, redirect automatico alla **pagina di Riconciliazione schema** (vedi sezione dedicata) prima di abilitare la Fase A
6. Mostra banner persistente in admin: "Foglio nuovo connesso — esegui Fase A (Riallinea da foglio) prima di abilitare il push automatico"
7. Solo dopo aver completato la Fase A almeno una volta sul nuovo foglio, la Fase B torna attiva

### Riconciliazione schema (evoluzione delle colonne del foglio)

Il foglio Google può cambiare nel tempo: nuove colonne aggiunte, colonne rinominate, colonne rimosse. Il plugin gestisce questa evoluzione preservando i dati WordPress.

**Registro campi dinamico**

[`plugin/itcca-allievi/includes/class-fields.php`](plugin/itcca-allievi/includes/class-fields.php) espone:

- `Fields::baseline()`: array hardcoded dei 42 campi INSARRI di partenza
- `Fields::all()`: baseline con applicati gli overrides (rename/delete/add)
- `Fields::overrides()` / `Fields::save_overrides()`: unica fonte degli scostamenti, memorizzata in `wp_options['itcca_schema_overrides']`
- `Fields::deleted_fields()`, `Fields::active_storable()`, `Fields::is_deleted()`: helper di filtro

Struttura `itcca_schema_overrides`:

```php
[
  'renames'   => [ 'animale' => ['csv' => 'Zodiaco', 'renamed_at' => '...'] ],
  'deletes'   => [ 'el_dx'   => ['original_csv' => 'El Dx', 'deleted_at' => '...'] ],
  'additions' => [ 'allergie' => ['csv'=>'Allergie','label'=>'...','type'=>'textarea','section'=>'altro','public'=>false,'added_at'=>'...'] ],
]
```

**Effetti automatici**

Tutti i consumer del registro (form pubblico, edit utente, list table, export CSV, push Fase B) usano già `Fields::all()` / `Fields::public_fields()` / `Fields::by_csv()`. Conseguenze gratis grazie al refactor:

- Una colonna rinominata mantiene la stessa `user_meta` WP (zero perdita di dati) ma il push usa il nuovo nome CSV
- Una colonna aggiunta diventa un campo WP a tutti gli effetti (incluso opzionalmente il form pubblico)
- Una colonna rimossa resta editabile in admin in una sezione "Campi rimossi (storico)", ma non viene scritta su Sheet (`GoogleSheets::build_row_for_user` itera sull'header, non trova il campo perché il CSV non c'è) e non appare nell'export CSV (`ExportCsv::csv_header` esclude i `deleted`)

**Pagina di riconciliazione**

Quando l'utente seleziona un nuovo foglio o cambia tab via Picker, [`plugin/itcca-allievi/includes/class-google-picker.php`](plugin/itcca-allievi/includes/class-google-picker.php) legge l'header e confronta con lo schema corrente. Se ci sono differenze viene visualizzata la **pagina di Riconciliazione schema** (submenu nascosto `itcca-allievi-schema`, anche rilanciabile manualmente dalle impostazioni con il bottone "Riconcilia schema").

La pagina ([`plugin/itcca-allievi/includes/class-schema-reconcile.php`](plugin/itcca-allievi/includes/class-schema-reconcile.php) `SchemaReconcile::render_page`) mostra tre tabelle:

1. **Colonne mancanti**: ogni riga ha radio "Rinominata in [select delle nuove]" oppure "Davvero rimossa (marca deleted_)". Il rename è pre-selezionato in base a una euristica di **fuzzy match** (`similar_text` 70% + prossimità di posizione 30%, soglia 60%). I campi pubblici obbligatori del baseline (Cognome, Nome, Sesso, CF, Email, ...) non possono essere marcati come rimossi: l'UI forza il rename.
2. **Colonne nuove**: per ognuna non scelta come target di rename, configurazione di label, tipo (`text/textarea/date/flag/decimal/year/email/tel`), sezione, e flag "visibile nel form pubblico". Se il CSV combacia esattamente con un campo baseline currently-deleted, viene proposto un checkbox "Ripristina" che rimuove la voce da `deletes`.
3. **Anteprima riepilogo** + bottone "Applica e procedi con Fase A" → salva `itcca_schema_overrides`, resetta `aligned_at` del foglio attivo, redirect a Fase A.

**Flow Picker → Riconciliazione → Fase A**

```mermaid
flowchart TD
    pick[Picker: utente seleziona foglio]
    pick --> setSheet[ajax_set_sheet salva spreadsheet_id]
    setSheet --> settings[Settings: scegli tab]
    settings --> tabChosen[ajax_set_tab]
    tabChosen --> readHdr[Leggi header del tab via Sheets API]
    readHdr --> diff{Diff vs schema corrente}
    diff -->|vuoto| phaseA[Fase A - Allineamento dati]
    diff -->|non vuoto| reconcile[Pagina Riconciliazione Schema]
    reconcile --> userConfirm[Utente conferma rename/add/delete]
    userConfirm --> applyOver[Salva itcca_schema_overrides]
    applyOver --> phaseA
    phaseA --> done[Foglio allineato, Fase B attiva]
```

**Reversibilità**

Tutte le operazioni sono reversibili rimuovendo l'entry corrispondente da `itcca_schema_overrides`. I dati in `user_meta` non vengono mai cancellati dalla riconciliazione: il "delete" è uno snapshot conservativo.

### Diagramma del flusso

```mermaid
flowchart TD
    subgraph FaseA["Fase A: foglio -> WP (one-shot ad ogni cambio foglio)"]
        AdminA["Admin Fabio"] -->|"Seleziona foglio"| Picker["Google Picker"]
        Picker -->|"spreadsheet_id"| Settings["Settings + Storico fogli"]
        Settings -->|"Riallinea"| Reader["Sheets API values.get"]
        Reader -->|"match per CF"| Diff["Report diff: aggiorna / importa / solo WP"]
        Diff -->|"preview + conferma"| ApplyA["Apply: update user_meta + crea nuovi allievi"]
        ApplyA --> WPUserA["WP user + user_meta + itcca_sheet_row"]
    end

    subgraph FaseB["Fase B: WP -> foglio (continuo, abilitata dopo Fase A)"]
        Visitatore["Visitatore sito"] -->|"compila"| Form["Shortcode itcca_iscrizione"]
        Form --> WPUserB["WP user + user_meta"]
        AdminB["Admin Fabio"] -->|"edit"| AdminPage["Pagina Allievi ITCCA"]
        AdminPage --> WPUserB
        WPUserB -->|"on save hook"| Queue["class-phase-b-push (queue)"]
        Queue -->|"OAuth tokens"| Writer["Sheets API append/update"]
        Writer --> FoglioGoogle["Foglio dell'anno"]
    end

    FaseA -.->|"abilita"| FaseB
```

### Sicurezza e GDPR

- Nonces su tutti i form (pubblici e admin)
- Capability check: `manage_options` per impostazioni, `edit_users` per pagina allievi
- Sanitize/escape rigorosi (`sanitize_text_field`, `absint`, `wp_kses_post`, regex CF)
- Token OAuth cifrati a riposo
- Hook GDPR `wp_privacy_personal_data_exporters` e `wp_privacy_personal_data_erasers`
- Honeypot + rate limiting (transient) sul form pubblico

### Testing manuale nel Docker

1. `docker compose up -d` → wizard di setup WP su `localhost:8080`
2. Attiva plugin, completa impostazioni Google
3. Compila form da pagina con shortcode → verifica utente creato + riga su Sheet
4. Modifica meta dalla pagina admin → verifica update sulla riga del Sheet
5. Riavvia container → tutti i dati persistono (volumi nominati)

### Note operative

- **`INS`**: dal CSV è sempre `INSARRI`, quindi è impostazione globale (default popolato per ogni nuovo utente)
- **Campi calcolati `X` e `Anno`**: derivati al rendering, non salvati. Anno = "anniPratica,etàAlleggerita" come nel CSV (es. `22,38` per uno che ha iniziato nel 2004 ed è nato nel 1975) — confermo la formula esatta in fase di implementazione guardando più righe
- **Date**: salvataggio interno in `Y-m-d`, presentazione in `d/m/Y` come nel CSV
- **Quote**: salvate come decimale, formattazione `€ 20` solo in vista/export
- **Importazione iniziale**: comando WP-CLI `wp itcca import-csv` opzionale per popolare gli allievi esistenti dal CSV che mi hai allegato (utile per testare con dati reali fin da subito)