versione 2.0.0

This commit is contained in:
2026-06-09 11:02:01 +02:00
parent 6f106e3130
commit d82b123bfd
38 changed files with 1552 additions and 83 deletions
+129 -1
View File
@@ -28,6 +28,25 @@ App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
- Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker - Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker
- **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear - **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear
## Tag System
### Implementazione Completa (Opzione B)
- **Migration**: `2026_06_08_194155_create_tags_tables.php` — tabelle `tags` + `taggables`
- **Model `Tag.php`**: morphToMany a 5 entity types, auto-slug su creating, color badge
- **Trait `HasTagsLight.php`**: `tags()` morphToMany, scopes `withAllTags`/`withAnyTags`
- **5 Models aggiornati**: Individuo, Gruppo, Evento, Documento, MailingList — usano `HasTagsLight`
- **ImpostazioniController**: CRUD completo per Tag (store, update, destroy, reorder) con tab nella pagina Impostazioni
- **Tag selector**: `_tag-selector.blade.php` — search + pill badges con colori, incluso in create/edit di Individui, Gruppi, Eventi, MailingList
- **Tag filtering**: `?tag[]=slug` supportato in `IndividuoController@index`, `GruppoController@index`, `EventoController@index`, `DocumentoController@index`, `MailingListController@index` via `withAnyTags` scope
- **Tag column in index views**: Colonna "Tag" con badge colorati cliccabili (collegamento a index filtrato) in individui, gruppi, eventi, documenti, mailing-liste
- **Tag filter widget**: `_tag-filter-bar.blade.php` — barra cliccabile con badge colorati sopra ogni index table (individui, gruppi, eventi, documenti, mailing-liste). Attiva/disattiva filtro `?tag[]=` con un click, evidenzia tag attivi, include "Cancella filtri" quando attivo
- **RicercaController**: Controller per ricerca unificata per tag — mostra risultati aggregati da tutte 5 entity types con info-box counters
- **Route `GET /ricerca`**: Registrata come `ricerca.index``RicercaController@index`
- **Sidebar link**: "Ricerca per Tag" (icona `fas fa-tags`) sotto la sezione Report, visibile con permesso `individui`
- **Mass tag action for Documenti**: `POST /documenti/mass-tag``DocumentoController@massTag` — modal with tag selector + assign/remove radio, processes selected documents in chunks of 100 via `syncWithoutDetaching()` (assign) or `detach()` (remove)
- **Report tag columns**: `tags.nome` disponibile come colonna per individui/gruppi/eventi/documenti nei report personalizzati
- **Report tag filters**: Custom report form include tag multi-select (Select2) + modalità OR/AND; `runCustomReport()` applica filtri via `withAllTags`/`withAnyTags`
## Bug Fix Recenti ## Bug Fix Recenti
### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo ### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo
@@ -184,5 +203,114 @@ php artisan config:clear
Tutte verificate con `php -l` (nessun errore di sintassi). Tutte verificate con `php -l` (nessun errore di sintassi).
### 2026-06-09 — `@stack('scripts')` mancante nel layout
**Problema**: Il partial `_tag-selector.blade.php` usa `@push('scripts')` per iniettare le funzioni JS `toggleTag`, `removeTag`, `filterTags`. Il layout `adminlte.blade.php` aveva solo `@yield('scripts')` (per `@section`), ma **non** `@stack('scripts')` (per `@push`). Di conseguenza il JavaScript interattivo del tag selector (clic sui badge, ricerca, rimozione) non veniva mai caricato su nessuna pagina create/edit.
**Fix**: `resources/views/layouts/adminlte.blade.php:305` — Aggiunto `@stack('scripts')` subito prima di `@yield('scripts')`.
### 2026-06-09 — Tag support completato per MailingList + Report + Ricerca
**MailingList — Controller & Views** (`app/Http/Controllers/MailingListController.php`):
- `index()`: eager-load `tags`, supporto `?tag[]=` filter via `withAnyTags`, pass `$allTags` per filter bar
- `create()`: pass `$tags` per tag selector
- `store()`: validazione `tags.* exists:tags,id`, sync dopo create. Auto-fetch email da `Individuo::contatti()` quando email non fornita
- `edit()`: eager-load `tags`, pass `$tags` + `$selectedTags`
- `update()`: validazione `tags.*`, sync se presente, detach se assente. Auto-fetch email da `Individuo::contatti()` quando email non fornita. Diff logica: `$toRemove = array_diff($existingIds, $newIds)` corretto — rimuove solo contatti deselezionati/esplicitamente rimossi
- `show()`: eager-load `tags`
**MailingList — Views**:
- `index.blade.php`: aggiunto `@include('partials._tag-filter-bar')`, colonna "Tag" con badge colorati cliccabili
- `create.blade.php`: incluso `_tag-selector` dopo descrizione
- `edit.blade.php`: incluso `_tag-selector` dopo descrizione (prima della sezione contatti). Submit handler include `#individui-select.selectedOptions` in `contatti_json` con `email: ''` (già presente da fix precedente)
- `show.blade.php`: aggiunta card Tag nel pannello sinistro con badge colorati (o "Nessun tag assegnato")
**ReportColumnRegistry** (`app/Helpers/ReportColumnRegistry.php`):
- Aggiunta colonna `'tags.nome'` con label 'Tag' a 4 entity types: individui, gruppi, eventi, documenti
**ReportController** (`app/Http/Controllers/ReportController.php`):
- `runCustomReport()`: supporta `config['tag_filter']` (array di tag IDs) + `config['tag_filter_mode']` ('any'/'all')
- `storeCustom()`: ora salva `tag_filter` e `tag_filter_mode` nella config del report salvato
- Carica `$allTags` nell'index e passa alla vista
- Converte IDs → slugs via `Tag::whereIn('id', ...)->pluck('slug')`, applica `withAllTags`/`withAnyTags`
- Eager-load `tags` automaticamente quando la colonna `tags.nome` è selezionata
**Report — View** (`resources/views/report/index.blade.php`):
- Aggiunto filtro tag multi-select nel form "Crea Report Personalizzato" (Select2 con badge colorati)
- Aggiunto select per modalità filtro (OR/AND)
- JS: `initTagFilterSelect2()` / `destroyTagFilterSelect2()` con template colorato
**RicercaController** (`app/Http/Controllers/RicercaController.php`):
- Aggiunta MailingList alle query per tag slug
- `results['mailingLists']` e `totals['mailingLists']` passati alla view
**Ricerca — View** (`resources/views/ricerca/index.blade.php`):
- Aggiunto info-box Mailing List (colore bg-purple) con link "Vedi tutti" → `mailing-liste.index?tag[]=`
- Aggiunta tabella dettaglio Mailing List nel risultato di ricerca (nome, contatti, stato, azione show)
- Fix: `route('documenti.edit')``url('/documenti/' . $doc->id . '/edit')` (route non esiste)
### 2026-06-09 — Fix: storeCustom() non salvava tag_filter
**Problema**: `ReportController@storeCustom()` non salvava `tag_filter` e `tag_filter_mode` nella config del report personalizzato. Quando un report salvato veniva eseguito, `runCustomReport()` non trovava i filtri tag nella config e non li applicava.
**Fix**: Aggiunte righe 143-148 in `ReportController.php`:
```php
if ($request->has('tag_filter')) {
$config['tag_filter'] = $request->input('tag_filter');
}
if ($request->filled('tag_filter_mode')) {
$config['tag_filter_mode'] = $request->input('tag_filter_mode');
}
```
### 2026-06-09 — Fix: MailingList edit — "Carica contatti" non inseriva i selezionati
**Problema**: Due bug nel JS `caricaSelezionati()` in `mailing-liste/edit.blade.php`:
1. **Typo**: `ind.cogname` invece di `ind.cognome` — il campo Cognome nella tabella era sempre vuoto/undefined (API restituisce `cognome`)
2. **Email gate**: `if (ind.emails && ind.emails.length > 0)` filtro bloccante — individui senza email salvata in `contatti` venivano SILENZIOSAMENTE scartati, mai aggiunti alla tabella
**Fix**:
1. `ind.cogname``ind.cognome` (righe 227 e 256)
2. Rimossa condizione `if (ind.emails...)` — ora anche individui senza email vengono aggiunti alla tabella
3. `email: ind.emails[0]``email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : ''` — email vuota se nessuna presente
4. Controller già fixato per auto-risolvere email da `Individuo::contatti()` al salvataggio
### 2026-06-09 — Fix: MailingListController email gate bloccava nuovi contatti senza email
**Problema**: `store()` e `update()` in `MailingListController` usavano `!empty($contatto['email'])` per decidere se creare un contatto. L'email era richiesta ma il MailingContact non memorizza l'email — la risolve dall'Individuo al momento del display/invio. Questo impediva di aggiungere individui senza email (o con email non nei contatti) alla mailing list.
**Fix**: Rimossa completamente la verifica email sia in `store()` che in `update()`. Ora ogni `individuo_id` valido viene aggiunto come MailingContact. L'email viene risolta al display dal model `$contact->individuo->contatti->where('tipo', 'email')->first()?->valore`.
Prima (bloccante):
```php
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { create }
```
Dopo (semplice):
```php
if (!empty($contatto['individuo_id'])) { create }
```
## Prossimi Passi ## Prossimi Passi
- *(nessuno — in attesa di nuove richieste)* - [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
- Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
+4
View File
@@ -132,6 +132,7 @@ class ReportColumnRegistry
'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'], 'contatti.telefono' => ['label' => 'Telefono (da contatti)', 'type' => 'relation'],
'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'], 'contatti.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'],
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'gruppi' => [ 'gruppi' => [
@@ -151,6 +152,7 @@ class ReportColumnRegistry
'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'], 'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'],
'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'], 'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'],
'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'], 'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'eventi' => [ 'eventi' => [
@@ -171,6 +173,7 @@ class ReportColumnRegistry
'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'], 'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'],
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'], 'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'], 'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'documenti' => [ 'documenti' => [
@@ -183,6 +186,7 @@ class ReportColumnRegistry
'note' => ['label' => 'Note', 'type' => 'direct'], 'note' => ['label' => 'Note', 'type' => 'direct'],
'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'], 'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'],
'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'], 'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
], ],
'contatti' => [ 'contatti' => [
+56 -4
View File
@@ -11,6 +11,7 @@ use App\Models\Individuo;
use App\Models\Gruppo; use App\Models\Gruppo;
use App\Models\Evento; use App\Models\Evento;
use App\Models\StorageRepository; use App\Models\StorageRepository;
use App\Models\Tag;
use App\Models\TipologiaDocumento; use App\Models\TipologiaDocumento;
use App\Services\StorageRepositoryService; use App\Services\StorageRepositoryService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
@@ -42,9 +43,14 @@ class DocumentoController extends Controller
$folderId = $request->get('folder_id'); $folderId = $request->get('folder_id');
$repoId = $request->get('repository_id'); $repoId = $request->get('repository_id');
$query = Documento::with(['user', 'target', 'cartella', 'repository']) $query = Documento::with(['user', 'target', 'cartella', 'repository', 'tags'])
->orderByDesc('created_at'); ->orderByDesc('created_at');
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
if ($repoId) { if ($repoId) {
$query->where('repository_id', $repoId); $query->where('repository_id', $repoId);
} elseif ($folderId) { } elseif ($folderId) {
@@ -54,6 +60,7 @@ class DocumentoController extends Controller
} }
$documenti = $query->paginate(20); $documenti = $query->paginate(20);
$allTags = Tag::orderBy('name')->get();
$cartelle = DocumentoCartella::with('children') $cartelle = DocumentoCartella::with('children')
->whereNull('parent_id') ->whereNull('parent_id')
->orderBy('nome') ->orderBy('nome')
@@ -89,7 +96,7 @@ class DocumentoController extends Controller
'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle', 'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle',
'currentRepo', 'repositories', 'currentRepo', 'repositories',
'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie',
'cartelleMoveOptions' 'cartelleMoveOptions', 'allTags'
)); ));
} }
@@ -106,13 +113,15 @@ class DocumentoController extends Controller
public function edit($documento) public function edit($documento)
{ {
$this->authorizeWrite('documenti'); $this->authorizeWrite('documenti');
$documento = Documento::with('target')->findOrFail($documento); $documento = Documento::with(['target', 'tags'])->findOrFail($documento);
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get();
$eventi = Evento::orderBy('nome_evento')->get(); $eventi = Evento::orderBy('nome_evento')->get();
$mailingLists = \App\Models\MailingList::orderBy('nome')->get(); $mailingLists = \App\Models\MailingList::orderBy('nome')->get();
$tipologie = TipologiaDocumento::attive(); $tipologie = TipologiaDocumento::attive();
return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie')); $tags = Tag::orderBy('name')->get();
$selectedTags = $documento->tags->pluck('id')->toArray();
return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', 'tags', 'selectedTags'));
} }
public function options(Request $request) public function options(Request $request)
@@ -165,6 +174,8 @@ class DocumentoController extends Controller
'visibilita_target_id' => 'nullable|integer', 'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string', 'visibilita_target_type' => 'nullable|string',
'note' => 'nullable|string', 'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico'; $data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
@@ -185,6 +196,12 @@ class DocumentoController extends Controller
$documento->update($data); $documento->update($data);
if ($request->has('tags')) {
$documento->tags()->sync($request->tags);
} else {
$documento->tags()->detach();
}
return redirect('/documenti')->with('success', 'Documento aggiornato.'); return redirect('/documenti')->with('success', 'Documento aggiornato.');
} }
@@ -706,6 +723,41 @@ class DocumentoController extends Controller
return back()->with('success', count($ids) . ' documenti associati.'); return back()->with('success', count($ids) . ' documenti associati.');
} }
public function massTag(Request $request)
{
$this->authorizeWrite('documenti');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun documento selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Documento::whereIn('id', $ids)->chunk(100, function ($documenti) use ($tagIds, $mode, &$count) {
foreach ($documenti as $documento) {
if ($mode === 'assign') {
$documento->tags()->syncWithoutDetaching($tagIds);
} else {
$documento->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return back()->with('success', "Tag $actionLabel per $count documenti.");
}
public function massMove(Request $request): RedirectResponse public function massMove(Request $request): RedirectResponse
{ {
$this->authorizeWrite('documenti'); $this->authorizeWrite('documenti');
+1 -1
View File
@@ -341,7 +341,7 @@ class EmailController extends Controller
$message->delete(); $message->delete();
} }
return back()->with('success', 'Email spostata nel cestino.'); return redirect()->route('email.index', ['folder' => 'inbox'])->with('success', 'Email spostata nel cestino.');
} }
public function downloadAttachment($id) public function downloadAttachment($id)
+25 -6
View File
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Evento; use App\Models\Evento;
use App\Models\Gruppo; use App\Models\Gruppo;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Tag;
use App\Models\TipologiaEvento; use App\Models\TipologiaEvento;
use App\Services\IcsExportService; use App\Services\IcsExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -16,7 +17,12 @@ class EventoController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
$this->authorizeRead('eventi'); $this->authorizeRead('eventi');
$query = Evento::with(['gruppi', 'responsabili']); $query = Evento::with(['gruppi', 'responsabili', 'tags']);
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
if ($request->filled('search')) { if ($request->filled('search')) {
$query->where('nome_evento', 'like', '%' . $request->search . '%'); $query->where('nome_evento', 'like', '%' . $request->search . '%');
@@ -47,8 +53,9 @@ class EventoController extends Controller
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
$eventi = $query->paginate($perPage); $eventi = $query->paginate($perPage);
$gruppi = Gruppo::orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get();
$allTags = Tag::orderBy('name')->get();
return view('eventi.index', compact('eventi', 'gruppi')); return view('eventi.index', compact('eventi', 'gruppi', 'allTags'));
} }
public function create(Request $request) public function create(Request $request)
@@ -58,8 +65,9 @@ class EventoController extends Controller
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null; $selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null;
$tipologieEventi = TipologiaEvento::attive(); $tipologieEventi = TipologiaEvento::attive();
$tags = Tag::orderBy('name')->get();
return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi')); return view('eventi.create', compact('gruppi', 'individui', 'selectedGruppo', 'tipologieEventi', 'tags'));
} }
public function store(Request $request) public function store(Request $request)
@@ -88,6 +96,8 @@ class EventoController extends Controller
'responsabili.*' => 'exists:individui,id', 'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500', 'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string', 'luogo_url_maps' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$evento = Evento::create([ $evento = Evento::create([
@@ -110,6 +120,10 @@ class EventoController extends Controller
'luogo_url_maps' => $data['luogo_url_maps'] ?? null, 'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]); ]);
if (!empty($data['tags'])) {
$evento->tags()->attach($data['tags']);
}
if (!empty($data['gruppi'])) { if (!empty($data['gruppi'])) {
$evento->gruppi()->attach($data['gruppi']); $evento->gruppi()->attach($data['gruppi']);
} }
@@ -124,19 +138,21 @@ class EventoController extends Controller
public function show($evento) public function show($evento)
{ {
$this->authorizeRead('eventi'); $this->authorizeRead('eventi');
$evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti'])->findOrFail($evento); $evento = Evento::with(['gruppi', 'responsabili.contatti', 'documenti', 'tags'])->findOrFail($evento);
return view('eventi.show', compact('evento')); return view('eventi.show', compact('evento'));
} }
public function edit($evento) public function edit($evento)
{ {
$this->authorizeWrite('eventi'); $this->authorizeWrite('eventi');
$evento = Evento::with(['gruppi', 'responsabili', 'documenti'])->findOrFail($evento); $evento = Evento::with(['gruppi', 'responsabili', 'documenti', 'tags'])->findOrFail($evento);
$gruppi = Gruppo::orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$tipologieEventi = TipologiaEvento::attive(); $tipologieEventi = TipologiaEvento::attive();
$tags = Tag::orderBy('name')->get();
$selectedTags = $evento->tags->pluck('id')->toArray();
return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi')); return view('eventi.edit', compact('evento', 'gruppi', 'individui', 'tipologieEventi', 'tags', 'selectedTags'));
} }
public function update(Request $request, $evento) public function update(Request $request, $evento)
@@ -167,6 +183,8 @@ class EventoController extends Controller
'responsabili.*' => 'exists:individui,id', 'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500', 'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string', 'luogo_url_maps' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$evento->update([ $evento->update([
@@ -189,6 +207,7 @@ class EventoController extends Controller
'luogo_url_maps' => $data['luogo_url_maps'] ?? null, 'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]); ]);
$evento->tags()->sync($data['tags'] ?? []);
$evento->gruppi()->sync($data['gruppi'] ?? []); $evento->gruppi()->sync($data['gruppi'] ?? []);
$evento->responsabili()->sync($data['responsabili'] ?? []); $evento->responsabili()->sync($data['responsabili'] ?? []);
+91 -10
View File
@@ -7,6 +7,7 @@ namespace App\Http\Controllers;
use App\Models\Gruppo; use App\Models\Gruppo;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Diocesi; use App\Models\Diocesi;
use App\Models\Tag;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator; use Illuminate\Pagination\Paginator;
@@ -27,8 +28,8 @@ class GruppoController extends Controller
->where('is_default', true) ->where('is_default', true)
->first(); ->first();
$allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email']; $allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email', 'tag'];
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili']; $defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili', 'tag'];
if ($vista && !empty($vista->colonne_visibili)) { if ($vista && !empty($vista->colonne_visibili)) {
$visibleColumns = $vista->colonne_visibili; $visibleColumns = $vista->colonne_visibili;
@@ -36,9 +37,14 @@ class GruppoController extends Controller
$visibleColumns = $defaultVisible; $visibleColumns = $defaultVisible;
} }
$allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar']) $gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']);
->orderBy('nome')
->get() if (request()->filled('tag')) {
$tagSlugs = (array) request()->input('tag');
$gruppiQuery->withAnyTags($tagSlugs);
}
$allGruppi = $gruppiQuery->orderBy('nome')->get()
->map(function ($gruppo) { ->map(function ($gruppo) {
$depth = 0; $depth = 0;
$parent = $gruppo->parent; $parent = $gruppo->parent;
@@ -65,12 +71,15 @@ class GruppoController extends Controller
$gruppi = $allGruppi; $gruppi = $allGruppi;
} }
$allTags = Tag::orderBy('name')->get();
return view('gruppi.index', compact( return view('gruppi.index', compact(
'gruppi', 'gruppi',
'allColumns', 'allColumns',
'visibleColumns', 'visibleColumns',
'viewMode', 'viewMode',
'vista' 'vista',
'allTags'
)); ));
} }
@@ -104,7 +113,8 @@ class GruppoController extends Controller
$allGruppi = Gruppo::orderBy('nome')->get(); $allGruppi = Gruppo::orderBy('nome')->get();
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null; $selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui')); $tags = Tag::orderBy('name')->get();
return view('gruppi.create', compact('diocesi', 'allGruppi', 'selectedParent', 'individui', 'tags'));
} }
public function store(Request $request) public function store(Request $request)
@@ -122,6 +132,8 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255', 'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2', 'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string', 'mappa_posizione' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
if (!empty($data['responsabile_ids'])) { if (!empty($data['responsabile_ids'])) {
@@ -130,6 +142,10 @@ class GruppoController extends Controller
$gruppo = Gruppo::create($data); $gruppo = Gruppo::create($data);
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
}
if ($request->has('individui')) { if ($request->has('individui')) {
foreach ($request->individui as $individuoData) { foreach ($request->individui as $individuoData) {
if (!empty($individuoData['individuo_id'])) { if (!empty($individuoData['individuo_id'])) {
@@ -149,7 +165,7 @@ class GruppoController extends Controller
$this->authorizeRead('gruppi'); $this->authorizeRead('gruppi');
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) { $gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione'); $q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar', 'eventi' => function ($q) { }, 'avatar', 'tags', 'eventi' => function ($q) {
$q->where('is_incontro_gruppo', true); $q->where('is_incontro_gruppo', true);
}, 'eventi.responsabili.contatti'])->findOrFail($id); }, 'eventi.responsabili.contatti'])->findOrFail($id);
return view('gruppi.show', compact('gruppo')); return view('gruppi.show', compact('gruppo'));
@@ -158,11 +174,13 @@ class GruppoController extends Controller
public function edit($id) public function edit($id)
{ {
$this->authorizeWrite('gruppi'); $this->authorizeWrite('gruppi');
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar'])->findOrFail($id); $gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar', 'tags'])->findOrFail($id);
$diocesi = Diocesi::orderBy('nome')->get(); $diocesi = Diocesi::orderBy('nome')->get();
$gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get(); $gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get();
$membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get(); $membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri')); $tags = Tag::orderBy('name')->get();
$selectedTags = $gruppo->tags->pluck('id')->toArray();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags'));
} }
public function update(Request $request, $id) public function update(Request $request, $id)
@@ -181,6 +199,8 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255', 'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2', 'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string', 'mappa_posizione' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
if (!empty($data['responsabile_ids'])) { if (!empty($data['responsabile_ids'])) {
@@ -191,6 +211,12 @@ class GruppoController extends Controller
$gruppo->update($data); $gruppo->update($data);
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
} else {
$gruppo->tags()->detach();
}
if ($request->has('individui')) { if ($request->has('individui')) {
$gruppo->individui()->detach(); $gruppo->individui()->detach();
foreach ($request->individui as $individuoData) { foreach ($request->individui as $individuoData) {
@@ -410,4 +436,59 @@ class GruppoController extends Controller
'Content-Disposition' => 'attachment; filename="template_gruppi.csv"', 'Content-Disposition' => 'attachment; filename="template_gruppi.csv"',
]); ]);
} }
public function massTag(Request $request)
{
$this->authorizeWrite('gruppi');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Gruppo::whereIn('id', $ids)->chunk(100, function ($gruppi) use ($tagIds, $mode, &$count) {
foreach ($gruppi as $gruppo) {
if ($mode === 'assign') {
$gruppo->tags()->syncWithoutDetaching($tagIds);
} else {
$gruppo->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return redirect()->route('gruppi.index')->with('success', "Tag {$actionLabel} per {$count} gruppi.");
}
public function massElimina(Request $request)
{
$this->authorizeDelete('gruppi');
$ids = $request->input('ids', []);
if (!is_array($ids) || empty($ids)) {
return redirect()->route('gruppi.index')->with('error', 'Nessun gruppo selezionato.');
}
$gruppi = Gruppo::whereIn('id', $ids)->get();
$count = 0;
foreach ($gruppi as $gruppo) {
$gruppo->individui()->detach();
$gruppo->delete();
$count++;
}
return redirect()->route('gruppi.index')->with('success', "{$count} gruppi eliminati con successo.");
}
} }
@@ -9,6 +9,7 @@ use App\Models\EmailSetting;
use App\Models\Ruolo; use App\Models\Ruolo;
use App\Models\SenderAccount; use App\Models\SenderAccount;
use App\Models\StorageRepository; use App\Models\StorageRepository;
use App\Models\Tag;
use App\Models\TipologiaDocumento; use App\Models\TipologiaDocumento;
use App\Models\TipologiaEvento; use App\Models\TipologiaEvento;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@@ -26,6 +27,7 @@ class ImpostazioniController extends Controller
$tipologie = TipologiaDocumento::orderBy('ordine')->get(); $tipologie = TipologiaDocumento::orderBy('ordine')->get();
$tipologieEventi = TipologiaEvento::orderBy('ordine')->get(); $tipologieEventi = TipologiaEvento::orderBy('ordine')->get();
$ruoli = Ruolo::orderBy('ordine')->get(); $ruoli = Ruolo::orderBy('ordine')->get();
$tags = Tag::orderBy('order_column')->get();
$appSettings = AppSetting::first(); $appSettings = AppSetting::first();
$emailSettings = EmailSetting::first() ?? new EmailSetting(); $emailSettings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get(); $senderAccounts = SenderAccount::orderBy('email_address')->get();
@@ -33,7 +35,7 @@ class ImpostazioniController extends Controller
$googleDriveNewToken = session('google_drive_new_token'); $googleDriveNewToken = session('google_drive_new_token');
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get(); $calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni')); return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
} }
public function saveAppSettings(Request $request) public function saveAppSettings(Request $request)
@@ -371,6 +373,70 @@ class ImpostazioniController extends Controller
return response()->json(['success' => true]); return response()->json(['success' => true]);
} }
public function tagStore(Request $request)
{
$this->authorizeWrite('settings');
$validated = $request->validate([
'name' => 'required|string|max:100|unique:tags,name',
'color' => 'nullable|string|max:7',
]);
$maxOrdine = Tag::max('order_column') ?? 0;
Tag::create([
'name' => $validated['name'],
'slug' => str()->slug($validated['name']),
'color' => $validated['color'] ?? null,
'order_column' => $maxOrdine + 1,
]);
return redirect('/impostazioni#tag')->with('success', 'Tag aggiunto.');
}
public function tagUpdate(Request $request, $id)
{
$this->authorizeWrite('settings');
$tag = Tag::findOrFail($id);
$validated = $request->validate([
'name' => 'required|string|max:100|unique:tags,name,' . $id,
'color' => 'nullable|string|max:7',
]);
$tag->update([
'name' => $validated['name'],
'slug' => str()->slug($validated['name']),
'color' => $validated['color'] ?? null,
]);
return redirect('/impostazioni#tag')->with('success', 'Tag aggiornato.');
}
public function tagDestroy($id)
{
$this->authorizeDelete('settings');
$tag = Tag::findOrFail($id);
$tag->delete();
return redirect('/impostazioni#tag')->with('success', 'Tag eliminato.');
}
public function tagReorder(Request $request)
{
$this->authorizeWrite('settings');
$order = $request->input('order', []);
foreach ($order as $index => $id) {
Tag::where('id', $id)->update(['order_column' => $index]);
}
return response()->json(['success' => true]);
}
public function uploadLogo(Request $request) public function uploadLogo(Request $request)
{ {
$this->authorizeWrite('settings'); $this->authorizeWrite('settings');
+106 -7
View File
@@ -5,8 +5,10 @@ namespace App\Http\Controllers;
use App\Models\Individuo; use App\Models\Individuo;
use App\Models\Contatto; use App\Models\Contatto;
use App\Models\Documento; use App\Models\Documento;
use App\Models\Gruppo;
use App\Models\Comune; use App\Models\Comune;
use App\Models\Diocesi; use App\Models\Diocesi;
use App\Models\Tag;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@@ -18,7 +20,16 @@ class IndividuoController extends Controller
{ {
$this->authorizeRead('individui'); $this->authorizeRead('individui');
$perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20; $perPage = in_array((int) $request->perPage, [10, 20, 25, 50, 100]) ? (int) $request->perPage : 20;
$individui = Individuo::with('contatti')->orderBy('cognome')->orderBy('nome')->paginate($perPage);
$query = Individuo::with('contatti', 'tags');
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
$individui = $query->orderBy('cognome')->orderBy('nome')->paginate($perPage);
$allTags = Tag::orderBy('name')->get();
$vista = null; $vista = null;
@@ -41,14 +52,16 @@ class IndividuoController extends Controller
['key' => 'nome', 'label' => 'Nome'], ['key' => 'nome', 'label' => 'Nome'],
['key' => 'email', 'label' => 'Email'], ['key' => 'email', 'label' => 'Email'],
['key' => 'telefono', 'label' => 'Telefono'], ['key' => 'telefono', 'label' => 'Telefono'],
['key' => 'tag', 'label' => 'Tag'],
]; ];
$defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono']; $defaultVisible = ['codice', 'cognome', 'nome', 'email', 'telefono', 'tag'];
$visibleColumns = $vista && !empty($vista->colonne_visibili) $visibleColumns = $vista && !empty($vista->colonne_visibili)
? $vista->colonne_visibili ? $vista->colonne_visibili
: $defaultVisible; : $defaultVisible;
return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns')); $gruppi = Gruppo::orderBy('nome')->get(['id', 'nome']);
return view('individui.index', compact('individui', 'vista', 'allColumns', 'visibleColumns', 'allTags', 'gruppi'));
} }
public function exportCSV(Request $request) public function exportCSV(Request $request)
@@ -110,7 +123,8 @@ public function create()
$this->authorizeWrite('individui'); $this->authorizeWrite('individui');
$comuni = Comune::orderBy('nome')->get(); $comuni = Comune::orderBy('nome')->get();
$diocesi = Diocesi::orderBy('nome')->get(); $diocesi = Diocesi::orderBy('nome')->get();
return view('individui.create', compact('comuni', 'diocesi')); $tags = Tag::orderBy('name')->get();
return view('individui.create', compact('comuni', 'diocesi', 'tags'));
} }
public function store(Request $request) public function store(Request $request)
@@ -129,10 +143,16 @@ public function create()
'numero_documento' => 'nullable|string|max:50', 'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date', 'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string', 'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$individuo = Individuo::create($data); $individuo = Individuo::create($data);
if ($request->has('tags')) {
$individuo->tags()->sync($request->tags);
}
if ($request->has('contatti')) { if ($request->has('contatti')) {
foreach ($request->contatti as $contatto) { foreach ($request->contatti as $contatto) {
if (!empty($contatto['tipo']) && !empty($contatto['valore'])) { if (!empty($contatto['tipo']) && !empty($contatto['valore'])) {
@@ -154,7 +174,7 @@ public function create()
$this->authorizeRead('individui'); $this->authorizeRead('individui');
$individuo = Individuo::with(['contatti', 'gruppi' => function ($q) { $individuo = Individuo::with(['contatti', 'gruppi' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione'); $q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar'])->findOrFail($individuo); }, 'avatar', 'tags'])->findOrFail($individuo);
return view('individui.show', compact('individuo')); return view('individui.show', compact('individuo'));
} }
@@ -180,8 +200,10 @@ public function create()
public function edit($individuo) public function edit($individuo)
{ {
$this->authorizeWrite('individui'); $this->authorizeWrite('individui');
$individuo = Individuo::with(['contatti', 'gruppi', 'avatar'])->findOrFail($individuo); $individuo = Individuo::with(['contatti', 'gruppi', 'avatar', 'tags'])->findOrFail($individuo);
return view('individui.edit', compact('individuo')); $tags = Tag::orderBy('name')->get();
$selectedTags = $individuo->tags->pluck('id')->toArray();
return view('individui.edit', compact('individuo', 'tags', 'selectedTags'));
} }
public function update(Request $request, $individuo) public function update(Request $request, $individuo)
@@ -205,9 +227,17 @@ public function create()
'numero_documento' => 'nullable|string|max:50', 'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date', 'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string', 'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$individuo->update($data); $individuo->update($data);
if ($request->has('tags')) {
$individuo->tags()->sync($request->tags);
} else {
$individuo->tags()->detach();
}
\Log::info('Individuo dati salvati', ['id' => $individuo->id]); \Log::info('Individuo dati salvati', ['id' => $individuo->id]);
@@ -308,6 +338,75 @@ public function create()
return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.'); return redirect()->route('individui.index')->with('success', count($individui) . ' individui eliminati con successo.');
} }
public function massGruppo(Request $request)
{
$this->authorizeWrite('individui');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.');
}
$data = $request->validate([
'gruppi' => 'required|array',
'gruppi.*' => 'exists:gruppi,id',
'mode' => 'required|in:assign,remove',
]);
$gruppiIds = $data['gruppi'];
$mode = $data['mode'];
$count = 0;
foreach ($ids as $id) {
$individuo = Individuo::find($id);
if (!$individuo) continue;
if ($mode === 'assign') {
$individuo->gruppi()->syncWithoutDetaching($gruppiIds);
} else {
$individuo->gruppi()->detach($gruppiIds);
}
$count++;
}
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return redirect()->route('individui.index')->with('success', "Gruppi {$actionLabel} per {$count} individui.");
}
public function massTag(Request $request)
{
$this->authorizeWrite('individui');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return redirect()->route('individui.index')->with('error', 'Nessun individuo selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Individuo::whereIn('id', $ids)->chunk(100, function ($individui) use ($tagIds, $mode, &$count) {
foreach ($individui as $individuo) {
if ($mode === 'assign') {
$individuo->tags()->syncWithoutDetaching($tagIds);
} else {
$individuo->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return redirect()->route('individui.index')->with('success', "Tag {$actionLabel} per {$count} individui.");
}
public function elementiCollegati($individuo) public function elementiCollegati($individuo)
{ {
$individuo = Individuo::findOrFail($individuo); $individuo = Individuo::findOrFail($individuo);
+17 -1
View File
@@ -95,9 +95,17 @@ class MailingController extends Controller
} }
} }
$emails = $destinatari->flatMap(fn($ind) => $ind->contatti->where('tipo', 'email')->pluck('valore')) $totaleDestinatari = $destinatari->count();
$emails = $destinatari->map(fn($ind) => $ind->contatti->where('tipo', 'email')->first()?->valore)
->filter()->unique()->values(); ->filter()->unique()->values();
$skipped = $totaleDestinatari - $emails->count();
if ($skipped > 0) {
Log::info("Mailing invia: {$skipped} contatti saltati perché senza email", [
'lista_id' => $data['lista_id'] ?? null,
]);
}
if ($emails->isEmpty()) { if ($emails->isEmpty()) {
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput(); return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
} }
@@ -229,9 +237,17 @@ class MailingController extends Controller
->with('individuo.contatti') ->with('individuo.contatti')
->get(); ->get();
$totaleContatti = $contatti->count();
$emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore) $emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore)
->filter()->unique()->values(); ->filter()->unique()->values();
$skipped = $totaleContatti - $emails->count();
if ($skipped > 0) {
Log::info("Mailing invio elabora: {$skipped} contatti saltati perché senza email", [
'liste_ids' => $data['liste'],
]);
}
if ($emails->isEmpty()) { if ($emails->isEmpty()) {
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.'); return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
} }
+35 -11
View File
@@ -3,23 +3,31 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\MailingList; use App\Models\MailingList;
use App\Models\Tag;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class MailingListController extends Controller class MailingListController extends Controller
{ {
public function index() public function index(Request $request)
{ {
$this->authorizeRead('mailing'); $this->authorizeRead('mailing');
$mailingLists = MailingList::with(['user', 'contatti']) $query = MailingList::with(['user', 'contatti', 'tags']);
->orderBy('nome')
->get(); if ($request->filled('tag')) {
return view('mailing-liste.index', compact('mailingLists')); $tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
$mailingLists = $query->orderBy('nome')->get();
$allTags = Tag::orderBy('name')->get();
return view('mailing-liste.index', compact('mailingLists', 'allTags'));
} }
public function create() public function create()
{ {
$this->authorizeWrite('mailing'); $this->authorizeWrite('mailing');
return view('mailing-liste.create'); $tags = Tag::orderBy('name')->get();
return view('mailing-liste.create', compact('tags'));
} }
public function store(Request $request) public function store(Request $request)
@@ -30,6 +38,8 @@ class MailingListController extends Controller
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'attiva' => 'boolean', 'attiva' => 'boolean',
'contatti_json' => 'nullable|string', 'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$data['user_id'] = auth()->id(); $data['user_id'] = auth()->id();
@@ -42,11 +52,15 @@ class MailingListController extends Controller
'user_id' => $data['user_id'], 'user_id' => $data['user_id'],
]); ]);
if ($request->has('tags')) {
$lista->tags()->sync($request->tags);
}
if (!empty($data['contatti_json'])) { if (!empty($data['contatti_json'])) {
$contatti = json_decode($data['contatti_json'], true); $contatti = json_decode($data['contatti_json'], true);
if (is_array($contatti)) { if (is_array($contatti)) {
foreach ($contatti as $contatto) { foreach ($contatti as $contatto) {
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { if (!empty($contatto['individuo_id'])) {
$lista->contatti()->create([ $lista->contatti()->create([
'individuo_id' => $contatto['individuo_id'], 'individuo_id' => $contatto['individuo_id'],
'opt_in' => true, 'opt_in' => true,
@@ -63,15 +77,17 @@ class MailingListController extends Controller
public function show($mailing_liste) public function show($mailing_liste)
{ {
$this->authorizeRead('mailing'); $this->authorizeRead('mailing');
$mailingList = MailingList::with(['contatti.individuo.contatti'])->findOrFail($mailing_liste); $mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailing_liste);
return view('mailing-liste.show', compact('mailingList')); return view('mailing-liste.show', compact('mailingList'));
} }
public function edit($mailingList) public function edit($mailingList)
{ {
$this->authorizeWrite('mailing'); $this->authorizeWrite('mailing');
$mailingList = MailingList::with('contatti.individuo.contatti')->findOrFail($mailingList); $mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailingList);
return view('mailing-liste.edit', compact('mailingList')); $tags = Tag::orderBy('name')->get();
$selectedTags = $mailingList->tags->pluck('id')->toArray();
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags'));
} }
public function update(Request $request, $mailingList) public function update(Request $request, $mailingList)
@@ -84,6 +100,8 @@ class MailingListController extends Controller
'descrizione' => 'nullable|string', 'descrizione' => 'nullable|string',
'attiva' => 'boolean', 'attiva' => 'boolean',
'contatti_json' => 'nullable|string', 'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]); ]);
$data['attiva'] = $data['attiva'] ?? false; $data['attiva'] = $data['attiva'] ?? false;
@@ -94,6 +112,12 @@ class MailingListController extends Controller
'attiva' => $data['attiva'], 'attiva' => $data['attiva'],
]); ]);
if ($request->has('tags')) {
$mailingList->tags()->sync($request->tags);
} else {
$mailingList->tags()->detach();
}
if (!empty($data['contatti_json'])) { if (!empty($data['contatti_json'])) {
$newContatti = json_decode($data['contatti_json'], true); $newContatti = json_decode($data['contatti_json'], true);
if (is_array($newContatti)) { if (is_array($newContatti)) {
@@ -107,7 +131,7 @@ class MailingListController extends Controller
$toAdd = array_diff($newIds, $existingIds); $toAdd = array_diff($newIds, $existingIds);
foreach ($newContatti as $contatto) { foreach ($newContatti as $contatto) {
if (in_array($contatto['individuo_id'], $toAdd) && !empty($contatto['email'])) { if (in_array($contatto['individuo_id'], $toAdd)) {
$mailingList->contatti()->create([ $mailingList->contatti()->create([
'individuo_id' => $contatto['individuo_id'], 'individuo_id' => $contatto['individuo_id'],
'opt_in' => true, 'opt_in' => true,
+158 -2
View File
@@ -11,6 +11,7 @@ use App\Models\Evento;
use App\Models\Documento; use App\Models\Documento;
use App\Models\MailingList; use App\Models\MailingList;
use App\Models\Contatto; use App\Models\Contatto;
use App\Models\Tag;
use App\Models\TipologiaDocumento; use App\Models\TipologiaDocumento;
use App\Models\ReportCustom; use App\Models\ReportCustom;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -47,12 +48,15 @@ class ReportController extends Controller
$columnOptions[$type] = ReportColumnRegistry::getColumns($type); $columnOptions[$type] = ReportColumnRegistry::getColumns($type);
} }
$allTags = Tag::orderBy('name')->get();
return view('report.index', compact( return view('report.index', compact(
'stats', 'stats',
'prebuiltReports', 'prebuiltReports',
'customReports', 'customReports',
'tipologieDocumento', 'tipologieDocumento',
'columnOptions' 'columnOptions',
'allTags'
)); ));
} }
@@ -136,6 +140,14 @@ class ReportController extends Controller
$config['search_columns'] = $request->input('search_columns'); $config['search_columns'] = $request->input('search_columns');
} }
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');
}
ReportCustom::create([ ReportCustom::create([
'user_id' => auth()->id(), 'user_id' => auth()->id(),
'nome' => $data['nome'], 'nome' => $data['nome'],
@@ -252,6 +264,9 @@ class ReportController extends Controller
'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter), 'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter),
'contatti_completo' => $this->reportContattiCompleto(), 'contatti_completo' => $this->reportContattiCompleto(),
'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter), 'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter),
'tag_statistiche' => $this->reportTagStatistiche(),
'individui_per_tag' => $this->reportIndividuiPerTag(),
'documenti_per_tag' => $this->reportDocumentiPerTag(),
default => null, default => null,
}; };
} }
@@ -403,6 +418,30 @@ class ReportController extends Controller
'colore' => 'teal', 'colore' => 'teal',
'has_tipologia_filter' => false, 'has_tipologia_filter' => false,
], ],
[
'id' => 'tag_statistiche',
'nome' => 'Statistiche Tag',
'descrizione' => 'Distribuzione degli elementi per tag (individui, gruppi, eventi, documenti, mailing list)',
'icona' => 'fa-tags',
'colore' => 'indigo',
'has_tipologia_filter' => false,
],
[
'id' => 'individui_per_tag',
'nome' => 'Individui per Tag',
'descrizione' => 'Elenco completo degli individui raggruppati per tag assegnato',
'icona' => 'fa-user-tag',
'colore' => 'info',
'has_tipologia_filter' => false,
],
[
'id' => 'documenti_per_tag',
'nome' => 'Documenti per Tag',
'descrizione' => 'Elenco completo dei documenti raggruppati per tag assegnato',
'icona' => 'fa-tags',
'colore' => 'secondary',
'has_tipologia_filter' => false,
],
]; ];
} }
@@ -927,6 +966,102 @@ class ReportController extends Controller
]; ];
} }
protected function reportTagStatistiche(): array
{
$tags = Tag::orderBy('name')->get();
$rows = $tags->map(function ($tag) {
return [
'name' => $tag->name,
'color' => $tag->color ?? '#6c757d',
'individui' => $tag->individui()->count(),
'gruppi' => $tag->gruppi()->count(),
'eventi' => $tag->eventi()->count(),
'documenti' => $tag->documenti()->count(),
'mailing' => $tag->mailingLists()->count(),
'totale' => $tag->count,
];
})->toArray();
$totalIndividui = array_sum(array_column($rows, 'individui'));
$totalGruppi = array_sum(array_column($rows, 'gruppi'));
$totalEventi = array_sum(array_column($rows, 'eventi'));
$totalDocumenti = array_sum(array_column($rows, 'documenti'));
$totalMailing = array_sum(array_column($rows, 'mailing'));
return [
'title' => 'Statistiche Tag',
'headers' => ['Tag', 'Individui', 'Gruppi', 'Eventi', 'Documenti', 'Mailing List', 'Totale Elementi'],
'rows' => array_map(fn($r) => [$r['name'], $r['individui'], $r['gruppi'], $r['eventi'], $r['documenti'], $r['mailing'], $r['totale']], $rows),
'summary' => 'Tag: ' . count($tags) . ' | Individui: ' . $totalIndividui . ' | Gruppi: ' . $totalGruppi . ' | Eventi: ' . $totalEventi . ' | Documenti: ' . $totalDocumenti . ' | Mailing List: ' . $totalMailing,
'detail_rows' => $rows,
];
}
protected function reportIndividuiPerTag(): array
{
$individui = Individuo::with('tags')
->whereHas('tags')
->orderBy('cognome')
->orderBy('nome')
->get();
$rows = $individui->flatMap(function ($ind) {
if ($ind->tags->isEmpty()) {
return collect();
}
return $ind->tags->map(function ($tag) use ($ind) {
return [
'tag' => $tag->name,
'tag_color' => $tag->color ?? '#6c757d',
'codice' => $ind->codice_id,
'cognome' => $ind->cognome,
'nome' => $ind->nome,
'email' => $ind->email_primaria ?? '-',
];
});
})->toArray();
return [
'title' => 'Individui per Tag',
'headers' => ['Tag', 'Codice', 'Cognome', 'Nome', 'Email'],
'rows' => array_map(fn($r) => [$r['tag'], $r['codice'], $r['cognome'], $r['nome'], $r['email']], $rows),
'summary' => 'Totale righe (individuo × tag): ' . count($rows),
'detail_rows' => $rows,
];
}
protected function reportDocumentiPerTag(): array
{
$documenti = Documento::with('tags')
->whereHas('tags')
->orderBy('nome_file')
->get();
$rows = $documenti->flatMap(function ($doc) {
if ($doc->tags->isEmpty()) {
return collect();
}
return $doc->tags->map(function ($tag) use ($doc) {
return [
'tag' => $tag->name,
'tag_color' => $tag->color ?? '#6c757d',
'nome_file' => $doc->nome_file,
'tipologia' => ucfirst((string) $doc->tipologia),
'data' => $doc->created_at->format('d/m/Y'),
];
});
})->toArray();
return [
'title' => 'Documenti per Tag',
'headers' => ['Tag', 'Nome File', 'Tipologia', 'Data Caricamento'],
'rows' => array_map(fn($r) => [$r['tag'], $r['nome_file'], $r['tipologia'], $r['data']], $rows),
'summary' => 'Totale righe (documento × tag): ' . count($rows),
'detail_rows' => $rows,
];
}
protected function runCustomReport(ReportCustom $report): array protected function runCustomReport(ReportCustom $report): array
{ {
$tipo = $report->tipo_report; $tipo = $report->tipo_report;
@@ -954,11 +1089,32 @@ class ReportController extends Controller
$columns = $config['columns'] ?? []; $columns = $config['columns'] ?? [];
$hasRelations = false; $hasRelations = false;
// Always load tags if tags column or tag filter is present
$needsTags = in_array('tags.nome', $columns) || !empty($config['tag_filter']);
if ($needsTags) {
$query->with('tags');
}
foreach ($columns as $col) { foreach ($columns as $col) {
if (str_contains($col, '.')) { if (str_contains($col, '.')) {
$hasRelations = true; $hasRelations = true;
$relation = explode('.', $col)[0]; $relation = explode('.', $col)[0];
$query->with($relation); if ($relation !== 'tags') {
$query->with($relation);
}
}
}
// Apply tag filter
if (!empty($config['tag_filter'])) {
$tagSlugs = Tag::whereIn('id', (array) $config['tag_filter'])->pluck('slug')->toArray();
if (!empty($tagSlugs)) {
$mode = $config['tag_filter_mode'] ?? 'any';
if ($mode === 'all') {
$query->withAllTags($tagSlugs);
} else {
$query->withAnyTags($tagSlugs);
}
} }
} }
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
@@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Documento extends Model class Documento extends Model
{ {
use HasTagsLight;
protected $table = 'documenti'; protected $table = 'documenti';
protected $fillable = [ protected $fillable = [
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk', 'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Evento extends Model class Evento extends Model
{ {
use HasTagsLight;
protected $table = 'eventi'; protected $table = 'eventi';
protected $fillable = [ protected $fillable = [
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Gruppo extends Model class Gruppo extends Model
{ {
use HasTagsLight;
protected $table = 'gruppi'; protected $table = 'gruppi';
protected $fillable = [ protected $fillable = [
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids', 'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
class Individuo extends Model class Individuo extends Model
{ {
use HasTagsLight;
protected $table = 'individui'; protected $table = 'individui';
protected $fillable = [ protected $fillable = [
'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita', 'tenant_id', 'codice_id', 'cognome', 'nome', 'data_nascita',
+3
View File
@@ -5,9 +5,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Traits\HasTagsLight;
class MailingList extends Model class MailingList extends Model
{ {
use HasTagsLight;
protected $table = 'mailing_lists'; protected $table = 'mailing_lists';
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva']; protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
+36
View File
@@ -103,6 +103,42 @@
@endif @endif
</div> </div>
</div> </div>
@include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette'])
</div>
<div class="card-body">
<table class="table table-sm table-borderless">
<tr>
<td style="width: 100px;"><strong>File:</strong></td>
<td>{{ $documento->file_path }}</td>
</tr>
<tr>
<td><strong>Tipo MIME:</strong></td>
<td>{{ $documento->mime_type ?? '-' }}</td>
</tr>
<tr>
<td><strong>Dimensione:</strong></td>
<td>{{ $documento->dimensione ? number_format($documento->dimensione / 1024, 1) . ' KB' : '-' }}</td>
</tr>
<tr>
<td><strong>Creato:</strong></td>
<td>{{ $documento->created_at->format('d/m/Y H:i') }}</td>
</tr>
@if($documento->user)
<tr>
<td><strong>Utente:</strong></td>
<td>{{ $documento->user->name }}</td>
</tr>
@endif
</table>
@if($documento->file_path)
<hr>
<a href="/documenti/{{ $documento->id }}/download" class="btn btn-primary btn-block">
<i class="fas fa-download mr-1"></i> Scarica
</a>
@endif
</div>
</div>
</div> </div>
</div> </div>
+67
View File
@@ -123,6 +123,8 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
</div> </div>
</div> </div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
<div class="card-body p-0"> <div class="card-body p-0">
{{-- Toolbar azioni massive --}} {{-- Toolbar azioni massive --}}
<form id="massForm" method="POST"> <form id="massForm" method="POST">
@@ -144,6 +146,9 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
<button type="button" class="btn btn-sm btn-secondary" onclick="$('#massMoveModal').modal('show')"> <button type="button" class="btn btn-sm btn-secondary" onclick="$('#massMoveModal').modal('show')">
<i class="fas fa-folder-open mr-1"></i> Sposta selezionati in... <i class="fas fa-folder-open mr-1"></i> Sposta selezionati in...
</button> </button>
<button type="button" class="btn btn-sm btn-info" onclick="$('#massTagModal').modal('show')">
<i class="fas fa-tags mr-1"></i> Tag
</button>
<button type="button" class="btn btn-sm btn-success" onclick="massDownload()"> <button type="button" class="btn btn-sm btn-success" onclick="massDownload()">
<i class="fas fa-download mr-1"></i> Scarica <i class="fas fa-download mr-1"></i> Scarica
</button> </button>
@@ -226,6 +231,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
</span> </span>
@endif @endif
</div> </div>
<div class="text-center mb-1" style="display:flex;flex-wrap:wrap;gap:2px;justify-content:center;">
@foreach($documento->tags as $tag)
<a href="{{ route('documenti.index', ['tag[]' => $tag->slug]) }}" class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff; font-size:0.7rem;">{{ $tag->name }}</a>
@endforeach
</div>
<div class="small text-muted text-center"> <div class="small text-muted text-center">
@if($documento->user) @if($documento->user)
{{ $documento->user->name }} {{ $documento->user->name }}
@@ -295,6 +305,7 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
<th style="width: 100px;">Tipologia</th> <th style="width: 100px;">Tipologia</th>
<th style="width: 90px;">Dimensione</th> <th style="width: 90px;">Dimensione</th>
<th style="width: 120px;">Contesto</th> <th style="width: 120px;">Contesto</th>
<th style="width: 100px;">Tag</th>
<th style="width: 110px;">Data</th> <th style="width: 110px;">Data</th>
<th style="width: 130px;">Azioni</th> <th style="width: 130px;">Azioni</th>
</tr> </tr>
@@ -314,6 +325,7 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
<td class="small text-muted">-</td> <td class="small text-muted">-</td>
<td class="small text-muted">-</td> <td class="small text-muted">-</td>
<td class="small text-muted">-</td> <td class="small text-muted">-</td>
<td class="small text-muted">-</td>
<td> <td>
<a href="/documenti?folder_id={{ $cartella->id }}" class="btn btn-xs btn-warning" title="Apri cartella"> <a href="/documenti?folder_id={{ $cartella->id }}" class="btn btn-xs btn-warning" title="Apri cartella">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
@@ -423,6 +435,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
<span class="text-muted small">-</span> <span class="text-muted small">-</span>
@endswitch @endswitch
</td> </td>
<td>
@foreach($documento->tags as $tag)
<a href="{{ route('documenti.index', ['tag[]' => $tag->slug]) }}" class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff;">{{ $tag->name }}</a>
@endforeach
</td>
<td class="small">{{ $documento->created_at->format('d/m/Y') }}</td> <td class="small">{{ $documento->created_at->format('d/m/Y') }}</td>
<td> <td>
@if($documento->file_path) @if($documento->file_path)
@@ -834,6 +851,50 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
</div> </div>
</div> </div>
{{-- MODAL: Gestione Tag massiva --}}
<div class="modal fade" id="massTagModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-tags mr-2"></i>Gestione Tag</h5>
<button type="button" class="close" onclick="$('#massTagModal').modal('hide')">
<span>&times;</span>
</button>
</div>
<form id="massTagForm" method="POST" action="/documenti/mass-tag">
@csrf
<div class="modal-body">
<p class="mb-2">Operazione su <strong id="massTagCount">0</strong> documenti selezionati.</p>
<div class="form-group mb-3">
<label class="font-weight-bold">Azione</label>
<div class="d-flex" style="gap:1.5rem;">
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="mtModeAssign" value="assign" checked>
<label class="form-check-label" for="mtModeAssign">Assegna tag</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="mtModeRemove" value="remove">
<label class="form-check-label" for="mtModeRemove">Rimuovi tag</label>
</div>
</div>
</div>
@include('partials._tag-selector', ['label' => 'Seleziona tag'])
<input type="hidden" name="ids" id="massTagIds" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="$('#massTagModal').modal('hide')">Annulla</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-tags mr-1"></i> Applica
</button>
</div>
</form>
</div>
</div>
</div>
{{-- MODAL: Sposta documento --}} {{-- MODAL: Sposta documento --}}
<div class="modal fade" id="moveModal" tabindex="-1" role="dialog"> <div class="modal fade" id="moveModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <div class="modal-dialog modal-dialog-scrollable" role="document">
@@ -1218,6 +1279,12 @@ document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', func
document.getElementById('massMoveCount').textContent = ids.length; document.getElementById('massMoveCount').textContent = ids.length;
}); });
document.getElementById('massTagModal')?.addEventListener('show.bs.modal', function() {
const ids = getSelectedIds();
document.getElementById('massTagIds').value = ids.join(',');
document.getElementById('massTagCount').textContent = ids.length;
});
document.getElementById('massMoveNewFolderBtn')?.addEventListener('click', function() { document.getElementById('massMoveNewFolderBtn')?.addEventListener('click', function() {
Swal.fire({ Swal.fire({
title: 'Nuova cartella', title: 'Nuova cartella',
+9
View File
@@ -247,6 +247,15 @@
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3>
</div>
<div class="card-body">
@include('partials._tag-selector', ['selectedTags' => [], 'label' => 'Etichette'])
</div>
</div>
<div class="mt-3"> <div class="mt-3">
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva <i class="fas fa-save mr-1"></i> Salva
+9
View File
@@ -252,6 +252,15 @@
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3>
</div>
<div class="card-body">
@include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette'])
</div>
</div>
<div class="mt-3"> <div class="mt-3">
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva Modifiche <i class="fas fa-save mr-1"></i> Salva Modifiche
+9 -1
View File
@@ -43,6 +43,8 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
@endif @endif
</div> </div>
</div> </div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
<div class="card-body"> <div class="card-body">
<form method="GET" class="mb-3"> <form method="GET" class="mb-3">
<div class="row"> <div class="row">
@@ -144,6 +146,7 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
@endif @endif
</a> </a>
</th> </th>
<th>Tag</th>
<th style="width: 120px;"> <th style="width: 120px;">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'created_at', 'direction' => request('sort') === 'created_at' && request('direction') === 'asc' ? 'desc' : 'asc']) }}" class="text-dark"> <a href="{{ request()->fullUrlWithQuery(['sort' => 'created_at', 'direction' => request('sort') === 'created_at' && request('direction') === 'asc' ? 'desc' : 'asc']) }}" class="text-dark">
Creato Creato
@@ -221,6 +224,11 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
<small class="text-muted">+{{ $evento->responsabili->count() - 2 }} altri</small> <small class="text-muted">+{{ $evento->responsabili->count() - 2 }} altri</small>
@endif @endif
</td> </td>
<td>
@foreach($evento->tags as $tag)
<a href="{{ route('eventi.index', ['tag[]' => $tag->slug]) }}" class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff;">{{ $tag->name }}</a>
@endforeach
</td>
<td><small class="text-muted">{{ $evento->created_at->format('d/m/Y') }}</small></td> <td><small class="text-muted">{{ $evento->created_at->format('d/m/Y') }}</small></td>
<td> <td>
<a href="{{ url('/eventi/' . $evento->id) }}" class="btn btn-xs btn-info" title="Visualizza"> <a href="{{ url('/eventi/' . $evento->id) }}" class="btn btn-xs btn-info" title="Visualizza">
@@ -241,7 +249,7 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="9" class="text-center text-muted py-4"> <td colspan="10" class="text-center text-muted py-4">
<i class="fas fa-calendar fa-2x mb-2"></i> <i class="fas fa-calendar fa-2x mb-2"></i>
<p class="mb-0">Nessun evento trovato</p> <p class="mb-0">Nessun evento trovato</p>
</td> </td>
+11
View File
@@ -202,6 +202,17 @@
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header"><h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3></div>
<div class="card-body">
@forelse($evento->tags as $tag)
<span class="badge" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;padding:4px 10px;border-radius:12px;font-size:0.85rem;">{{ $tag->name }}</span>
@empty
<p class="text-muted mb-0"><i class="fas fa-info-circle mr-1"></i>Nessun tag assegnato.</p>
@endforelse
</div>
</div>
@if($evento->note) @if($evento->note)
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header"><h3 class="card-title"><i class="fas fa-sticky-note mr-2"></i>Note</h3></div> <div class="card-header"><h3 class="card-title"><i class="fas fa-sticky-note mr-2"></i>Note</h3></div>
+9
View File
@@ -117,6 +117,15 @@
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3>
</div>
<div class="card-body">
@include('partials._tag-selector', ['selectedTags' => [], 'label' => 'Etichette'])
</div>
</div>
<div class="mt-3"> <div class="mt-3">
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva <i class="fas fa-save mr-1"></i> Salva
+9
View File
@@ -270,6 +270,15 @@
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3>
</div>
<div class="card-body">
@include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette'])
</div>
</div>
<div class="mt-3"> <div class="mt-3">
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva Modifiche <i class="fas fa-save mr-1"></i> Salva Modifiche
+147
View File
@@ -17,6 +17,7 @@ $columnLabels = [
'citta' => 'Città', 'citta' => 'Città',
'telefono' => 'Telefono', 'telefono' => 'Telefono',
'email' => 'Email', 'email' => 'Email',
'tag' => 'Tag',
]; ];
$vistaDefaultJson = $vista ? $vista->toJson() : 'null'; $vistaDefaultJson = $vista ? $vista->toJson() : 'null';
$allColumnsJson = json_encode($allColumns); $allColumnsJson = json_encode($allColumns);
@@ -51,6 +52,22 @@ $visibleColumnsJson = json_encode($visibleColumns);
<i class="fas fa-sitemap mr-1"></i> Albero <i class="fas fa-sitemap mr-1"></i> Albero
</button> </button>
</div> </div>
<div class="btn-group mr-1">
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle" data-toggle="dropdown">
<i class="fas fa-cog mr-1"></i> Azioni
</button>
<div class="dropdown-menu">
@if($canWriteGruppi)
<a class="dropdown-item" href="#" onclick="showMassTagModal(); return false;">
<i class="fas fa-tags mr-2"></i>Assegna Tag
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#" onclick="deleteSelected(); return false;">
<i class="fas fa-trash mr-2"></i>Elimina Selezionati
</a>
@endif
</div>
</div>
<button type="button" class="btn btn-primary btn-sm mr-1" onclick="showSaveVistaModal()"> <button type="button" class="btn btn-primary btn-sm mr-1" onclick="showSaveVistaModal()">
<i class="fas fa-save mr-1"></i> Salva Vista <i class="fas fa-save mr-1"></i> Salva Vista
</button> </button>
@@ -65,11 +82,19 @@ $visibleColumnsJson = json_encode($visibleColumns);
</div> </div>
</div> </div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
@if($viewMode === 'table') @if($viewMode === 'table')
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table table-bordered table-hover table-striped mb-0" id="gruppi-table"> <table class="table table-bordered table-hover table-striped mb-0" id="gruppi-table">
<thead class="thead-light"> <thead class="thead-light">
<tr> <tr>
<th style="width: 40px;">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="select-all" onchange="toggleSelectAll(this)">
<label class="custom-control-label" for="select-all"></label>
</div>
</th>
@if(in_array('nome', $visibleColumns)) @if(in_array('nome', $visibleColumns))
<th data-sortable="true" onclick="sortTable('nome')" style="cursor:pointer;">Nome <i class="fas fa-sort float-right text-muted"></i></th> <th data-sortable="true" onclick="sortTable('nome')" style="cursor:pointer;">Nome <i class="fas fa-sort float-right text-muted"></i></th>
@endif @endif
@@ -103,12 +128,21 @@ $visibleColumnsJson = json_encode($visibleColumns);
@if(in_array('email', $visibleColumns)) @if(in_array('email', $visibleColumns))
<th>Email</th> <th>Email</th>
@endif @endif
@if(in_array('tag', $visibleColumns))
<th>Tag</th>
@endif
<th style="width: 120px;">Azioni</th> <th style="width: 120px;">Azioni</th>
</tr> </tr>
</thead> </thead>
<tbody id="table-body"> <tbody id="table-body">
@foreach($gruppi as $gruppo) @foreach($gruppi as $gruppo)
<tr data-id="{{ $gruppo->id }}"> <tr data-id="{{ $gruppo->id }}">
<td>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input row-checkbox" id="select-{{ $gruppo->id }}" value="{{ $gruppo->id }}">
<label class="custom-control-label" for="select-{{ $gruppo->id }}"></label>
</div>
</td>
@if(in_array('nome', $visibleColumns)) @if(in_array('nome', $visibleColumns))
<td> <td>
<span style="padding-left: {{ ($gruppo->depth ?? 0) * 20 }}px;"> <span style="padding-left: {{ ($gruppo->depth ?? 0) * 20 }}px;">
@@ -154,6 +188,13 @@ $visibleColumnsJson = json_encode($visibleColumns);
@if(in_array('email', $visibleColumns)) @if(in_array('email', $visibleColumns))
<td>-</td> <td>-</td>
@endif @endif
@if(in_array('tag', $visibleColumns))
<td>
@foreach($gruppo->tags as $tag)
<a href="{{ route('gruppi.index', ['tag[]' => $tag->slug]) }}" class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff;">{{ $tag->name }}</a>
@endforeach
</td>
@endif
<td> <td>
<a href="{{ route('gruppi.show', $gruppo->id) }}" class="btn btn-xs btn-info" title="Visualizza"> <a href="{{ route('gruppi.show', $gruppo->id) }}" class="btn btn-xs btn-info" title="Visualizza">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
@@ -309,6 +350,68 @@ $visibleColumnsJson = json_encode($visibleColumns);
</div> </div>
</div> </div>
</div> </div>
{{-- MODAL: Gestione Tag massiva --}}
<div class="modal fade" id="massTagModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header bg-info">
<h5 class="modal-title text-white"><i class="fas fa-tags mr-2"></i>Gestione Tag</h5>
<button type="button" class="close text-white" data-dismiss="modal"><span>&times;</span></button>
</div>
<form id="massTagForm" method="POST" action="/gruppi/mass-tag">
@csrf
<div class="modal-body">
<p class="mb-2">Operazione su <strong id="massTagCount">0</strong> gruppi selezionati.</p>
<div class="form-group mb-3">
<label class="font-weight-bold">Azione</label>
<div class="d-flex" style="gap:1.5rem;">
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="grMtModeAssign" value="assign" checked>
<label class="form-check-label" for="grMtModeAssign">Assegna tag</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="grMtModeRemove" value="remove">
<label class="form-check-label" for="grMtModeRemove">Rimuovi tag</label>
</div>
</div>
</div>
@include('partials._tag-selector', ['label' => 'Seleziona tag'])
<input type="hidden" name="ids" id="massTagIds" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary"><i class="fas fa-tags mr-1"></i> Applica</button>
</div>
</form>
</div>
</div>
</div>
{{-- MODAL: Conferma Eliminazione massiva --}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog">
@if($canDeleteGruppi)
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-danger">
<h5 class="modal-title text-white"><i class="fas fa-trash-alt mr-2"></i>Conferma Eliminazione</h5>
<button type="button" class="close text-white" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
<p>Stai per eliminare <strong id="delete-count">0</strong> gruppi.</p>
<div class="alert alert-warning mt-3 mb-0">
<small><i class="fas fa-info-circle mr-1"></i>I membri verranno scollegati ma non eliminati.</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="button" class="btn btn-danger" id="confirm-delete-btn"><i class="fas fa-trash mr-1"></i> Elimina</button>
</div>
</div>
</div>
@endif
</div>
@endsection @endsection
@section('scripts') @section('scripts')
@@ -317,6 +420,50 @@ function switchView(mode) {
window.location.href = '?view=' + mode; window.location.href = '?view=' + mode;
} }
function toggleSelectAll(source) {
document.querySelectorAll('.row-checkbox').forEach(function(cb) {
cb.checked = source.checked;
});
}
function getSelectedIds() {
return Array.from(document.querySelectorAll('.row-checkbox:checked')).map(cb => cb.value);
}
function showMassTagModal() {
const ids = getSelectedIds();
if (ids.length === 0) {
alert('Seleziona almeno un gruppo');
return;
}
document.getElementById('massTagIds').value = ids.join(',');
document.getElementById('massTagCount').textContent = ids.length;
$('#massTagModal').modal('show');
}
function deleteSelected() {
const ids = getSelectedIds();
if (ids.length === 0) {
alert('Seleziona almeno un gruppo');
return;
}
document.getElementById('delete-count').textContent = ids.length;
document.getElementById('confirm-delete-btn').onclick = function() {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/gruppi/mass-elimina';
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '{{ csrf_token() }}';
let html = '<input type="hidden" name="_token" value="' + csrfToken + '">';
ids.forEach(function(id) {
html += '<input type="hidden" name="ids[]" value="' + id + '">';
});
form.innerHTML = html;
document.body.appendChild(form);
form.submit();
};
$('#deleteModal').modal('show');
}
function toggleItem(id) { function toggleItem(id) {
const el = document.getElementById('group-' + id); const el = document.getElementById('group-' + id);
const icon = document.getElementById('icon-' + id); const icon = document.getElementById('icon-' + id);
+11
View File
@@ -247,6 +247,17 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
</div> </div>
@endif @endif
<div class="card mt-3">
<div class="card-header"><h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3></div>
<div class="card-body">
@forelse($gruppo->tags as $tag)
<span class="badge" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;padding:4px 10px;border-radius:12px;font-size:0.85rem;">{{ $tag->name }}</span>
@empty
<p class="text-muted mb-0"><i class="fas fa-info-circle mr-1"></i>Nessun tag assegnato.</p>
@endforelse
</div>
</div>
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header"> <div class="card-header">
<h3 class="card-title"> <h3 class="card-title">
@@ -44,6 +44,9 @@
<a href="#ruoli" class="list-group-item list-group-item-action" data-toggle="tab"> <a href="#ruoli" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-users-cog mr-2"></i> Tipi di Ruolo <i class="nav-icon fas fa-users-cog mr-2"></i> Tipi di Ruolo
</a> </a>
<a href="#tag" class="list-group-item list-group-item-action" data-toggle="tab">
<i class="nav-icon fas fa-tags mr-2"></i> Tag
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -1068,6 +1071,136 @@
</div> </div>
</div> </div>
</div> </div>
{{-- === TAG === --}}
<div class="tab-pane" id="tag">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Tag</h3>
</div>
<div class="card-body">
<p class="text-muted">Gestisci i tag (etichette) per classificare individui, gruppi, eventi e documenti.</p>
@if(session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="mb-3">
<button type="button" class="btn btn-success btn-sm" data-toggle="modal" data-target="#addTagModal">
<i class="fas fa-plus"></i> Nuovo Tag
</button>
</div>
<table class="table table-bordered table-striped" id="tag-table">
<thead>
<tr>
<th style="width: 40px;">Ordine</th>
<th>Tag</th>
<th>Nome</th>
<th>Slug</th>
<th style="width: 100px;">Colore</th>
<th style="width: 150px;">Azioni</th>
</tr>
</thead>
<tbody id="tag-sortable">
@foreach($tags as $tag)
<tr data-id="{{ $tag->id }}">
<td class="text-center">
<i class="fas fa-arrows-alt handle" style="cursor: grab;"></i>
</td>
<td>
<span class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff;">
{{ $tag->name }}
</span>
</td>
<td><span class="tag-nome">{{ $tag->name }}</span></td>
<td><code>{{ $tag->slug }}</code></td>
<td>
<span class="tag-color-preview" style="display:inline-block;width:24px;height:24px;border-radius:4px;background:{{ $tag->color ?? '#6c757d' }};border:1px solid #dee2e6;vertical-align:middle;"></span>
<span class="tag-color-value">{{ $tag->color ?? '-' }}</span>
</td>
<td>
<button type="button" class="btn btn-xs btn-warning" onclick="editTag({{ $tag->id }}, '{{ addslashes($tag->name) }}', '{{ $tag->color ?? '' }}')">
<i class="fas fa-edit"></i>
</button>
<form method="POST" action="{{ route('impostazioni.tag.destroy', $tag->id) }}" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-xs btn-danger" onclick="return confirm('Eliminare questo tag?')">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- ===== MODALI TAG ===== --}}
<div class="modal fade" id="addTagModal">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="{{ route('impostazioni.tag.store') }}">
@csrf
<div class="modal-header">
<h4 class="modal-title">Nuovo Tag</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="tagName">Nome *</label>
<input type="text" name="name" id="tagName" class="form-control" required maxlength="100" placeholder="es. giovani">
</div>
<div class="form-group">
<label for="tagColor">Colore</label>
<input type="color" name="color" id="tagColor" class="form-control" style="height:38px;padding:4px;" value="#007bff">
<small class="text-muted">Scegli un colore per il badge del tag</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">Salva</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="editTagModal">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="" id="editTagForm">
@csrf
@method('PUT')
<div class="modal-header">
<h4 class="modal-title">Modifica Tag</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="editTagName">Nome *</label>
<input type="text" name="name" id="editTagName" class="form-control" required maxlength="100">
</div>
<div class="form-group">
<label for="editTagColor">Colore</label>
<input type="color" name="color" id="editTagColor" class="form-control" style="height:38px;padding:4px;">
<small class="text-muted">Scegli un colore per il badge del tag</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">Salva</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@@ -1618,6 +1751,32 @@ var sortableTipologieEventi = Sortable.create(document.getElementById('tipologie
} }
}); });
function editTag(id, name, color) {
$('#editTagName').val(name);
$('#editTagColor').val(color || '#007bff');
$('#editTagForm').attr('action', '/impostazioni/tag/' + id);
$('#editTagModal').modal('show');
}
var sortableTag = Sortable.create(document.getElementById('tag-sortable'), {
handle: '.handle',
animation: 150,
onEnd: function(evt) {
var order = [];
$('#tag-sortable tr').each(function() {
order.push($(this).data('id'));
});
fetch('{{ route('impostazioni.tag.reorder') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Content-Type': 'application/json',
},
body: JSON.stringify({ order: order })
});
}
});
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr('href'); var target = $(e.target).attr('href');
if (target === '#ruoli') { if (target === '#ruoli') {
@@ -1626,6 +1785,9 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
if (target === '#tipologie-eventi') { if (target === '#tipologie-eventi') {
sortableTipologieEventi.refresh(); sortableTipologieEventi.refresh();
} }
if (target === '#tag') {
sortableTag.refresh();
}
if (target === '#calendario') { if (target === '#calendario') {
if (typeof sortableCalendario !== 'undefined') { if (typeof sortableCalendario !== 'undefined') {
sortableCalendario.refresh(); sortableCalendario.refresh();
@@ -108,6 +108,15 @@
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3>
</div>
<div class="card-body">
@include('partials._tag-selector', ['selectedTags' => [], 'label' => 'Etichette'])
</div>
</div>
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header"> <div class="card-header">
<h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti</h3> <h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti</h3>
+10
View File
@@ -139,6 +139,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3>
</div>
<div class="card-body">
@include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette'])
</div>
</div>
<div class="card mt-3"> <div class="card mt-3">
<div class="card-header"> <div class="card-header">
<h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti</h3> <h3 class="card-title"><i class="fas fa-address-book mr-2"></i>Contatti</h3>
+126 -3
View File
@@ -9,6 +9,7 @@ $columnLabels = [
'nome' => 'Nome', 'nome' => 'Nome',
'email' => 'Email', 'email' => 'Email',
'telefono' => 'Telefono', 'telefono' => 'Telefono',
'tag' => 'Tag',
]; ];
$vistaDefaultJson = $vista ? $vista->toJson() : 'null'; $vistaDefaultJson = $vista ? $vista->toJson() : 'null';
$allColumnsJson = json_encode($allColumns); $allColumnsJson = json_encode($allColumns);
@@ -47,6 +48,12 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
<a class="dropdown-item" href="#" onclick="showCreateMailingListModal(); return false;"> <a class="dropdown-item" href="#" onclick="showCreateMailingListModal(); return false;">
<i class="fas fa-list mr-2"></i>Crea Mailing List <i class="fas fa-list mr-2"></i>Crea Mailing List
</a> </a>
<a class="dropdown-item" href="#" onclick="showMassGruppoModal(); return false;">
<i class="fas fa-users mr-2"></i>Assegna Gruppo
</a>
<a class="dropdown-item" href="#" onclick="showMassTagModal(); return false;">
<i class="fas fa-tags mr-2"></i>Assegna Tag
</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#" onclick="deleteSelected(); return false;"> <a class="dropdown-item text-danger" href="#" onclick="deleteSelected(); return false;">
<i class="fas fa-trash mr-2"></i>Elimina Selezionati <i class="fas fa-trash mr-2"></i>Elimina Selezionati
@@ -124,6 +131,8 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
</div> </div>
</div> </div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
<div class="card-body p-0"> <div class="card-body p-0">
@if($individui->count() > 0) @if($individui->count() > 0)
<table class="table table-bordered table-hover table-striped mb-0" id="individui-table"> <table class="table table-bordered table-hover table-striped mb-0" id="individui-table">
@@ -150,6 +159,9 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
@if(in_array('telefono', $visibleColumns)) @if(in_array('telefono', $visibleColumns))
<th data-sortable="true" data-column="telefono" onclick="sortTable('telefono')">Telefono <i class="fas fa-sort float-right"></i></th> <th data-sortable="true" data-column="telefono" onclick="sortTable('telefono')">Telefono <i class="fas fa-sort float-right"></i></th>
@endif @endif
@if(in_array('tag', $visibleColumns))
<th>Tag</th>
@endif
<th style="width: 120px;">Azioni</th> <th style="width: 120px;">Azioni</th>
</tr> </tr>
</thead> </thead>
@@ -181,6 +193,13 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
@if(in_array('telefono', $visibleColumns)) @if(in_array('telefono', $visibleColumns))
<td>{{ $ind->getTelefonoPrimarioAttribute() ?: '-' }}</td> <td>{{ $ind->getTelefonoPrimarioAttribute() ?: '-' }}</td>
@endif @endif
@if(in_array('tag', $visibleColumns))
<td>
@foreach($ind->tags as $tag)
<a href="{{ route('individui.index', ['tag[]' => $tag->slug]) }}" class="badge" style="background-color: {{ $tag->color ?? '#6c757d' }}; color: #fff;">{{ $tag->name }}</a>
@endforeach
</td>
@endif
<td> <td>
@if($canWriteIndividui) @if($canWriteIndividui)
<a href="{{ url('/individui/' . $ind->id . '/edit') }}" class="btn btn-xs btn-warning" title="Modifica"> <a href="{{ url('/individui/' . $ind->id . '/edit') }}" class="btn btn-xs btn-warning" title="Modifica">
@@ -415,6 +434,88 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
</div> </div>
</div> </div>
{{-- MODAL: Assegna Gruppo massivo --}}
<div class="modal fade" id="massGruppoModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-info">
<h5 class="modal-title text-white"><i class="fas fa-users mr-2"></i>Assegna Gruppo</h5>
<button type="button" class="close text-white" data-dismiss="modal"><span>&times;</span></button>
</div>
<form id="massGruppoForm" method="POST" action="/individui/mass-gruppo">
@csrf
<div class="modal-body">
<p class="mb-2">Operazione su <strong id="massGruppoCount">0</strong> individui selezionati.</p>
<div class="form-group mb-3">
<label class="font-weight-bold">Azione</label>
<div class="d-flex" style="gap:1.5rem;">
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="mgModeAssign" value="assign" checked>
<label class="form-check-label" for="mgModeAssign">Assegna a gruppo</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="mgModeRemove" value="remove">
<label class="form-check-label" for="mgModeRemove">Rimuovi da gruppo</label>
</div>
</div>
</div>
<div class="form-group">
<label>Seleziona Gruppi</label>
<select name="gruppi[]" class="form-control" multiple size="8">
@foreach($gruppi as $g)
<option value="{{ $g->id }}">{{ $g->nome }}</option>
@endforeach
</select>
<small class="text-muted">Seleziona con Ctrl/Cmd per scegliere più gruppi</small>
</div>
<input type="hidden" name="ids" id="massGruppoIds" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary"><i class="fas fa-users mr-1"></i> Applica</button>
</div>
</form>
</div>
</div>
</div>
{{-- MODAL: Gestione Tag massiva --}}
<div class="modal fade" id="massTagModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header bg-info">
<h5 class="modal-title text-white"><i class="fas fa-tags mr-2"></i>Gestione Tag</h5>
<button type="button" class="close text-white" data-dismiss="modal"><span>&times;</span></button>
</div>
<form id="massTagForm" method="POST" action="/individui/mass-tag">
@csrf
<div class="modal-body">
<p class="mb-2">Operazione su <strong id="massTagCount">0</strong> individui selezionati.</p>
<div class="form-group mb-3">
<label class="font-weight-bold">Azione</label>
<div class="d-flex" style="gap:1.5rem;">
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="indMtModeAssign" value="assign" checked>
<label class="form-check-label" for="indMtModeAssign">Assegna tag</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="mode" id="indMtModeRemove" value="remove">
<label class="form-check-label" for="indMtModeRemove">Rimuovi tag</label>
</div>
</div>
</div>
@include('partials._tag-selector', ['label' => 'Seleziona tag'])
<input type="hidden" name="ids" id="massTagIds" value="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary"><i class="fas fa-tags mr-1"></i> Applica</button>
</div>
</form>
</div>
</div>
</div>
@endsection @endsection
@section('scripts') @section('scripts')
@@ -444,7 +545,7 @@ function applyColumnFilter() {
if (!col || !val) return; if (!col || !val) return;
const rows = document.querySelectorAll('#table-body tr'); const rows = document.querySelectorAll('#table-body tr');
const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[col]; const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, tag: 6, azioni: 7 }[col];
rows.forEach(row => { rows.forEach(row => {
const cells = row.querySelectorAll('td'); const cells = row.querySelectorAll('td');
@@ -476,7 +577,7 @@ function sortTable(column) {
currentSort = { column: column, direction: 'asc' }; currentSort = { column: column, direction: 'asc' };
} }
const colIndex = { codice: 0, cognome: 1, nome: 2, email: 3, telefono: 4, azioni: 5 }[column]; const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, tag: 6, azioni: 7 }[column];
const tbody = document.getElementById('table-body'); const tbody = document.getElementById('table-body');
const rows = Array.from(tbody.querySelectorAll('tr')); const rows = Array.from(tbody.querySelectorAll('tr'));
@@ -500,7 +601,7 @@ function sortTable(column) {
} }
function toggleColumn(column, visible) { function toggleColumn(column, visible) {
const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, azioni: 6 }[column]; const colIndex = { codice: 1, cognome: 2, nome: 3, email: 4, telefono: 5, tag: 6, azioni: 7 }[column];
const ths = document.querySelectorAll('#individui-table thead th'); const ths = document.querySelectorAll('#individui-table thead th');
const rows = document.querySelectorAll('#table-body tr'); const rows = document.querySelectorAll('#table-body tr');
@@ -890,6 +991,28 @@ function updateMlCheckboxEmail(id, email) {
} }
} }
function showMassGruppoModal() {
const ids = getSelectedIds();
if (ids.length === 0) {
alert('Seleziona almeno un individuo');
return;
}
document.getElementById('massGruppoIds').value = ids.join(',');
document.getElementById('massGruppoCount').textContent = ids.length;
$('#massGruppoModal').modal('show');
}
function showMassTagModal() {
const ids = getSelectedIds();
if (ids.length === 0) {
alert('Seleziona almeno un individuo');
return;
}
document.getElementById('massTagIds').value = ids.join(',');
document.getElementById('massTagCount').textContent = ids.length;
$('#massTagModal').modal('show');
}
async function createMailingList() { async function createMailingList() {
const nome = document.getElementById('ml-nome').value.trim(); const nome = document.getElementById('ml-nome').value.trim();
if (!nome) { if (!nome) {
+15
View File
@@ -88,6 +88,21 @@
</div> </div>
</div> </div>
<div class="row mt-3">
<div class="col-md-12">
<div class="card">
<div class="card-header"><h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3></div>
<div class="card-body">
@forelse($individuo->tags as $tag)
<span class="badge" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;padding:4px 10px;border-radius:12px;font-size:0.85rem;">{{ $tag->name }}</span>
@empty
<p class="text-muted mb-0"><i class="fas fa-info-circle mr-1"></i>Nessun tag assegnato.</p>
@endforelse
</div>
</div>
</div>
</div>
@if($individuo->note) @if($individuo->note)
<div class="row mt-3"> <div class="row mt-3">
<div class="col-md-8"> <div class="col-md-8">
@@ -199,6 +199,14 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
</a> </a>
</li> </li>
@endif @endif
@if(Auth::user()->canAccess('individui'))
<li class="nav-item">
<a href="{{ route('ricerca.index') }}" class="nav-link {{ request()->routeIs('ricerca.*') ? 'active' : '' }}">
<i class="nav-icon fas fa-tags"></i>
<p>Ricerca per Tag</p>
</a>
</li>
@endif
@if(Auth::user()->canAccess('report')) @if(Auth::user()->canAccess('report'))
<li class="nav-item"> <li class="nav-item">
<a href="{{ route('report.index') }}" class="nav-link {{ request()->routeIs('report.*') ? 'active' : '' }}"> <a href="{{ route('report.index') }}" class="nav-link {{ request()->routeIs('report.*') ? 'active' : '' }}">
@@ -294,6 +302,7 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/js/adminlte.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/js/adminlte.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
@stack('scripts')
@yield('scripts') @yield('scripts')
</body> </body>
</html> </html>
@@ -30,6 +30,9 @@
<label>Descrizione</label> <label>Descrizione</label>
<textarea name="descrizione" class="form-control" rows="2"></textarea> <textarea name="descrizione" class="form-control" rows="2"></textarea>
</div> </div>
<hr>
@include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => old('tags', []), 'label' => 'Tag'])
<div class="alert alert-info"> <div class="alert alert-info">
<i class="fas fa-info-circle mr-1"></i> <i class="fas fa-info-circle mr-1"></i>
<strong>Come aggiungere contatti:</strong> vai su <em>Individui</em>, seleziona le righe desired, e usa il menu <em>Azioni &gt; Crea Mailing List</em> per creare una lista dai contatti selezionati. <strong>Come aggiungere contatti:</strong> vai su <em>Individui</em>, seleziona le righe desired, e usa il menu <em>Azioni &gt; Crea Mailing List</em> per creare una lista dai contatti selezionati.
+108 -35
View File
@@ -31,6 +31,8 @@
<textarea name="descrizione" class="form-control" rows="2">{{ $mailingList->descrizione }}</textarea> <textarea name="descrizione" class="form-control" rows="2">{{ $mailingList->descrizione }}</textarea>
</div> </div>
@include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => $selectedTags ?? [], 'label' => 'Tag'])
<hr> <hr>
<div class="row"> <div class="row">
@@ -64,6 +66,13 @@
</button> </button>
</div> </div>
<div id="no-email-warning" class="alert alert-warning alert-dismissible" role="alert" style="display:none;">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>Attenzione:</strong> I seguenti individui non hanno un indirizzo email nei contatti:
<ul id="no-email-list" class="mb-0 mt-1"></ul>
</div>
<div class="form-group"> <div class="form-group">
<label>Contatti da includere (<span id="contatti-count">{{ $mailingList->contatti->count() }}</span>)</label> <label>Contatti da includere (<span id="contatti-count">{{ $mailingList->contatti->count() }}</span>)</label>
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;"> <div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
@@ -185,11 +194,17 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Errore individui:', err); console.error('Errore individui:', err);
}); });
contattiCaricati = Array.from(document.querySelectorAll('.contatto-checkbox')).map(cb => ({ contattiCaricati = Array.from(document.querySelectorAll('.contatto-checkbox')).map(cb => {
individuo_id: parseInt(cb.value), const row = cb.closest('tr');
email: cb.dataset.email, return {
existing: true individuo_id: parseInt(cb.value),
})); email: cb.dataset.email || '',
existing: true,
codice_id: row ? (row.querySelector('td:nth-child(2)')?.textContent?.trim() || '') : '',
cognome: row ? (row.querySelector('td:nth-child(3)')?.textContent?.trim() || '') : '',
nome: row ? (row.querySelector('td:nth-child(4)')?.textContent?.trim() || '') : ''
};
});
}); });
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
@@ -217,17 +232,15 @@ function caricaSelezionati() {
}) })
.then(data => { .then(data => {
data.forEach(function(ind) { data.forEach(function(ind) {
if (ind.emails && ind.emails.length > 0) { if (!contattiCaricati.find(c => c.individuo_id === ind.id)) {
if (!contattiCaricati.find(c => c.individuo_id === ind.id)) { contattiCaricati.push({
contattiCaricati.push({ individuo_id: ind.id,
individuo_id: ind.id, codice_id: ind.codice_id,
codice_id: ind.codice_id, cognome: ind.cognome,
cognome: ind.cogname, nome: ind.nome,
nome: ind.nome, email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : '',
email: ind.emails[0], origini: ['Gruppo']
origini: ['Gruppo'] });
});
}
} }
}); });
}) })
@@ -246,17 +259,15 @@ function caricaSelezionati() {
}) })
.then(data => { .then(data => {
data.forEach(function(ind) { data.forEach(function(ind) {
if (ind.emails && ind.emails.length > 0) { if (!contattiCaricati.find(c => c.individuo_id === ind.id)) {
if (!contattiCaricati.find(c => c.individuo_id === ind.id)) { contattiCaricati.push({
contattiCaricati.push({ individuo_id: ind.id,
individuo_id: ind.id, codice_id: ind.codice_id,
codice_id: ind.codice_id, cognome: ind.cognome,
cognome: ind.cogname, nome: ind.nome,
nome: ind.nome, email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : '',
email: ind.emails[0], origini: [' Individuo']
origini: [' Individuo'] });
});
}
} }
}); });
}) })
@@ -327,25 +338,87 @@ function deselezionaTutti() {
} }
document.getElementById('ml-form').addEventListener('submit', function(e) { document.getElementById('ml-form').addEventListener('submit', function(e) {
e.preventDefault();
const form = this;
const selected = []; const selected = [];
document.querySelectorAll('.contatto-checkbox:checked').forEach(function(cb) { document.querySelectorAll('.contatto-checkbox:checked').forEach(function(cb) {
const row = cb.closest('tr');
selected.push({ selected.push({
individuo_id: parseInt(cb.value), individuo_id: parseInt(cb.value),
email: cb.dataset.email email: cb.dataset.email || '',
_label: row
? (row.querySelector('td:nth-child(2)')?.textContent?.trim() || '') + ' - ' +
(row.querySelector('td:nth-child(3)')?.textContent?.trim() || '') + ' ' +
(row.querySelector('td:nth-child(4)')?.textContent?.trim() || '')
: 'ID ' + cb.value
}); });
}); });
const indSelect = document.getElementById('individui-select');
if (indSelect) {
Array.from(indSelect.selectedOptions).forEach(function(opt) {
const id = parseInt(opt.value);
if (!selected.find(function(s) { return s.individuo_id === id; })) {
selected.push({
individuo_id: id,
email: '',
_label: opt.textContent
});
}
});
}
if (selected.length === 0) { if (selected.length === 0) {
e.preventDefault(); const gruppiSelect = document.getElementById('gruppi-select');
alert('Seleziona almeno un contatto con email'); if (gruppiSelect && gruppiSelect.selectedOptions.length > 0) {
alert('Devi cliccare "Carica contatti" per caricare i membri dei gruppi prima di salvare.');
} else {
alert('Seleziona almeno un contatto.');
}
return; return;
} }
const input = document.createElement('input'); function completaInvio() {
input.type = 'hidden'; const input = document.createElement('input');
input.name = 'contatti_json'; input.type = 'hidden';
input.value = JSON.stringify(selected); input.name = 'contatti_json';
this.appendChild(input); input.value = JSON.stringify(selected.map(function(s) {
return { individuo_id: s.individuo_id, email: s.email };
}));
form.appendChild(input);
form.submit();
}
const senzaEmail = selected.filter(function(s) { return !s.email; });
if (senzaEmail.length > 0) {
const elencoHtml = '<ul class="text-left mb-0">' +
senzaEmail.map(function(s) {
return '<li>' + s._label + '</li>';
}).join('') +
'</ul>';
Swal.fire({
title: 'Contatti senza email',
html: '<p class="mb-2">I seguenti ' + senzaEmail.length + ' contatti non hanno un indirizzo email:</p>' +
elencoHtml +
'<p class="mt-3 mb-0 text-muted">La email verrà risolta automaticamente quando l\'individuo verrà aggiornato. Salvare comunque?</p>',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Salva lo stesso',
cancelButtonText: 'Annulla',
confirmButtonColor: '#28a745',
cancelButtonColor: '#6c757d'
}).then(function(result) {
if (result.isConfirmed) {
completaInvio();
}
});
} else {
completaInvio();
}
}); });
</script> </script>
@endsection @endsection
@@ -34,6 +34,7 @@ $canDeleteMailing = Auth::user()->canDelete('mailing');
@endif @endif
</div> </div>
</div> </div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags ?? collect()])
<div class="card-body p-0"> <div class="card-body p-0">
@if($mailingLists->count() > 0) @if($mailingLists->count() > 0)
<table class="table table-bordered table-hover table-striped mb-0"> <table class="table table-bordered table-hover table-striped mb-0">
@@ -50,6 +51,7 @@ $canDeleteMailing = Auth::user()->canDelete('mailing');
<th>Nome</th> <th>Nome</th>
<th>Descrizione</th> <th>Descrizione</th>
<th style="width: 100px;">Contatti</th> <th style="width: 100px;">Contatti</th>
<th>Tag</th>
<th style="width: 100px;">Stato</th> <th style="width: 100px;">Stato</th>
<th style="width: 130px;">Creata il</th> <th style="width: 130px;">Creata il</th>
<th style="width: 100px;">Azioni</th> <th style="width: 100px;">Azioni</th>
@@ -73,6 +75,16 @@ $canDeleteMailing = Auth::user()->canDelete('mailing');
<td> <td>
<span class="badge badge-info">{{ $lista->contatti->count() }}</span> <span class="badge badge-info">{{ $lista->contatti->count() }}</span>
</td> </td>
<td>
@foreach($lista->tags as $tag)
<a href="{{ request()->url() }}?tag[]={{ $tag->slug }}" class="badge" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;font-size:0.75rem;text-decoration:none;">
{{ $tag->name }}
</a>
@endforeach
@if($lista->tags->isEmpty())
<span class="text-muted small">-</span>
@endif
</td>
<td> <td>
@if($lista->attiva) @if($lista->attiva)
<span class="badge badge-success">Attiva</span> <span class="badge badge-success">Attiva</span>
@@ -49,6 +49,23 @@
</form> </form>
</div> </div>
</div> </div>
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>Tag</h3>
</div>
<div class="card-body">
@if($mailingList->tags->isNotEmpty())
@foreach($mailingList->tags as $tag)
<span class="badge" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;font-size:0.85rem;padding:6px 12px;border-radius:12px;margin:2px;">
{{ $tag->name }}
</span>
@endforeach
@else
<p class="text-muted mb-0">Nessun tag assegnato</p>
@endif
</div>
</div>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
+50
View File
@@ -295,6 +295,22 @@
<label>Limite risultati</label> <label>Limite risultati</label>
<input type="number" name="limit" class="form-control" placeholder="Es: 100" min="1"> <input type="number" name="limit" class="form-control" placeholder="Es: 100" min="1">
</div> </div>
<div class="form-group">
<label>Filtra per Tag</label>
<select name="tag_filter[]" id="tag_filter" class="form-control select2-tags" multiple style="width: 100%;">
@foreach($allTags as $tag)
<option value="{{ $tag->id }}" style="background-color:{{ $tag->color ?? '#6c757d' }};">{{ $tag->name }}</option>
@endforeach
</select>
<small class="text-muted">Filtra i risultati per tag</small>
</div>
<div class="form-group">
<label>Modalità filtro tag</label>
<select name="tag_filter_mode" class="form-control">
<option value="any">Almeno un tag (OR)</option>
<option value="all">Tutti i tag selezionati (AND)</option>
</select>
</div>
<button type="submit" class="btn btn-primary btn-block"> <button type="submit" class="btn btn-primary btn-block">
<i class="fas fa-save mr-1"></i> Salva Report <i class="fas fa-save mr-1"></i> Salva Report
</button> </button>
@@ -346,6 +362,37 @@ document.addEventListener('DOMContentLoaded', function() {
columnsSelectInitialized = false; columnsSelectInitialized = false;
} }
let tagFilterSelect2Initialized = false;
function initTagFilterSelect2() {
if (tagFilterSelect2Initialized) return;
$('#tag_filter').select2({
theme: 'bootstrap4',
placeholder: 'Seleziona tag...',
allowClear: true,
width: '100%',
templateResult: function(opt) {
if (!opt.id) return opt.text;
var color = $(opt.element).css('background-color');
var $span = $('<span style="padding:2px 8px;border-radius:10px;color:#fff;background-color:' + color + '">' + opt.text + '</span>');
return $span;
},
templateSelection: function(opt) {
if (!opt.id) return opt.text;
var color = $(opt.element).css('background-color');
var $span = $('<span style="padding:2px 8px;border-radius:10px;color:#fff;background-color:' + color + '">' + opt.text + '</span>');
return $span;
}
});
tagFilterSelect2Initialized = true;
}
function destroyTagFilterSelect2() {
if (!tagFilterSelect2Initialized) return;
$('#tag_filter').select2('destroy');
tagFilterSelect2Initialized = false;
}
function getSortOptions(tipoReport) { function getSortOptions(tipoReport) {
const cols = columnOptions[tipoReport] || {}; const cols = columnOptions[tipoReport] || {};
const options = []; const options = [];
@@ -365,8 +412,10 @@ document.addEventListener('DOMContentLoaded', function() {
$('#collapseThree').on('shown.bs.collapse', function () { $('#collapseThree').on('shown.bs.collapse', function () {
initColumnsSelect2(); initColumnsSelect2();
initTagFilterSelect2();
}).on('hidden.bs.collapse', function () { }).on('hidden.bs.collapse', function () {
destroyColumnsSelect2(); destroyColumnsSelect2();
destroyTagFilterSelect2();
}); });
if ($('#collapseTwo').hasClass('show')) { if ($('#collapseTwo').hasClass('show')) {
@@ -375,6 +424,7 @@ document.addEventListener('DOMContentLoaded', function() {
if ($('#collapseThree').hasClass('show')) { if ($('#collapseThree').hasClass('show')) {
initColumnsSelect2(); initColumnsSelect2();
initTagFilterSelect2();
} }
$('#tipo_report').on('change', function() { $('#tipo_report').on('change', function() {
+14
View File
@@ -17,6 +17,7 @@ use App\Http\Controllers\MailingController;
use App\Http\Controllers\MailingListController; use App\Http\Controllers\MailingListController;
use App\Http\Controllers\ReportController; use App\Http\Controllers\ReportController;
use App\Http\Controllers\ImpostazioniController; use App\Http\Controllers\ImpostazioniController;
use App\Http\Controllers\RicercaController;
use App\Http\Controllers\Admin\UtenteController; use App\Http\Controllers\Admin\UtenteController;
use App\Http\Controllers\Admin\RuoloController; use App\Http\Controllers\Admin\RuoloController;
use App\Http\Controllers\Admin\ActivityLogController; use App\Http\Controllers\Admin\ActivityLogController;
@@ -75,6 +76,8 @@ Route::get('individui/{individuo}/report', [IndividuoController::class, 'report'
Route::get('individui/{individuo}/collegati', [IndividuoController::class, 'elementiCollegati'])->middleware('auth'); Route::get('individui/{individuo}/collegati', [IndividuoController::class, 'elementiCollegati'])->middleware('auth');
Route::post('individui/{individuo}/elimina', [IndividuoController::class, 'elimina'])->middleware('auth'); Route::post('individui/{individuo}/elimina', [IndividuoController::class, 'elimina'])->middleware('auth');
Route::post('individui/mass-elimina', [IndividuoController::class, 'massElimina'])->middleware('auth'); Route::post('individui/mass-elimina', [IndividuoController::class, 'massElimina'])->middleware('auth');
Route::post('individui/mass-gruppo', [IndividuoController::class, 'massGruppo'])->middleware('auth');
Route::post('individui/mass-tag', [IndividuoController::class, 'massTag'])->middleware('auth');
Route::post('individui/{individuo}/avatar', [IndividuoController::class, 'uploadAvatar'])->middleware('auth'); Route::post('individui/{individuo}/avatar', [IndividuoController::class, 'uploadAvatar'])->middleware('auth');
Route::get('individui/{individuo}/avatar', [IndividuoController::class, 'getAvatar'])->middleware('auth'); Route::get('individui/{individuo}/avatar', [IndividuoController::class, 'getAvatar'])->middleware('auth');
Route::delete('individui/{individuo}/avatar', [IndividuoController::class, 'deleteAvatar'])->middleware('auth'); Route::delete('individui/{individuo}/avatar', [IndividuoController::class, 'deleteAvatar'])->middleware('auth');
@@ -97,6 +100,8 @@ Route::post('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarControl
Route::get('gruppi/{gruppo}/avatar-file', [\App\Http\Controllers\GruppoAvatarController::class, 'show'])->middleware('auth'); Route::get('gruppi/{gruppo}/avatar-file', [\App\Http\Controllers\GruppoAvatarController::class, 'show'])->middleware('auth');
Route::delete('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarController::class, 'destroy'])->middleware('auth'); Route::delete('gruppi/{gruppo}/avatar', [\App\Http\Controllers\GruppoAvatarController::class, 'destroy'])->middleware('auth');
Route::post('gruppi/mass-tag', [GruppoController::class, 'massTag'])->middleware('auth');
Route::post('gruppi/mass-elimina', [GruppoController::class, 'massElimina'])->middleware('auth');
Route::resource('gruppi', GruppoController::class)->middleware('auth'); Route::resource('gruppi', GruppoController::class)->middleware('auth');
Route::get('eventi/calendar', [EventoController::class, 'calendar'])->name('eventi.calendar')->middleware('auth'); Route::get('eventi/calendar', [EventoController::class, 'calendar'])->name('eventi.calendar')->middleware('auth');
Route::get('eventi/calendar/events', [EventoController::class, 'calendarEvents'])->name('eventi.calendar.events')->middleware('auth'); Route::get('eventi/calendar/events', [EventoController::class, 'calendarEvents'])->name('eventi.calendar.events')->middleware('auth');
@@ -133,6 +138,7 @@ Route::post('documenti/mass-update', [DocumentoController::class, 'massUpdate'])
Route::post('documenti/mass-associate', [DocumentoController::class, 'massAssociate'])->middleware('auth'); Route::post('documenti/mass-associate', [DocumentoController::class, 'massAssociate'])->middleware('auth');
Route::post('documenti/mass-move', [DocumentoController::class, 'massMove'])->middleware('auth'); Route::post('documenti/mass-move', [DocumentoController::class, 'massMove'])->middleware('auth');
Route::post('documenti/mass-download', [DocumentoController::class, 'massDownload'])->middleware('auth'); Route::post('documenti/mass-download', [DocumentoController::class, 'massDownload'])->middleware('auth');
Route::post('documenti/mass-tag', [DocumentoController::class, 'massTag'])->middleware('auth');
Route::post('documenti/check-links', [DocumentoController::class, 'checkMassLinks'])->middleware('auth'); Route::post('documenti/check-links', [DocumentoController::class, 'checkMassLinks'])->middleware('auth');
Route::get('documenti/options', [DocumentoController::class, 'options'])->middleware('auth'); Route::get('documenti/options', [DocumentoController::class, 'options'])->middleware('auth');
@@ -186,6 +192,11 @@ Route::post('/impostazioni/logo/remove', [ImpostazioniController::class, 'remove
Route::post('/impostazioni/app-settings', [ImpostazioniController::class, 'saveAppSettings'])->middleware('auth')->name('impostazioni.app-settings.save'); Route::post('/impostazioni/app-settings', [ImpostazioniController::class, 'saveAppSettings'])->middleware('auth')->name('impostazioni.app-settings.save');
Route::post('/impostazioni/migra-percorso', [ImpostazioniController::class, 'migrateStoragePath'])->middleware('auth')->name('impostazioni.migrate-percorso'); Route::post('/impostazioni/migra-percorso', [ImpostazioniController::class, 'migrateStoragePath'])->middleware('auth')->name('impostazioni.migrate-percorso');
Route::post('/impostazioni/tag', [ImpostazioniController::class, 'tagStore'])->middleware('auth')->name('impostazioni.tag.store');
Route::put('/impostazioni/tag/{id}', [ImpostazioniController::class, 'tagUpdate'])->middleware('auth')->name('impostazioni.tag.update');
Route::delete('/impostazioni/tag/{id}', [ImpostazioniController::class, 'tagDestroy'])->middleware('auth')->name('impostazioni.tag.destroy');
Route::post('/impostazioni/tag/reorder', [ImpostazioniController::class, 'tagReorder'])->middleware('auth')->name('impostazioni.tag.reorder');
Route::get('/impostazioni/email', [EmailSettingsController::class, 'index'])->middleware('auth')->name('impostazioni.email.index'); Route::get('/impostazioni/email', [EmailSettingsController::class, 'index'])->middleware('auth')->name('impostazioni.email.index');
Route::post('/impostazioni/email', [EmailSettingsController::class, 'save'])->middleware('auth')->name('impostazioni.email.save'); Route::post('/impostazioni/email', [EmailSettingsController::class, 'save'])->middleware('auth')->name('impostazioni.email.save');
Route::post('/impostazioni/email/test', [EmailSettingsController::class, 'testConnection'])->middleware('auth')->name('impostazioni.email.test'); Route::post('/impostazioni/email/test', [EmailSettingsController::class, 'testConnection'])->middleware('auth')->name('impostazioni.email.test');
@@ -206,6 +217,9 @@ Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\Calendari
Route::post('/calendario-connessioni/sync-all', [App\Http\Controllers\CalendarioConnessioneController::class, 'syncAll'])->middleware('auth')->name('calendario-connessioni.sync-all'); Route::post('/calendario-connessioni/sync-all', [App\Http\Controllers\CalendarioConnessioneController::class, 'syncAll'])->middleware('auth')->name('calendario-connessioni.sync-all');
Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder'); Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder');
// Ricerca per Tag
Route::get('/ricerca', [RicercaController::class, 'index'])->middleware('auth')->name('ricerca.index');
// Storage Repositories (Opzione B) // Storage Repositories (Opzione B)
Route::post('/storage-repositories', [StorageRepositoryController::class, 'store'])->middleware('auth')->name('storage-repositories.store'); Route::post('/storage-repositories', [StorageRepositoryController::class, 'store'])->middleware('auth')->name('storage-repositories.store');
Route::put('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'update'])->middleware('auth')->name('storage-repositories.update'); Route::put('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'update'])->middleware('auth')->name('storage-repositories.update');