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_activecheckbox 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_namecolumns suemail_settings EmailSetting: metodigetEffectiveFromAddress(),getEffectiveFromName(),getEffectiveReplyTo()- Tutti i metodi di invio (
sendViaImap,sendViaSystem,sendPasswordResetNotification,storeDraft,storeSentMessage,testSmtp) ora usanogetEffectiveFromAddress()/getEffectiveFromName()per il mittente - Reply-To automatico impostato su
from_emailquando disponibile - UI: nuovi campi "Email Mittente Ufficiale" e "Nome Mittente Ufficiale" in entrambe le pagine impostazioni email
- Migration:
- 2026-06-22: Backup system enhancements:
BackupService::run()ora accetta$options(include_files, include_env) per override temporanei senza persistere su DBBackupRunCommand --no-files/--no-envnon salva più permanentemente la configurazione (era un bug)manifest.jsonora includeincluded_componentsarray (database, env, files)- Vista backup: badge .env (grigio) e Files (giallo) nella colonna "Contenuto" basati sul manifest
run()restituiscestepsarray; 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
LengthAwarePaginatorper 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— tabelletags+taggables - Model
Tag.php: morphToMany a 5 entity types, auto-slug su creating, color badge - Trait
HasTagsLight.php:tags()morphToMany, scopeswithAllTags/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[]=slugsupportato inIndividuoController@index,GruppoController@index,EventoController@index,DocumentoController@index,MailingListController@indexviawithAnyTagsscope - 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 comericerca.index→RicercaController@index - Sidebar link: "Ricerca per Tag" (icona
fas fa-tags) sotto la sezione Report, visibile con permessoindividui - 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 viasyncWithoutDetaching()(assign) ordetach()(remove) - Report tag columns:
tags.nomedisponibile 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 viawithAllTags/withAnyTags
Bug Fix Recenti
2026-06-07 — Colonne mancanti in eventi e gruppo_individuo
descrizione_eventoeis_incontro_gruppomancanti in install.sql → creata migration + ALTER TABLEruolo_nel_gruppomancante in install.sql per pivotgruppo_individuoVistaReport.php: aggiuntois_defaulta$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:
- 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/*'
- Aggiunte esclusioni per upload utente:
--exclude='storage/app/public'--exclude='storage/app/private'
- Creato
.gitignoreinstorage/framework/views/(mancante) - Istruzioni post-estrazione riscritte con:
mkdir -pper tutte le directory necessarie (cache, sessions, views, logs, public, backups, documenti, bootstrap/cache)chmod -R 775echown -R www-data:www-dataper permessiphp 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:
storage/framework/sessions/non scrivibile dal web serverAPP_URLin.envnon corrisponde all'URL realeSESSION_SECURE_COOKIE=truema 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/storageera 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:linkfalliva perché il symlink rotto esisteva già.- Sottodirectory
logos/,documenti/eventi/,avatars/gruppi/non create dalmkdir -p.
Fix:
- Aggiunto
--exclude='public/storage'al tar — il symlink va ricreato sul target. - Aggiunto
rm -f public/storageprima diphp artisan storage:linkper evitare conflitto. - Usato
ln -sf ../storage/app/public public/storage(invece diphp artisan storage:link --relativeche richiedesymfony/filesystemnon sempre presente). - Aggiunte al
mkdir -ple sottodirectory mancanti:storage/app/public/logos(logo upload)storage/app/public/documenti/eventi(event documenti)storage/app/private/avatars/gruppi(avatar gruppi)
- Aggiunta creazione
.gitignoreinstorage/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.phpresources/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:
- HTML checkbox
signature_enabledsenza hidden fallback: quando non spuntato, il campo non veniva inviato → non presente in$validated→updateOrCreatenon lo aggiornava mai afalse. - La vecchia pagina
/impostazioni/aveva l'handlershown.bs.tabper il tab Firma (#email-firma) in un solo punto su due, causando possibile mancata inizializzazione dell'editor Quill su alcuni flussi di navigazione. - La verifica nel controller controllava solo
imap_hosteemail_address— anche se signature/signature_enabled fallivano, l'utente vedeva "salvato con successo".
Fix:
resources/views/admin/email-settings/index.blade.php:268— Aggiunto<input type="hidden" name="signature_enabled" value="0">prima della checkboxresources/views/impostazioni/index.blade.php:603— Stesso hidden fallbackresources/views/impostazioni/index.blade.php:1633— Aggiunto#email-firmaal primo handlershown.bs.tab(il secondo handler esisteva già ma il primo no)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:
resources/views/admin/email-settings/index.blade.php:251— Aggiunto<input type="hidden" name="is_active" value="0">prima della checkboxresources/views/impostazioni/index.blade.php:586— Stesso hidden fallbackEmailSetting::getActive()ora riceve sempreis_activedal 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/callbackche gestisce automaticamente il callback OAuth e salva refresh token instorage_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 metodosyncAll()che itera tutte le connessioni attive (is_active = true), eseguesyncEvents()per ognuna, e restituisce JSON con risultati aggregati.routes/web.php: Aggiunta routePOST /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
- Invia POST AJAX a
- Esposto
window.calendarglobalmente 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:
app/Models/AppSetting.php: Sostituitofile_exists(public_path('storage/' . $path))conStorage::disk('public')->exists($path)easset('storage/' . $path)conStorage::disk('public')->url($path). Il controllo ora avviene direttamente sustorage/app/public/(disk root), bypassando il symlink.app/Http/Controllers/ImpostazioniController.php: InuploadLogo(), aggiunta creazione automatica del symlinkpublic/storage → storage/app/publicse 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:
2026_06_02_000002_add_uid_esterno_to_eventi_table.php—uid_esterno2026_05_12_000002_change_ruolo_to_multi.php—ruolo_ids(up),ruolo_id(down)2026_05_28_000001_add_storage_disk_to_documenti.php—storage_disk2026_05_27_113246_add_repository_id_to_documenti.php—repository_id2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php—log_falliti,mittente_nome,mittente_email2026_05_27_000002_add_cartella_id_to_documenti.php—cartella_id2026_05_16_000004_add_signature_enabled_to_email_settings.php—signature_enabled2026_05_16_000003_add_signature_to_email_settings.php—signature2024_01_01_000017_add_occorrenza_mese_to_eventi.php—occorrenza_mese2024_01_01_000016_add_eventi_recurrence_fields.php—giorno_mese,mesi_recorrenza,mese_annuale2026_05_10_000002_add_acl_tables.php—permissions(users)2026_05_12_142009_add_location_to_eventi_table.php—luogo_indirizzo,luogo_url_maps2026_05_12_000003_change_responsabile_to_multi.php—responsabile_ids(up),responsabile_id(down)2024_01_01_000023_add_is_default_to_viste_report_table.php—is_default2024_01_01_000024_add_user_id_to_mailing_lists_table.php—user_id2026_05_12_000001_create_ruoli_table.php—ruolo_id(gruppo_individuo) + data migration annessa2024_01_01_000021_add_user_id_to_documenti_table.php—user_id2024_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-loadtags, supporto?tag[]=filter viawithAnyTags, pass$allTagsper filter barcreate(): pass$tagsper tag selectorstore(): validazionetags.* exists:tags,id, sync dopo create. Auto-fetch email daIndividuo::contatti()quando email non fornitaedit(): eager-loadtags, pass$tags+$selectedTagsupdate(): validazionetags.*, sync se presente, detach se assente. Auto-fetch email daIndividuo::contatti()quando email non fornita. Diff logica:$toRemove = array_diff($existingIds, $newIds)corretto — rimuove solo contatti deselezionati/esplicitamente rimossishow(): eager-loadtags
MailingList — Views:
index.blade.php: aggiunto@include('partials._tag-filter-bar'), colonna "Tag" con badge colorati cliccabilicreate.blade.php: incluso_tag-selectordopo descrizioneedit.blade.php: incluso_tag-selectordopo descrizione (prima della sezione contatti). Submit handler include#individui-select.selectedOptionsincontatti_jsonconemail: ''(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(): supportaconfig['tag_filter'](array di tag IDs) +config['tag_filter_mode']('any'/'all')storeCustom(): ora salvatag_filteretag_filter_modenella config del report salvato- Carica
$allTagsnell'index e passa alla vista - Converte IDs → slugs via
Tag::whereIn('id', ...)->pluck('slug'), applicawithAllTags/withAnyTags - Eager-load
tagsautomaticamente quando la colonnatags.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']etotals['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:
- Typo:
ind.cognameinvece diind.cognome— il campo Cognome nella tabella era sempre vuoto/undefined (API restituiscecognome) - Email gate:
if (ind.emails && ind.emails.length > 0)filtro bloccante — individui senza email salvata incontattivenivano SILENZIOSAMENTE scartati, mai aggiunti alla tabella
Fix:
ind.cogname→ind.cognome(righe 227 e 256)- Rimossa condizione
if (ind.emails...)— ora anche individui senza email vengono aggiunti alla tabella email: ind.emails[0]→email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : ''— email vuota se nessuna presente- 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@phpnel partialtable-settings.blade.phpa 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) daindividuiegruppi— duplicate rispetto al nuovo modal unificato - [DONE] Rimosse vecchie funzioni JS (
saveVista(),toggleColumn(),showSaveVistaModal()) daindividuiegruppi - [DONE] Individui:
$allColumnsespanse 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, ...}insortTable()eapplyColumnFilter()) - [DONE] I controller per eventi, documenti, mailing-liste hanno già
$tableColumnscon flagvisible— stesso meccanismo di gruppi. Nessuna modifica necessaria. - [DONE] Fix: pulsante edit vista usa
data-*attributi invece dionclickconaddslashes(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) ecolonne_ordine(json) aviste_report - Guardia
Schema::hasColumnper compatibilità cross-DB (MySQL ↔ SQLite) - Già eseguita in batch 38
Backend:
VistaReport.php:$fillable+$casts(array) per entrambi i nuovi campiVistaReportController@store/@update: validazione estesa concolonne_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:$allColumnsespanse con tutte le colonne DB disponibili (da 6 a 17)IndividuoController: aggiuntouse App\Models\VistaReportper la query$userVistas
Frontend JS (public/js/column-manager.js):
- Classe
ColumnManager: resize via mouse drag su handle, reorder via HTML5 DnD, persistenza larghezze insessionStorage, fallback davista-dataDOM 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@phpblock — ora usa$userVistaspassato dal controller - Non usa più
Auth::id()direttamente — riceve$userVistasgià popolato
Views — tutte e 5 aggiornate:
individui/index.blade.php:$allColumnsnella 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 sudata-column sortTable()eapplyColumnFilter()ora usanogetColumnIndex()invece di hardcoded mappa
gruppi/index.blade.php:- RIMOSSE modal legacy:
#saveVistaModal,#vistaListModal - RIMOSSO pulsante "Salva Vista" legacy
- RIMOSSE funzioni JS legacy:
showSaveVistaModal(),saveVista()
- RIMOSSE modal legacy:
eventi/index.blade.php: colonne dinamiche via$visibleColumns,@section('scripts')con ColumnManager +initTableSettings()mailing-liste/index.blade.php: stessa strutturadocumenti/index.blade.php: ColumnManager init (non reorderable), column-manager.js incluso- Tutti i
<th>hannodata-columnattributo per mapping JS - Tutte chiamano
initTableSettings()doponew ColumnManager()nelDOMContentLoaded
Performance fix:
- Rimossa query
VistaReport::where(...)->get()dal@phpblock intable-settings.blade.php(1 query extra per ogni pagina load su 5 entità) - Spostata nei 5 controller come
$userVistaspassata viacompact() - 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
$visibleColumnsquando$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(chiamavashowSaveVistaModal()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:
- Sostituito
onclickcondata-*attributi (data-vista-id,data-vista-nome,data-vista-default,data-vista-colonne) usando{{ }}di Blade (che applicahtmlspecialchars, codificando"→"sicuro in HTML). editVista()ora accetta un array direttamente (non più JSON string da parsare).- Click handler registrato in
initTableSettings()viadocument.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:
VistaReportController@update(linea 101-103): aggiuntoif ($request->expectsJson()) { return response()->json([...]); }— così il fetch riceve JSON 200, non un redirect 302.- Fetch headers in
table-settings.blade.php: aggiunto'Accept': 'application/json'— necessario perchéexpectsJson()controlla l'headerAccept, nonContent-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: aggiuntiauth_method(varchar 20, default 'password') +google_oauth_connection_id(FK → google_oauth_connections, nullOnDelete)sender_accounts: stesse colonne- Guardia
Schema::hasColumnper 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 → datetimeuser(): BelongsTo UserisExpired(): controllo scadenza tokenrefresh(): refresh automatico via GoogleOAuthServicegetValidAccessToken(): 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=consentgetAuthUrl(service): URL di autorizzazione con scopes specifici per serviziohandleCallback(code, service): scambia code per token, crea/aggiorna GoogleOAuthConnectiongetAccessToken(connection): cache 5 min, refresh automatico se expiredrefreshToken(connection): fetch nuovo token via refresh_token, elimina se refresh falliscerevoke(connection): revoca token Google, elimina record DBcreateXoAuth2SmtpTransport(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 OAuthcallback(request): gestisce risposta Google, crea connessione, redirect a impostazioni con success/errorrevoke(connection): revoca connessione (solo proprietario)status(): JSON con stato connessioni
Config/Route changes:
config/services.php: sezionegoogleconclient_id,client_secret,redirect(usaconfig('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=googlein URL (dopo callback)
Modifiche EmailSetting.php:
$fillable+auth_method,google_oauth_connection_id$casts+google_oauth_connection_id→ integergoogleOAuthConnection(): BelongsTo relationshipgetImapConfig(): se auth_method=oauth, impostaauthentication => 'oauth'e password = access tokengetDecryptedPassword(): se OAuth, risolve token invece di decrittare passwordgetDecryptedSmtpPassword(): stessa logica per SMTPresolveOAuthToken(): carica relazione + chiama GoogleOAuthService::getAccessTokengetImapClient(): passaauthentication => 'oauth'se auth_method=oauthgetSmtpMailer(): se OAuth →buildOAuthSmtpMailer()(XOAuth2 solo), altrimenti DSN normalebuildOAuthSmtpMailer(): EsmtpTransport + XOAuth2AuthenticatorbuildSmtpDsn(): estratto da EmailSettingsController (password decrittata)
Modifiche SenderAccount.php:
- Stesso pattern:
auth_method,google_oauth_connection_id, relationship,resolveOAuthToken() getDecryptedPassword(): OAuth-awaresendEmail(): se OAuth →buildOAuthMailer()(XOAuth2), altrimenti DSNbuildOAuthMailer(): EsmtpTransport + XOAuth2Authenticator
Modifiche EmailSettingsController.php:
index(): passa$googleStatus→GoogleOAuthService::getConnectionStatus()save(): validazione + salvataggioauth_method,google_oauth_connection_idtestSmtp(): ora usa$settings->getSmtpMailer()invece di costruire DSN manualmentesenderStore()/senderUpdate(): validazione + salvataggioauth_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à
XOAuth2Authenticatorin vendor (vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php), inviaAUTH 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\GmailNON 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=trueper 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:
- Eventi: modal mass tag HTML corrotto (
×→<div class="modal fade" id="deleteModal") - Eventi + Documenti: nessun feedback visivo dopo submit mass tag (mancava
session('error')su Eventi; mancavano ENTRAMBI su Documenti) - Eventi + Documenti: nessuna icona TAG per-riga nelle azioni
- Documenti: mancava funzione
openSingleTag()JS
Fix:
resources/views/eventi/index.blade.php:340— Ripristinato×nel close button del mass tag modalresources/views/eventi/index.blade.php:22-27— Aggiunto@if(session('error'))alert dangerresources/views/eventi/index.blade.php:283-285— Aggiunto pulsante per-rigafa-tags→openSingleTag(eventId)resources/views/eventi/index.blade.php:427-431— Aggiunta funzione JSopenSingleTag(eventId)resources/views/documenti/index.blade.php:21-32— Aggiunti@if(session('success'))+@if(session('error'))alertresources/views/documenti/index.blade.php:283-285e484-486— Aggiunto pulsante per-rigafa-tagsin vista griglia + listaresources/views/documenti/index.blade.php:1318-1323— Aggiunta funzione JSopenSingleTag(docId)con querySelector per.doc-checkbox[value=...]- Verifica JS brace balance: OK su entrambi (diff=0)
- 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 metodoupdate()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_tipoveniva 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), mentreupdate()usavaTipologiaDocumento::opzioni()(dinamico). Nuove tipologie aggiunte via UI non sarebbero state accettate dastore(). - Fix: Allineato
store()a usareTipologiaDocumento::opzioni()comeupdate().
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 -lsu 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):
- Toolbar button:
onclick="$('#massTagModal').modal('show')"→onclick="showMassTagModal()" - Rimosso listener
show.bs.modalche popolava i campi - Aggiunta funzione
showMassTagModal()(stesso pattern di Gruppi/Individui) che validagetSelectedIds()e popola i campi prima di aprire la modale openSingleTag()ora chiamashowMassTagModal()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()) azzeraconfig[password]econfig[client_secret]per sicurezza (valore'', placeholder......) encryptAndSetConfig()inCalendarioConnessione.phpcontrollava$config[$field] !== ''e saltava la crittografia, ma poi eseguiva$this->config = $config— la stringa vuota sovrascriveva il valore crittato precedenteStorageRepositoryNON 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_activesenza<input type="hidden" name="is_active" value="0"> - Quando non spuntato, il campo non veniva inviato →
$request->boolean('is_active', true)restituiva sempretrue - La connessione restava sempre attiva indipendentemente dallo switch
Root cause 3 — Query duplicata nella view:
resources/views/impostazioni/index.blade.php:773ri-eseguivaCalendarioConnessione::orderBy('ordine')->get()nonostante il controller lo passasse già viacompact()
Fix:
app/Models/CalendarioConnessione.php:56—encryptAndSetConfig()ora accettaarray $existingConfig = []e preserva i valori crittati esistenti quando il campo submitted è vuoto (stesso pattern diStorageRepositoryService::encryptSensitiveConfig())app/Http/Controllers/CalendarioConnessioneController.php:67—update()passa$connessione->configcome secondo parametro aencryptAndSetConfig()resources/views/impostazioni/index.blade.php:1576— Aggiunto<input type="hidden" name="is_active" value="0">prima della checkboxresources/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):
- Rimosso listener
show.bs.modal - Creata funzione
showMassTagModal()(con fallback perdata-pre-selected) openSingleTag()ora usashowMassTagModal()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_SECRETin.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'if2026_05_26_000001_create_tipologie_eventi_table.php— spostato insert dentro l'if2026_05_12_000001_create_ruoli_table.php— spostato insert dentro l'if2026_05_11_000006_add_email_attachment_tipologia.php—insert()→updateOrInsert()
Seeder: firstOrCreate
DiocesiSeeder.php:Diocesi::create()→::firstOrCreate(['nome' => ...])ComuniSeeder.php:Comune::create()→::firstOrCreate(['codice_istat' => ...])DatabaseSeeder.php:User::create()→::firstOrCreate(['email' => ...])+ guardiarole_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:
- Nessuna route mass-tag per mailing-liste — Eventi, Documenti, Individui, Gruppi avevano
POST /{entity}/mass-tagma MailingList no - Nessun metodo
massTag()in MailingListController — assente rispetto agli altri 4 controller - Nessun pulsante per-riga tag nella index view (mancava
<button type="button" onclick="openSingleTag()">) - Nessun pulsante toolbar "Tag" nella index view (mancava il mass tag button)
- Checkbox
attivasenza hidden fallback in create/edit — stesso bug ricorrente: deselezionando, il campo non veniva inviato
Fix:
app/Http/Controllers/MailingListController.php:189-214— Aggiunto metodomassTag()(stesso pattern diEventoController@massTag):- Autorizzazione via
$this->authorizeWrite('mailing') - Validazione:
tagsrequired array,moderequired in:assign,remove - Processa in chunk(100) via
syncWithoutDetaching()(assign) /detach()(remove) - Restituisce
back()->with('success', ...)
- Autorizzazione via
routes/web.php:177— Aggiunta routePOST mailing-liste/mass-tag→MailingListController@massTag(name:mailing-liste.mass-tag)resources/views/mailing-liste/index.blade.php:select-allcheckbox: permesso esteso dacanDeleteMailingacanWriteMailing || canDeleteMailingrow-checkbox: stessa estensione permesso- Toolbar: aggiunto pulsante "Tag" (btn-sm btn-info,
showMassTagModal()) prima di "Elimina Selezionati" - Azioni per-riga: aggiunto pulsante
fa-tagsconopenSingleTag(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, hiddenids) - Aggiunte funzioni JS:
showMassTagModal(),openSingleTag(listId)
resources/views/mailing-liste/create.blade.php:23— Aggiunto<input type="hidden" name="attiva" value="0">prima della checkboxresources/views/mailing-liste/edit.blade.php:23— Stesso hidden fallbackresources/views/mailing-liste/edit.blade.php:11— Fix percorso hardcoded:url('/mailing-liste/' . ...)→route('mailing-liste.update', ...)
Verifica:
- PHP lint: OK su controller + routes
- JS brace balance: OK su index view
- Route list:
POST mailing-liste/mass-tag→MailingListController@massTag(name:mailing-liste.mass-tag)
2026-06-18 — Fix import CSV: freeze server + bug IndividuoController
Problema freeze server (Gruppi + Individui)
Importando CSV con molte righe, il server si bloccava:
- PHP max_execution_time (30s) — killava il processo su import grandi
- Session lock — altre richieste dello stesso utente restavano in attesa
- Nessuna transazione DB — ogni
create()era una INSERT individuale autocommit - 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— aggiuntiuse DB, set_time_limit, session_write_close, fgetcsv length=0, DB::transactionapp/Http/Controllers/IndividuoController.php— strict_types, Log import, DB import, header check, trim, set_time_limit, session_write_close, DB::transaction
Verifica
php -lsu 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/ebootstrap/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 subfolderFORCE_HTTPS,TRUSTED_PROXIES— proxy/HTTPS configurabileSESSION_SECURE_COOKIE,SESSION_SAME_SITE— sessione cross-protocolAPP_ENV=production,APP_DEBUG=false— safe defaults per produzione
build-dist.sh
- Aggiunto
php diagnose.phpcome passo #0 (pre-installazione) e #7 (verifica finale) - Istruzioni post-estrazione complete
File modificati
diagnose.php(nuovo) — standalone diagnostica.env.example— variabili mancanti aggiuntebuild-dist.sh— passo diagnose
Verifica
php -l diagnose.php: OKphp 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:
- Critico — Middleware duplicato in
bootstrap/app.php:EncryptCookies,AddQueuedCookiesToResponse,StartSession,ShareErrorsFromSessionerano 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. SESSION_SECURE_COOKIEnon impostato nel.env— conAPP_URL=https+FORCE_HTTPS=true, Laravel impostava il flagSecuresul 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_URLda HTTPS a HTTP (ambiente locale senza HTTPS) - Cambiato
APP_PROTOCOLdahttpsahttp - Cambiato
FORCE_HTTPSdatrueafalse - Cambiato
TRUSTED_PROXIESda*a CIDR espliciti
diagnose.php — 4 bug fix:
unlink()senzafile_exists()nelle linee 863 e 900: causava PHP Warning quando curl non creava il cookie jar (es. HTTPS non raggiungibile). Aggiuntofile_exists($cookieJar) && unlink($cookieJar).- Regex middleware duplicato non matchava array multi-riga: il pattern
[^\]]*non matcha newline. Sostituito con(.+?)con flags. Classe name extraction semplificata a([a-zA-Z]+)::class. CURLOPT_COOKIEJARnon funzionante in questo ambiente (curl 8.14.1 + PHP 8.4 — il file non viene creato dacurl_close()). RiscrittotestCsrfLogin()per parsare manualmente gli headerSet-Cookiedalla prima risposta e reinviarli viaCURLOPT_COOKIEnella POST. Eliminata dipendenza daCOOKIEJAR/COOKIEFILE.- 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 adeguatidiagnose.php— fix unlink warning, regex middleware multi-line, CSRF test manual-cookie, verbose debug
2026-06-19 — diagnose.php: storage false positivo + ForceHttps registration check
Fix 1 — Storage permission false positive
Problema: diagnose.php segnava 15 storage directory come ❌ NON SCRIVIBILE quando php diagnose.php veniva eseguito da CLI (utente opencode). Le directory sono di proprietà www-data:www-data con permessi 775, quindi il web server può scrivere — il falso positivo generava confusione.
Diagnosi:
- CLI esegue come
opencode→is_writable()controlla i permessi "other" (5 = r-x) →false - Web server esegue come
www-data→ ownerrwx→true - Questo è normale su server condivisi/Debian standard
Fix (diagnose.php:659-660):
- Aggiunto
elseif ($owner === WEB_USER && in_array($perms, ['775', '777', '755', '770'])) - Se il proprietario è
www-datae permessi sono775/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:
- ✅
"registrato correttamente"— se ForceHttps::class è argomento di una chiamata$middleware->xxx() - ⚠️
"IMPORTATO ma NON registrato"— se la stringa ForceHttps esiste (use import) ma non in una chiamata - ⚠️
"NON presente"— se ForceHttps non esiste nel file
- ✅
Verifica
php -l diagnose.php: No syntax errors detectedphp diagnose.phpsu 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, generapost-deploy.sh(heredoc) e lo include nell'archivio - Dopo il
tar,post-deploy.shviene 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 --forceesegue solo migration pendenti - Sicuro: modalità
checknon modifica nulla - Interattivo: chiede conferma prima di azioni distruttive
- Batch:
--yesper automazione CI/CD - Compatibile: integra
diagnose.phpgià esistente (check + fix) - Autopulente:
post-deploy.shnon resta nella working dir di build
Verifica
bash -n build-dist.sh: syntax OKbash -n post-deploy.sh: syntax OKbash post-deploy.sh check: eseguito correttamentetar tzf glastree-*.tar.gz | grep post-deploy: presente nell'archiviorm -f post-deploy.shdopo tar: pulizia OK
2026-06-22 — Estensione Gruppi: contatti propri + diocesi multiple
Obiettivo: Aggiungere contatti (email, telefono) ai gruppi e supportare assegnazione di più diocesi.
Cosa è stato fatto
Migration (2 nuove tabelle):
2026_06_22_000002_create_gruppo_contatti_table.php—gruppo_contatticon: id, gruppo_id (FK cascade), tipo, valore, etichetta, is_primary, timestamps2026_06_22_000003_create_diocesi_gruppo_table.php— pivotdiocesi_gruppocon: 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_primarygruppo(): BelongsTo Gruppo
Model Gruppo.php:
diocesi(): cambiata daBelongsToaBelongsToManyviadiocesi_gruppogruppoContatti(): nuovaHasManyrelation- Accessor
getEmailPrimariaAttribute(): primo contatto email (is_primary preferito) - Accessor
getTelefonoPrimarioAttribute(): primo contatto telefono/cellulare (is_primary preferito) $appends: già presenteemail_primaria,telefono_primario(invariato)$fillable:diocesi_idmantenuto (nullable) per backward compat con import CSV
Model Diocesi.php:
gruppi(): cambiata daHasManyaBelongsToManyviadiocesi_gruppo
GruppoController.php:
store()/update(): validazione cambiata dadiocesi_id(nullable|exists) adiocesi_ids(nullable|array|exists)store()/update(): nuova validazionecontattiarray con tipo (in:email,telefono,cellulare), valore, etichetta, is_primarystore(): crea contatti dopo create; sync diocesiupdate(): cancella+ricrea contatti se presenti; sync diocesi (o detach se assenti)edit(): eager-loadgruppoContatti, passa$selectedDiocesiIdsshow(): eager-loadgruppoContattiindex(): eager-loadgruppoContatticreate(): 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 multipleselect 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
$selectedDiocesiIdspre-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?->nomea collection implode - Aggiunte righe Email/Telefono (da accessor) e "Altri Contatti" nella card info
View gruppi/index.blade.php:
- Diocesi: da
$gruppo->diocesi?->nomea collection implode - Telefono/Email: da hardcoded
-a$gruppo->telefono_primario/$gruppo->email_primaria
View gruppi/partials/tree-item.blade.php:
- Diocesi: da
$gruppo->diocesi?->nomea collection implode
View individui/show.blade.php + individui/edit.blade.php:
- Diocesi nei gruppi dell'individuo: da
$gruppo->diocesi?->nomea collection implode
Backward compatibility
- Colonna
diocesi_idsugruppimantenuta (nullable) per import CSV - CSV import (
importStore()) usa ancoradiocesi_id→ singola diocesi $fillableinclude ancoradiocesi_id- Nessuna modifica a
IndividuooContattoesistenti
Verifica
php -lsu tutti i file modificati: OK- Migrations eseguite: ✅
- Relazioni verificate:
diocesi()BelongsToMany,gruppoContatti()HasMany,email_primaria/telefono_primarioaccessors 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_grupponon eseguita (PENDING) GruppoContatto.phpnon nell'autoloader (composer dump-autoloadnon eseguito)storage/logs/laravel.lognon 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:
sudo chmod -R 775 storage bootstrap/cache— permessisudo usermod -a -G www-data opencode— utente CLI nel gruppo www-data- Inseriti manualmente 3 migration record mancanti in
migrationstable (già eseguite in passato ma mai registrate) php artisan migrate --force— eseguita2026_06_22_000003_create_diocesi_gruppo_tablecomposer dump-autoload— autoloader rigenerato (41464 classi)php artisan view:clear && php artisan route:clear && php artisan config:clear
Risultato:
class_exists(App\Models\GruppoContatto)→ ✅gruppipage 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:
allowClear: truein Select2 multiple mode non serve (ogni tag ha già la X per rimuoverlo) e causa conflitti di rendering- Bootstrap 4 applica
height: 65pxal textarea del search field - Nessun CSS specifico per normalizzare il search field inline
Fix:
- Rimosso
allowClear: trueda entrambi i file (create.blade.php,edit.blade.php) - Aggiunto CSS per normalizzare
height: 28px,border: none,background: transparent,width: autoconmin-width: 30px
File modificati:
resources/views/gruppi/create.blade.php— rimosso allowClear, aggiunto CSS search fieldresources/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 templateSelectionresources/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:
DiocesiSeeder.phpriscritto 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)aggiuntofirstOrCreateper idempotenza
build-dist.sh(post-deploy.shgenerato):- Upgrade: aggiunto step
[6/10] Seed diocesidopo migrazioni (rinumerati da [1-5/9] → [1-10/10]) - Install: aggiunto step
[9/11] Seed diocesidopo migrazioni (rinumerato da [1-8/10] → [1-11/11]) - Usa
--forceper bypassare conferma in produzione
- Upgrade: aggiunto step
File modificati:
database/seeders/DiocesiSeeder.php— riscritto con 225 nomi + fix encodingbuild-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:
SESSION_DRIVER=database: cambiato dafileadatabase. La sessione su DB non soffre di:- Permessi filesystem errati
- Lock concorrente su file
- Pulizia sessioni scadute lottery-based
- Migration
2026_06_23_000001_create_sessions_table.php: con guardSchema::hasTableper idempotenza cross-DB diagnose.php: aggiunto test CSRF su IP locale automatico. Rilevahostname -Ied esegue POST login → verifica 419 suhttp://<lan-ip>/login.enve.env.example:SESSION_DRIVER=file→SESSION_DRIVER=database- 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=databasediagnose.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) amailing_lists
Model MailingList.php:
sender_account_idaggiunto a$fillablesenderAccount(): nuova relazione BelongsTo →SenderAccount
Controller MailingListController.php:
create(): passa$senderAccounts(SenderAccount::active()->get()) alla viewedit(): passa$senderAccountsalla view, eager-loadsenderAccountstore(): validazionesender_account_idnullable|exists, salvato in createupdate(): validazionesender_account_idnullable|exists, salvato in update
View mailing-liste/create.blade.php:
- Aggiunto select per
sender_account_iddopo il select firma
View mailing-liste/edit.blade.php:
- Aggiunto select per
sender_account_iddopo il select firma, conselectedse 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->senderAccountsemittente_idnon fornito esplicitamente (stesso pattern del firma_id fallback già esistente)invioElabora(): fallback amittente_id/firma_iddalla 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 relazioneapp/Http/Controllers/MailingListController.php— sender_account in create/edit/store/updateapp/Http/Controllers/MailingController.php— fallback mittente/firma da mailing listresources/views/email/compose.blade.php— firma spostata dopo il bodyresources/views/mailing-liste/create.blade.php— select mittente aggiuntoresources/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:
- JS — Bootstrap 4 event compatibility:
addEventListener('show.bs.modal', ...)al mass move modal usava native DOM listener, ma Bootstrap 4.6.2 disponeshow.bs.modalcome evento jQuery. Il listener non veniva mai eseguito → hidden inputmassMoveIdsmai popolato →explode(',', '')→['']→empty([''])èfalse→Documento::whereIn('id', [''])(0 rows affected) → falso successo. - PHP — No guardia su array vuoto dopo explode:
massMove(),massDownload(),massDestroy(),massAssociate(),massTag()inDocumentoController.phpnon 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: aggiuntoarray_values(array_filter($ids, fn($v) => is_numeric($v)))inmassMove(),massDownload(),massDestroy(),massAssociate(),massTag()— filtra stringhe vuote e non-numeriche prima di ogniwhereInmassMove(): aggiunto controllo esplicitocount($ids)per validare singolo documento nel flusso per-documento
File modificati:
resources/views/documenti/index.blade.php—addEventListener→ jQuery.on()a riga 1306app/Http/Controllers/DocumentoController.php—array_filterguard 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 allineamassMoveModalal pattern esistente - Nessun test automatizzato presente nel progetto (no Pest, no PHPUnit config)