Files
glastree/MEMORY.md
T
2026-06-23 08:05:51 +02:00

1093 lines
73 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-23**: Email compose: firma spostata dopo il corpo; Mailing list: aggiunto `sender_account_id` (mittente predefinito salvato in creazione, usato all'invio come fallback)
- **2026-06-23**: Fix formati nome: responsabili gruppi e individui email ora mostrano "Nome Cognome"; mailing list edit: aggiunto pulsante "Rimuovi selezionati"
- **2026-06-22**: Aggiunto supporto Email Mittente Ufficiale (from_email/from_name) per invio con indirizzo diverso dalle credenziali SMTP/IMAP
- Migration: `from_email`, `from_name` columns su `email_settings`
- `EmailSetting`: metodi `getEffectiveFromAddress()`, `getEffectiveFromName()`, `getEffectiveReplyTo()`
- Tutti i metodi di invio (`sendViaImap`, `sendViaSystem`, `sendPasswordResetNotification`, `storeDraft`, `storeSentMessage`, `testSmtp`) ora usano `getEffectiveFromAddress()`/`getEffectiveFromName()` per il mittente
- Reply-To automatico impostato su `from_email` quando disponibile
- UI: nuovi campi "Email Mittente Ufficiale" e "Nome Mittente Ufficiale" in entrambe le pagine impostazioni email
- **2026-06-22**: Backup system enhancements:
- `BackupService::run()` ora accetta `$options` (include_files, include_env) per override temporanei senza persistere su DB
- `BackupRunCommand --no-files/--no-env` non salva più permanentemente la configurazione (era un bug)
- `manifest.json` ora include `included_components` array (database, env, files)
- Vista backup: badge .env (grigio) e Files (giallo) nella colonna "Contenuto" basati sul manifest
- `run()` restituisce `steps` array; mostrato in flash message (controller) e output (command)
- **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
## 2026-06-19 — Fix 419: Duplicated middleware + session env vars + diagnose.php fixes
### Problema
`POST /login` restituiva **419 Page Expired** su HTTP. La causa era doppia:
1. **Critico — Middleware duplicato in `bootstrap/app.php`**: `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, `ShareErrorsFromSession` erano appesi al web group via `$middleware->web(append: [...])`. Questi middleware sono **già attivi di default** in Laravel 13. Appenderli una seconda volta causava doppia crittografia/decrittografia dei cookie di sessione → session ID corrotto → token CSRF non riconosciuto → 419.
2. **`SESSION_SECURE_COOKIE` non impostato** nel `.env` — con `APP_URL=https` + `FORCE_HTTPS=true`, Laravel impostava il flag `Secure` sul cookie di sessione. Poiché il server serviva solo HTTP, il browser non inviava il cookie → session persa → 419.
### Fix apportati
**`bootstrap/app.php`** (righe 22-27):
- Rimosso l'intero blocco `$middleware->web(append: [...])` che duplicava EncryptCookies, AddQueuedCookiesToResponse, StartSession, ShareErrorsFromSession.
- Sostituito con commento esplicativo per prevenire future reintroduzioni.
**`.env`**:
- Aggiunto `SESSION_SECURE_COOKIE=false` — impedisce il flag Secure su HTTP
- Aggiunto `SESSION_SAME_SITE=lax` — esplicito per consistenza cross-protocol
- Cambiato `APP_URL` da HTTPS a HTTP (ambiente locale senza HTTPS)
- Cambiato `APP_PROTOCOL` da `https` a `http`
- Cambiato `FORCE_HTTPS` da `true` a `false`
- Cambiato `TRUSTED_PROXIES` da `*` a CIDR espliciti
**`diagnose.php`** — 4 bug fix:
1. **`unlink()` senza `file_exists()`** nelle linee 863 e 900: causava PHP Warning quando curl non creava il cookie jar (es. HTTPS non raggiungibile). Aggiunto `file_exists($cookieJar) && unlink($cookieJar)`.
2. **Regex middleware duplicato non matchava array multi-riga**: il pattern `[^\]]*` non matcha newline. Sostituito con `(.+?)` con flag `s`. Classe name extraction semplificata a `([a-zA-Z]+)::class`.
3. **`CURLOPT_COOKIEJAR` non funzionante** in questo ambiente (curl 8.14.1 + PHP 8.4 — il file non viene creato da `curl_close()`). Riscritto `testCsrfLogin()` per parsare manualmente gli header `Set-Cookie` dalla prima risposta e reinviarli via `CURLOPT_COOKIE` nella POST. Eliminata dipendenza da `COOKIEJAR`/`COOKIEFILE`.
4. Aggiunto verbose debug per codici HTTP POST anomali (≠ 302/200).
### Risultato finale
```
✅ Passed: 63
⚠️ Warnings: 8 (upload/post_max_size, engine opzionali mancanti)
❌ Failed: 1 (spazio disco 94.6% — server admin)
```
- Test CSRF: ✅ POST login → HTTP 302 (nessun 419)
- Solo il disco (94.6%) è critico — non risolvibile via script.
### File modificati
- `bootstrap/app.php` — rimosso middleware web duplicato
- `.env` — aggiunte SESSION_SECURE_COOKIE, SESSION_SAME_SITE; APP_URL/APP_PROTOCOL/FORCE_HTTPS adeguati
- `diagnose.php` — fix unlink warning, regex middleware multi-line, CSRF test manual-cookie, verbose debug
## 2026-06-19 — diagnose.php: storage false positivo + ForceHttps registration check
### Fix 1 — Storage permission false positive
**Problema**: `diagnose.php` segnava 15 storage directory come ❌ NON SCRIVIBILE quando `php diagnose.php` veniva eseguito da CLI (utente `opencode`). Le directory sono di proprietà `www-data:www-data` con permessi `775`, quindi il web server **può** scrivere — il falso positivo generava confusione.
**Diagnosi**:
- CLI esegue come `opencode``is_writable()` controlla i permessi "other" (5 = r-x) → `false`
- Web server esegue come `www-data` → owner `rwx``true`
- Questo è normale su server condivisi/Debian standard
**Fix** (`diagnose.php:659-660`):
- Aggiunto `elseif ($owner === WEB_USER && in_array($perms, ['775', '777', '755', '770']))`
- Se il proprietario è `www-data` e permessi sono `775`/`777`/`755`/`770`, mostra ⚠️ **warning** invece di ❌ **fail**, con messaggio "NON scrivibile da CLI ma WEB server sì"
- Altrimenti mostra fail come prima
### Fix 2 — ForceHttps registration check
**Problema**: Il controllo ForceHttps usava `str_contains($appContent, 'ForceHttps')` che matchava anche il semplice `use App\Http\Middleware\ForceHttps` (import), segnando ✅ "registrato" anche quando il middleware non era attivo in alcun gruppo.
**Stato attuale**: `ForceHttps::class` è **importato** ma **non registrato** in nessun middleware group (`$middleware->append()`, `->web()`, ecc.) in `bootstrap/app.php`.
**Fix** (`diagnose.php:871-879`):
- Sostituito con `preg_match('/\$middleware\s*->\s*\w+\s*\(.*?ForceHttps::class/s', $appContent)`
- 3 stati:
1.`"registrato correttamente"` — se ForceHttps::class è argomento di una chiamata `$middleware->xxx()`
2. ⚠️ `"IMPORTATO ma NON registrato"` — se la stringa ForceHttps esiste (use import) ma non in una chiamata
3. ⚠️ `"NON presente"` — se ForceHttps non esiste nel file
### Verifica
- `php -l diagnose.php`: No syntax errors detected
- `php diagnose.php` su locale: ForceHttps ora segnala ⚠️ "IMPORTATO ma NON registrato" (corretto)
### File modificati
- `diagnose.php` — storage false positive + ForceHttps registration check
## 2026-06-22 — build-dist.sh: genera post-deploy.sh per upgrade/install automatico
**Obiettivo**: Includere nell'archivio di distribuzione uno script `post-deploy.sh` che esegue controllo e setup dell'installazione sul server target, sia per fresh install che per upgrade.
### Cosa è stato fatto
**`build-dist.sh`** riscritto:
- Prima del `tar`, genera `post-deploy.sh` (heredoc) e lo include nell'archivio
- Dopo il `tar`, `post-deploy.sh` viene rimosso localmente (non sporca la working dir)
- Istruzioni finali aggiornate: mostrano i tre comandi principali
**`post-deploy.sh`** (generato, non versionato):
Tre modalità d'uso:
| Comando | Cosa fa |
|---------|---------|
| `bash post-deploy.sh check` | Diagnostica stato installazione: migration pendenti, permessi directory, symlink, cache. Nessuna modifica. |
| `bash post-deploy.sh upgrade` | Check + upgrade esistente: crea directory, permessi, symlink, `migrate --force`, `key:generate` (se mancante), cache clear, verifica finale. Chiede conferma prima di eseguire. |
| `bash post-deploy.sh install` | Fresh install completo: come upgrade + copia `.env.example → .env` con pausa per modifica utente. |
| `bash post-deploy.sh upgrade --yes` | Batch mode (nessuna conferma richiesta) |
Fasi upgrade (8 step): directory → .gitignore → permessi → symlink → migrazioni → key → cache → diagnose
Fasi install (9 step): come upgrade + setup .env con pausa interattiva
### Vantaggi
- **Idempotente**: `migrate --force` esegue solo migration pendenti
- **Sicuro**: modalità `check` non modifica nulla
- **Interattivo**: chiede conferma prima di azioni distruttive
- **Batch**: `--yes` per automazione CI/CD
- **Compatibile**: integra `diagnose.php` già esistente (check + fix)
- **Autopulente**: `post-deploy.sh` non resta nella working dir di build
### Verifica
- `bash -n build-dist.sh`: syntax OK
- `bash -n post-deploy.sh`: syntax OK
- `bash post-deploy.sh check`: eseguito correttamente
- `tar tzf glastree-*.tar.gz | grep post-deploy`: presente nell'archivio
- `rm -f post-deploy.sh` dopo tar: pulizia OK
## 2026-06-22 — Estensione Gruppi: contatti propri + diocesi multiple
**Obiettivo**: Aggiungere contatti (email, telefono) ai gruppi e supportare assegnazione di più diocesi.
### Cosa è stato fatto
**Migration** (2 nuove tabelle):
- `2026_06_22_000002_create_gruppo_contatti_table.php``gruppo_contatti` con: id, gruppo_id (FK cascade), tipo, valore, etichetta, is_primary, timestamps
- `2026_06_22_000003_create_diocesi_gruppo_table.php` — pivot `diocesi_gruppo` con: id, gruppo_id (FK cascade), diocesi_id (FK cascade), unique(gruppo_id, diocesi_id)
**Model `GruppoContatto.php`** (nuovo):
- `$table = 'gruppo_contatti'`, fillable: gruppo_id, tipo, valore, etichetta, is_primary
- `gruppo()`: BelongsTo Gruppo
**Model `Gruppo.php`**:
- `diocesi()`: cambiata da `BelongsTo` a `BelongsToMany` via `diocesi_gruppo`
- `gruppoContatti()`: nuova `HasMany` relation
- Accessor `getEmailPrimariaAttribute()`: primo contatto email (is_primary preferito)
- Accessor `getTelefonoPrimarioAttribute()`: primo contatto telefono/cellulare (is_primary preferito)
- `$appends`: già presente `email_primaria`, `telefono_primario` (invariato)
- `$fillable`: `diocesi_id` mantenuto (nullable) per backward compat con import CSV
**Model `Diocesi.php`**:
- `gruppi()`: cambiata da `HasMany` a `BelongsToMany` via `diocesi_gruppo`
**`GruppoController.php`**:
- `store()`/`update()`: validazione cambiata da `diocesi_id` (nullable|exists) a `diocesi_ids` (nullable|array|exists)
- `store()`/`update()`: nuova validazione `contatti` array con tipo (in:email,telefono,cellulare), valore, etichetta, is_primary
- `store()`: crea contatti dopo create; sync diocesi
- `update()`: cancella+ricrea contatti se presenti; sync diocesi (o detach se assenti)
- `edit()`: eager-load `gruppoContatti`, passa `$selectedDiocesiIds`
- `show()`: eager-load `gruppoContatti`
- `index()`: eager-load `gruppoContatti`
- `create()`: invariato (passa già `$diocesi`)
**`ReportController.php`**:
- 4 occorrenze `$g->diocesi?->nome` → collection pattern `$g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-'`
**View `gruppi/create.blade.php`**:
- Select diocesi: cambiato da single `select name="diocesi_id"` a multiple `select name="diocesi_ids[]" class="select2-multi"`
- Aggiunta card "Contatti del Gruppo" con tabella inline (tipo select, valore input, etichetta, checkbox primario, elimina)
- Select2 CSS/JS caricati via CDN
**View `gruppi/edit.blade.php`**:
- Stessa modifica diocesi → Select2 multi con `$selectedDiocesiIds` pre-selezionati
- Aggiunta card "Contatti del Gruppo" con righe pre-popolate da `$gruppo->gruppoContatti`
- Select2 CSS/JS caricati via CDN
**View `gruppi/show.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
- Aggiunte righe Email/Telefono (da accessor) e "Altri Contatti" nella card info
**View `gruppi/index.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
- Telefono/Email: da hardcoded `-` a `$gruppo->telefono_primario` / `$gruppo->email_primaria`
**View `gruppi/partials/tree-item.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
**View `individui/show.blade.php` + `individui/edit.blade.php`**:
- Diocesi nei gruppi dell'individuo: da `$gruppo->diocesi?->nome` a collection implode
### Backward compatibility
- Colonna `diocesi_id` su `gruppi` mantenuta (nullable) per import CSV
- CSV import (`importStore()`) usa ancora `diocesi_id` → singola diocesi
- `$fillable` include ancora `diocesi_id`
- Nessuna modifica a `Individuo` o `Contatto` esistenti
### Verifica
- `php -l` su tutti i file modificati: OK
- Migrations eseguite: ✅
- Relazioni verificate: `diocesi()` BelongsToMany, `gruppoContatti()` HasMany, `email_primaria`/`telefono_primario` accessors funzionanti
## 2026-06-22 — Fix produzione: 500 pagina gruppi (migration pending + autoloader stale)
**Problema**: Dopo deploy su server produzione (`192.168.222.177`):
- Migration `diocesi_gruppo` non eseguita (PENDING)
- `GruppoContatto.php` non nell'autoloader (`composer dump-autoload` non eseguito)
- `storage/logs/laravel.log` non scrivibile da www-data
- 3 migration pre-esistenti non marcate come "ran" (google_oauth_connections, add_auth_method, add_google_id) — tabelle/colonne già esistenti ma migration record mancanti → bloccavano `migrate --force`
**Fix**:
1. `sudo chmod -R 775 storage bootstrap/cache` — permessi
2. `sudo usermod -a -G www-data opencode` — utente CLI nel gruppo www-data
3. Inseriti manualmente 3 migration record mancanti in `migrations` table (già eseguite in passato ma mai registrate)
4. `php artisan migrate --force` — eseguita `2026_06_22_000003_create_diocesi_gruppo_table`
5. `composer dump-autoload` — autoloader rigenerato (41464 classi)
6. `php artisan view:clear && php artisan route:clear && php artisan config:clear`
**Risultato**:
- `class_exists(App\Models\GruppoContatto)` → ✅
- `gruppi` page HTTP 200 (after login redirect 302) — 500 risolto
- Tutte le migration marked as Ran (batch 40-42)
- Views/routes/config cache pulite
**Lezione**: `build-dist.sh` fa `composer install --no-scripts` che non rigenera autoloader ottimizzato per il target. `post-deploy.sh upgrade` dovrebbe includere `composer dump-autoload` (o almeno `composer install --no-dev --optimize-autoloader`).
## 2026-06-22 — Fix: Select2 search field visibile sotto le diocesi selezionate
**Problema**: In create/edit gruppi, il `<textarea class="select2-search__field">` creato da Select2 in modalità multiple era visibile sotto i tag delle diocesi selezionate, con altezza 65px (ereditata da Bootstrap form-control).
**Causa**:
1. `allowClear: true` in Select2 multiple mode non serve (ogni tag ha già la X per rimuoverlo) e causa conflitti di rendering
2. Bootstrap 4 applica `height: 65px` al textarea del search field
3. Nessun CSS specifico per normalizzare il search field inline
**Fix**:
1. Rimosso `allowClear: true` da entrambi i file (`create.blade.php`, `edit.blade.php`)
2. Aggiunto CSS per normalizzare `height: 28px`, `border: none`, `background: transparent`, `width: auto` con `min-width: 30px`
**File modificati**:
- `resources/views/gruppi/create.blade.php` — rimosso allowClear, aggiunto CSS search field
- `resources/views/gruppi/edit.blade.php` — rimosso allowClear, aggiunto CSS search field
## 2026-06-23 — Fix: Select2 counter rimosso (utente vuole vedere tutti i nomi)
**Problema**: Il `templateSelection` con counter mostrava "5 diocesi selezionate" invece dei nomi.
**Fix**: Rimosso l'intero blocco `templateSelection` da entrambi create/edit. Ogni diocesi selezionata mostra ora il proprio nome.
**File modificati**:
- `resources/views/gruppi/create.blade.php` — rimosso templateSelection
- `resources/views/gruppi/edit.blade.php` — rimosso templateSelection
## 2026-06-23 — Fix: DiocesiSeeder + build-dist.sh per deploy
**Problema**: La tabella `diocesi` con 225 record importati da ODS non veniva popolata sul server target dopo deploy. Il seeder `DiocesiSeeder.php` aveva solo ~100 nomi obsoleti/inaccurati (es. "Mongolia", "Donegal", "Tirana").
**Fix**:
1. **`DiocesiSeeder.php`** riscritto completamente:
- 225 nomi corretti (Arcidiocesi/Diocesi/Sede/Patriarcato/Abbazia/Eparachia)
- Encoding: 7 nomi con mojibake da ODS fixati via `where('id', ...)->update()` (Trinità, Cefalù, Città, Forlì, Mondovì, Nardò, Perugia-Città)
- Apostrofo: "Val d Elsa" → "Val d'Elsa"
- `declare(strict_types=1)` aggiunto
- `firstOrCreate` per idempotenza
2. **`build-dist.sh`** (`post-deploy.sh` generato):
- Upgrade: aggiunto step `[6/10] Seed diocesi` dopo migrazioni (rinumerati da [1-5/9] → [1-10/10])
- Install: aggiunto step `[9/11] Seed diocesi` dopo migrazioni (rinumerato da [1-8/10] → [1-11/11])
- Usa `--force` per bypassare conferma in produzione
**File modificati**:
- `database/seeders/DiocesiSeeder.php` — riscritto con 225 nomi + fix encoding
- `build-dist.sh` — aggiunto step seed diocesi in upgrade/install
## 2026-06-23 — Fix 419: SESSION_DRIVER=database + diagnose CSRF su IP locale
**Problema**: Accesso via IP di rete locale (`http://192.168.222.174`) poteva causare 419 Page Expired. Sessione su file vulnerabile a permessi/LOCK del filesystem.
**Fix**:
1. **`SESSION_DRIVER=database`**: cambiato da `file` a `database`. La sessione su DB non soffre di:
- Permessi filesystem errati
- Lock concorrente su file
- Pulizia sessioni scadute lottery-based
2. **Migration `2026_06_23_000001_create_sessions_table.php`**: con guard `Schema::hasTable` per idempotenza cross-DB
3. **`diagnose.php`**: aggiunto test CSRF su IP locale automatico. Rileva `hostname -I` ed esegue POST login → verifica 419 su `http://<lan-ip>/login`
4. **`.env` e `.env.example`**: `SESSION_DRIVER=file``SESSION_DRIVER=database`
5. Vecchi file di sessione in `storage/framework/sessions/` eliminati
**Risultato**:
- diagnose.php: `✅ HTTP: POST login → HTTP 302 (nessun 419)`
- diagnose.php: `✅ LOCAL IP: POST login → HTTP 302 (nessun 419)`
- Sessioni persistono in tabella `sessions` (DB) invece di file
**File modificati**:
- `database/migrations/2026_06_23_000001_create_sessions_table.php` (nuovo)
- `.env` — SESSION_DRIVER=database
- `.env.example` — SESSION_DRIVER=database
- `diagnose.php` — test CSRF su IP locale
## 2026-06-23 — Email compose: firma dopo corpo + Mailing list: mittente e firma
**Obiettivo**: Spostare la select firma dopo il corpo email nella pagina di composizione email. Aggiungere mittente predefinito (`sender_account_id`) alle mailing list (salvato in creazione/edit, usato come fallback all'invio).
### Cosa è stato fatto
**Migration** (`2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php`):
- Aggiunta colonna `sender_account_id` (FK → sender_accounts, nullOnDelete) a `mailing_lists`
**Model `MailingList.php`**:
- `sender_account_id` aggiunto a `$fillable`
- `senderAccount()`: nuova relazione BelongsTo → `SenderAccount`
**Controller `MailingListController.php`**:
- `create()`: passa `$senderAccounts` (SenderAccount::active()->get()) alla view
- `edit()`: passa `$senderAccounts` alla view, eager-load `senderAccount`
- `store()`: validazione `sender_account_id` nullable|exists, salvato in create
- `update()`: validazione `sender_account_id` nullable|exists, salvato in update
**View `mailing-liste/create.blade.php`**:
- Aggiunto select per `sender_account_id` dopo il select firma
**View `mailing-liste/edit.blade.php`**:
- Aggiunto select per `sender_account_id` dopo il select firma, con `selected` se uguale a `$mailingList->sender_account_id`
**View `email/compose.blade.php`**:
- Spostato blocco firma (prima del body) → dopo il textarea body (prima della sezione allegati)
**Controller `MailingController.php`**:
- `invia()`: fallback a `$lista->senderAccount` se `mittente_id` non fornito esplicitamente (stesso pattern del firma_id fallback già esistente)
- `invioElabora()`: fallback a `mittente_id`/`firma_id` dalla mailing list se non forniti esplicitamente (solo quando una singola lista è selezionata)
**File modificati**:
- `database/migrations/2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php` (nuovo)
- `app/Models/MailingList.php` — fillable + senderAccount relazione
- `app/Http/Controllers/MailingListController.php` — sender_account in create/edit/store/update
- `app/Http/Controllers/MailingController.php` — fallback mittente/firma da mailing list
- `resources/views/email/compose.blade.php` — firma spostata dopo il body
- `resources/views/mailing-liste/create.blade.php` — select mittente aggiunto
- `resources/views/mailing-liste/edit.blade.php` — select mittente aggiunto