diff --git a/MEMORY.md b/MEMORY.md index fb680df9..6cf9d2e3 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -291,26 +291,92 @@ if (!empty($contatto['individuo_id'])) { create } ``` ## Prossimi Passi -- [DONE] Aggiungere filtro Tag nella vista Documenti — già presente (tag filter bar + colonna Tag in lista/griglia) -- [DONE] Gestire il caso `tag[]` vuoto — già gestito da `$request->filled('tag')` in tutti i 4 controller index -- [DONE] MailingList — tag support completato (controller, index/create/edit/show views, filter bar) -- [DONE] Report — tag columns in ReportColumnRegistry + tag filter in custom reports + UI multi-select -- [DONE] Ricerca per Tag — MailingList integrata nei risultati -- [DONE] `storeCustom()` ora salva correttamente `tag_filter` e `tag_filter_mode` -- [DONE] `MailingListController@store/update` — auto-fetch email da Individuo quando non fornita -- [DONE] Verificata presenza `@stack('scripts')` nel layout — presente a riga 305 (prima di `@yield('scripts')`) -- [DONE] Verificata mass tag action Documenti (`DocumentoController@massTag`) — funzionante -- [DONE] Verificato: tutti i 5 entity controllers passano `$allTags` alle viste index -- [DONE] La logica `$toRemove = array_diff($existingIds, $newIds)` in `update()` è corretta — rimuove solo contatti che l'utente ha esplicitamente deselezionato o rimosso dalla tabella. Non necessita modifiche. -- [DONE] SweetAlert2 conferma su salvataggio mailing list (edit.blade.php) quando ci sono contatti senza email -- [DONE] Normalizzata `invia()` da `flatMap`+`pluck` a `map`+`first()` (1 email per individuo, come `invioElabora()`) -- [DONE] Aggiunto logging contatti saltati in `invia()` e `invioElabora()` (quando senza email) -- [DONE] Fix: contattiCaricati initialization ora include codice_id/cognome/nome da righe DOM — contatti esistenti non più blank dopo Carica contatti -- [DONE] Fix: EmailController@destroy ora redirect a inbox invece che back() (non rimane su email cancellata) -- [DONE] Mass action Individui: Assegna Gruppo (route + controller + modal con select gruppi + Assign/Remove radio) -- [DONE] Mass action Individui: Assegna Tag (route + controller + modal con _tag-selector) -- [DONE] Mass action Gruppi: Assegna Tag (route + controller + modal con _tag-selector) -- [DONE] Mass action Gruppi: Elimina (route + controller + modal con conferma, detach membri + delete) -- [DONE] Fix: IndividuoController@massGruppo e @massTag ora convertono stringa ids in array (non funzionavano perché hidden input invia stringa, non array) -- [DONE] Ricerca per Tag multipli: controller accetta `?tag[]=slug1&tag[]=slug2`, usa `withAllTags` (AND logic) per 2+ tag. Vista aggiornata: checkbox cliccabili per multi-selezione, info boxes unificate, "Vedi tutti" passa tutti i tag +- [DONE] ... (existing items) - Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag +- [DONE] Fix performance: spostata query `VistaReport::where(...)->get()` da `@php` nel partial `table-settings.blade.php` a tutti i 5 controller (prima veniva eseguita 1 query per ogni pagina load per ogni entity, anche senza mai aprire la modale) +- [DONE] Rimosse vecchie modal legacy (`#saveVistaModal`, `#colonneModal`, `#vistaListModal`) da `individui` e `gruppi` — duplicate rispetto al nuovo modal unificato +- [DONE] Rimosse vecchie funzioni JS (`saveVista()`, `toggleColumn()`, `showSaveVistaModal()`) da `individui` e `gruppi` +- [DONE] Individui: `$allColumns` espanse da 6 a 17 colonne (aggiunte: data_nascita, genere, indirizzo, cap, citta, provincia, tipo_documento, numero_documento, scadenza_documento, note, created_at) — tutte con ``/`` condizionali nella vista +- [DONE] Individui: `getColumnIndex()` introdotta per lookup dinamico dell'indice colonna (sostituisce hardcoded `{codice:1, cognome:2, ...}` in `sortTable()` e `applyColumnFilter()`) +- [DONE] I controller per eventi, documenti, mailing-liste hanno già `$tableColumns` con flag `visible` — stesso meccanismo di gruppi. Nessuna modifica necessaria. +- [DONE] Fix: pulsante edit vista usa `data-*` attributi invece di `onclick` con `addslashes(json_encode(...))` — previene rottura HTML per JSON contenente `"` +- [DA FARE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print +- [DA FARE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload) +- [DA FARE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista + +## 2026-06-10 — Per-User Column Views (colonne visibili, larghezze, ordine) + +**Obiettivo**: Implementare viste colonne personalizzabili per utente su tutte le 5 entity list pages (individui, gruppi, eventi, documenti, mailing-liste), con persistenza tra login. + +### Cosa è stato fatto + +**Migration** (`2026_06_10_044946_add_column_widths_and_order_to_viste_report_table.php`): +- Aggiunge `colonne_larghezze` (json) e `colonne_ordine` (json) a `viste_report` +- Guardia `Schema::hasColumn` per compatibilità cross-DB (MySQL ↔ SQLite) +- Già eseguita in batch 38 + +**Backend**: +- `VistaReport.php`: `$fillable` + `$casts` (array) per entrambi i nuovi campi +- `VistaReportController@store`/`@update`: validazione estesa con `colonne_larghezze`, `colonne_ordine` (array) +- Tutti i 5 controller passano alla vista: `$tableColumns` (array `['key','label','visible']`), `$entityType`, `$columnWidths`, `$visibleColumns`, `$allColumns` (varia nome in base al controller) +- Tutti i 5 controller ora passano `$userVistas` (precaricate) invece di eseguire query nella view +- `IndividuoController`: `$allColumns` espanse con tutte le colonne DB disponibili (da 6 a 17) +- `IndividuoController`: aggiunto `use App\Models\VistaReport` per la query `$userVistas` + +**Frontend JS** (`public/js/column-manager.js`): +- Classe `ColumnManager`: resize via mouse drag su handle, reorder via HTML5 DnD, persistenza larghezze in `sessionStorage`, fallback da `vista-data` DOM element, `printWithSettings()` che clona la tabella in finestra print-ready + +**Modal** (`resources/views/partials/table-settings.blade.php`): +- Modal unificato con 3 tab: Colonne (checkbox visibilità + input larghezza + drag-reorder list), Viste (salva/carica/cancella), Stampa (pulsante print) +- Definisce `initTableSettings()` (NON auto-esegue) per evitare race condition con ColumnManager +- **RIMOSSA** query `VistaReport::where(...)->get()` dal `@php` block — ora usa `$userVistas` passato dal controller +- Non usa più `Auth::id()` direttamente — riceve `$userVistas` già popolato + +**Views** — tutte e 5 aggiornate: +- `individui/index.blade.php`: + - `$allColumns` nella vista ora ha 17 colonne (vs 6 prima) + - `` e `` condizionali per tutte le nuove colonne (data_nascita, genere, indirizzo, cap, citta, provincia, tipo_documento, numero_documento, scadenza_documento, note, created_at) + - RIMOSSE modal legacy: `#saveVistaModal`, `#colonneModal` + - RIMOSSE funzioni JS legacy: `saveVista()`, `toggleColumn()` + - RIMOSSO sync checkboxes nel `DOMContentLoaded` (non più necessari) + - NUOVA: `getColumnIndex(colKey)` — lookup dinamico dell'indice colonna basato su `data-column` + - `sortTable()` e `applyColumnFilter()` ora usano `getColumnIndex()` invece di hardcoded mappa +- `gruppi/index.blade.php`: + - RIMOSSE modal legacy: `#saveVistaModal`, `#vistaListModal` + - RIMOSSO pulsante "Salva Vista" legacy + - RIMOSSE funzioni JS legacy: `showSaveVistaModal()`, `saveVista()` +- `eventi/index.blade.php`: colonne dinamiche via `$visibleColumns`, `@section('scripts')` con ColumnManager + `initTableSettings()` +- `mailing-liste/index.blade.php`: stessa struttura +- `documenti/index.blade.php`: ColumnManager init (non reorderable), column-manager.js incluso +- Tutti i `` hanno `data-column` attributo per mapping JS +- Tutte chiamano `initTableSettings()` dopo `new ColumnManager()` nel `DOMContentLoaded` + +**Performance fix**: +- Rimossa query `VistaReport::where(...)->get()` dal `@php` block in `table-settings.blade.php` (1 query extra per ogni pagina load su 5 entità) +- Spostata nei 5 controller come `$userVistas` passata via `compact()` +- Totale: -5 query per pagina load (una per ogni entity, anche su pagine non di index) + +**Verifica**: +- JS brace balance: OK su tutti i 5 file (node check) +- PHP lint: OK su tutti i 5 controller +- Presenza `data-column`: individui=19, gruppi=14, eventi=9, mailing-liste=7, documenti=8 +- Fallback `$visibleColumns` quando `$vista === null`: presente e corretto in tutti i 5 controller +- Nessun residuo di vecchie modal legacy in individui e gruppi +- Rimosso bottone "Salva Vista" orfano da `individui/index.blade.php` (chiamava `showSaveVistaModal()` non più esistente) +- Aggiunto pulsante modifica ✏️ nel tab Viste per ogni vista salvata: popola il form "Salva vista corrente" con nome, default flag e colonne visibili; cambia bottone in "Aggiorna" e fa PUT `/viste/{id}` invece di POST; zero query extra +### 2026-06-10 — Fix: editVista onclick → data-* attributes + +**Problema**: Il pulsante modifica vista usava `onclick` con `addslashes(json_encode(...))`. In un attributo HTML delimitato da `"`, il backslash-escaping di `\"` non è gestito in modo standard dai browser — il JSON contenente `"` poteva rompere l'attributo HTML. + +**Fix**: +1. Sostituito `onclick` con `data-*` attributi (`data-vista-id`, `data-vista-nome`, `data-vista-default`, `data-vista-colonne`) usando `{{ }}` di Blade (che applica `htmlspecialchars`, codificando `"` → `"` sicuro in HTML). +2. `editVista()` ora accetta un array direttamente (non più JSON string da parsare). +3. Click handler registrato in `initTableSettings()` via `document.querySelectorAll('.edit-vista-btn')` + `dataset` (browser decodifica automaticamente `"` → `"`). + +### 2026-06-10 — Fix: 405 Method Not Allowed su update vista (via AJAX fetch) + +**Problema**: Modificando una vista e cliccando "Aggiorna", `fetch()` inviava `PUT /viste/{id}` ma il controller `VistaReportController@update` restituiva `return back()` (302 redirect). `fetch()` seguiva il redirect — in alcuni browser il metodo PUT veniva preservato, ma la route `gruppi` accetta solo GET → 405 Method Not Allowed. + +**Fix**: +1. `VistaReportController@update` (linea 101-103): aggiunto `if ($request->expectsJson()) { return response()->json([...]); }` — così il fetch riceve JSON 200, non un redirect 302. +2. Fetch headers in `table-settings.blade.php`: aggiunto `'Accept': 'application/json'` — necessario perché `expectsJson()` controlla l'header `Accept`, non `Content-Type`. diff --git a/app/Http/Controllers/DocumentoController.php b/app/Http/Controllers/DocumentoController.php index 4fa7146b..00d7aa0d 100644 --- a/app/Http/Controllers/DocumentoController.php +++ b/app/Http/Controllers/DocumentoController.php @@ -92,11 +92,45 @@ class DocumentoController extends Controller 'children' => $f->children->map(fn($c) => ['id' => $c->id, 'nome' => $c->nome]), ]); + $vista = \App\Models\VistaReport::where('user_id', auth()->id()) + ->where('tipo', 'documenti') + ->where('is_default', true) + ->first(); + + $userVistas = \App\Models\VistaReport::where('user_id', auth()->id()) + ->where('tipo', 'documenti') + ->orderBy('is_default', 'desc') + ->orderBy('nome') + ->get(); + + $allColumnDefs = [ + ['key' => 'nome_file', 'label' => 'Nome'], + ['key' => 'tipologia', 'label' => 'Tipologia'], + ['key' => 'dimensione', 'label' => 'Dimensione'], + ['key' => 'visibilita', 'label' => 'Contesto'], + ['key' => 'tag', 'label' => 'Tag'], + ['key' => 'created_at', 'label' => 'Data'], + ]; + $defaultVisible = ['nome_file', 'tipologia', 'dimensione', 'visibilita', 'tag', 'created_at']; + $visibleColumns = $vista && !empty($vista->colonne_visibili) + ? $vista->colonne_visibili + : $defaultVisible; + + $tableColumns = array_map(fn($col) => [ + 'key' => $col['key'], + 'label' => $col['label'], + 'visible' => in_array($col['key'], $visibleColumns), + ], $allColumnDefs); + $entityType = 'documenti'; + $columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : []; + return view('documenti.index', compact( 'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle', 'currentRepo', 'repositories', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', - 'cartelleMoveOptions', 'allTags' + 'cartelleMoveOptions', 'allTags', + 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', + 'userVistas' )); } diff --git a/app/Http/Controllers/EventoController.php b/app/Http/Controllers/EventoController.php index fcf82d21..0897c52b 100644 --- a/app/Http/Controllers/EventoController.php +++ b/app/Http/Controllers/EventoController.php @@ -55,7 +55,42 @@ class EventoController extends Controller $gruppi = Gruppo::orderBy('nome')->get(); $allTags = Tag::orderBy('name')->get(); - return view('eventi.index', compact('eventi', 'gruppi', 'allTags')); + $vista = \App\Models\VistaReport::where('user_id', auth()->id()) + ->where('tipo', 'eventi') + ->where('is_default', true) + ->first(); + + $userVistas = \App\Models\VistaReport::where('user_id', auth()->id()) + ->where('tipo', 'eventi') + ->orderBy('is_default', 'desc') + ->orderBy('nome') + ->get(); + + $allColumnDefs = [ + ['key' => 'nome_evento', 'label' => 'Nome Evento'], + ['key' => 'descrizione_evento', 'label' => 'Descrizione'], + ['key' => 'tipo_evento', 'label' => 'Tipologia'], + ['key' => 'tipo_recorrenza', 'label' => 'Ricorrenza'], + ['key' => 'gruppi', 'label' => 'Gruppi'], + ['key' => 'responsabili', 'label' => 'Responsabili'], + ['key' => 'tag', 'label' => 'Tag'], + ['key' => 'data_specifica', 'label' => 'Data'], + ['key' => 'created_at', 'label' => 'Creato'], + ]; + $defaultVisible = ['nome_evento', 'descrizione_evento', 'tipo_evento', 'tipo_recorrenza', 'gruppi', 'responsabili', 'tag', 'created_at']; + $visibleColumns = $vista && !empty($vista->colonne_visibili) + ? $vista->colonne_visibili + : $defaultVisible; + + $tableColumns = array_map(fn($col) => [ + 'key' => $col['key'], + 'label' => $col['label'], + 'visible' => in_array($col['key'], $visibleColumns), + ], $allColumnDefs); + $entityType = 'eventi'; + $columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : []; + + return view('eventi.index', compact('eventi', 'gruppi', 'allTags', 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', 'userVistas')); } public function create(Request $request) diff --git a/app/Http/Controllers/GruppoController.php b/app/Http/Controllers/GruppoController.php index 4a9ffcbd..30c3f4f7 100644 --- a/app/Http/Controllers/GruppoController.php +++ b/app/Http/Controllers/GruppoController.php @@ -28,7 +28,27 @@ class GruppoController extends Controller ->where('is_default', true) ->first(); - $allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email', 'tag']; + $userVistas = \App\Models\VistaReport::where('user_id', $user->id) + ->where('tipo', 'gruppi') + ->orderBy('is_default', 'desc') + ->orderBy('nome') + ->get(); + + $allColumnDefs = [ + ['key' => 'nome', 'label' => 'Nome'], + ['key' => 'descrizione', 'label' => 'Descrizione'], + ['key' => 'diocesi', 'label' => 'Diocesi'], + ['key' => 'livello', 'label' => 'Livello'], + ['key' => 'padre', 'label' => 'Gruppo Padre'], + ['key' => 'membri', 'label' => 'Membri'], + ['key' => 'responsabili', 'label' => 'Responsabili'], + ['key' => 'indirizzo', 'label' => 'Indirizzo'], + ['key' => 'citta', 'label' => 'Città'], + ['key' => 'telefono', 'label' => 'Telefono'], + ['key' => 'email', 'label' => 'Email'], + ['key' => 'tag', 'label' => 'Tag'], + ]; + $allColumns = array_column($allColumnDefs, 'key'); $defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili', 'tag']; if ($vista && !empty($vista->colonne_visibili)) { @@ -37,6 +57,14 @@ class GruppoController extends Controller $visibleColumns = $defaultVisible; } + $tableColumns = array_map(fn($col) => [ + 'key' => $col['key'], + 'label' => $col['label'], + 'visible' => in_array($col['key'], $visibleColumns), + ], $allColumnDefs); + $entityType = 'gruppi'; + $columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : []; + $gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']); if (request()->filled('tag')) { @@ -79,7 +107,11 @@ class GruppoController extends Controller 'visibleColumns', 'viewMode', 'vista', - 'allTags' + 'allTags', + 'tableColumns', + 'entityType', + 'columnWidths', + 'userVistas' )); } diff --git a/app/Http/Controllers/IndividuoController.php b/app/Http/Controllers/IndividuoController.php index 98f40f30..3008fadc 100644 --- a/app/Http/Controllers/IndividuoController.php +++ b/app/Http/Controllers/IndividuoController.php @@ -5,14 +5,15 @@ namespace App\Http\Controllers; use App\Models\Individuo; use App\Models\Contatto; use App\Models\Documento; -use App\Models\Gruppo; -use App\Models\Comune; -use App\Models\Diocesi; -use App\Models\Tag; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Storage; -use Illuminate\Validation\Rule; -use Symfony\Component\HttpFoundation\StreamedResponse; + use App\Models\Gruppo; + use App\Models\Comune; + use App\Models\Diocesi; + use App\Models\Tag; + use App\Models\VistaReport; + use Illuminate\Http\Request; + use Illuminate\Support\Facades\Storage; + use Illuminate\Validation\Rule; + use Symfony\Component\HttpFoundation\StreamedResponse; class IndividuoController extends Controller { @@ -50,9 +51,20 @@ class IndividuoController extends Controller ['key' => 'codice', 'label' => 'Codice'], ['key' => 'cognome', 'label' => 'Cognome'], ['key' => 'nome', 'label' => 'Nome'], + ['key' => 'data_nascita', 'label' => 'Data Nascita'], + ['key' => 'genere', 'label' => 'Genere'], + ['key' => 'indirizzo', 'label' => 'Indirizzo'], + ['key' => 'cap', 'label' => 'CAP'], + ['key' => 'citta', 'label' => 'Città'], + ['key' => 'provincia', 'label' => 'Provincia'], + ['key' => 'tipo_documento', 'label' => 'Tipo Documento'], + ['key' => 'numero_documento', 'label' => 'Numero Documento'], + ['key' => 'scadenza_documento', 'label' => 'Scadenza Documento'], ['key' => 'email', 'label' => 'Email'], ['key' => 'telefono', 'label' => 'Telefono'], + ['key' => 'note', 'label' => 'Note'], ['key' => 'tag', 'label' => 'Tag'], + ['key' => 'created_at', 'label' => 'Creato il'], ]; $defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono', 'tag']; @@ -60,8 +72,22 @@ class IndividuoController extends Controller ? $vista->colonne_visibili : $defaultVisible; + $entityType = 'individui'; + $tableColumns = array_map(fn($col) => [ + 'key' => $col['key'], + 'label' => $col['label'], + 'visible' => in_array($col['key'], $visibleColumns), + ], $allColumns); + $columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : []; + + $userVistas = VistaReport::where('user_id', auth()->id()) + ->where('tipo', 'individui') + ->orderBy('is_default', 'desc') + ->orderBy('nome') + ->get(); + $gruppi = Gruppo::orderBy('nome')->get(['id', 'nome']); - return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns', 'allTags', 'gruppi')); + return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns', 'allTags', 'gruppi', 'entityType', 'tableColumns', 'columnWidths', 'userVistas')); } public function exportCSV(Request $request) diff --git a/app/Http/Controllers/MailingListController.php b/app/Http/Controllers/MailingListController.php index 4cd77458..9efce682 100644 --- a/app/Http/Controllers/MailingListController.php +++ b/app/Http/Controllers/MailingListController.php @@ -20,7 +20,40 @@ class MailingListController extends Controller $mailingLists = $query->orderBy('nome')->get(); $allTags = Tag::orderBy('name')->get(); - return view('mailing-liste.index', compact('mailingLists', 'allTags')); + + $vista = \App\Models\VistaReport::where('user_id', auth()->id()) + ->where('tipo', 'mailing-liste') + ->where('is_default', true) + ->first(); + + $userVistas = \App\Models\VistaReport::where('user_id', auth()->id()) + ->where('tipo', 'mailing-liste') + ->orderBy('is_default', 'desc') + ->orderBy('nome') + ->get(); + + $allColumnDefs = [ + ['key' => 'nome', 'label' => 'Nome'], + ['key' => 'descrizione', 'label' => 'Descrizione'], + ['key' => 'contatti_count', 'label' => 'Contatti'], + ['key' => 'tag', 'label' => 'Tag'], + ['key' => 'attiva', 'label' => 'Stato'], + ['key' => 'created_at', 'label' => 'Creata il'], + ]; + $defaultVisible = ['nome', 'descrizione', 'contatti_count', 'tag', 'attiva', 'created_at']; + $visibleColumns = $vista && !empty($vista->colonne_visibili) + ? $vista->colonne_visibili + : $defaultVisible; + + $tableColumns = array_map(fn($col) => [ + 'key' => $col['key'], + 'label' => $col['label'], + 'visible' => in_array($col['key'], $visibleColumns), + ], $allColumnDefs); + $entityType = 'mailing-liste'; + $columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : []; + + return view('mailing-liste.index', compact('mailingLists', 'allTags', 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', 'userVistas')); } public function create() diff --git a/app/Http/Controllers/VistaReportController.php b/app/Http/Controllers/VistaReportController.php index 3e1672d1..50642766 100644 --- a/app/Http/Controllers/VistaReportController.php +++ b/app/Http/Controllers/VistaReportController.php @@ -33,9 +33,11 @@ class VistaReportController extends Controller $this->authorizeWrite('viste'); $data = $request->validate([ 'nome' => 'required|string|max:255', - 'tipo' => 'required|in:individui,gruppi,documenti,eventi', + 'tipo' => 'required|in:individui,gruppi,documenti,eventi,mailing-liste', 'colonne_visibili' => 'nullable|array', 'colonne_ordinamento' => 'nullable|array', + 'colonne_larghezze' => 'nullable|array', + 'colonne_ordine' => 'nullable|array', 'filtri' => 'nullable|array', 'ricerca' => 'nullable|string', 'is_default' => 'nullable|boolean', @@ -55,6 +57,8 @@ class VistaReportController extends Controller 'tipo' => $data['tipo'], 'colonne_visibili' => $data['colonne_visibili'] ?? null, 'colonne_ordinamento' => $data['colonne_ordinamento'] ?? null, + 'colonne_larghezze' => $data['colonne_larghezze'] ?? null, + 'colonne_ordine' => $data['colonne_ordine'] ?? null, 'filtri' => $data['filtri'] ?? null, 'ricerca' => $data['ricerca'] ?? null, 'is_default' => $isDefault, @@ -78,6 +82,8 @@ class VistaReportController extends Controller 'nome' => 'required|string|max:255', 'colonne_visibili' => 'nullable|array', 'colonne_ordinamento' => 'nullable|array', + 'colonne_larghezze' => 'nullable|array', + 'colonne_ordine' => 'nullable|array', 'filtri' => 'nullable|array', 'ricerca' => 'nullable|string', 'is_default' => 'nullable|boolean', @@ -92,6 +98,10 @@ class VistaReportController extends Controller $vistaReport->update($data); + if ($request->expectsJson()) { + return response()->json(['success' => true, 'message' => 'Vista aggiornata.']); + } + return back()->with('success', 'Vista aggiornata.'); } diff --git a/app/Models/VistaReport.php b/app/Models/VistaReport.php index 5b15fd7e..d2c3d167 100644 --- a/app/Models/VistaReport.php +++ b/app/Models/VistaReport.php @@ -10,13 +10,16 @@ class VistaReport extends Model protected $table = 'viste_report'; protected $fillable = [ 'user_id', 'nome', 'tipo', 'colonne_visibili', - 'colonne_ordinamento', 'filtri', 'ricerca', 'is_default' + 'colonne_ordinamento', 'filtri', 'ricerca', 'is_default', + 'colonne_larghezze', 'colonne_ordine', ]; protected $casts = [ 'colonne_visibili' => 'array', 'colonne_ordinamento' => 'array', 'filtri' => 'array', + 'colonne_larghezze' => 'array', + 'colonne_ordine' => 'array', ]; public function user(): BelongsTo diff --git a/database/migrations/2026_06_10_044946_add_column_widths_and_order_to_viste_report_table.php b/database/migrations/2026_06_10_044946_add_column_widths_and_order_to_viste_report_table.php new file mode 100644 index 00000000..aa969567 --- /dev/null +++ b/database/migrations/2026_06_10_044946_add_column_widths_and_order_to_viste_report_table.php @@ -0,0 +1,32 @@ +json('colonne_larghezze')->nullable()->after('colonne_visibili'); + } + if (!Schema::hasColumn('viste_report', 'colonne_ordine')) { + $table->json('colonne_ordine')->nullable()->after('colonne_larghezze'); + } + }); + } + + public function down(): void + { + Schema::table('viste_report', function (Blueprint $table) { + if (Schema::hasColumn('viste_report', 'colonne_larghezze')) { + $table->dropColumn('colonne_larghezze'); + } + if (Schema::hasColumn('viste_report', 'colonne_ordine')) { + $table->dropColumn('colonne_ordine'); + } + }); + } +}; diff --git a/public/js/column-manager.js b/public/js/column-manager.js new file mode 100644 index 00000000..749ae12d --- /dev/null +++ b/public/js/column-manager.js @@ -0,0 +1,384 @@ +class ColumnManager { + constructor(tableId, options = {}) { + this.tableId = tableId; + this.table = document.getElementById(tableId); + if (!this.table) return; + + this.options = Object.assign({ + entityType: 'individui', + storageKey: null, + resizable: true, + reorderable: true, + persistWidths: true, + persistOrder: true, + }, options); + + this.columns = []; + this.columnWidths = {}; + this.columnOrder = []; + this.dragState = null; + this.resizeState = null; + + this.init(); + } + + init() { + this.loadState(); + this.initColumns(); + if (this.options.resizable) this.initResize(); + if (this.options.reorderable) this.initReorder(); + this.applyState(); + } + + initColumns() { + const ths = this.table.querySelectorAll('thead th'); + ths.forEach((th, idx) => { + const colKey = th.dataset.column || `col_${idx}`; + const col = { + index: idx, + key: colKey, + element: th, + width: th.style.width || '', + visible: th.style.display !== 'none', + label: th.textContent.trim(), + }; + this.columns.push(col); + }); + } + + loadState() { + if (this.options.storageKey) { + try { + const saved = sessionStorage.getItem(this.options.storageKey); + if (saved) { + const data = JSON.parse(saved); + this.columnWidths = data.widths || {}; + this.columnOrder = data.order || []; + return; + } + } catch (e) {} + } + const vistaEl = document.getElementById('vista-data'); + if (vistaEl && vistaEl.textContent && vistaEl.textContent !== 'null') { + try { + const vista = JSON.parse(vistaEl.textContent); + this.columnWidths = vista.colonne_larghezze || {}; + this.columnOrder = vista.colonne_ordine || []; + } catch (e) {} + } + } + + persistState() { + if (!this.options.storageKey) return; + try { + sessionStorage.setItem(this.options.storageKey, JSON.stringify({ + widths: this.columnWidths, + order: this.columnOrder, + })); + } catch (e) {} + } + + applyState() { + if (this.columnOrder.length > 0) { + this.reorderColumns(this.columnOrder, false); + } + if (Object.keys(this.columnWidths).length > 0) { + this.applyWidths(); + } + } + + applyWidths() { + this.columns.forEach(col => { + const w = this.columnWidths[col.key]; + if (w) { + col.element.style.width = w; + } + }); + } + + initResize() { + this.table.querySelectorAll('thead th').forEach(th => { + const handle = document.createElement('div'); + handle.className = 'col-resize-handle'; + handle.style.cssText = 'position:absolute;right:0;top:0;bottom:0;width:5px;cursor:col-resize;z-index:10;'; + th.style.position = 'relative'; + th.appendChild(handle); + + handle.addEventListener('mousedown', (e) => { + e.preventDefault(); + e.stopPropagation(); + const thEl = th; + const startX = e.clientX; + const startWidth = thEl.offsetWidth; + + const onMouseMove = (ev) => { + const diff = ev.clientX - startX; + const newWidth = Math.max(30, startWidth + diff); + thEl.style.width = newWidth + 'px'; + const colKey = thEl.dataset.column; + if (colKey) { + this.columnWidths[colKey] = newWidth + 'px'; + this.persistState(); + } + }; + + const onMouseUp = () => { + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + }); + }); + } + + initReorder() { + const thead = this.table.querySelector('thead'); + if (!thead) return; + + let dragCol = null; + let dragIndex = -1; + let placeholder = null; + + thead.querySelectorAll('th').forEach(th => { + if (th.querySelector('.col-resize-handle')) return; + th.draggable = true; + + th.addEventListener('dragstart', (e) => { + dragCol = th; + dragIndex = Array.from(thead.querySelectorAll('th')).indexOf(th); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', ''); + th.classList.add('dragging'); + }); + + th.addEventListener('dragend', () => { + th.classList.remove('dragging'); + if (placeholder && placeholder.parentNode) { + placeholder.parentNode.removeChild(placeholder); + } + dragCol = null; + dragIndex = -1; + placeholder = null; + }); + + th.addEventListener('dragover', (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + if (!dragCol || dragCol === th) return; + + if (!placeholder) { + placeholder = document.createElement('th'); + placeholder.style.cssText = 'border:2px dashed #007bff;background:rgba(0,123,255,0.1);width:0;padding:0;'; + dragCol.parentNode.insertBefore(placeholder, dragCol); + } + + const rect = th.getBoundingClientRect(); + const midX = rect.left + rect.width / 2; + const parent = th.parentNode; + + if (e.clientX < midX) { + parent.insertBefore(placeholder, th); + } else { + parent.insertBefore(placeholder, th.nextSibling); + } + }); + + th.addEventListener('drop', (e) => { + e.preventDefault(); + if (!dragCol || dragCol === th) return; + + const parent = th.parentNode; + const allTh = Array.from(parent.querySelectorAll('th')).filter(el => el !== placeholder); + + const fromIdx = allTh.indexOf(dragCol); + let toIdx = allTh.indexOf(th); + + if (placeholder) { + toIdx = Array.from(parent.children).indexOf(placeholder); + if (placeholder.parentNode) placeholder.parentNode.removeChild(placeholder); + placeholder = null; + } + + if (fromIdx < 0) return; + + const actualRows = this.table.querySelectorAll('tr'); + actualRows.forEach(row => { + const cells = row.querySelectorAll('td, th'); + if (cells[fromIdx] && cells[toIdx]) { + if (toIdx > fromIdx) { + row.insertBefore(cells[fromIdx], cells[toIdx].nextSibling); + } else { + row.insertBefore(cells[fromIdx], cells[toIdx]); + } + } + }); + + this.updateColumnOrder(); + }); + }); + } + + updateColumnOrder() { + const ths = this.table.querySelectorAll('thead th'); + this.columnOrder = []; + const keysInOrder = []; + ths.forEach((th, idx) => { + const colKey = th.dataset.column || `col_${idx}`; + keysInOrder.push(colKey); + this.columnOrder.push(colKey); + }); + this.columns.sort((a, b) => keysInOrder.indexOf(a.key) - keysInOrder.indexOf(b.key)); + this.columns.forEach((col, idx) => col.index = idx); + this.persistState(); + } + + reorderColumns(keyOrder, persist = true) { + if (!keyOrder || keyOrder.length === 0) return; + const thead = this.table.querySelector('thead'); + if (!thead) return; + + const rows = this.table.querySelectorAll('tr'); + const keyToThIndex = {}; + const allTh = Array.from(thead.querySelectorAll('th')); + + allTh.forEach((th, idx) => { + const key = th.dataset.column || `col_${idx}`; + keyToThIndex[key] = idx; + }); + + rows.forEach(row => { + const cells = row.querySelectorAll('td, th'); + const reordered = []; + let lastRef = null; + + keyOrder.forEach(key => { + const srcIdx = keyToThIndex[key]; + if (srcIdx !== undefined && cells[srcIdx]) { + reordered.push(cells[srcIdx]); + } + }); + + reordered.forEach(cell => { + if (lastRef) { + row.insertBefore(cell, lastRef.nextSibling); + } else { + row.insertBefore(cell, row.firstChild); + } + lastRef = cell; + }); + }); + + this.columnOrder = [...keyOrder]; + this.columns.sort((a, b) => keyOrder.indexOf(a.key) - keyOrder.indexOf(b.key)); + this.columns.forEach((col, idx) => col.index = idx); + + if (persist) this.persistState(); + } + + setVisible(columnKey, visible) { + const th = this.table.querySelector(`th[data-column="${columnKey}"]`); + if (th) th.style.display = visible ? '' : 'none'; + + const colIdx = this.columns.findIndex(c => c.key === columnKey); + if (colIdx >= 0) { + this.columns[colIdx].visible = visible; + const rows = this.table.querySelectorAll('tbody tr, thead tr'); + rows.forEach(row => { + const cells = row.querySelectorAll('td, th'); + if (cells[colIdx]) { + cells[colIdx].style.display = visible ? '' : 'none'; + } + }); + } + } + + getColumnWidths() { + return { ...this.columnWidths }; + } + + getColumnOrder() { + return [...this.columnOrder]; + } + + getVisibleColumns() { + return this.columns.filter(c => c.visible).map(c => c.key); + } + + getColumnMetadata() { + return { + larghezze: this.getColumnWidths(), + ordine: this.getColumnOrder(), + visibili: this.getVisibleColumns(), + }; + } + + printWithSettings() { + const printWindow = window.open('', '_blank'); + if (!printWindow) return; + + const styles = Array.from(document.styleSheets) + .filter(sheet => { + try { return sheet.cssRules; } catch (e) { return false; } + }) + .map(sheet => Array.from(sheet.cssRules).map(rule => rule.cssText).join('\n')) + .join('\n'); + + const printStyles = ` + @media print { + body { font-family: 'DejaVu Sans', Arial, sans-serif; font-size: 10pt; } + .no-print { display: none !important; } + .card { border: none !important; box-shadow: none !important; } + .table { border-collapse: collapse; width: 100%; } + .table th { background: #f5f5f5 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } + .table th, .table td { border: 1px solid #ddd; padding: 4px 6px; } + a { text-decoration: none; color: #000; } + .badge { border: 1px solid #999; padding: 1px 4px; font-size: 8pt; } + .pagination, .card-header .card-tools { display: none !important; } + .page-break { page-break-before: always; } + } + `; + + const title = document.title || 'Stampa'; + const pageTitle = document.querySelector('.page_title, .card-title h1, h1')?.textContent?.trim() || title; + + let content = ''; + const printArea = this.table?.cloneNode(true); + if (printArea) { + printArea.querySelectorAll('.col-resize-handle').forEach(h => h.remove()); + content = printArea.outerHTML; + } else { + content = document.getElementById(this.tableId)?.outerHTML || ''; + } + + printWindow.document.write(` + + + + + ${title} + + + +
+ + +
+

${pageTitle}

+ ${content} +

+ Stampato il ${new Date().toLocaleDateString('it-IT')} — ${document.location.href} +

+ + + `); + printWindow.document.close(); + } +} + +window.ColumnManager = ColumnManager; diff --git a/resources/views/documenti/index.blade.php b/resources/views/documenti/index.blade.php index 83268fce..37294264 100644 --- a/resources/views/documenti/index.blade.php +++ b/resources/views/documenti/index.blade.php @@ -5,6 +5,9 @@ @php $canWriteDocumenti = Auth::user()->canManage('documenti'); $canDeleteDocumenti = Auth::user()->canDelete('documenti'); +$vistaDefaultJson = isset($vista) ? $vista->toJson() : 'null'; +$visibleColumnsJson = json_encode($visibleColumns ?? []); +$tableColumnsJson = json_encode($tableColumns ?? []); @endphp @section('css') @@ -293,21 +296,21 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti'); {{-- View: Lista --}}