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

75 KiB

🧠 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.indexRicercaController@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-tagDocumentoController@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:

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 $validatedupdateOrCreate 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-allsyncAll (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.phpuid_esterno
  2. 2026_05_12_000002_change_ruolo_to_multi.phpruolo_ids (up), ruolo_id (down)
  3. 2026_05_28_000001_add_storage_disk_to_documenti.phpstorage_disk
  4. 2026_05_27_113246_add_repository_id_to_documenti.phprepository_id
  5. 2026_05_26_000003_add_log_falliti_to_mailing_messaggi.phplog_falliti, mittente_nome, mittente_email
  6. 2026_05_27_000002_add_cartella_id_to_documenti.phpcartella_id
  7. 2026_05_16_000004_add_signature_enabled_to_email_settings.phpsignature_enabled
  8. 2026_05_16_000003_add_signature_to_email_settings.phpsignature
  9. 2024_01_01_000017_add_occorrenza_mese_to_eventi.phpoccorrenza_mese
  10. 2024_01_01_000016_add_eventi_recurrence_fields.phpgiorno_mese, mesi_recorrenza, mese_annuale
  11. 2026_05_10_000002_add_acl_tables.phppermissions (users)
  12. 2026_05_12_142009_add_location_to_eventi_table.phpluogo_indirizzo, luogo_url_maps
  13. 2026_05_12_000003_change_responsabile_to_multi.phpresponsabile_ids (up), responsabile_id (down)
  14. 2024_01_01_000023_add_is_default_to_viste_report_table.phpis_default
  15. 2024_01_01_000024_add_user_id_to_mailing_lists_table.phpuser_id
  16. 2026_05_12_000001_create_ruoli_table.phpruolo_id (gruppo_individuo) + data migration annessa
  17. 2024_01_01_000021_add_user_id_to_documenti_table.phpuser_id
  18. 2024_01_01_000013_add_numero_documento_to_individui_table.phpnumero_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:

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.cognameind.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):

if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { create }

Dopo (semplice):

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 "
  • [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)
    • <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 $googleStatusGoogleOAuthService::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-tagsopenSingleTag(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:56encryptAndSetConfig() 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:67update() 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.phpinsert()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-tagMailingListController@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-tagMailingListController@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

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:

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 opencodeis_writable() controlla i permessi "other" (5 = r-x) → false
  • Web server esegue come www-data → owner rwxtrue
  • 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.phpgruppo_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=fileSESSION_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

2026-06-23 — Fix document move: modal event listener + PHP empty-ids guard

Problema: Spostare documenti (singolo via icona cartella o mass move via toolbar) mostrava "successo" ma non spostava nulla. Il documento rimaneva nella cartella originale.

Root cause:

  1. JS — Bootstrap 4 event compatibility: addEventListener('show.bs.modal', ...) al mass move modal usava native DOM listener, ma Bootstrap 4.6.2 dispone show.bs.modal come evento jQuery. Il listener non veniva mai eseguito → hidden input massMoveIds mai popolato → explode(',', '')['']empty(['']) è falseDocumento::whereIn('id', ['']) (0 rows affected) → falso successo.
  2. PHP — No guardia su array vuoto dopo explode: massMove(), massDownload(), massDestroy(), massAssociate(), massTag() in DocumentoController.php non filtravano valori vuoti/non-numerici da $ids.

Fix:

  • resources/views/documenti/index.blade.php:1306: document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', ...)$('#massMoveModal').on('show.bs.modal', ...) (jQuery event, coerente con tutti gli altri listener nello stesso file)
  • app/Http/Controllers/DocumentoController.php: aggiunto array_values(array_filter($ids, fn($v) => is_numeric($v))) in massMove(), massDownload(), massDestroy(), massAssociate(), massTag() — filtra stringhe vuote e non-numeriche prima di ogni whereIn
  • massMove(): aggiunto controllo esplicito count($ids) per validare singolo documento nel flusso per-documento

File modificati:

  • resources/views/documenti/index.blade.phpaddEventListener → jQuery .on() a riga 1306
  • app/Http/Controllers/DocumentoController.phparray_filter guard in 5 mass-action metodi

Verifica:

  • PHP lint: OK su controller
  • JS brace balance: OK (diff=0)
  • Tutti gli altri listener modal nel file usano già jQuery .on() — il fix allinea massMoveModal al pattern esistente
  • Nessun test automatizzato presente nel progetto (no Pest, no PHPUnit config)