# đź§  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 - **2026-06-23**: UsabilitĂ  viste colonne: se non si clicca ✏️ su una vista, la vista attiva corrente viene auto-assunta come target di modifica. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rilevamento rename via `currentVistaOriginalNome`) ## 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 `` 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 `` 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 ``/`` 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 `"` - [DONE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print - [DONE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload) - [DONE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista - [DONE] UsabilitĂ  viste colonne: se non si seleziona una vista esplicita (✏️), la vista attiva corrente viene auto-assunta come target. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rename detection) ## 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) - `` e `` 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 `` 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 `"` → `"` 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 `"` → `"`). ### 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=\1auth=Bearer \1\1` - **ImapEngine**: giĂ  supporta `'authentication' => 'oauth'` in config → `AUTHENTICATE XOAUTH2 ` - **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 (`×` → `