Files
glastree/MEMORY.md
T
2026-06-19 09:09:52 +02:00

754 lines
53 KiB
Markdown

# 🧠 MEMORY.md — Stato Progetto
## Obiettivo
App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
## Riassunto Generale
| Area | Stato | Ultima modifica |
|------|-------|-----------------|
| **Core** (Individui, Gruppi, Eventi, Documenti, Mailing) | ✅ Completo | CRUD, relazioni, filtri, paginazione, ACL, export |
| **Tag System** | ✅ Completo | morphToMany su 5 entità, filtri OR/AND, report, ricerca unificata |
| **Viste Colonne** | ✅ Completo | Per-User column visibility/width/order, resize/drag-reorder, print |
| **Email (IMAP/SMTP)** | ✅ Completo | Sync messaggi, firma, allegati, mittenti multipli, XOAUTH2 OAuth |
| **Google OAuth 2.0** | ✅ Implementato | Unified (Email/Drive/Calendar), UI impostazioni, XOAUTH2 SMTP+IMAP |
| **Repository Remoti** | ✅ Completo | WebDAV, Google Drive, OAuth |
| **Calendario** | ✅ Completo | Google Calendar, CalDAV, sync bidirezionale, pulsante sync, fix salvataggio campi sensibili |
| **Report** | ✅ Completo | Custom report con colonne/filtri salvabili, tag filter |
| **Help / Documentazione** | ✅ Completo | Tab Google Drive + Calendar in help page, PDF export |
| **Distribuzione** | ✅ Completo | build-dist.sh, fix path/cache/permessi/symlink |
### Cronologia modifiche principali
- **2026-06-07**: Fix colonne mancanti (eventi, gruppo_individuo)
- **2026-06-08**: build-dist.sh fix, logo remote fix, 18 migration guardie, help Calendar, sync pulsante, form annidato fix, checkbox hidden fallback, DiagnoseEmail command
- **2026-06-09**: Tag completato (MailingList, Report, Ricerca), @stack scripts fix, storeCustom tag_filter fix, MailingList edit JS fix
- **2026-06-10**: Per-User Column Views, ColumnManager JS, editVista data-* fix, 405 AJAX fix
- **2026-06-17**: Google OAuth 2.0 unificato (revert Socialite, unified OAuth per Email/Drive/Calendar, XOAUTH2 SMTP+IMAP, UI impostazioni)
- **2026-06-17**: Fix CalendarioConnessione — `encryptAndSetConfig()` perdeva campi sensibili in edit, `is_active` checkbox senza hidden fallback, query duplicata nella view
- **2026-06-18**: Fix import CSV — `declare(strict_types=1)` mancante in IndividuoController, header check assente, Log::warning() su errori; fix freeze server (set_time_limit, session_write_close, DB::transaction, fgetcsv length illimitato) in GruppoController e IndividuoController
- **2026-06-18**: Diagnose script (`diagnose.php`), .env.example aggiornato (SESSION_SECURE_COOKIE, FORCE_HTTPS, TRUSTED_PROXIES), build-dist.sh aggiornato con passo diagnose
## Funzionalità Implementate
### Page-length selector
- **4 pagine**: Individui, Eventi, Email, Gruppi
- **Valori**: 10, 20, 25, 50, 100 (default 20)
- Dropdown nel card-footer con campi hidden per preservare query params
- GruppoController: usa `LengthAwarePaginator` per paginare la collezione ordinata gerarchicamente
### CSV Import Gruppi
- Stesso pattern di `IndividuoController` (import GET, importStore POST, downloadTemplate GET)
- Colonne: nome, descrizione, parent_id, diocesi_id, indirizzo_incontro, cap_incontro, città_incontro, sigla_provincia_incontro
- Fault-tolerant: skip righe vuote, log errori, try/catch su ogni riga
### ICS Import Eventi
- Usa `Sabre\VObject\Reader::read()` già in vendor/
- Mapping: SUMMARY→nome_evento, DESCRIPTION→descrizione, DTSTART→data_specifica+ora_inizio, DTEND→durata_minuti, LOCATION→luogo_indirizzo, UID→uid_esterno
- Dedup via uid_esterno
- RRULE: import come evento singolo con prima occorrenza
### build-dist.sh (script di distribuzione)
- Genera `glastree-YYYYMMDD_HHMM.tar.gz`
- Include: sorgenti, vendor (production)
- Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker
- **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear
## Tag System
### Implementazione Completa (Opzione B)
- **Migration**: `2026_06_08_194155_create_tags_tables.php` — tabelle `tags` + `taggables`
- **Model `Tag.php`**: morphToMany a 5 entity types, auto-slug su creating, color badge
- **Trait `HasTagsLight.php`**: `tags()` morphToMany, scopes `withAllTags`/`withAnyTags`
- **5 Models aggiornati**: Individuo, Gruppo, Evento, Documento, MailingList — usano `HasTagsLight`
- **ImpostazioniController**: CRUD completo per Tag (store, update, destroy, reorder) con tab nella pagina Impostazioni
- **Tag selector**: `_tag-selector.blade.php` — search + pill badges con colori, incluso in create/edit di Individui, Gruppi, Eventi, MailingList
- **Tag filtering**: `?tag[]=slug` supportato in `IndividuoController@index`, `GruppoController@index`, `EventoController@index`, `DocumentoController@index`, `MailingListController@index` via `withAnyTags` scope
- **Tag column in index views**: Colonna "Tag" con badge colorati cliccabili (collegamento a index filtrato) in individui, gruppi, eventi, documenti, mailing-liste
- **Tag filter widget**: `_tag-filter-bar.blade.php` — barra cliccabile con badge colorati sopra ogni index table (individui, gruppi, eventi, documenti, mailing-liste). Attiva/disattiva filtro `?tag[]=` con un click, evidenzia tag attivi, include "Cancella filtri" quando attivo
- **RicercaController**: Controller per ricerca unificata per tag — mostra risultati aggregati da tutte 5 entity types con info-box counters
- **Route `GET /ricerca`**: Registrata come `ricerca.index``RicercaController@index`
- **Sidebar link**: "Ricerca per Tag" (icona `fas fa-tags`) sotto la sezione Report, visibile con permesso `individui`
- **Mass tag action for Documenti**: `POST /documenti/mass-tag``DocumentoController@massTag` — modal with tag selector + assign/remove radio, processes selected documents in chunks of 100 via `syncWithoutDetaching()` (assign) or `detach()` (remove)
- **Report tag columns**: `tags.nome` disponibile come colonna per individui/gruppi/eventi/documenti nei report personalizzati
- **Report tag filters**: Custom report form include tag multi-select (Select2) + modalità OR/AND; `runCustomReport()` applica filtri via `withAllTags`/`withAnyTags`
## Bug Fix Recenti
### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo
- `descrizione_evento` e `is_incontro_gruppo` mancanti in install.sql → creata migration + ALTER TABLE
- `ruolo_nel_gruppo` mancante in install.sql per pivot `gruppo_individuo`
- `VistaReport.php`: aggiunto `is_default` a `$fillable`
### 2026-06-08 — build-dist.sh: "provide valid cache path" su server remoto
**Problema**: Le esclusioni in `build-dist.sh` usavano `--exclude='storage/framework/cache'` (e simili per sessions, views, logs), che escludevano **l'intera directory** dall'archivio. Dopo l'estrazione, le directory necessarie a Laravel non esistevano.
**Fix**:
1. Cambiati exclude patterns per **contenuti soltanto** (non la directory):
- `--exclude='storage/framework/cache/*'` (non `/cache`)
- `--exclude='storage/framework/sessions/*'`
- `--exclude='storage/framework/views/*'`
- `--exclude='storage/logs/*'`
- `--exclude='storage/debugbar/*'`
2. Aggiunte esclusioni per upload utente:
- `--exclude='storage/app/public'`
- `--exclude='storage/app/private'`
3. Creato `.gitignore` in `storage/framework/views/` (mancante)
4. Istruzioni post-estrazione riscritte con:
- `mkdir -p` per tutte le directory necessarie (cache, sessions, views, logs, public, backups, documenti, bootstrap/cache)
- `chmod -R 775` e `chown -R www-data:www-data` per permessi
- `php artisan config:clear`, `route:clear`, `view:clear`
## Problema 419 (Page Expired) su Produzione
Il codice NON ha errori di CSRF — tutte le form hanno `@csrf`. Il problema è configurazione sessione del server:
1. `storage/framework/sessions/` non scrivibile dal web server
2. `APP_URL` in `.env` non corrisponde all'URL reale
3. `SESSION_SECURE_COOKIE=true` ma sito in HTTP
Soluzione:
```bash
chmod -R 775 storage/framework/sessions
php artisan config:clear
```
### 2026-06-08 — build-dist.sh: impossibilità salvare logo su server remoto
**Problema**:
- `public/storage` era un symlink **assoluto** (`/var/www/html/glastree/storage/app/public`) incluso nell'archivio → all'estrazione su altro server puntava a path inesistente.
- `php artisan storage:link` falliva perché il symlink rotto esisteva già.
- Sottodirectory `logos/`, `documenti/eventi/`, `avatars/gruppi/` non create dal `mkdir -p`.
**Fix**:
1. Aggiunto `--exclude='public/storage'` al tar — il symlink va ricreato sul target.
2. Aggiunto `rm -f public/storage` prima di `php artisan storage:link` per evitare conflitto.
3. Usato `ln -sf ../storage/app/public public/storage` (invece di `php artisan storage:link --relative` che richiede `symfony/filesystem` non sempre presente).
4. Aggiunte al `mkdir -p` le sottodirectory mancanti:
- `storage/app/public/logos` (logo upload)
- `storage/app/public/documenti/eventi` (event documenti)
- `storage/app/private/avatars/gruppi` (avatar gruppi)
5. Aggiunta creazione `.gitignore` in `storage/app/public/` (placeholder Laravel).
### Fix form annidato salvataggio email
**2026-06-08 — Problema**: Il form di DELETE era **annidato dentro** il form di SAVE in entrambe le pagine:
- `resources/views/impostazioni/index.blade.php`
- `resources/views/admin/email-settings/index.blade.php`
- HTML invalido: il browser chiudeva implicitamente il form SAVE all'apertura del form DELETE, causando l'invio alla route sbagliata.
**Fix**: Spostato il form DELETE fuori dal form SAVE, separato in un `div` autonomo.
### Fix campi mancanti in validazione EmailSettingsController@save
**2026-06-08**: `signature`, `signature_enabled`, `smtp_username`, `smtp_password` non erano nelle regole di validazione di `save()`. Laravel scarta i campi non validati, quindi non venivano mai scritti nel DB.
**Fix**: Aggiunte le regole mancanti a `$request->validate()`.
### DiagnoseEmail Command
- `php artisan diagnose:email` — controlla APP_KEY, tabella email_settings, record, decrittazione password, sender_accounts, log errori, estensioni PHP.
### 2026-06-08 — Firma email ancora non salvata (secondo tentativo)
**Problema**: La firma (campo `signature`) non veniva salvata nonostante le regole di validazione fossero state aggiunte.
**Root cause analysis**:
1. HTML checkbox `signature_enabled` senza hidden fallback: quando non spuntato, il campo non veniva inviato → non presente in `$validated``updateOrCreate` non lo aggiornava mai a `false`.
2. La vecchia pagina `/impostazioni/` aveva l'handler `shown.bs.tab` per il tab Firma (`#email-firma`) in un solo punto su due, causando possibile mancata inizializzazione dell'editor Quill su alcuni flussi di navigazione.
3. La verifica nel controller controllava solo `imap_host` e `email_address` — anche se signature/signature_enabled fallivano, l'utente vedeva "salvato con successo".
**Fix**:
1. `resources/views/admin/email-settings/index.blade.php:268` — Aggiunto `<input type="hidden" name="signature_enabled" value="0">` prima della checkbox
2. `resources/views/impostazioni/index.blade.php:603` — Stesso hidden fallback
3. `resources/views/impostazioni/index.blade.php:1633` — Aggiunto `#email-firma` al primo handler `shown.bs.tab` (il secondo handler esisteva già ma il primo no)
4. `app/Http/Controllers/Admin/EmailSettingsController.php:70-82` — Aggiunto log di warning se signature o signature_enabled non corrispondono dopo il salvataggio
### 2026-06-08 — Sync email: "Nessuna configurazione email attiva" nonostante test IMAP funzioni
**Problema**: Cliccando "Sincronizza Ora" nelle impostazioni email o "Ricevi/Invia" nella pagina email, viene mostrato "Nessuna configurazione email attiva". Il test IMAP funziona correttamente.
**Root cause**: Stesso bug dei checkbox senza hidden fallback. Il campo `is_active` nel tab Sincronizzazione non veniva inviato quando non spuntato. Alla prima configurazione, se l'utente non visitava esplicitamente il tab Sync e attivava lo switch, `is_active` rimaneva `false` (default DB). Il test IMAP usa `EmailSetting::first()` (ignora `is_active`), mentre `syncEmails()` e `syncNow()` usano `EmailSetting::getActive()` che filtra per `is_active = true`.
**Fix**:
1. `resources/views/admin/email-settings/index.blade.php:251` — Aggiunto `<input type="hidden" name="is_active" value="0">` prima della checkbox
2. `resources/views/impostazioni/index.blade.php:586` — Stesso hidden fallback
3. `EmailSetting::getActive()` ora riceve sempre `is_active` dal form (0 o 1) grazie all'hidden fallback
## 2026-06-08 — Help page: aggiunto tab "Calendario" con documentazione OAuth
- **`resources/views/help/index.blade.php`**: Aggiunto tab "Calendario" nel menu laterale (dopo Google Drive) e relativo tab-pane content con:
- Requisiti (account Google, API Calendar, OAuth credentials)
- Passo 1: Google Cloud Console — abilitare Google Calendar API, creare OAuth Client ID (Web Application), redirect URI `https://developers.google.com/oauthplayground`
- Passo 2: Ottenere Refresh Token via Google OAuth Playground (Use your own OAuth credentials, scope `calendar`)
- Passo 3: Configurazione nell'app (Impostazioni → Calendario, nuova connessione Google Calendar, inserire Client ID/Secret/Refresh Token)
- Test connessione e sincronizzazione
- Nota: refresh token permanente finché non revocato
- **`resources/views/help/pdf.blade.php`**: Aggiunta sezione "3. Google Calendar" nel TOC e contenuto, rinumerazione da 4 a 8 per sezioni successive
### Differenza chiave Google Calendar vs Google Drive
- **Drive**: endpoint server `/auth/google-drive/callback` che gestisce automaticamente il callback OAuth e salva refresh token in `storage_repositories`
- **Calendar**: **NON** ha endpoint server dedicato. Refresh token ottenuto manualmente via Google OAuth Playground e incollato nel campo "Refresh Token" del form
## 2026-06-08 — Pulsante Sync nella pagina calendario eventi
- **`app/Http/Controllers/CalendarioConnessioneController.php`**: Aggiunto metodo `syncAll()` che itera tutte le connessioni attive (`is_active = true`), esegue `syncEvents()` per ognuna, e restituisce JSON con risultati aggregati.
- **`routes/web.php`**: Aggiunta route `POST /calendario-connessioni/sync-all``syncAll` (name: `calendario-connessioni.sync-all`).
- **`resources/views/eventi/calendar.blade.php`**: Aggiunto pulsante "Sync" nel card-tools che:
- Invia POST AJAX a `sync-all`
- Mostra spinner durante l'operazione
- Al completamento mostra alert con riepilogo (connessioni, importati, esportati)
- Richiama `window.calendar.refetchEvents()` per aggiornare la vista FullCalendar
- Esposto `window.calendar` globalmente per accesso dall'handler sync
## 2026-06-08 — Fix logo non visibile su installazione remota
**Problema**: Il logo uploadato non veniva visualizzato su installazioni remote, pur salvandosi correttamente su disco.
**Root cause**: `getLogoPath()` e `getLogoSmallPath()` in `AppSetting.php` usavano `file_exists(public_path('storage/' . $path))` per verificare l'esistenza del file prima di restituire l'URL. Questo controllo dipendeva dal symlink `public/storage → storage/app/public`. Su installazioni remote dove il symlink era assente o mal configurato, `file_exists` restituiva `false` → metodo restituiva `null` → logo non mostrato.
**Fix**:
1. **`app/Models/AppSetting.php`**: Sostituito `file_exists(public_path('storage/' . $path))` con `Storage::disk('public')->exists($path)` e `asset('storage/' . $path)` con `Storage::disk('public')->url($path)`. Il controllo ora avviene direttamente su `storage/app/public/` (disk root), bypassando il symlink.
2. **`app/Http/Controllers/ImpostazioniController.php`**: In `uploadLogo()`, aggiunta creazione automatica del symlink `public/storage → storage/app/public` se mancante, per garantire che il browser possa servire l'immagine.
## 2026-06-08 — Guardie Schema::hasColumn() aggiunte a 18 migration
**Problema**: 18 migration files aggiungevano colonne a tabelle esistenti senza `Schema::hasColumn()` guard. Se eseguite su DB dove le colonne già esistevano (es. da install.sql), causavano errore "Duplicate column".
**Fix**: Aggiunto `if (!Schema::hasColumn(...))` wrapper a ogni colonna aggiunta nelle seguenti migration:
1. `2026_06_02_000002_add_uid_esterno_to_eventi_table.php``uid_esterno`
2. `2026_05_12_000002_change_ruolo_to_multi.php``ruolo_ids` (up), `ruolo_id` (down)
3. `2026_05_28_000001_add_storage_disk_to_documenti.php``storage_disk`
4. `2026_05_27_113246_add_repository_id_to_documenti.php``repository_id`
5. `2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php``log_falliti`, `mittente_nome`, `mittente_email`
6. `2026_05_27_000002_add_cartella_id_to_documenti.php``cartella_id`
7. `2026_05_16_000004_add_signature_enabled_to_email_settings.php``signature_enabled`
8. `2026_05_16_000003_add_signature_to_email_settings.php``signature`
9. `2024_01_01_000017_add_occorrenza_mese_to_eventi.php``occorrenza_mese`
10. `2024_01_01_000016_add_eventi_recurrence_fields.php``giorno_mese`, `mesi_recorrenza`, `mese_annuale`
11. `2026_05_10_000002_add_acl_tables.php``permissions` (users)
12. `2026_05_12_142009_add_location_to_eventi_table.php``luogo_indirizzo`, `luogo_url_maps`
13. `2026_05_12_000003_change_responsabile_to_multi.php``responsabile_ids` (up), `responsabile_id` (down)
14. `2024_01_01_000023_add_is_default_to_viste_report_table.php``is_default`
15. `2024_01_01_000024_add_user_id_to_mailing_lists_table.php``user_id`
16. `2026_05_12_000001_create_ruoli_table.php``ruolo_id` (gruppo_individuo) + data migration annessa
17. `2024_01_01_000021_add_user_id_to_documenti_table.php``user_id`
18. `2024_01_01_000013_add_numero_documento_to_individui_table.php``numero_documento`
Tutte verificate con `php -l` (nessun errore di sintassi).
### 2026-06-09 — `@stack('scripts')` mancante nel layout
**Problema**: Il partial `_tag-selector.blade.php` usa `@push('scripts')` per iniettare le funzioni JS `toggleTag`, `removeTag`, `filterTags`. Il layout `adminlte.blade.php` aveva solo `@yield('scripts')` (per `@section`), ma **non** `@stack('scripts')` (per `@push`). Di conseguenza il JavaScript interattivo del tag selector (clic sui badge, ricerca, rimozione) non veniva mai caricato su nessuna pagina create/edit.
**Fix**: `resources/views/layouts/adminlte.blade.php:305` — Aggiunto `@stack('scripts')` subito prima di `@yield('scripts')`.
### 2026-06-09 — Tag support completato per MailingList + Report + Ricerca
**MailingList — Controller & Views** (`app/Http/Controllers/MailingListController.php`):
- `index()`: eager-load `tags`, supporto `?tag[]=` filter via `withAnyTags`, pass `$allTags` per filter bar
- `create()`: pass `$tags` per tag selector
- `store()`: validazione `tags.* exists:tags,id`, sync dopo create. Auto-fetch email da `Individuo::contatti()` quando email non fornita
- `edit()`: eager-load `tags`, pass `$tags` + `$selectedTags`
- `update()`: validazione `tags.*`, sync se presente, detach se assente. Auto-fetch email da `Individuo::contatti()` quando email non fornita. Diff logica: `$toRemove = array_diff($existingIds, $newIds)` corretto — rimuove solo contatti deselezionati/esplicitamente rimossi
- `show()`: eager-load `tags`
**MailingList — Views**:
- `index.blade.php`: aggiunto `@include('partials._tag-filter-bar')`, colonna "Tag" con badge colorati cliccabili
- `create.blade.php`: incluso `_tag-selector` dopo descrizione
- `edit.blade.php`: incluso `_tag-selector` dopo descrizione (prima della sezione contatti). Submit handler include `#individui-select.selectedOptions` in `contatti_json` con `email: ''` (già presente da fix precedente)
- `show.blade.php`: aggiunta card Tag nel pannello sinistro con badge colorati (o "Nessun tag assegnato")
**ReportColumnRegistry** (`app/Helpers/ReportColumnRegistry.php`):
- Aggiunta colonna `'tags.nome'` con label 'Tag' a 4 entity types: individui, gruppi, eventi, documenti
**ReportController** (`app/Http/Controllers/ReportController.php`):
- `runCustomReport()`: supporta `config['tag_filter']` (array di tag IDs) + `config['tag_filter_mode']` ('any'/'all')
- `storeCustom()`: ora salva `tag_filter` e `tag_filter_mode` nella config del report salvato
- Carica `$allTags` nell'index e passa alla vista
- Converte IDs → slugs via `Tag::whereIn('id', ...)->pluck('slug')`, applica `withAllTags`/`withAnyTags`
- Eager-load `tags` automaticamente quando la colonna `tags.nome` è selezionata
**Report — View** (`resources/views/report/index.blade.php`):
- Aggiunto filtro tag multi-select nel form "Crea Report Personalizzato" (Select2 con badge colorati)
- Aggiunto select per modalità filtro (OR/AND)
- JS: `initTagFilterSelect2()` / `destroyTagFilterSelect2()` con template colorato
**RicercaController** (`app/Http/Controllers/RicercaController.php`):
- Aggiunta MailingList alle query per tag slug
- `results['mailingLists']` e `totals['mailingLists']` passati alla view
**Ricerca — View** (`resources/views/ricerca/index.blade.php`):
- Aggiunto info-box Mailing List (colore bg-purple) con link "Vedi tutti" → `mailing-liste.index?tag[]=`
- Aggiunta tabella dettaglio Mailing List nel risultato di ricerca (nome, contatti, stato, azione show)
- Fix: `route('documenti.edit')``url('/documenti/' . $doc->id . '/edit')` (route non esiste)
### 2026-06-09 — Fix: storeCustom() non salvava tag_filter
**Problema**: `ReportController@storeCustom()` non salvava `tag_filter` e `tag_filter_mode` nella config del report personalizzato. Quando un report salvato veniva eseguito, `runCustomReport()` non trovava i filtri tag nella config e non li applicava.
**Fix**: Aggiunte righe 143-148 in `ReportController.php`:
```php
if ($request->has('tag_filter')) {
$config['tag_filter'] = $request->input('tag_filter');
}
if ($request->filled('tag_filter_mode')) {
$config['tag_filter_mode'] = $request->input('tag_filter_mode');
}
```
### 2026-06-09 — Fix: MailingList edit — "Carica contatti" non inseriva i selezionati
**Problema**: Due bug nel JS `caricaSelezionati()` in `mailing-liste/edit.blade.php`:
1. **Typo**: `ind.cogname` invece di `ind.cognome` — il campo Cognome nella tabella era sempre vuoto/undefined (API restituisce `cognome`)
2. **Email gate**: `if (ind.emails && ind.emails.length > 0)` filtro bloccante — individui senza email salvata in `contatti` venivano SILENZIOSAMENTE scartati, mai aggiunti alla tabella
**Fix**:
1. `ind.cogname``ind.cognome` (righe 227 e 256)
2. Rimossa condizione `if (ind.emails...)` — ora anche individui senza email vengono aggiunti alla tabella
3. `email: ind.emails[0]``email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : ''` — email vuota se nessuna presente
4. Controller già fixato per auto-risolvere email da `Individuo::contatti()` al salvataggio
### 2026-06-09 — Fix: MailingListController email gate bloccava nuovi contatti senza email
**Problema**: `store()` e `update()` in `MailingListController` usavano `!empty($contatto['email'])` per decidere se creare un contatto. L'email era richiesta ma il MailingContact non memorizza l'email — la risolve dall'Individuo al momento del display/invio. Questo impediva di aggiungere individui senza email (o con email non nei contatti) alla mailing list.
**Fix**: Rimossa completamente la verifica email sia in `store()` che in `update()`. Ora ogni `individuo_id` valido viene aggiunto come MailingContact. L'email viene risolta al display dal model `$contact->individuo->contatti->where('tipo', 'email')->first()?->valore`.
Prima (bloccante):
```php
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { create }
```
Dopo (semplice):
```php
if (!empty($contatto['individuo_id'])) { create }
```
## Prossimi Passi
- [DONE] ... (existing items)
- Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
- [DONE] Fix performance: spostata query `VistaReport::where(...)->get()` da `@php` nel partial `table-settings.blade.php` a tutti i 5 controller (prima veniva eseguita 1 query per ogni pagina load per ogni entity, anche senza mai aprire la modale)
- [DONE] Rimosse vecchie modal legacy (`#saveVistaModal`, `#colonneModal`, `#vistaListModal`) da `individui` e `gruppi` — duplicate rispetto al nuovo modal unificato
- [DONE] Rimosse vecchie funzioni JS (`saveVista()`, `toggleColumn()`, `showSaveVistaModal()`) da `individui` e `gruppi`
- [DONE] Individui: `$allColumns` espanse da 6 a 17 colonne (aggiunte: data_nascita, genere, indirizzo, cap, citta, provincia, tipo_documento, numero_documento, scadenza_documento, note, created_at) — tutte con `<th>`/`<td>` condizionali nella vista
- [DONE] Individui: `getColumnIndex()` introdotta per lookup dinamico dell'indice colonna (sostituisce hardcoded `{codice:1, cognome:2, ...}` in `sortTable()` e `applyColumnFilter()`)
- [DONE] I controller per eventi, documenti, mailing-liste hanno già `$tableColumns` con flag `visible` — stesso meccanismo di gruppi. Nessuna modifica necessaria.
- [DONE] Fix: pulsante edit vista usa `data-*` attributi invece di `onclick` con `addslashes(json_encode(...))` — previene rottura HTML per JSON contenente `"`
- [DA FARE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print
- [DA FARE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload)
- [DA FARE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista
## 2026-06-10 — Per-User Column Views (colonne visibili, larghezze, ordine)
**Obiettivo**: Implementare viste colonne personalizzabili per utente su tutte le 5 entity list pages (individui, gruppi, eventi, documenti, mailing-liste), con persistenza tra login.
### Cosa è stato fatto
**Migration** (`2026_06_10_044946_add_column_widths_and_order_to_viste_report_table.php`):
- Aggiunge `colonne_larghezze` (json) e `colonne_ordine` (json) a `viste_report`
- Guardia `Schema::hasColumn` per compatibilità cross-DB (MySQL ↔ SQLite)
- Già eseguita in batch 38
**Backend**:
- `VistaReport.php`: `$fillable` + `$casts` (array) per entrambi i nuovi campi
- `VistaReportController@store`/`@update`: validazione estesa con `colonne_larghezze`, `colonne_ordine` (array)
- Tutti i 5 controller passano alla vista: `$tableColumns` (array `['key','label','visible']`), `$entityType`, `$columnWidths`, `$visibleColumns`, `$allColumns` (varia nome in base al controller)
- Tutti i 5 controller ora passano `$userVistas` (precaricate) invece di eseguire query nella view
- `IndividuoController`: `$allColumns` espanse con tutte le colonne DB disponibili (da 6 a 17)
- `IndividuoController`: aggiunto `use App\Models\VistaReport` per la query `$userVistas`
**Frontend JS** (`public/js/column-manager.js`):
- Classe `ColumnManager`: resize via mouse drag su handle, reorder via HTML5 DnD, persistenza larghezze in `sessionStorage`, fallback da `vista-data` DOM element, `printWithSettings()` che clona la tabella in finestra print-ready
**Modal** (`resources/views/partials/table-settings.blade.php`):
- Modal unificato con 3 tab: Colonne (checkbox visibilità + input larghezza + drag-reorder list), Viste (salva/carica/cancella), Stampa (pulsante print)
- Definisce `initTableSettings()` (NON auto-esegue) per evitare race condition con ColumnManager
- **RIMOSSA** query `VistaReport::where(...)->get()` dal `@php` block — ora usa `$userVistas` passato dal controller
- Non usa più `Auth::id()` direttamente — riceve `$userVistas` già popolato
**Views** — tutte e 5 aggiornate:
- `individui/index.blade.php`:
- `$allColumns` nella vista ora ha 17 colonne (vs 6 prima)
- `<th>` e `<td>` condizionali per tutte le nuove colonne (data_nascita, genere, indirizzo, cap, citta, provincia, tipo_documento, numero_documento, scadenza_documento, note, created_at)
- RIMOSSE modal legacy: `#saveVistaModal`, `#colonneModal`
- RIMOSSE funzioni JS legacy: `saveVista()`, `toggleColumn()`
- RIMOSSO sync checkboxes nel `DOMContentLoaded` (non più necessari)
- NUOVA: `getColumnIndex(colKey)` — lookup dinamico dell'indice colonna basato su `data-column`
- `sortTable()` e `applyColumnFilter()` ora usano `getColumnIndex()` invece di hardcoded mappa
- `gruppi/index.blade.php`:
- RIMOSSE modal legacy: `#saveVistaModal`, `#vistaListModal`
- RIMOSSO pulsante "Salva Vista" legacy
- RIMOSSE funzioni JS legacy: `showSaveVistaModal()`, `saveVista()`
- `eventi/index.blade.php`: colonne dinamiche via `$visibleColumns`, `@section('scripts')` con ColumnManager + `initTableSettings()`
- `mailing-liste/index.blade.php`: stessa struttura
- `documenti/index.blade.php`: ColumnManager init (non reorderable), column-manager.js incluso
- Tutti i `<th>` hanno `data-column` attributo per mapping JS
- Tutte chiamano `initTableSettings()` dopo `new ColumnManager()` nel `DOMContentLoaded`
**Performance fix**:
- Rimossa query `VistaReport::where(...)->get()` dal `@php` block in `table-settings.blade.php` (1 query extra per ogni pagina load su 5 entità)
- Spostata nei 5 controller come `$userVistas` passata via `compact()`
- Totale: -5 query per pagina load (una per ogni entity, anche su pagine non di index)
**Verifica**:
- JS brace balance: OK su tutti i 5 file (node check)
- PHP lint: OK su tutti i 5 controller
- Presenza `data-column`: individui=19, gruppi=14, eventi=9, mailing-liste=7, documenti=8
- Fallback `$visibleColumns` quando `$vista === null`: presente e corretto in tutti i 5 controller
- Nessun residuo di vecchie modal legacy in individui e gruppi
- Rimosso bottone "Salva Vista" orfano da `individui/index.blade.php` (chiamava `showSaveVistaModal()` non più esistente)
- Aggiunto pulsante modifica ✏️ nel tab Viste per ogni vista salvata: popola il form "Salva vista corrente" con nome, default flag e colonne visibili; cambia bottone in "Aggiorna" e fa PUT `/viste/{id}` invece di POST; zero query extra
### 2026-06-10 — Fix: editVista onclick → data-* attributes
**Problema**: Il pulsante modifica vista usava `onclick` con `addslashes(json_encode(...))`. In un attributo HTML delimitato da `"`, il backslash-escaping di `\"` non è gestito in modo standard dai browser — il JSON contenente `"` poteva rompere l'attributo HTML.
**Fix**:
1. Sostituito `onclick` con `data-*` attributi (`data-vista-id`, `data-vista-nome`, `data-vista-default`, `data-vista-colonne`) usando `{{ }}` di Blade (che applica `htmlspecialchars`, codificando `"``&quot;` sicuro in HTML).
2. `editVista()` ora accetta un array direttamente (non più JSON string da parsare).
3. Click handler registrato in `initTableSettings()` via `document.querySelectorAll('.edit-vista-btn')` + `dataset` (browser decodifica automaticamente `&quot;``"`).
### 2026-06-10 — Fix: 405 Method Not Allowed su update vista (via AJAX fetch)
**Problema**: Modificando una vista e cliccando "Aggiorna", `fetch()` inviava `PUT /viste/{id}` ma il controller `VistaReportController@update` restituiva `return back()` (302 redirect). `fetch()` seguiva il redirect — in alcuni browser il metodo PUT veniva preservato, ma la route `gruppi` accetta solo GET → 405 Method Not Allowed.
**Fix**:
1. `VistaReportController@update` (linea 101-103): aggiunto `if ($request->expectsJson()) { return response()->json([...]); }` — così il fetch riceve JSON 200, non un redirect 302.
2. Fetch headers in `table-settings.blade.php`: aggiunto `'Accept': 'application/json'` — necessario perché `expectsJson()` controlla l'header `Accept`, non `Content-Type`.
---
## 2026-06-17 — Google OAuth 2.0 Unificato (Email/Drive/Calendar)
**Obiettivo**: Implementare OAuth 2.0 centralizzato per tutti i servizi Google (Gmail email, Google Drive, Google Calendar), con UI nelle impostazioni e supporto XOAUTH2 per SMTP/IMAP via Symfony Mailer + ImapEngine. Sistema password attuale deve funzionare in parallelo.
### Cosa è stato fatto
**Enum** (`app/Enums/GoogleService.php`):
- Definisce 3 casi: `Email`, `Drive`, `Calendar`
- Metodi: `scopes()` (array scopes OAuth per servizio), `label()` (nome italiano), `icon()`, `description()` (testo help)
**Migration** (`2026_06_17_000001_create_google_oauth_connections_table.php`):
- Tabella `google_oauth_connections`: id, user_id (FK), service, email, access_token, refresh_token, expires_at, timestamps
- Unique constraint `[user_id, service, email]`
- Già eseguita in batch 39
**Migration** (`2026_06_17_000002_add_oauth_columns.php`):
- `email_settings`: aggiunti `auth_method` (varchar 20, default 'password') + `google_oauth_connection_id` (FK → google_oauth_connections, nullOnDelete)
- `sender_accounts`: stesse colonne
- Guardia `Schema::hasColumn` per idempotenza
- Già eseguita in batch 39
**Model** (`app/Models/GoogleOAuthConnection.php`):
- `$fillable`: user_id, service, email, access_token, refresh_token, expires_at
- `$casts`: expires_at → datetime
- `user()`: BelongsTo User
- `isExpired()`: controllo scadenza token
- `refresh()`: refresh automatico via GoogleOAuthService
- `getValidAccessToken()`: refresh se expired, ritorna token valido
**Service** (`app/Services/GoogleOAuthService.php`):
- `buildClient()`: configura Google_Client con client_id/secret/redirect_uri, access_type=offline, prompt=consent
- `getAuthUrl(service)`: URL di autorizzazione con scopes specifici per servizio
- `handleCallback(code, service)`: scambia code per token, crea/aggiorna GoogleOAuthConnection
- `getAccessToken(connection)`: cache 5 min, refresh automatico se expired
- `refreshToken(connection)`: fetch nuovo token via refresh_token, elimina se refresh fallisce
- `revoke(connection)`: revoca token Google, elimina record DB
- `createXoAuth2SmtpTransport(email, token)`: SMTP transport con solo XOAuth2Authenticator (evita tentativi PLAIN/LOGIN)
- `getConnectionStatus()`: restituisce array con stato connessioni per ogni servizio
**Controller** (`app/Http/Controllers/GoogleOAuthController.php`):
- `redirect(service)`: reindirizza a Google OAuth
- `callback(request)`: gestisce risposta Google, crea connessione, redirect a impostazioni con success/error
- `revoke(connection)`: revoca connessione (solo proprietario)
- `status()`: JSON con stato connessioni
**Config/Route changes**:
- `config/services.php`: sezione `google` con `client_id`, `client_secret`, `redirect` (usa `config('app.url')` per compatibilità CLI)
- `routes/web.php`: 4 nuove route (`google-oauth.redirect`, `google-oauth.callback`, `google-oauth.revoke`, `google-oauth.status`) + `use App\Http\Controllers\GoogleOAuthController`
**View** (`resources/views/impostazioni/_google-oauth.blade.php`):
- Card con lista servizi (Gmail, Drive, Calendar), badge connesso/non connesso, pulsanti Connetti/Disconnetti
- JS: auto-click tab se `?tab=google` in URL (dopo callback)
**Modifiche EmailSetting.php**:
- `$fillable` + `auth_method`, `google_oauth_connection_id`
- `$casts` + `google_oauth_connection_id` → integer
- `googleOAuthConnection()`: BelongsTo relationship
- `getImapConfig()`: se auth_method=oauth, imposta `authentication => 'oauth'` e password = access token
- `getDecryptedPassword()`: se OAuth, risolve token invece di decrittare password
- `getDecryptedSmtpPassword()`: stessa logica per SMTP
- `resolveOAuthToken()`: carica relazione + chiama GoogleOAuthService::getAccessToken
- `getImapClient()`: passa `authentication => 'oauth'` se auth_method=oauth
- `getSmtpMailer()`: se OAuth → `buildOAuthSmtpMailer()` (XOAuth2 solo), altrimenti DSN normale
- `buildOAuthSmtpMailer()`: EsmtpTransport + XOAuth2Authenticator
- `buildSmtpDsn()`: estratto da EmailSettingsController (password decrittata)
**Modifiche SenderAccount.php**:
- Stesso pattern: `auth_method`, `google_oauth_connection_id`, relationship, `resolveOAuthToken()`
- `getDecryptedPassword()`: OAuth-aware
- `sendEmail()`: se OAuth → `buildOAuthMailer()` (XOAuth2), altrimenti DSN
- `buildOAuthMailer()`: EsmtpTransport + XOAuth2Authenticator
**Modifiche EmailSettingsController.php**:
- `index()`: passa `$googleStatus``GoogleOAuthService::getConnectionStatus()`
- `save()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id`
- `testSmtp()`: ora usa `$settings->getSmtpMailer()` invece di costruire DSN manualmente
- `senderStore()`/`senderUpdate()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id`
**Modifiche impostazioni/index.blade.php**:
- Sidebar link "Google" (icona `fab fa-google`) prima del link "Calendario"
- Tab-pane con `@include("impostazioni._google-oauth")` prima del tab calendario
### Note tecniche
- **XOAUTH2**: Symfony Mailer ha già `XOAuth2Authenticator` in vendor (`vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php`), invia `AUTH XOAUTH2 user=<email>\1auth=Bearer <token>\1\1`
- **ImapEngine**: già supporta `'authentication' => 'oauth'` in config → `AUTHENTICATE XOAUTH2 <base64>`
- **google/apiclient**: presente in vendor, usato per Google_Client (buildClient, fetchAccessTokenWithAuthCode, refresh, revoke). `Google\Service\Gmail` NON disponibile ma non necessario (XOAUTH2 bypassa API REST)
- **Permessi filesystem**: nuovi file copiati con sudo (www-data:www-data, 644), index.blade.php modificato con sed
- **refresh token**: richiede `access_type=offline` + `prompt=consent` + `includeGrantedScopes=true` per garantire refresh token sempre al primo auth
- **config('app.url')** in services.php invece di `url()` helper (url() fallisce in CLI context)
### 2026-06-17 — Bug Fix: Tag massivo e per-riga per Eventi + Documenti
**Problemi riscontrati**:
1. Eventi: modal mass tag HTML corrotto (`&times;``<div class="modal fade" id="deleteModal"`)
2. Eventi + Documenti: nessun feedback visivo dopo submit mass tag (mancava `session('error')` su Eventi; mancavano ENTRAMBI su Documenti)
3. Eventi + Documenti: nessuna icona TAG per-riga nelle azioni
4. Documenti: mancava funzione `openSingleTag()` JS
**Fix**:
1. `resources/views/eventi/index.blade.php:340` — Ripristinato `&times;` nel close button del mass tag modal
2. `resources/views/eventi/index.blade.php:22-27` — Aggiunto `@if(session('error'))` alert danger
3. `resources/views/eventi/index.blade.php:283-285` — Aggiunto pulsante per-riga `fa-tags``openSingleTag(eventId)`
4. `resources/views/eventi/index.blade.php:427-431` — Aggiunta funzione JS `openSingleTag(eventId)`
5. `resources/views/documenti/index.blade.php:21-32` — Aggiunti `@if(session('success'))` + `@if(session('error'))` alert
6. `resources/views/documenti/index.blade.php:283-285` e `484-486` — Aggiunto pulsante per-riga `fa-tags` in vista griglia + lista
7. `resources/views/documenti/index.blade.php:1318-1323` — Aggiunta funzione JS `openSingleTag(docId)` con querySelector per `.doc-checkbox[value=...]`
8. Verifica JS brace balance: OK su entrambi (diff=0)
9. Verifica PHP lint: OK su entrambi i controller
## 2026-06-17 — Bug Fix: Documenti + Eventi (syntax error, strict_types, validazione)
### Problemi trovati e fixati
**1. CRITICO — Syntax error PHP in DocumentoController.php:239**
- **Problema**: Parentesi `)` extra alla fine del ternario: `/documenti');` invece di `/documenti';`
- **Root cause**: `$redirect = ... : '/documenti');` — parentesi di chiusura in eccesso
- **Effetto**: PHP parse error: `Unclosed '{' on line 197 does not match ')'` — l'intero metodo `update()` era rotto
- **Fix**: Rimosso `)` extra
**2. declare(strict_types=1) mancante** (violazione AGENTS.md)
- **Problema**: 5 file del dominio documenti/eventi non avevano `declare(strict_types=1)`, obbligatorio per PHP 8.4
- **File fixati**: `Documento.php`, `Evento.php`, `EventoController.php`, `TipologiaDocumento.php`, `EventoDocumentoController.php`
**3. Validazione assente `contesto_tipo` in DocumentoController@update**
- **Problema**: Il campo `contesto_tipo` veniva letto dal raw request (`$request->contesto_tipo`) senza validazione. Un utente poteva forzare valori arbitrari.
- **Fix**: Aggiunto `'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing'` alle regole di validazione. Ora si usa il valore validato (`$contestoTipo`) invece del raw request.
**4. Tipologia hardcoded in DocumentoController@store**
- **Problema**: `store()` usava `'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro'` (hardcoded), mentre `update()` usava `TipologiaDocumento::opzioni()` (dinamico). Nuove tipologie aggiunte via UI non sarebbero state accettate da `store()`.
- **Fix**: Allineato `store()` a usare `TipologiaDocumento::opzioni()` come `update()`.
**5. JS brace balance verificato**
- `resources/views/documenti/index.blade.php` → OK (diff=0)
- `resources/views/eventi/index.blade.php` → OK (diff=0)
### Verifica
- `php -l` su tutti e 6 i file modificati: nessun errore di sintassi
## 2026-06-17 — Fix: Eventi mass tag "nessun evento selezionato"
**Problema**: La toolbar button chiamava `$('#massTagModal').modal('show')` direttamente, affidandosi a un listener `show.bs.modal` per popolare l'hidden field `massTagIds`. Se l'evento non veniva intercettato, `ids` restava vuoto e il server rispondeva "Nessun evento selezionato".
**Fix** (`resources/views/eventi/index.blade.php`):
1. Toolbar button: `onclick="$('#massTagModal').modal('show')"``onclick="showMassTagModal()"`
2. Rimosso listener `show.bs.modal` che popolava i campi
3. Aggiunta funzione `showMassTagModal()` (stesso pattern di Gruppi/Individui) che valida `getSelectedIds()` e popola i campi **prima** di aprire la modale
4. `openSingleTag()` ora chiama `showMassTagModal()` invece di mostrare la modale direttamente
## 2026-06-17 — Fix: Documenti — root mostra documenti di tutte le cartelle
**Problema**: Alla root (`/documenti`), la query includeva tutti i documenti locali (`WHERE repository_id IS NULL`), mostrando documenti di tutte le cartelle in un unico elenco piatto.
**Fix** (`app/Http/Controllers/DocumentoController.php`):
- Aggiunto `->whereNull('cartella_id')` alla query di root, così mostra solo documenti **senza cartella** (radice). Per vedere i documenti dentro una cartella bisogna navigarci dentro con `?folder_id=X`.
## 2026-06-17 — Fix: CalendarioConnessione — salvataggio perdeva password/client_secret in edit + is_active sempre true
**Problema**: Salvando una connessione calendario esistente (CalDAV o Google Calendar), i campi sensibili (password, client_secret) venivano sovrascritti con stringa vuota. Il test connessione e la sincronizzazione fallivano silenziosamente.
**Root cause 1 — `encryptAndSetConfig()` sovrascrive campi vuoti**:
- Il JS di edit (`editCalendario()`) azzera `config[password]` e `config[client_secret]` per sicurezza (valore `''`, placeholder `......`)
- `encryptAndSetConfig()` in `CalendarioConnessione.php` controllava `$config[$field] !== ''` e saltava la crittografia, ma poi eseguiva `$this->config = $config` — la stringa vuota sovrascriveva il valore crittato precedente
- `StorageRepository` NON aveva questo bug: `StorageRepositoryService::encryptSensitiveConfig()` preserva i campi esistenti con `} elseif (empty($config[$field]) && !empty($existingConfig[$field])) { $config[$field] = $existingConfig[$field]; }`
**Root cause 2 — `is_active` checkbox senza hidden fallback**:
- Stesso bug già fixato per email (2026-06-08): checkbox `is_active` senza `<input type="hidden" name="is_active" value="0">`
- Quando non spuntato, il campo non veniva inviato → `$request->boolean('is_active', true)` restituiva sempre `true`
- La connessione restava sempre attiva indipendentemente dallo switch
**Root cause 3 — Query duplicata nella view**:
- `resources/views/impostazioni/index.blade.php:773` ri-eseguiva `CalendarioConnessione::orderBy('ordine')->get()` nonostante il controller lo passasse già via `compact()`
**Fix**:
1. **`app/Models/CalendarioConnessione.php:56`** — `encryptAndSetConfig()` ora accetta `array $existingConfig = []` e preserva i valori crittati esistenti quando il campo submitted è vuoto (stesso pattern di `StorageRepositoryService::encryptSensitiveConfig()`)
2. **`app/Http/Controllers/CalendarioConnessioneController.php:67`** — `update()` passa `$connessione->config` come secondo parametro a `encryptAndSetConfig()`
3. **`resources/views/impostazioni/index.blade.php:1576`** — Aggiunto `<input type="hidden" name="is_active" value="0">` prima della checkbox
4. **`resources/views/impostazioni/index.blade.php:773`** — Rimossa query duplicata `@php $calendarioConnessioni = ...`
**Verifica**:
- PHP lint: OK su entrambi i file modificati
- JS brace balance: OK
## 2026-06-17 — Fix: Documenti mass tag "nessun documento selezionato"
**Problema**: Stesso identico bug di Eventi — toolbar button chiamava `$('#massTagModal').modal('show')` e il listener `show.bs.modal` non sempre popolava i campi.
**Fix** (`resources/views/documenti/index.blade.php`):
1. Rimosso listener `show.bs.modal`
2. Creata funzione `showMassTagModal()` (con fallback per `data-pre-selected`)
3. `openSingleTag()` ora usa `showMassTagModal()` invece di mostrare la modale direttamente
## 2026-06-17 — Fix: ColumnManager pin colonne select/azioni con `cells[]` invece di `querySelector`
**Problema**: Il pinning di `select` e `azioni` usava `row.querySelector('[data-column="..."]')` che funziona solo sul `<thead>` (dove `<th>` ha `data-column`), ma non sul `<tbody>` (dove i `<td>` non hanno `data-column`).
**Fix** (`public/js/column-manager.js`): Sostituito con `cells[keyToThIndex['select']]` che usa l'indice della colonna, valido sia per `th` che per `td`.
### DA FARE
- Configurare `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` in `.env`
- Test end-to-end: flusso OAuth completo (redirect → auth → callback → connessione creata)
- Test XOAUTH2 SMTP: invio email via connessione OAuth
- Test IMAP OAuth: sync email via connessione OAuth
- Verificare refresh token automatico allo scadere
- Verificare revoca e riconnessione
## 2026-06-17 — Fix: tutte le migration + seeders idempotenti
### Problema
`php artisan migrate` falliva su tabelle già esistenti (`tenants`, `tipologie_documenti`, ecc.).
`php artisan db:seed` creava duplicati su re-run (admin user, diocesi, comuni).
Migrations con `insert` dopo il guard `hasTable` violavano unique constraint al re-run.
### Migration: hasTable guard su 28 CREATE mancanti
Aggiunto `if (!Schema::hasTable('table_name')) { ... }` wrapper a 28 migration file (45 `Schema::create()` calls) via script Node.js, più `2026_06_17_000001_create_google_oauth_connections_table.php` manualmente.
### Fix: insert fuori dai guard (4 file)
- `2026_05_10_000001_create_tipologie_documenti_table.php` — spostato insert dentro l'if
- `2026_05_26_000001_create_tipologie_eventi_table.php` — spostato insert dentro l'if
- `2026_05_12_000001_create_ruoli_table.php` — spostato insert dentro l'if
- `2026_05_11_000006_add_email_attachment_tipologia.php``insert()``updateOrInsert()`
### Seeder: firstOrCreate
- `DiocesiSeeder.php`: `Diocesi::create()``::firstOrCreate(['nome' => ...])`
- `ComuniSeeder.php`: `Comune::create()``::firstOrCreate(['codice_istat' => ...])`
- `DatabaseSeeder.php`: `User::create()``::firstOrCreate(['email' => ...])` + guardia `role_preset_id`
### Risultato
`php artisan migrate --seed` è ora completamente idempotente. Zero errori su 64 migration + 6 seeder. Re-run è un no-op.
## 2026-06-17 — Fix: MailingList tag management (mass tag + per-riga + attiva checkbox)
**Problema**: Nella GUI non era possibile assegnare tag alle mailing list né singolarmente né via azione massiva.
**Root cause**:
1. **Nessuna route mass-tag per mailing-liste** — Eventi, Documenti, Individui, Gruppi avevano `POST /{entity}/mass-tag` ma MailingList no
2. **Nessun metodo `massTag()` in MailingListController** — assente rispetto agli altri 4 controller
3. **Nessun pulsante per-riga tag** nella index view (mancava `<button type="button" onclick="openSingleTag()">`)
4. **Nessun pulsante toolbar "Tag"** nella index view (mancava il mass tag button)
5. **Checkbox `attiva` senza hidden fallback** in create/edit — stesso bug ricorrente: deselezionando, il campo non veniva inviato
**Fix**:
1. **`app/Http/Controllers/MailingListController.php:189-214`** — Aggiunto metodo `massTag()` (stesso pattern di `EventoController@massTag`):
- Autorizzazione via `$this->authorizeWrite('mailing')`
- Validazione: `tags` required array, `mode` required in:assign,remove
- Processa in chunk(100) via `syncWithoutDetaching()` (assign) / `detach()` (remove)
- Restituisce `back()->with('success', ...)`
2. **`routes/web.php:177`** — Aggiunta route `POST mailing-liste/mass-tag``MailingListController@massTag` (name: `mailing-liste.mass-tag`)
3. **`resources/views/mailing-liste/index.blade.php`**:
- `select-all` checkbox: permesso esteso da `canDeleteMailing` a `canWriteMailing || canDeleteMailing`
- `row-checkbox`: stessa estensione permesso
- Toolbar: aggiunto pulsante "Tag" (btn-sm btn-info, `showMassTagModal()`) prima di "Elimina Selezionati"
- Azioni per-riga: aggiunto pulsante `fa-tags` con `openSingleTag(lista.id)` dopo il pulsante show
- Aggiunto `@if(session('error'))` alert
- Aggiunta modale `#massTagModal` (stesso pattern di eventi: titolo, counter, radio assign/remove, `_tag-selector`, hidden `ids`)
- Aggiunte funzioni JS: `showMassTagModal()`, `openSingleTag(listId)`
4. **`resources/views/mailing-liste/create.blade.php:23`** — Aggiunto `<input type="hidden" name="attiva" value="0">` prima della checkbox
5. **`resources/views/mailing-liste/edit.blade.php:23`** — Stesso hidden fallback
6. **`resources/views/mailing-liste/edit.blade.php:11`** — Fix percorso hardcoded: `url('/mailing-liste/' . ...)``route('mailing-liste.update', ...)`
**Verifica**:
- PHP lint: OK su controller + routes
- JS brace balance: OK su index view
- Route list: `POST mailing-liste/mass-tag``MailingListController@massTag` (name: `mailing-liste.mass-tag`)
## 2026-06-18 — Fix import CSV: freeze server + bug IndividuoController
### Problema freeze server (Gruppi + Individui)
Importando CSV con molte righe, il server si bloccava:
1. **PHP max_execution_time (30s)** — killava il processo su import grandi
2. **Session lock** — altre richieste dello stesso utente restavano in attesa
3. **Nessuna transazione DB** — ogni `create()` era una INSERT individuale autocommit
4. **fgetcsv length=1000** — righe CSV più lunghe di 1000 caratteri venivano troncate
### Fix applicati a `GruppoController@importStore` e `IndividuoController@importStore`
```php
set_time_limit(0);
session_write_close();
$header = fgetcsv($handle, 0, ','); // length=0 = nessun limite
DB::beginTransaction();
// ... loop ...
DB::commit();
```
### Bug trovati in `IndividuoController@importStore`
| Bug | Fix |
|-----|-----|
| `declare(strict_types=1)` mancante | ✅ Aggiunto |
| Indentazione errata negli import (spazi extra su 8 righe) | ✅ Corretto |
| `use Illuminate\Support\Facades\Log` mancante | ✅ Aggiunto |
| Nessun controllo `$header === false` — CSV vuoto causava TypeError | ✅ Aggiunto |
| Nessun `array_map('trim', $header)` | ✅ Aggiunto |
| Nessun `Log::warning()` su eccezioni riga | ✅ Aggiunto |
| `fgetcsv` length=1000 (vs 0) | ✅ Portato a 0 |
| Nessun `set_time_limit(0)` / `session_write_close()` | ✅ Aggiunto |
| Nessun `DB::beginTransaction()` / `DB::commit()` | ✅ Aggiunto |
### File modificati
- `app/Http/Controllers/GruppoController.php` — aggiunti `use DB`, set_time_limit, session_write_close, fgetcsv length=0, DB::transaction
- `app/Http/Controllers/IndividuoController.php` — strict_types, Log import, DB import, header check, trim, set_time_limit, session_write_close, DB::transaction
### Verifica
- `php -l` su entrambi: nessun errore di sintassi
## 2026-06-18 — Diagnose script + .env.example + build-dist.sh
### `diagnose.php` — Script standalone di diagnostica
Creato script standalone per verificare installazione su server remoto. Non richiede Laravel.
**Cosa verifica**:
- PHP version (≥ 8.2), 15 estensioni required, 4 opzionali
- INI settings (execution_time, memory, upload/post max)
- .env: APP_KEY decodifica (32 bytes), APP_URL, SESSION_SECURE_COOKIE, DB, FORCE_HTTPS
- 13 directory in `storage/` e `bootstrap/cache`: esistenza, permessi, scrivibilità
- Symlink `public/storage` (supporto sia link assoluti che relativi)
- Test scrittura/lettura file sessione
- Connessione DB via PDO, tabelle principali, conteggio records, engine InnoDB
- Middleware bootstrap/app.php: ForceHttps registrato, TrustProxies, duplicazioni web group
- Vendor: autoload, composer, node_modules, public/build, artisan
- CSRF: HTTP GET login page, estrazione token, POST con token, verifica 419
- Spazio disco: totale, libero, percentuale
**Uso**:
```bash
php diagnose.php
php diagnose.php --verbose # output dettagliato
```
Exit code: 0 = OK, 1 = errori critici.
### `.env.example` aggiornato
Aggiunte variabili mancanti:
- `APP_PROTOCOL`, `APP_SUBFOLDER` — supporto subfolder
- `FORCE_HTTPS`, `TRUSTED_PROXIES` — proxy/HTTPS configurabile
- `SESSION_SECURE_COOKIE`, `SESSION_SAME_SITE` — sessione cross-protocol
- `APP_ENV=production`, `APP_DEBUG=false` — safe defaults per produzione
### `build-dist.sh`
- Aggiunto `php diagnose.php` come passo #0 (pre-installazione) e #7 (verifica finale)
- Istruzioni post-estrazione complete
### File modificati
- `diagnose.php` (nuovo) — standalone diagnostica
- `.env.example` — variabili mancanti aggiunte
- `build-dist.sh` — passo diagnose
### Verifica
- `php -l diagnose.php`: OK
- `php diagnose.php`: eseguito con 58/68 check superati su server locale