versione 2.0.0
This commit is contained in:
@@ -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
|
||||
@@ -184,5 +203,114 @@ php artisan config:clear
|
||||
|
||||
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
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'] ?? []);
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,10 +227,18 @@ 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]);
|
||||
|
||||
if ($request->has('contatti')) {
|
||||
@@ -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);
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,13 +1089,34 @@ 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];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($config['search'])) {
|
||||
$search = $config['search'];
|
||||
|
||||
@@ -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,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,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,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',
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>×</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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>×</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>×</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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>×</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>×</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) {
|
||||
|
||||
@@ -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 > Crea Mailing List</em> per creare una lista dai contatti selezionati.
|
||||
|
||||
@@ -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">×</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 => ({
|
||||
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
|
||||
}));
|
||||
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,18 +232,16 @@ 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,
|
||||
cognome: ind.cognome,
|
||||
nome: ind.nome,
|
||||
email: ind.emails[0],
|
||||
email: (ind.emails && ind.emails.length > 0) ? ind.emails[0] : '',
|
||||
origini: ['Gruppo']
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
@@ -246,18 +259,16 @@ 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,
|
||||
cognome: ind.cognome,
|
||||
nome: ind.nome,
|
||||
email: ind.emails[0],
|
||||
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;
|
||||
}
|
||||
|
||||
function completaInvio() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'contatti_json';
|
||||
input.value = JSON.stringify(selected);
|
||||
this.appendChild(input);
|
||||
input.value = JSON.stringify(selected.map(function(s) {
|
||||
return { individuo_id: s.individuo_id, email: s.email };
|
||||
}));
|
||||
form.appendChild(input);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
const senzaEmail = selected.filter(function(s) { return !s.email; });
|
||||
|
||||
if (senzaEmail.length > 0) {
|
||||
const elencoHtml = '<ul class="text-left mb-0">' +
|
||||
senzaEmail.map(function(s) {
|
||||
return '<li>' + s._label + '</li>';
|
||||
}).join('') +
|
||||
'</ul>';
|
||||
|
||||
Swal.fire({
|
||||
title: 'Contatti senza email',
|
||||
html: '<p class="mb-2">I seguenti ' + senzaEmail.length + ' contatti non hanno un indirizzo email:</p>' +
|
||||
elencoHtml +
|
||||
'<p class="mt-3 mb-0 text-muted">La email verrà risolta automaticamente quando l\'individuo verrà aggiornato. Salvare comunque?</p>',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Salva lo stesso',
|
||||
cancelButtonText: 'Annulla',
|
||||
confirmButtonColor: '#28a745',
|
||||
cancelButtonColor: '#6c757d'
|
||||
}).then(function(result) {
|
||||
if (result.isConfirmed) {
|
||||
completaInvio();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
completaInvio();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@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">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user