Compare commits

9 Commits

Author SHA1 Message Date
inext c959f72c66 Merge branch 'dev'
da dev version 2
2026-06-09 11:46:09 +02:00
inext 7999c663fe ver 2.0.1 2026-06-09 11:44:40 +02:00
inext 708cadfac2 Merge branch 'dev'
-
2026-06-09 11:27:06 +02:00
inext a35b0056e1 finale 2.0.0.0 2026-06-09 11:26:32 +02:00
inext b7842bf2e0 finale 2.0.0 2026-06-09 11:22:56 +02:00
inext 06387568cb finale 2.0.0 2026-06-09 11:09:00 +02:00
inext d82b123bfd versione 2.0.0 2026-06-09 11:02:01 +02:00
inext 6f106e3130 vers 1.2.10 2026-06-08 21:04:06 +02:00
inext ce48481bc8 fix 1.2.9 2026-06-08 21:02:12 +02:00
67 changed files with 2330 additions and 172 deletions
+155 -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
- **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear
## Tag System
### Implementazione Completa (Opzione B)
- **Migration**: `2026_06_08_194155_create_tags_tables.php` — tabelle `tags` + `taggables`
- **Model `Tag.php`**: morphToMany a 5 entity types, auto-slug su creating, color badge
- **Trait `HasTagsLight.php`**: `tags()` morphToMany, scopes `withAllTags`/`withAnyTags`
- **5 Models aggiornati**: Individuo, Gruppo, Evento, Documento, MailingList — usano `HasTagsLight`
- **ImpostazioniController**: CRUD completo per Tag (store, update, destroy, reorder) con tab nella pagina Impostazioni
- **Tag selector**: `_tag-selector.blade.php` — search + pill badges con colori, incluso in create/edit di Individui, Gruppi, Eventi, MailingList
- **Tag filtering**: `?tag[]=slug` supportato in `IndividuoController@index`, `GruppoController@index`, `EventoController@index`, `DocumentoController@index`, `MailingListController@index` via `withAnyTags` scope
- **Tag column in index views**: Colonna "Tag" con badge colorati cliccabili (collegamento a index filtrato) in individui, gruppi, eventi, documenti, mailing-liste
- **Tag filter widget**: `_tag-filter-bar.blade.php` — barra cliccabile con badge colorati sopra ogni index table (individui, gruppi, eventi, documenti, mailing-liste). Attiva/disattiva filtro `?tag[]=` con un click, evidenzia tag attivi, include "Cancella filtri" quando attivo
- **RicercaController**: Controller per ricerca unificata per tag — mostra risultati aggregati da tutte 5 entity types con info-box counters
- **Route `GET /ricerca`**: Registrata come `ricerca.index``RicercaController@index`
- **Sidebar link**: "Ricerca per Tag" (icona `fas fa-tags`) sotto la sezione Report, visibile con permesso `individui`
- **Mass tag action for Documenti**: `POST /documenti/mass-tag``DocumentoController@massTag` — modal with tag selector + assign/remove radio, processes selected documents in chunks of 100 via `syncWithoutDetaching()` (assign) or `detach()` (remove)
- **Report tag columns**: `tags.nome` disponibile come colonna per individui/gruppi/eventi/documenti nei report personalizzati
- **Report tag filters**: Custom report form include tag multi-select (Select2) + modalità OR/AND; `runCustomReport()` applica filtri via `withAllTags`/`withAnyTags`
## Bug Fix Recenti
### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo
@@ -158,5 +177,140 @@ php artisan config:clear
1. **`app/Models/AppSetting.php`**: Sostituito `file_exists(public_path('storage/' . $path))` con `Storage::disk('public')->exists($path)` e `asset('storage/' . $path)` con `Storage::disk('public')->url($path)`. Il controllo ora avviene direttamente su `storage/app/public/` (disk root), bypassando il symlink.
2. **`app/Http/Controllers/ImpostazioniController.php`**: In `uploadLogo()`, aggiunta creazione automatica del symlink `public/storage → storage/app/public` se mancante, per garantire che il browser possa servire l'immagine.
## 2026-06-08 — Guardie Schema::hasColumn() aggiunte a 18 migration
**Problema**: 18 migration files aggiungevano colonne a tabelle esistenti senza `Schema::hasColumn()` guard. Se eseguite su DB dove le colonne già esistevano (es. da install.sql), causavano errore "Duplicate column".
**Fix**: Aggiunto `if (!Schema::hasColumn(...))` wrapper a ogni colonna aggiunta nelle seguenti migration:
1. `2026_06_02_000002_add_uid_esterno_to_eventi_table.php``uid_esterno`
2. `2026_05_12_000002_change_ruolo_to_multi.php``ruolo_ids` (up), `ruolo_id` (down)
3. `2026_05_28_000001_add_storage_disk_to_documenti.php``storage_disk`
4. `2026_05_27_113246_add_repository_id_to_documenti.php``repository_id`
5. `2026_05_26_000003_add_log_falliti_to_mailing_messaggi.php``log_falliti`, `mittente_nome`, `mittente_email`
6. `2026_05_27_000002_add_cartella_id_to_documenti.php``cartella_id`
7. `2026_05_16_000004_add_signature_enabled_to_email_settings.php``signature_enabled`
8. `2026_05_16_000003_add_signature_to_email_settings.php``signature`
9. `2024_01_01_000017_add_occorrenza_mese_to_eventi.php``occorrenza_mese`
10. `2024_01_01_000016_add_eventi_recurrence_fields.php``giorno_mese`, `mesi_recorrenza`, `mese_annuale`
11. `2026_05_10_000002_add_acl_tables.php``permissions` (users)
12. `2026_05_12_142009_add_location_to_eventi_table.php``luogo_indirizzo`, `luogo_url_maps`
13. `2026_05_12_000003_change_responsabile_to_multi.php``responsabile_ids` (up), `responsabile_id` (down)
14. `2024_01_01_000023_add_is_default_to_viste_report_table.php``is_default`
15. `2024_01_01_000024_add_user_id_to_mailing_lists_table.php``user_id`
16. `2026_05_12_000001_create_ruoli_table.php``ruolo_id` (gruppo_individuo) + data migration annessa
17. `2024_01_01_000021_add_user_id_to_documenti_table.php``user_id`
18. `2024_01_01_000013_add_numero_documento_to_individui_table.php``numero_documento`
Tutte verificate con `php -l` (nessun errore di sintassi).
### 2026-06-09 — `@stack('scripts')` mancante nel layout
**Problema**: Il partial `_tag-selector.blade.php` usa `@push('scripts')` per iniettare le funzioni JS `toggleTag`, `removeTag`, `filterTags`. Il layout `adminlte.blade.php` aveva solo `@yield('scripts')` (per `@section`), ma **non** `@stack('scripts')` (per `@push`). Di conseguenza il JavaScript interattivo del tag selector (clic sui badge, ricerca, rimozione) non veniva mai caricato su nessuna pagina create/edit.
**Fix**: `resources/views/layouts/adminlte.blade.php:305` — Aggiunto `@stack('scripts')` subito prima di `@yield('scripts')`.
### 2026-06-09 — Tag support completato per MailingList + Report + Ricerca
**MailingList — Controller & Views** (`app/Http/Controllers/MailingListController.php`):
- `index()`: eager-load `tags`, supporto `?tag[]=` filter via `withAnyTags`, pass `$allTags` per filter bar
- `create()`: pass `$tags` per tag selector
- `store()`: validazione `tags.* exists:tags,id`, sync dopo create. Auto-fetch email da `Individuo::contatti()` quando email non fornita
- `edit()`: eager-load `tags`, pass `$tags` + `$selectedTags`
- `update()`: validazione `tags.*`, sync se presente, detach se assente. Auto-fetch email da `Individuo::contatti()` quando email non fornita. Diff logica: `$toRemove = array_diff($existingIds, $newIds)` corretto — rimuove solo contatti deselezionati/esplicitamente rimossi
- `show()`: eager-load `tags`
**MailingList — Views**:
- `index.blade.php`: aggiunto `@include('partials._tag-filter-bar')`, colonna "Tag" con badge colorati cliccabili
- `create.blade.php`: incluso `_tag-selector` dopo descrizione
- `edit.blade.php`: incluso `_tag-selector` dopo descrizione (prima della sezione contatti). Submit handler include `#individui-select.selectedOptions` in `contatti_json` con `email: ''` (già presente da fix precedente)
- `show.blade.php`: aggiunta card Tag nel pannello sinistro con badge colorati (o "Nessun tag assegnato")
**ReportColumnRegistry** (`app/Helpers/ReportColumnRegistry.php`):
- Aggiunta colonna `'tags.nome'` con label 'Tag' a 4 entity types: individui, gruppi, eventi, documenti
**ReportController** (`app/Http/Controllers/ReportController.php`):
- `runCustomReport()`: supporta `config['tag_filter']` (array di tag IDs) + `config['tag_filter_mode']` ('any'/'all')
- `storeCustom()`: ora salva `tag_filter` e `tag_filter_mode` nella config del report salvato
- Carica `$allTags` nell'index e passa alla vista
- Converte IDs → slugs via `Tag::whereIn('id', ...)->pluck('slug')`, applica `withAllTags`/`withAnyTags`
- Eager-load `tags` automaticamente quando la colonna `tags.nome` è selezionata
**Report — View** (`resources/views/report/index.blade.php`):
- Aggiunto filtro tag multi-select nel form "Crea Report Personalizzato" (Select2 con badge colorati)
- Aggiunto select per modalità filtro (OR/AND)
- JS: `initTagFilterSelect2()` / `destroyTagFilterSelect2()` con template colorato
**RicercaController** (`app/Http/Controllers/RicercaController.php`):
- Aggiunta MailingList alle query per tag slug
- `results['mailingLists']` e `totals['mailingLists']` passati alla view
**Ricerca — View** (`resources/views/ricerca/index.blade.php`):
- Aggiunto info-box Mailing List (colore bg-purple) con link "Vedi tutti" → `mailing-liste.index?tag[]=`
- Aggiunta tabella dettaglio Mailing List nel risultato di ricerca (nome, contatti, stato, azione show)
- Fix: `route('documenti.edit')``url('/documenti/' . $doc->id . '/edit')` (route non esiste)
### 2026-06-09 — Fix: storeCustom() non salvava tag_filter
**Problema**: `ReportController@storeCustom()` non salvava `tag_filter` e `tag_filter_mode` nella config del report personalizzato. Quando un report salvato veniva eseguito, `runCustomReport()` non trovava i filtri tag nella config e non li applicava.
**Fix**: Aggiunte righe 143-148 in `ReportController.php`:
```php
if ($request->has('tag_filter')) {
$config['tag_filter'] = $request->input('tag_filter');
}
if ($request->filled('tag_filter_mode')) {
$config['tag_filter_mode'] = $request->input('tag_filter_mode');
}
```
### 2026-06-09 — Fix: MailingList edit — "Carica contatti" non inseriva i selezionati
**Problema**: Due bug nel JS `caricaSelezionati()` in `mailing-liste/edit.blade.php`:
1. **Typo**: `ind.cogname` invece di `ind.cognome` — il campo Cognome nella tabella era sempre vuoto/undefined (API restituisce `cognome`)
2. **Email gate**: `if (ind.emails && ind.emails.length > 0)` filtro bloccante — individui senza email salvata in `contatti` venivano SILENZIOSAMENTE scartati, mai aggiunti alla tabella
**Fix**:
1. `ind.cogname``ind.cognome` (righe 227 e 256)
2. Rimossa condizione `if (ind.emails...)` — ora anche individui senza email vengono aggiunti alla tabella
3. `email: ind.emails[0]``email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : ''` — email vuota se nessuna presente
4. Controller già fixato per auto-risolvere email da `Individuo::contatti()` al salvataggio
### 2026-06-09 — Fix: MailingListController email gate bloccava nuovi contatti senza email
**Problema**: `store()` e `update()` in `MailingListController` usavano `!empty($contatto['email'])` per decidere se creare un contatto. L'email era richiesta ma il MailingContact non memorizza l'email — la risolve dall'Individuo al momento del display/invio. Questo impediva di aggiungere individui senza email (o con email non nei contatti) alla mailing list.
**Fix**: Rimossa completamente la verifica email sia in `store()` che in `update()`. Ora ogni `individuo_id` valido viene aggiunto come MailingContact. L'email viene risolta al display dal model `$contact->individuo->contatti->where('tipo', 'email')->first()?->valore`.
Prima (bloccante):
```php
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) { create }
```
Dopo (semplice):
```php
if (!empty($contatto['individuo_id'])) { create }
```
## Prossimi Passi
- *(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.cellulare' => ['label' => 'Cellulare (da contatti)', 'type' => 'relation'],
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
],
'gruppi' => [
@@ -151,6 +152,7 @@ class ReportColumnRegistry
'parent.nome' => ['label' => 'Gruppo Padre', 'type' => 'relation'],
'diocesi.nome' => ['label' => 'Diocesi', 'type' => 'relation'],
'individui_count' => ['label' => 'Numero Membri', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
],
'eventi' => [
@@ -171,6 +173,7 @@ class ReportColumnRegistry
'info_ricorrenza' => ['label' => 'Info Ricorrenza', 'type' => 'accessor'],
'gruppi.nome' => ['label' => 'Gruppi', 'type' => 'relation'],
'responsabili.nome_completo' => ['label' => 'Responsabili', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
],
'documenti' => [
@@ -183,6 +186,7 @@ class ReportColumnRegistry
'note' => ['label' => 'Note', 'type' => 'direct'],
'created_at' => ['label' => 'Data Caricamento', 'type' => 'direct'],
'user.name' => ['label' => 'Caricato Da', 'type' => 'relation'],
'tags.nome' => ['label' => 'Tag', 'type' => 'relation'],
],
'contatti' => [
+56 -4
View File
@@ -11,6 +11,7 @@ use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\StorageRepository;
use App\Models\Tag;
use App\Models\TipologiaDocumento;
use App\Services\StorageRepositoryService;
use Illuminate\Http\JsonResponse;
@@ -42,9 +43,14 @@ class DocumentoController extends Controller
$folderId = $request->get('folder_id');
$repoId = $request->get('repository_id');
$query = Documento::with(['user', 'target', 'cartella', 'repository'])
$query = Documento::with(['user', 'target', 'cartella', 'repository', 'tags'])
->orderByDesc('created_at');
if ($request->filled('tag')) {
$tagSlugs = (array) $request->input('tag');
$query->withAnyTags($tagSlugs);
}
if ($repoId) {
$query->where('repository_id', $repoId);
} elseif ($folderId) {
@@ -54,6 +60,7 @@ class DocumentoController extends Controller
}
$documenti = $query->paginate(20);
$allTags = Tag::orderBy('name')->get();
$cartelle = DocumentoCartella::with('children')
->whereNull('parent_id')
->orderBy('nome')
@@ -89,7 +96,7 @@ class DocumentoController extends Controller
'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle',
'currentRepo', 'repositories',
'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie',
'cartelleMoveOptions'
'cartelleMoveOptions', 'allTags'
));
}
@@ -106,13 +113,15 @@ class DocumentoController extends Controller
public function edit($documento)
{
$this->authorizeWrite('documenti');
$documento = Documento::with('target')->findOrFail($documento);
$documento = Documento::with(['target', 'tags'])->findOrFail($documento);
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get();
$eventi = Evento::orderBy('nome_evento')->get();
$mailingLists = \App\Models\MailingList::orderBy('nome')->get();
$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)
@@ -165,6 +174,8 @@ class DocumentoController extends Controller
'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string',
'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
@@ -185,6 +196,12 @@ class DocumentoController extends Controller
$documento->update($data);
if ($request->has('tags')) {
$documento->tags()->sync($request->tags);
} else {
$documento->tags()->detach();
}
return redirect('/documenti')->with('success', 'Documento aggiornato.');
}
@@ -706,6 +723,41 @@ class DocumentoController extends Controller
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
{
$this->authorizeWrite('documenti');
+1 -1
View File
@@ -341,7 +341,7 @@ class EmailController extends Controller
$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)
+25 -6
View File
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Evento;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Tag;
use App\Models\TipologiaEvento;
use App\Services\IcsExportService;
use Illuminate\Http\Request;
@@ -16,7 +17,12 @@ class EventoController extends Controller
public function index(Request $request)
{
$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')) {
$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;
$eventi = $query->paginate($perPage);
$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)
@@ -58,8 +65,9 @@ class EventoController extends Controller
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$selectedGruppo = $request->query('gruppo_id') ? Gruppo::find($request->query('gruppo_id')) : null;
$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)
@@ -88,6 +96,8 @@ class EventoController extends Controller
'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$evento = Evento::create([
@@ -110,6 +120,10 @@ class EventoController extends Controller
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]);
if (!empty($data['tags'])) {
$evento->tags()->attach($data['tags']);
}
if (!empty($data['gruppi'])) {
$evento->gruppi()->attach($data['gruppi']);
}
@@ -124,19 +138,21 @@ class EventoController extends Controller
public function show($evento)
{
$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'));
}
public function edit($evento)
{
$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();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$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)
@@ -167,6 +183,8 @@ class EventoController extends Controller
'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$evento->update([
@@ -189,6 +207,7 @@ class EventoController extends Controller
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]);
$evento->tags()->sync($data['tags'] ?? []);
$evento->gruppi()->sync($data['gruppi'] ?? []);
$evento->responsabili()->sync($data['responsabili'] ?? []);
+91 -10
View File
@@ -7,6 +7,7 @@ namespace App\Http\Controllers;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Diocesi;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
@@ -27,8 +28,8 @@ class GruppoController extends Controller
->where('is_default', true)
->first();
$allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email'];
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili'];
$allColumns = ['nome', 'descrizione', 'diocesi', 'livello', 'padre', 'membri', 'responsabili', 'indirizzo', 'citta', 'telefono', 'email', 'tag'];
$defaultVisible = ['nome', 'livello', 'padre', 'membri', 'responsabili', 'tag'];
if ($vista && !empty($vista->colonne_visibili)) {
$visibleColumns = $vista->colonne_visibili;
@@ -36,9 +37,14 @@ class GruppoController extends Controller
$visibleColumns = $defaultVisible;
}
$allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar'])
->orderBy('nome')
->get()
$gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']);
if (request()->filled('tag')) {
$tagSlugs = (array) request()->input('tag');
$gruppiQuery->withAnyTags($tagSlugs);
}
$allGruppi = $gruppiQuery->orderBy('nome')->get()
->map(function ($gruppo) {
$depth = 0;
$parent = $gruppo->parent;
@@ -65,12 +71,15 @@ class GruppoController extends Controller
$gruppi = $allGruppi;
}
$allTags = Tag::orderBy('name')->get();
return view('gruppi.index', compact(
'gruppi',
'allColumns',
'visibleColumns',
'viewMode',
'vista'
'vista',
'allTags'
));
}
@@ -104,7 +113,8 @@ class GruppoController extends Controller
$allGruppi = Gruppo::orderBy('nome')->get();
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
$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)
@@ -122,6 +132,8 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
if (!empty($data['responsabile_ids'])) {
@@ -130,6 +142,10 @@ class GruppoController extends Controller
$gruppo = Gruppo::create($data);
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
}
if ($request->has('individui')) {
foreach ($request->individui as $individuoData) {
if (!empty($individuoData['individuo_id'])) {
@@ -149,7 +165,7 @@ class GruppoController extends Controller
$this->authorizeRead('gruppi');
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar', 'eventi' => function ($q) {
}, 'avatar', 'tags', 'eventi' => function ($q) {
$q->where('is_incontro_gruppo', true);
}, 'eventi.responsabili.contatti'])->findOrFail($id);
return view('gruppi.show', compact('gruppo'));
@@ -158,11 +174,13 @@ class GruppoController extends Controller
public function edit($id)
{
$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();
$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();
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)
@@ -181,6 +199,8 @@ class GruppoController extends Controller
'città_incontro' => 'nullable|string|max:255',
'sigla_provincia_incontro' => 'nullable|string|max:2',
'mappa_posizione' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
if (!empty($data['responsabile_ids'])) {
@@ -191,6 +211,12 @@ class GruppoController extends Controller
$gruppo->update($data);
if ($request->has('tags')) {
$gruppo->tags()->sync($request->tags);
} else {
$gruppo->tags()->detach();
}
if ($request->has('individui')) {
$gruppo->individui()->detach();
foreach ($request->individui as $individuoData) {
@@ -410,4 +436,59 @@ class GruppoController extends Controller
'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\SenderAccount;
use App\Models\StorageRepository;
use App\Models\Tag;
use App\Models\TipologiaDocumento;
use App\Models\TipologiaEvento;
use Illuminate\Http\RedirectResponse;
@@ -26,6 +27,7 @@ class ImpostazioniController extends Controller
$tipologie = TipologiaDocumento::orderBy('ordine')->get();
$tipologieEventi = TipologiaEvento::orderBy('ordine')->get();
$ruoli = Ruolo::orderBy('ordine')->get();
$tags = Tag::orderBy('order_column')->get();
$appSettings = AppSetting::first();
$emailSettings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get();
@@ -33,7 +35,7 @@ class ImpostazioniController extends Controller
$googleDriveNewToken = session('google_drive_new_token');
$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)
@@ -371,6 +373,70 @@ class ImpostazioniController extends Controller
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)
{
$this->authorizeWrite('settings');
+106 -7
View File
@@ -5,8 +5,10 @@ namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Models\Contatto;
use App\Models\Documento;
use App\Models\Gruppo;
use App\Models\Comune;
use App\Models\Diocesi;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
@@ -18,7 +20,16 @@ class IndividuoController extends Controller
{
$this->authorizeRead('individui');
$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;
@@ -41,14 +52,16 @@ class IndividuoController extends Controller
['key' => 'nome', 'label' => 'Nome'],
['key' => 'email', 'label' => 'Email'],
['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)
? $vista->colonne_visibili
: $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)
@@ -110,7 +123,8 @@ public function create()
$this->authorizeWrite('individui');
$comuni = Comune::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)
@@ -129,10 +143,16 @@ public function create()
'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$individuo = Individuo::create($data);
if ($request->has('tags')) {
$individuo->tags()->sync($request->tags);
}
if ($request->has('contatti')) {
foreach ($request->contatti as $contatto) {
if (!empty($contatto['tipo']) && !empty($contatto['valore'])) {
@@ -154,7 +174,7 @@ public function create()
$this->authorizeRead('individui');
$individuo = Individuo::with(['contatti', 'gruppi' => function ($q) {
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
}, 'avatar'])->findOrFail($individuo);
}, 'avatar', 'tags'])->findOrFail($individuo);
return view('individui.show', compact('individuo'));
}
@@ -180,8 +200,10 @@ public function create()
public function edit($individuo)
{
$this->authorizeWrite('individui');
$individuo = Individuo::with(['contatti', 'gruppi', 'avatar'])->findOrFail($individuo);
return view('individui.edit', compact('individuo'));
$individuo = Individuo::with(['contatti', 'gruppi', 'avatar', 'tags'])->findOrFail($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)
@@ -205,9 +227,17 @@ public function create()
'numero_documento' => 'nullable|string|max:50',
'scadenza_documento' => 'nullable|date',
'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$individuo->update($data);
if ($request->has('tags')) {
$individuo->tags()->sync($request->tags);
} else {
$individuo->tags()->detach();
}
\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.');
}
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)
{
$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();
$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()) {
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
}
@@ -229,9 +237,17 @@ class MailingController extends Controller
->with('individuo.contatti')
->get();
$totaleContatti = $contatti->count();
$emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore)
->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()) {
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
}
+35 -11
View File
@@ -3,23 +3,31 @@
namespace App\Http\Controllers;
use App\Models\MailingList;
use App\Models\Tag;
use Illuminate\Http\Request;
class MailingListController extends Controller
{
public function index()
public function index(Request $request)
{
$this->authorizeRead('mailing');
$mailingLists = MailingList::with(['user', 'contatti'])
->orderBy('nome')
->get();
return view('mailing-liste.index', compact('mailingLists'));
$query = MailingList::with(['user', 'contatti', 'tags']);
if ($request->filled('tag')) {
$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()
{
$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)
@@ -30,6 +38,8 @@ class MailingListController extends Controller
'descrizione' => 'nullable|string',
'attiva' => 'boolean',
'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$data['user_id'] = auth()->id();
@@ -42,11 +52,15 @@ class MailingListController extends Controller
'user_id' => $data['user_id'],
]);
if ($request->has('tags')) {
$lista->tags()->sync($request->tags);
}
if (!empty($data['contatti_json'])) {
$contatti = json_decode($data['contatti_json'], true);
if (is_array($contatti)) {
foreach ($contatti as $contatto) {
if (!empty($contatto['individuo_id']) && !empty($contatto['email'])) {
if (!empty($contatto['individuo_id'])) {
$lista->contatti()->create([
'individuo_id' => $contatto['individuo_id'],
'opt_in' => true,
@@ -63,15 +77,17 @@ class MailingListController extends Controller
public function show($mailing_liste)
{
$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'));
}
public function edit($mailingList)
{
$this->authorizeWrite('mailing');
$mailingList = MailingList::with('contatti.individuo.contatti')->findOrFail($mailingList);
return view('mailing-liste.edit', compact('mailingList'));
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($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)
@@ -84,6 +100,8 @@ class MailingListController extends Controller
'descrizione' => 'nullable|string',
'attiva' => 'boolean',
'contatti_json' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$data['attiva'] = $data['attiva'] ?? false;
@@ -94,6 +112,12 @@ class MailingListController extends Controller
'attiva' => $data['attiva'],
]);
if ($request->has('tags')) {
$mailingList->tags()->sync($request->tags);
} else {
$mailingList->tags()->detach();
}
if (!empty($data['contatti_json'])) {
$newContatti = json_decode($data['contatti_json'], true);
if (is_array($newContatti)) {
@@ -107,7 +131,7 @@ class MailingListController extends Controller
$toAdd = array_diff($newIds, $existingIds);
foreach ($newContatti as $contatto) {
if (in_array($contatto['individuo_id'], $toAdd) && !empty($contatto['email'])) {
if (in_array($contatto['individuo_id'], $toAdd)) {
$mailingList->contatti()->create([
'individuo_id' => $contatto['individuo_id'],
'opt_in' => true,
+158 -2
View File
@@ -11,6 +11,7 @@ use App\Models\Evento;
use App\Models\Documento;
use App\Models\MailingList;
use App\Models\Contatto;
use App\Models\Tag;
use App\Models\TipologiaDocumento;
use App\Models\ReportCustom;
use Illuminate\Http\Request;
@@ -47,12 +48,15 @@ class ReportController extends Controller
$columnOptions[$type] = ReportColumnRegistry::getColumns($type);
}
$allTags = Tag::orderBy('name')->get();
return view('report.index', compact(
'stats',
'prebuiltReports',
'customReports',
'tipologieDocumento',
'columnOptions'
'columnOptions',
'allTags'
));
}
@@ -136,6 +140,14 @@ class ReportController extends Controller
$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([
'user_id' => auth()->id(),
'nome' => $data['nome'],
@@ -252,6 +264,9 @@ class ReportController extends Controller
'documenti_completo' => $this->reportDocumentiCompleto($tipologiaFilter),
'contatti_completo' => $this->reportContattiCompleto(),
'scadenze_documenti' => $this->reportScadenzeDocumenti($tipologiaFilter),
'tag_statistiche' => $this->reportTagStatistiche(),
'individui_per_tag' => $this->reportIndividuiPerTag(),
'documenti_per_tag' => $this->reportDocumentiPerTag(),
default => null,
};
}
@@ -403,6 +418,30 @@ class ReportController extends Controller
'colore' => 'teal',
'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
{
$tipo = $report->tipo_report;
@@ -954,11 +1089,32 @@ class ReportController extends Controller
$columns = $config['columns'] ?? [];
$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) {
if (str_contains($col, '.')) {
$hasRelations = true;
$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);
}
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tag;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\Documento;
use App\Models\MailingList;
use Illuminate\Http\Request;
class RicercaController extends Controller
{
public function index(Request $request)
{
$this->authorizeRead('individui');
$allTags = Tag::orderBy('name')->get();
$tagSlugs = (array) $request->input('tag', []);
if (empty($tagSlugs)) {
return view('ricerca.index', compact('allTags'));
}
$tagSlugs = array_filter($tagSlugs);
$selectedTags = Tag::whereIn('slug', $tagSlugs)->get();
$results = [];
$totals = [];
if (count($tagSlugs) === 1) {
$tag = $selectedTags->first();
$results['individui'] = $tag->individui()->orderBy('cognome')->orderBy('nome')->get();
$results['gruppi'] = $tag->gruppi()->orderBy('nome')->get();
$results['eventi'] = $tag->eventi()->orderBy('data_specifica')->orderBy('nome_evento')->get();
$results['documenti'] = $tag->documenti()->orderBy('nome_file')->get();
$results['mailingLists'] = $tag->mailingLists()->orderBy('nome')->get();
} else {
$results['individui'] = Individuo::withAllTags($tagSlugs)->orderBy('cognome')->orderBy('nome')->get();
$results['gruppi'] = Gruppo::withAllTags($tagSlugs)->orderBy('nome')->get();
$results['eventi'] = Evento::withAllTags($tagSlugs)->orderBy('data_specifica')->orderBy('nome_evento')->get();
$results['documenti'] = Documento::withAllTags($tagSlugs)->orderBy('nome_file')->get();
$results['mailingLists'] = MailingList::withAllTags($tagSlugs)->orderBy('nome')->get();
}
$totals = [
'individui' => $results['individui']->count(),
'gruppi' => $results['gruppi']->count(),
'eventi' => $results['eventi']->count(),
'documenti' => $results['documenti']->count(),
'mailingLists' => $results['mailingLists']->count(),
];
return view('ricerca.index', compact('selectedTags', 'tagSlugs', 'results', 'totals', 'allTags'));
}
}
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
@@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Documento extends Model
{
use HasTagsLight;
protected $table = 'documenti';
protected $fillable = [
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -9,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Evento extends Model
{
use HasTagsLight;
protected $table = 'eventi';
protected $fillable = [
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Gruppo extends Model
{
use HasTagsLight;
protected $table = 'gruppi';
protected $fillable = [
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
+2
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\HasTagsLight;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
class Individuo extends Model
{
use HasTagsLight;
protected $table = 'individui';
protected $fillable = [
'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\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Traits\HasTagsLight;
class MailingList extends Model
{
use HasTagsLight;
protected $table = 'mailing_lists';
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Tag extends Model
{
protected $table = 'tags';
protected $fillable = ['name', 'slug', 'color', 'order_column'];
protected $casts = [
'order_column' => 'integer',
];
protected static function boot(): void
{
parent::boot();
static::creating(function (self $tag) {
if (empty($tag->slug)) {
$tag->slug = str()->slug($tag->name);
}
});
}
public function individui(): MorphToMany
{
return $this->morphedByMany(Individuo::class, 'taggable');
}
public function gruppi(): MorphToMany
{
return $this->morphedByMany(Gruppo::class, 'taggable');
}
public function eventi(): MorphToMany
{
return $this->morphedByMany(Evento::class, 'taggable');
}
public function documenti(): MorphToMany
{
return $this->morphedByMany(Documento::class, 'taggable');
}
public function mailingLists(): MorphToMany
{
return $this->morphedByMany(MailingList::class, 'taggable');
}
public function getCountAttribute(): int
{
return $this->individui()->count()
+ $this->gruppi()->count()
+ $this->eventi()->count()
+ $this->documenti()->count()
+ $this->mailingLists()->count();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Traits;
use App\Models\Tag;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
trait HasTagsLight
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function scopeWithAllTags($query, array $tags)
{
foreach ($tags as $tag) {
$query->whereHas('tags', fn ($q) => $q->where('slug', $tag));
}
return $query;
}
public function scopeWithAnyTags($query, array $tags)
{
$query->whereHas('tags', fn ($q) => $q->whereIn('slug', $tags));
return $query;
}
}
@@ -8,9 +8,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('individui', function (Blueprint $table) {
$table->string('numero_documento', 50)->nullable()->after('tipo_documento');
});
if (!Schema::hasColumn('individui', 'numero_documento')) {
Schema::table('individui', function (Blueprint $table) {
$table->string('numero_documento', 50)->nullable()->after('tipo_documento');
});
}
}
public function down(): void
@@ -8,11 +8,13 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->string('giorno_mese')->nullable()->after('giorno_settimana');
$table->string('mesi_recorrenza')->nullable()->after('giorno_mese');
$table->string('mese_annuale')->nullable()->after('mesi_recorrenza');
});
if (!Schema::hasColumn('eventi', 'giorno_mese')) {
Schema::table('eventi', function (Blueprint $table) {
$table->string('giorno_mese')->nullable()->after('giorno_settimana');
$table->string('mesi_recorrenza')->nullable()->after('giorno_mese');
$table->string('mese_annuale')->nullable()->after('mesi_recorrenza');
});
}
}
public function down(): void
@@ -8,9 +8,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese');
});
if (!Schema::hasColumn('eventi', 'occorrenza_mese')) {
Schema::table('eventi', function (Blueprint $table) {
$table->unsignedTinyInteger('occorrenza_mese')->nullable()->after('giorno_mese');
});
}
}
public function down(): void
@@ -8,9 +8,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
});
if (!Schema::hasColumn('documenti', 'user_id')) {
Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
});
}
}
public function down(): void
@@ -8,10 +8,12 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('viste_report', function (Blueprint $table) {
$table->boolean('is_default')->default(false)->after('nome');
$table->index('is_default');
});
if (!Schema::hasColumn('viste_report', 'is_default')) {
Schema::table('viste_report', function (Blueprint $table) {
$table->boolean('is_default')->default(false)->after('nome');
$table->index('is_default');
});
}
}
public function down(): void
@@ -8,10 +8,12 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('mailing_lists', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
$table->index('user_id');
});
if (!Schema::hasColumn('mailing_lists', 'user_id')) {
Schema::table('mailing_lists', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null')->after('tenant_id');
$table->index('user_id');
});
}
}
public function down(): void
@@ -8,9 +8,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->json('permissions')->nullable()->after('is_admin');
});
if (!Schema::hasColumn('users', 'permissions')) {
Schema::table('users', function (Blueprint $table) {
$table->json('permissions')->nullable()->after('is_admin');
});
}
Schema::create('activity_logs', function (Blueprint $table) {
$table->id();
@@ -38,25 +38,27 @@ return new class extends Migration
]);
}
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo');
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
$table->index('ruolo_id');
});
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->unsignedBigInteger('ruolo_id')->nullable()->after('ruolo_nel_gruppo');
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
$table->index('ruolo_id');
});
$ruoliMap = DB::table('ruoli')->pluck('id', 'nome')->toArray();
$ruoliMap = DB::table('ruoli')->pluck('id', 'nome')->toArray();
$pivotRecords = DB::table('gruppo_individuo')
->whereNotNull('ruolo_nel_gruppo')
->where('ruolo_nel_gruppo', '!=', '')
->distinct()
->pluck('ruolo_nel_gruppo');
$pivotRecords = DB::table('gruppo_individuo')
->whereNotNull('ruolo_nel_gruppo')
->where('ruolo_nel_gruppo', '!=', '')
->distinct()
->pluck('ruolo_nel_gruppo');
foreach ($pivotRecords as $ruoloNome) {
if (isset($ruoliMap[$ruoloNome])) {
DB::table('gruppo_individuo')
->where('ruolo_nel_gruppo', $ruoloNome)
->update(['ruolo_id' => $ruoliMap[$ruoloNome]]);
foreach ($pivotRecords as $ruoloNome) {
if (isset($ruoliMap[$ruoloNome])) {
DB::table('gruppo_individuo')
->where('ruolo_nel_gruppo', $ruoloNome)
->update(['ruolo_id' => $ruoliMap[$ruoloNome]]);
}
}
}
}
@@ -13,9 +13,11 @@ return new class extends Migration
$table->dropColumn('ruolo_id');
});
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->json('ruolo_ids')->nullable()->after('individuo_id');
});
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) {
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->json('ruolo_ids')->nullable()->after('individuo_id');
});
}
}
public function down(): void
@@ -24,9 +26,11 @@ return new class extends Migration
$table->dropColumn('ruolo_ids');
});
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id');
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
});
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->unsignedBigInteger('ruolo_id')->nullable()->after('individuo_id');
$table->foreign('ruolo_id')->references('id')->on('ruoli')->onDelete('set null');
});
}
}
};
@@ -13,9 +13,11 @@ return new class extends Migration
$table->dropColumn('responsabile_id');
});
Schema::table('gruppi', function (Blueprint $table) {
$table->json('responsabile_ids')->nullable()->after('diocesi_id');
});
if (!Schema::hasColumn('gruppi', 'responsabile_ids')) {
Schema::table('gruppi', function (Blueprint $table) {
$table->json('responsabile_ids')->nullable()->after('diocesi_id');
});
}
}
public function down(): void
@@ -24,9 +26,11 @@ return new class extends Migration
$table->dropColumn('responsabile_ids');
});
Schema::table('gruppi', function (Blueprint $table) {
$table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id');
$table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null');
});
if (!Schema::hasColumn('gruppi', 'responsabile_id')) {
Schema::table('gruppi', function (Blueprint $table) {
$table->unsignedBigInteger('responsabile_id')->nullable()->after('diocesi_id');
$table->foreign('responsabile_id')->references('id')->on('individui')->onDelete('set null');
});
}
}
};
@@ -8,10 +8,12 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->string('luogo_indirizzo', 500)->nullable()->after('note');
$table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo');
});
if (!Schema::hasColumn('eventi', 'luogo_indirizzo')) {
Schema::table('eventi', function (Blueprint $table) {
$table->string('luogo_indirizzo', 500)->nullable()->after('note');
$table->text('luogo_url_maps')->nullable()->after('luogo_indirizzo');
});
}
}
public function down(): void
@@ -8,9 +8,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('email_settings', function (Blueprint $table) {
$table->longText('signature')->nullable()->after('reply_to');
});
if (!Schema::hasColumn('email_settings', 'signature')) {
Schema::table('email_settings', function (Blueprint $table) {
$table->longText('signature')->nullable()->after('reply_to');
});
}
}
public function down(): void
@@ -8,9 +8,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('email_settings', function (Blueprint $table) {
$table->boolean('signature_enabled')->default(true)->after('signature');
});
if (!Schema::hasColumn('email_settings', 'signature_enabled')) {
Schema::table('email_settings', function (Blueprint $table) {
$table->boolean('signature_enabled')->default(true)->after('signature');
});
}
}
public function down(): void
@@ -10,11 +10,13 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('mailing_messaggi', function (Blueprint $table) {
$table->json('log_falliti')->nullable()->after('invii_falliti');
$table->string('mittente_nome')->nullable()->after('log_falliti');
$table->string('mittente_email')->nullable()->after('mittente_nome');
});
if (!Schema::hasColumn('mailing_messaggi', 'log_falliti')) {
Schema::table('mailing_messaggi', function (Blueprint $table) {
$table->json('log_falliti')->nullable()->after('invii_falliti');
$table->string('mittente_nome')->nullable()->after('log_falliti');
$table->string('mittente_email')->nullable()->after('mittente_nome');
});
}
}
public function down(): void
@@ -8,9 +8,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null');
});
if (!Schema::hasColumn('documenti', 'cartella_id')) {
Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('cartella_id')->nullable()->constrained('documenti_cartelle')->onDelete('set null');
});
}
}
public function down(): void
@@ -8,13 +8,15 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('repository_id')
->nullable()
->constrained('storage_repositories')
->nullOnDelete()
->after('cartella_id');
});
if (!Schema::hasColumn('documenti', 'repository_id')) {
Schema::table('documenti', function (Blueprint $table) {
$table->foreignId('repository_id')
->nullable()
->constrained('storage_repositories')
->nullOnDelete()
->after('cartella_id');
});
}
}
public function down(): void
@@ -10,9 +10,11 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('documenti', function (Blueprint $table) {
$table->string('storage_disk', 50)->nullable()->after('repository_id');
});
if (!Schema::hasColumn('documenti', 'storage_disk')) {
Schema::table('documenti', function (Blueprint $table) {
$table->string('storage_disk', 50)->nullable()->after('repository_id');
});
}
}
public function down(): void
@@ -10,10 +10,12 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('eventi', function (Blueprint $table) {
$table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps');
$table->index('uid_esterno');
});
if (!Schema::hasColumn('eventi', 'uid_esterno')) {
Schema::table('eventi', function (Blueprint $table) {
$table->string('uid_esterno', 500)->nullable()->after('luogo_url_maps');
$table->index('uid_esterno');
});
}
}
public function down(): void
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_nel_gruppo')) {
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->string('ruolo_nel_gruppo')->nullable()->after('ruolo_ids');
});
}
}
public function down(): void
{
}
};
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('tags')) {
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('slug', 100)->unique();
$table->string('color', 7)->nullable();
$table->integer('order_column')->default(0);
$table->timestamps();
});
}
if (!Schema::hasTable('taggables')) {
Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
$table->primary(['tag_id', 'taggable_id', 'taggable_type']);
});
}
}
public function down(): void
{
Schema::dropIfExists('taggables');
Schema::dropIfExists('tags');
}
};
+36
View File
@@ -103,6 +103,42 @@
@endif
</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>
+67
View File
@@ -123,6 +123,8 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
</div>
</div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
<div class="card-body p-0">
{{-- Toolbar azioni massive --}}
<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')">
<i class="fas fa-folder-open mr-1"></i> Sposta selezionati in...
</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()">
<i class="fas fa-download mr-1"></i> Scarica
</button>
@@ -226,6 +231,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
</span>
@endif
</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">
@if($documento->user)
{{ $documento->user->name }}
@@ -295,6 +305,7 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
<th style="width: 100px;">Tipologia</th>
<th style="width: 90px;">Dimensione</th>
<th style="width: 120px;">Contesto</th>
<th style="width: 100px;">Tag</th>
<th style="width: 110px;">Data</th>
<th style="width: 130px;">Azioni</th>
</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>
<a href="/documenti?folder_id={{ $cartella->id }}" class="btn btn-xs btn-warning" title="Apri cartella">
<i class="fas fa-folder-open"></i>
@@ -423,6 +435,11 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
<span class="text-muted small">-</span>
@endswitch
</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>
@if($documento->file_path)
@@ -834,6 +851,50 @@ $canDeleteDocumenti = Auth::user()->canDelete('documenti');
</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 --}}
<div class="modal fade" id="moveModal" tabindex="-1" role="dialog">
<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('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() {
Swal.fire({
title: 'Nuova cartella',
+9
View File
@@ -247,6 +247,15 @@
</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">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva
+9
View File
@@ -252,6 +252,15 @@
</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">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva Modifiche
+9 -1
View File
@@ -43,6 +43,8 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
@endif
</div>
</div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
<div class="card-body">
<form method="GET" class="mb-3">
<div class="row">
@@ -144,6 +146,7 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
@endif
</a>
</th>
<th>Tag</th>
<th style="width: 120px;">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'created_at', 'direction' => request('sort') === 'created_at' && request('direction') === 'asc' ? 'desc' : 'asc']) }}" class="text-dark">
Creato
@@ -221,6 +224,11 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
<small class="text-muted">+{{ $evento->responsabili->count() - 2 }} altri</small>
@endif
</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>
<a href="{{ url('/eventi/' . $evento->id) }}" class="btn btn-xs btn-info" title="Visualizza">
@@ -241,7 +249,7 @@ $canDeleteEventi = Auth::user()->canDelete('eventi');
</tr>
@empty
<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>
<p class="mb-0">Nessun evento trovato</p>
</td>
+11
View File
@@ -202,6 +202,17 @@
</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)
<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>
+9
View File
@@ -117,6 +117,15 @@
</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">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva
+9
View File
@@ -270,6 +270,15 @@
</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">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Salva Modifiche
+147
View File
@@ -17,6 +17,7 @@ $columnLabels = [
'citta' => 'Città',
'telefono' => 'Telefono',
'email' => 'Email',
'tag' => 'Tag',
];
$vistaDefaultJson = $vista ? $vista->toJson() : 'null';
$allColumnsJson = json_encode($allColumns);
@@ -51,6 +52,22 @@ $visibleColumnsJson = json_encode($visibleColumns);
<i class="fas fa-sitemap mr-1"></i> Albero
</button>
</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()">
<i class="fas fa-save mr-1"></i> Salva Vista
</button>
@@ -65,11 +82,19 @@ $visibleColumnsJson = json_encode($visibleColumns);
</div>
</div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
@if($viewMode === 'table')
<div class="card-body p-0">
<table class="table table-bordered table-hover table-striped mb-0" id="gruppi-table">
<thead class="thead-light">
<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))
<th data-sortable="true" onclick="sortTable('nome')" style="cursor:pointer;">Nome <i class="fas fa-sort float-right text-muted"></i></th>
@endif
@@ -103,12 +128,21 @@ $visibleColumnsJson = json_encode($visibleColumns);
@if(in_array('email', $visibleColumns))
<th>Email</th>
@endif
@if(in_array('tag', $visibleColumns))
<th>Tag</th>
@endif
<th style="width: 120px;">Azioni</th>
</tr>
</thead>
<tbody id="table-body">
@foreach($gruppi as $gruppo)
<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))
<td>
<span style="padding-left: {{ ($gruppo->depth ?? 0) * 20 }}px;">
@@ -154,6 +188,13 @@ $visibleColumnsJson = json_encode($visibleColumns);
@if(in_array('email', $visibleColumns))
<td>-</td>
@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>
<a href="{{ route('gruppi.show', $gruppo->id) }}" class="btn btn-xs btn-info" title="Visualizza">
<i class="fas fa-eye"></i>
@@ -309,6 +350,68 @@ $visibleColumnsJson = json_encode($visibleColumns);
</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
@section('scripts')
@@ -317,6 +420,50 @@ function switchView(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) {
const el = document.getElementById('group-' + id);
const icon = document.getElementById('icon-' + id);
+11
View File
@@ -247,6 +247,17 @@ $canDeleteGruppi = Auth::user()->canDelete('gruppi');
</div>
@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-header">
<h3 class="card-title">
+52
View File
@@ -32,6 +32,11 @@
<i class="fas fa-calendar-alt mr-2"></i> Calendario
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-tag" data-toggle="tab">
<i class="fas fa-tags mr-2"></i> Tag e Ricerca
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
@@ -268,6 +273,49 @@
</div>
</div>
{{-- Tag e Ricerca --}}
<div class="tab-pane" id="help-tag">
<div class="card card-outline card-info">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i> Tag e Ricerca per Tag</h3>
</div>
<div class="card-body">
<h5>Cos'è il sistema di Tag</h5>
<p>I tag permettono di classificare e raggruppare trasversalmente elementi di tipo diverso: individui, gruppi, eventi, documenti e mailing list. A differenza delle tipologie (specifiche per ogni entità), i tag sono <strong>condivisi</strong> tra tutte le entità.</p>
<h5>Gestione Tag</h5>
<p>Vai in <strong>Impostazioni Tag</strong> per creare, modificare, riordinare ed eliminare i tag. Ogni tag ha:</p>
<ul>
<li><strong>Nome</strong> il nome visualizzato</li>
<li><strong>Colore</strong> colore personalizzato per il badge (es. <code>#dc3545</code> per rosso)</li>
<li><strong>Slug</strong> identificatore univoco generato automaticamente dal nome</li>
</ul>
<h5>Assegnare Tag</h5>
<p>I tag si assegnano nelle pagine di creazione e modifica di ciascuna entità individui, gruppi, eventi, documenti, mailing list tramite il selettore a pillole colorate. Clicca su un tag per selezionarlo (il badge diventa opaco), clicca di nuovo per deselezionarlo. Usa la barra di ricerca per filtrare i tag per nome.</p>
<h5>Assegnazione Massiva di Tag</h5>
<p>Puoi assegnare o rimuovere tag a più elementi contemporaneamente:</p>
<ul>
<li><strong>Individui:</strong> seleziona gli individui nella tabella, apri <strong>Azioni Assegna Tag</strong>, scegli Assegna/Rimuovi e seleziona i tag</li>
<li><strong>Gruppi:</strong> stessa procedura seleziona i gruppi, <strong>Azioni Assegna Tag</strong></li>
<li><strong>Documenti:</strong> seleziona i documenti, pulsante <strong>Tag</strong> nella toolbar</li>
</ul>
<h5>Filtrare per Tag</h5>
<p>In ogni pagina elenco (Individui, Gruppi, Eventi, Documenti, Mailing List) trovi una barra con tutti i tag. Clicca su un tag per filtrare la tabella mostrando solo gli elementi con quel tag. Clicca <strong>Cancella filtri</strong> per rimuovere il filtro.</p>
<h5>Ricerca per Tag</h5>
<p>La sezione <strong>Ricerca per Tag</strong> (sidebar Ricerca per Tag) permette di trovare elementi di qualsiasi tipo in base ai tag:</p>
<ul>
<li><strong>Tag singolo:</strong> seleziona un tag per vedere tutti gli elementi associati</li>
<li><strong>Tag multipli (AND):</strong> seleziona più tag (Ctrl+click) per trovare elementi che hanno <strong>tutti</strong> i tag selezionati utile per ricerche precise (es. "animatore" + "giovane")</li>
</ul>
<p>I risultati sono raggruppati per tipo con info-box colorati e tabelle dettagliate.</p>
</div>
</div>
</div>
{{-- Tipologie e Dati --}}
<div class="tab-pane" id="help-tipologie">
<div class="card card-outline card-success">
@@ -297,6 +345,10 @@
<li><strong>Evento</strong> visibile solo ai partecipanti di un evento</li>
<li><strong>Mailing</strong> visibile solo ai membri di una mailing list</li>
</ul>
<h5>Tag</h5>
<p>I <strong>tag</strong> affiancano le tipologie con un sistema trasversale di classificazione applicabile a individui, gruppi, eventi, documenti e mailing list. A differenza delle tipologie (specifiche per ogni entità), i tag sono condivisi tra tutte le entità e permettono ricerche incrociate.</p>
<p>Vedi la sezione <strong>Tag e Ricerca per Tag</strong> (nel menu laterale) per la gestione e l'assegnazione.</p>
</div>
</div>
</div>
+48
View File
@@ -68,6 +68,7 @@
<li><a href="#installazione">Guida all'Installazione</a></li>
<li><a href="#backup">Backup e Migrazione Server</a></li>
<li><a href="#docker">Docker</a></li>
<li><a href="#tag">Tag e Ricerca per Tag</a></li>
</ol>
</div>
@@ -273,6 +274,10 @@
<li><strong>Mailing</strong> visibile solo ai membri di una mailing list</li>
</ul>
<h3>Tag</h3>
<p>I <strong>tag</strong> affiancano le tipologie con un sistema trasversale di classificazione applicabile a individui, gruppi, eventi, documenti e mailing list. A differenza delle tipologie (specifiche per ogni entità), i tag sono condivisi tra tutte le entità e permettono ricerche incrociate.</p>
<p>Vedi la sezione <a href="#tag">Tag e Ricerca per Tag</a> per la gestione e l'assegnazione.</p>
<div class="page-break"></div>
{{-- 5. Email / SMTP --}}
@@ -324,6 +329,11 @@
<li>Invia email alla lista da <strong>Email Nuova Email</strong>, selezionando la mailing list come destinatario</li>
</ul>
<div class="callout callout-warning">
<h4>Avviso contatti senza email</h4>
<p>Se nella mailing list sono presenti individui che non hanno un indirizzo email nei contatti, il sistema mostra un avviso con l'elenco dei nominativi esclusi dall'invio. Verifica sempre la presenza di email valide prima di procedere con l'invio.</p>
</div>
<h3>Test connessione</h3>
<p>Dopo aver configurato i server, usa il pulsante <strong>Test Connessione</strong> per verificare che IMAP e SMTP funzionino correttamente.</p>
@@ -929,5 +939,43 @@ echo 'Utente creato con successo!';
</ul>
</div>
<div class="page-break"></div>
{{-- 9. Tag e Ricerca --}}
<h2 id="tag">9. Tag e Ricerca per Tag</h2>
<h3>Cos'è il sistema di Tag</h3>
<p>I tag permettono di classificare e raggruppare trasversalmente elementi di tipo diverso: individui, gruppi, eventi, documenti e mailing list. A differenza delle tipologie (specifiche per ogni entità), i tag sono <strong>condivisi</strong> tra tutte le entità.</p>
<h3>Gestione Tag</h3>
<p>Vai in <strong>Impostazioni → Tag</strong> per creare, modificare, riordinare ed eliminare i tag. Ogni tag ha:</p>
<ul>
<li><strong>Nome</strong> — il nome visualizzato</li>
<li><strong>Colore</strong> — colore personalizzato per il badge (es. <code>#dc3545</code> per rosso)</li>
<li><strong>Slug</strong> — identificatore univoco generato automaticamente dal nome</li>
</ul>
<h3>Assegnare Tag</h3>
<p>I tag si assegnano nelle pagine di creazione e modifica di ciascuna entità — individui, gruppi, eventi, documenti, mailing list — tramite il selettore a pillole colorate. Clicca su un tag per selezionarlo (il badge diventa opaco), clicca di nuovo per deselezionarlo. Usa la barra di ricerca per filtrare i tag per nome.</p>
<h3>Assegnazione Massiva di Tag</h3>
<p>Puoi assegnare o rimuovere tag a più elementi contemporaneamente:</p>
<ul>
<li><strong>Individui:</strong> seleziona gli individui nella tabella, apri <strong>Azioni → Assegna Tag</strong>, scegli Assegna/Rimuovi e seleziona i tag</li>
<li><strong>Gruppi:</strong> stessa procedura — seleziona i gruppi, <strong>Azioni → Assegna Tag</strong></li>
<li><strong>Documenti:</strong> seleziona i documenti, pulsante <strong>Tag</strong> nella toolbar</li>
</ul>
<h3>Filtrare per Tag</h3>
<p>In ogni pagina elenco (Individui, Gruppi, Eventi, Documenti, Mailing List) trovi una barra con tutti i tag. Clicca su un tag per filtrare la tabella mostrando solo gli elementi con quel tag. Clicca <strong>Cancella filtri</strong> per rimuovere il filtro.</p>
<h3>Ricerca per Tag</h3>
<p>La sezione <strong>Ricerca per Tag</strong> (sidebar → Ricerca per Tag) permette di trovare elementi di qualsiasi tipo in base ai tag:</p>
<ul>
<li><strong>Tag singolo:</strong> seleziona un tag per vedere tutti gli elementi associati</li>
<li><strong>Tag multipli (AND):</strong> seleziona più tag (Ctrl+click) per trovare elementi che hanno <strong>tutti</strong> i tag selezionati — utile per ricerche precise (es. "animatore" + "giovane")</li>
</ul>
<p>I risultati sono raggruppati per tipo con info-box colorati e tabelle dettagliate.</p>
</body>
</html>
@@ -44,6 +44,9 @@
<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
</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>
@@ -1068,6 +1071,136 @@
</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>
@@ -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) {
var target = $(e.target).attr('href');
if (target === '#ruoli') {
@@ -1626,6 +1785,9 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
if (target === '#tipologie-eventi') {
sortableTipologieEventi.refresh();
}
if (target === '#tag') {
sortableTag.refresh();
}
if (target === '#calendario') {
if (typeof sortableCalendario !== 'undefined') {
sortableCalendario.refresh();
@@ -108,6 +108,15 @@
</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-header">
<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 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-header">
<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',
'email' => 'Email',
'telefono' => 'Telefono',
'tag' => 'Tag',
];
$vistaDefaultJson = $vista ? $vista->toJson() : 'null';
$allColumnsJson = json_encode($allColumns);
@@ -47,6 +48,12 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
<a class="dropdown-item" href="#" onclick="showCreateMailingListModal(); return false;">
<i class="fas fa-list mr-2"></i>Crea Mailing List
</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>
<a class="dropdown-item text-danger" href="#" onclick="deleteSelected(); return false;">
<i class="fas fa-trash mr-2"></i>Elimina Selezionati
@@ -124,6 +131,8 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
</div>
</div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags])
<div class="card-body p-0">
@if($individui->count() > 0)
<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))
<th data-sortable="true" data-column="telefono" onclick="sortTable('telefono')">Telefono <i class="fas fa-sort float-right"></i></th>
@endif
@if(in_array('tag', $visibleColumns))
<th>Tag</th>
@endif
<th style="width: 120px;">Azioni</th>
</tr>
</thead>
@@ -181,6 +193,13 @@ $canDeleteIndividui = Auth::user()->canDelete('individui');
@if(in_array('telefono', $visibleColumns))
<td>{{ $ind->getTelefonoPrimarioAttribute() ?: '-' }}</td>
@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>
@if($canWriteIndividui)
<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>
{{-- 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
@section('scripts')
@@ -444,7 +545,7 @@ function applyColumnFilter() {
if (!col || !val) return;
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 => {
const cells = row.querySelectorAll('td');
@@ -476,7 +577,7 @@ function sortTable(column) {
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 rows = Array.from(tbody.querySelectorAll('tr'));
@@ -500,7 +601,7 @@ function sortTable(column) {
}
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 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() {
const nome = document.getElementById('ml-nome').value.trim();
if (!nome) {
+15
View File
@@ -88,6 +88,21 @@
</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)
<div class="row mt-3">
<div class="col-md-8">
@@ -199,6 +199,14 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
</a>
</li>
@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'))
<li class="nav-item">
<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/admin-lte@3.2/dist/js/adminlte.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
@stack('scripts')
@yield('scripts')
</body>
</html>
@@ -30,6 +30,9 @@
<label>Descrizione</label>
<textarea name="descrizione" class="form-control" rows="2"></textarea>
</div>
<hr>
@include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => old('tags', []), 'label' => 'Tag'])
<div class="alert alert-info">
<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.
+108 -35
View File
@@ -31,6 +31,8 @@
<textarea name="descrizione" class="form-control" rows="2">{{ $mailingList->descrizione }}</textarea>
</div>
@include('partials._tag-selector', ['allTags' => $tags ?? collect(), 'selectedTags' => $selectedTags ?? [], 'label' => 'Tag'])
<hr>
<div class="row">
@@ -64,6 +66,13 @@
</button>
</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">
<label>Contatti da includere (<span id="contatti-count">{{ $mailingList->contatti->count() }}</span>)</label>
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
@@ -185,11 +194,17 @@ document.addEventListener('DOMContentLoaded', function() {
console.error('Errore individui:', err);
});
contattiCaricati = Array.from(document.querySelectorAll('.contatto-checkbox')).map(cb => ({
individuo_id: parseInt(cb.value),
email: cb.dataset.email,
existing: true
}));
contattiCaricati = Array.from(document.querySelectorAll('.contatto-checkbox')).map(cb => {
const row = cb.closest('tr');
return {
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 || '';
@@ -217,17 +232,15 @@ function caricaSelezionati() {
})
.then(data => {
data.forEach(function(ind) {
if (ind.emails && ind.emails.length > 0) {
if (!contattiCaricati.find(c => c.individuo_id === ind.id)) {
contattiCaricati.push({
individuo_id: ind.id,
codice_id: ind.codice_id,
cognome: ind.cogname,
nome: ind.nome,
email: ind.emails[0],
origini: ['Gruppo']
});
}
if (!contattiCaricati.find(c => c.individuo_id === ind.id)) {
contattiCaricati.push({
individuo_id: ind.id,
codice_id: ind.codice_id,
cognome: ind.cognome,
nome: ind.nome,
email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : '',
origini: ['Gruppo']
});
}
});
})
@@ -246,17 +259,15 @@ function caricaSelezionati() {
})
.then(data => {
data.forEach(function(ind) {
if (ind.emails && ind.emails.length > 0) {
if (!contattiCaricati.find(c => c.individuo_id === ind.id)) {
contattiCaricati.push({
individuo_id: ind.id,
codice_id: ind.codice_id,
cognome: ind.cogname,
nome: ind.nome,
email: ind.emails[0],
origini: [' Individuo']
});
}
if (!contattiCaricati.find(c => c.individuo_id === ind.id)) {
contattiCaricati.push({
individuo_id: ind.id,
codice_id: ind.codice_id,
cognome: ind.cognome,
nome: ind.nome,
email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : '',
origini: [' Individuo']
});
}
});
})
@@ -327,25 +338,87 @@ function deselezionaTutti() {
}
document.getElementById('ml-form').addEventListener('submit', function(e) {
e.preventDefault();
const form = this;
const selected = [];
document.querySelectorAll('.contatto-checkbox:checked').forEach(function(cb) {
const row = cb.closest('tr');
selected.push({
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) {
e.preventDefault();
alert('Seleziona almeno un contatto con email');
const gruppiSelect = document.getElementById('gruppi-select');
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;
}
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'contatti_json';
input.value = JSON.stringify(selected);
this.appendChild(input);
function completaInvio() {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'contatti_json';
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>
@endsection
@@ -34,6 +34,7 @@ $canDeleteMailing = Auth::user()->canDelete('mailing');
@endif
</div>
</div>
@include('partials._tag-filter-bar', ['filterTags' => $allTags ?? collect()])
<div class="card-body p-0">
@if($mailingLists->count() > 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>Descrizione</th>
<th style="width: 100px;">Contatti</th>
<th>Tag</th>
<th style="width: 100px;">Stato</th>
<th style="width: 130px;">Creata il</th>
<th style="width: 100px;">Azioni</th>
@@ -73,6 +75,16 @@ $canDeleteMailing = Auth::user()->canDelete('mailing');
<td>
<span class="badge badge-info">{{ $lista->contatti->count() }}</span>
</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>
@if($lista->attiva)
<span class="badge badge-success">Attiva</span>
@@ -49,6 +49,23 @@
</form>
</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 class="col-md-8">
@@ -0,0 +1,34 @@
@php
$filterTags = $filterTags ?? (\App\Models\Tag::orderBy('name')->get());
$activeTags = (array) request('tag', []);
@endphp
@if($filterTags->isNotEmpty())
<div class="tag-filter-bar px-3 py-2 bg-light border-bottom" style="display:flex;flex-wrap:wrap;gap:4px;align-items:center;">
<span class="small font-weight-bold text-muted mr-1"><i class="fas fa-tags mr-1"></i>Tag:</span>
@foreach($filterTags as $tag)
@php
$isActive = in_array($tag->slug, $activeTags);
$query = request()->query();
if ($isActive) {
$newTags = array_values(array_diff($activeTags, [$tag->slug]));
if (empty($newTags)) {
unset($query['tag']);
} else {
$query['tag'] = $newTags;
}
} else {
$query['tag'] = array_merge($activeTags, [$tag->slug]);
}
$url = request()->url() . '?' . http_build_query($query);
@endphp
<a href="{{ $url }}" class="badge tag-filter-badge" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;{{ $isActive ? '' : 'opacity:0.45;' }}font-size:0.8rem;padding:4px 10px;border-radius:12px;text-decoration:none;">
{{ $tag->name }}
</a>
@endforeach
@if(!empty($activeTags))
<a href="{{ request()->url() }}" class="badge badge-light" style="font-size:0.8rem;padding:4px 10px;border-radius:12px;text-decoration:none;">
<i class="fas fa-times mr-1"></i>Cancella filtri
</a>
@endif
</div>
@endif
@@ -0,0 +1,123 @@
@php
$allTags = $allTags ?? \App\Models\Tag::orderBy('name')->get();
$selectedTags = old('tags', $selectedTags ?? []);
$tagFieldName = $tagFieldName ?? 'tags';
$selectorId = 'tag-selector-' . md5($tagFieldName);
$selectedTagObjects = $allTags->whereIn('id', $selectedTags);
@endphp
<div class="form-group" id="{{ $selectorId }}">
<label>{{ $label ?? 'Tag' }}</label>
<div class="tag-selector-container" style="border:1px solid #ced4da;border-radius:.25rem;padding:8px;background:#fff;">
<div class="tag-selected d-flex flex-wrap mb-1" style="gap:4px;">
@foreach($selectedTagObjects as $tag)
<span class="tag-pill badge d-inline-flex align-items-center" data-tag-id="{{ $tag->id }}" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;padding:4px 10px;border-radius:12px;font-size:0.85rem;">
{{ $tag->name }}
<button type="button" class="tag-remove btn btn-xs p-0 ml-1 text-white" style="line-height:1;font-size:14px;background:none;border:none;" onclick="removeTag(event, '{{ $selectorId }}', {{ $tag->id }})">&times;</button>
</span>
@endforeach
</div>
<div class="tag-search-input">
<input type="text" class="form-control form-control-sm" placeholder="Cerca tag..." oninput="filterTags(event, '{{ $selectorId }}')" autocomplete="off">
</div>
<div class="tag-available mt-1" style="max-height:140px;overflow-y:auto;display:flex;flex-wrap:wrap;gap:4px;">
@foreach($allTags as $tag)
<button type="button" class="tag-btn btn btn-sm {{ in_array($tag->id, $selectedTags) ? 'tag-selected' : '' }}" data-tag-id="{{ $tag->id }}" data-tag-name="{{ $tag->name }}" data-tag-color="{{ $tag->color ?? '#6c757d' }}" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;border-radius:12px;cursor:pointer;{{ in_array($tag->id, $selectedTags) ? '' : 'opacity:0.5;' }}" onclick="toggleTag(event, '{{ $selectorId }}', {{ $tag->id }})">
{{ $tag->name }}
</button>
@endforeach
@if($allTags->isEmpty())
<p class="text-muted small mb-0 w-100">Nessun tag disponibile. <a href="{{ route('impostazioni.index') }}#tag">Crea i tag</a> nelle Impostazioni.</p>
@endif
<p class="tag-no-match text-muted small mb-0 w-100" style="display:none;">Nessun tag corrisponde alla ricerca.</p>
</div>
</div>
<div class="tag-hidden-inputs">
@foreach($selectedTags as $selectedId)
<input type="hidden" name="{{ $tagFieldName }}[]" value="{{ $selectedId }}">
@endforeach
</div>
</div>
@once
@push('scripts')
<script>
function toggleTag(e, selId, tagId) {
e.preventDefault();
var container = document.getElementById(selId);
var btn = container.querySelector('.tag-btn[data-tag-id="' + tagId + '"]');
if (!btn) return;
var isSelected = btn.classList.contains('tag-selected');
var selectedBox = container.querySelector('.tag-selected');
var hiddenContainer = container.querySelector('.tag-hidden-inputs');
if (isSelected) {
btn.classList.remove('tag-selected');
btn.style.opacity = '0.5';
var pill = container.querySelector('.tag-pill[data-tag-id="' + tagId + '"]');
if (pill) pill.remove();
var hidden = hiddenContainer.querySelector('input[value="' + tagId + '"]');
if (hidden) hidden.remove();
} else {
btn.classList.add('tag-selected');
btn.style.opacity = '1';
var name = btn.dataset.tagName;
var color = btn.dataset.tagColor || '#6c757d';
var pill = document.createElement('span');
pill.className = 'tag-pill badge d-inline-flex align-items-center';
pill.style.cssText = 'background-color:' + color + ';color:#fff;padding:4px 10px;border-radius:12px;font-size:0.85rem;';
pill.dataset.tagId = tagId;
pill.innerHTML = name + '<button type="button" class="tag-remove btn btn-xs p-0 ml-1 text-white" style="line-height:1;font-size:14px;background:none;border:none;" onclick="removeTag(event, \'' + selId + '\', ' + tagId + ')">&times;</button>';
selectedBox.appendChild(pill);
if (hiddenContainer.querySelector('input[value="' + tagId + '"]')) return;
var hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = '{{ $tagFieldName }}[]';
hidden.value = tagId;
hiddenContainer.appendChild(hidden);
}
}
function removeTag(e, selId, tagId) {
e.preventDefault();
var container = document.getElementById(selId);
var btn = container.querySelector('.tag-btn[data-tag-id="' + tagId + '"]');
if (btn) {
btn.classList.remove('tag-selected');
btn.style.opacity = '0.5';
}
var pill = container.querySelector('.tag-pill[data-tag-id="' + tagId + '"]');
if (pill) pill.remove();
var hiddenContainer = container.querySelector('.tag-hidden-inputs');
var hidden = hiddenContainer.querySelector('input[value="' + tagId + '"]');
if (hidden) hidden.remove();
}
function filterTags(e, selId) {
var container = document.getElementById(selId);
var q = e.target.value.toLowerCase();
var btns = container.querySelectorAll('.tag-btn');
var visibleCount = 0;
btns.forEach(function(btn) {
var name = btn.dataset.tagName.toLowerCase();
if (name.includes(q)) {
btn.style.display = '';
visibleCount++;
} else {
btn.style.display = 'none';
}
});
var noMatch = container.querySelector('.tag-no-match');
if (noMatch) {
noMatch.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
</script>
@endpush
@endonce
+50
View File
@@ -295,6 +295,22 @@
<label>Limite risultati</label>
<input type="number" name="limit" class="form-control" placeholder="Es: 100" min="1">
</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">
<i class="fas fa-save mr-1"></i> Salva Report
</button>
@@ -346,6 +362,37 @@ document.addEventListener('DOMContentLoaded', function() {
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) {
const cols = columnOptions[tipoReport] || {};
const options = [];
@@ -365,8 +412,10 @@ document.addEventListener('DOMContentLoaded', function() {
$('#collapseThree').on('shown.bs.collapse', function () {
initColumnsSelect2();
initTagFilterSelect2();
}).on('hidden.bs.collapse', function () {
destroyColumnsSelect2();
destroyTagFilterSelect2();
});
if ($('#collapseTwo').hasClass('show')) {
@@ -375,6 +424,7 @@ document.addEventListener('DOMContentLoaded', function() {
if ($('#collapseThree').hasClass('show')) {
initColumnsSelect2();
initTagFilterSelect2();
}
$('#tipo_report').on('change', function() {
+155
View File
@@ -0,0 +1,155 @@
@extends('layouts.adminlte')
@section('title', 'Ricerca per Tag')
@section('page_title', 'Ricerca per Tag')
@section('content')
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>
@isset($selectedTags)
Risultati per:
@foreach($selectedTags as $t)
<span class="badge" style="background-color: {{ $t->color ?? '#6c757d' }}; color: #fff; font-size: 0.9rem; margin: 0 2px;">{{ $t->name }}</span>
@endforeach
@if(count($tagSlugs) > 1)
<small class="text-muted ml-1">(intersezione tutti i tag richiesti)</small>
@endif
@else
Seleziona uno o più tag
@endisset
</h3>
</div>
<div class="card-body">
@if(!isset($selectedTags) || $selectedTags->isEmpty())
<p class="text-muted">Clicca sui tag per selezionarli (Ctrl+click per più tag).</p>
<form method="GET" action="{{ route('ricerca.index') }}" id="tag-form">
<div style="display:flex;flex-wrap:wrap;gap:8px;">
@foreach($allTags as $t)
<label class="btn btn-sm" style="background-color: {{ $t->color ?? '#6c757d' }}; color: #fff; border-radius: 14px; cursor:pointer; opacity: 0.7;">
<input type="checkbox" name="tag[]" value="{{ $t->slug }}" class="tag-checkbox" style="display:none;" onchange="this.closest('label').style.opacity = this.checked ? '1' : '0.7'">
{{ $t->name }}
</label>
@endforeach
@if($allTags->isEmpty())
<p class="text-muted">Nessun tag disponibile. <a href="{{ route('impostazioni.index') }}#tag">Crea i tag</a> nelle Impostazioni.</p>
@endif
</div>
@if($allTags->isNotEmpty())
<div class="mt-3">
<button type="submit" class="btn btn-primary"><i class="fas fa-search mr-1"></i> Cerca</button>
</div>
@endif
</form>
@else
<div class="row">
@php
$indexRoutes = [
'individui' => 'individui.index',
'gruppi' => 'gruppi.index',
'eventi' => 'eventi.index',
'documenti' => 'documenti.index',
'mailingLists' => 'mailing-liste.index',
];
$tagParams = collect($tagSlugs)->map(fn($s) => "tag[]={$s}")->implode('&');
@endphp
@foreach([
['key' => 'individui', 'color' => 'info', 'icon' => 'fa-user', 'label' => 'Individui'],
['key' => 'gruppi', 'color' => 'success', 'icon' => 'fa-users', 'label' => 'Gruppi'],
['key' => 'eventi', 'color' => 'warning', 'icon' => 'fa-calendar-alt', 'label' => 'Eventi'],
['key' => 'documenti', 'color' => 'secondary', 'icon' => 'fa-file', 'label' => 'Documenti'],
['key' => 'mailingLists', 'color' => 'purple', 'icon' => 'fa-list', 'label' => 'Mailing List'],
] as $box)
<div class="col-md-6 col-lg-3 mb-4">
<div class="info-box bg-{{ $box['color'] }}">
<span class="info-box-icon"><i class="fas {{ $box['icon'] }}"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ $box['label'] }}</span>
<span class="info-box-number">{{ $totals[$box['key']] }}</span>
@if($totals[$box['key']] > 0 && isset($indexRoutes[$box['key']]))
<a href="{{ route($indexRoutes[$box['key']]) . '?' . $tagParams }}" class="small" style="color:#fff;">Vedi tutti &raquo;</a>
@endif
</div>
</div>
</div>
@endforeach
</div>
@php
$sections = [
'individui' => ['color' => 'info', 'icon' => 'fa-user', 'title' => 'Individui', 'cols' => ['Cognome', 'Nome', 'Email'], 'fields' => ['cognome', 'nome', 'email_primaria']],
'gruppi' => ['color' => 'success', 'icon' => 'fa-users', 'title' => 'Gruppi', 'cols' => ['Nome', 'Descrizione'], 'fields' => ['nome', 'descrizione_limit']],
'eventi' => ['color' => 'warning', 'icon' => 'fa-calendar-alt', 'title' => 'Eventi', 'cols' => ['Evento', 'Data', 'Ora'], 'fields' => ['nome_evento', 'data', 'ora']],
'documenti' => ['color' => 'secondary', 'icon' => 'fa-file', 'title' => 'Documenti', 'cols' => ['Nome File', 'Tipo'], 'fields' => ['nome_file', 'tipo']],
'mailingLists' => ['color' => 'purple', 'icon' => 'fa-list', 'title' => 'Mailing List', 'cols' => ['Nome', 'Contatti', 'Stato'], 'fields' => ['nome', 'contatti_count', 'stato']],
];
@endphp
@foreach($sections as $key => $section)
@if($totals[$key] > 0)
<div class="card card-{{ $section['color'] }} card-outline">
<div class="card-header">
<h5 class="card-title"><i class="fas {{ $section['icon'] }} mr-2"></i>{{ $section['title'] }} ({{ $totals[$key] }})</h5>
</div>
<div class="card-body p-0">
<table class="table table-bordered table-hover mb-0">
<thead><tr>
@foreach($section['cols'] as $col)<th>{{ $col }}</th>@endforeach
<th></th>
</tr></thead>
<tbody>
@foreach($results[$key] as $item)
<tr>
@if($key === 'individui')
<td>{{ $item->cognome }}</td>
<td>{{ $item->nome }}</td>
<td>{{ $item->email_primaria ?? '-' }}</td>
<td><a href="{{ route('individui.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'gruppi')
<td>{{ $item->nome }}</td>
<td>{{ Str::limit($item->descrizione, 80) ?? '-' }}</td>
<td><a href="{{ route('gruppi.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'eventi')
<td>{{ $item->nome_evento }}</td>
<td>{{ $item->data_specifica ? $item->data_specifica->format('d/m/Y') : ($item->tipo_recorrenza ?? '-') }}</td>
<td>{{ $item->ora_inizio ? $item->ora_inizio->format('H:i') : '-' }}</td>
<td><a href="{{ route('eventi.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'documenti')
<td>{{ $item->nome_file }}</td>
<td><span class="badge badge-info">{{ $item->tipo ?? '-' }}</span></td>
<td><a href="{{ url('/documenti/' . $item->id . '/edit') }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'mailingLists')
<td>{{ $item->nome }}</td>
<td><span class="badge badge-info">{{ $item->contatti->count() }}</span></td>
<td>
@if($item->attiva)
<span class="badge badge-success">Attiva</span>
@else
<span class="badge badge-secondary">Disattiva</span>
@endif
</td>
<td><a href="{{ route('mailing-liste.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@endforeach
@if(array_sum($totals) === 0)
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i> Nessun elemento trovato con tutti i tag selezionati.
</div>
@endif
<div class="mt-3">
<a href="{{ route('ricerca.index') }}" class="btn btn-secondary"><i class="fas fa-arrow-left mr-1"></i> Nuova ricerca</a>
</div>
@endisset
</div>
</div>
@endsection
+14
View File
@@ -17,6 +17,7 @@ use App\Http\Controllers\MailingController;
use App\Http\Controllers\MailingListController;
use App\Http\Controllers\ReportController;
use App\Http\Controllers\ImpostazioniController;
use App\Http\Controllers\RicercaController;
use App\Http\Controllers\Admin\UtenteController;
use App\Http\Controllers\Admin\RuoloController;
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::post('individui/{individuo}/elimina', [IndividuoController::class, 'elimina'])->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::get('individui/{individuo}/avatar', [IndividuoController::class, 'getAvatar'])->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::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::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');
@@ -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-move', [DocumentoController::class, 'massMove'])->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::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/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::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');
@@ -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/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)
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');
+2 -2
View File
@@ -3,7 +3,7 @@
'name' => 'laravel/laravel',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
'reference' => '7d7d384a0d719ca6fa83e01e50577b94c877e676',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -427,7 +427,7 @@
'laravel/laravel' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
'reference' => '7d7d384a0d719ca6fa83e01e50577b94c877e676',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),