finale 2.0.0

This commit is contained in:
2026-06-09 11:09:00 +02:00
parent d82b123bfd
commit 06387568cb
7 changed files with 500 additions and 0 deletions
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tag;
use App\Models\Individuo;
use App\Models\Gruppo;
use App\Models\Evento;
use App\Models\Documento;
use App\Models\MailingList;
use Illuminate\Http\Request;
class RicercaController extends Controller
{
public function index(Request $request)
{
$this->authorizeRead('individui');
$allTags = Tag::orderBy('name')->get();
$tagSlugs = (array) $request->input('tag', []);
if (empty($tagSlugs)) {
return view('ricerca.index', compact('allTags'));
}
$tagSlugs = array_filter($tagSlugs);
$selectedTags = Tag::whereIn('slug', $tagSlugs)->get();
$results = [];
$totals = [];
if (count($tagSlugs) === 1) {
$tag = $selectedTags->first();
$results['individui'] = $tag->individui()->orderBy('cognome')->orderBy('nome')->get();
$results['gruppi'] = $tag->gruppi()->orderBy('nome')->get();
$results['eventi'] = $tag->eventi()->orderBy('data_specifica')->orderBy('nome_evento')->get();
$results['documenti'] = $tag->documenti()->orderBy('nome_file')->get();
$results['mailingLists'] = $tag->mailingLists()->orderBy('nome')->get();
} else {
$results['individui'] = Individuo::withAllTags($tagSlugs)->orderBy('cognome')->orderBy('nome')->get();
$results['gruppi'] = Gruppo::withAllTags($tagSlugs)->orderBy('nome')->get();
$results['eventi'] = Evento::withAllTags($tagSlugs)->orderBy('data_specifica')->orderBy('nome_evento')->get();
$results['documenti'] = Documento::withAllTags($tagSlugs)->orderBy('nome_file')->get();
$results['mailingLists'] = MailingList::withAllTags($tagSlugs)->orderBy('nome')->get();
}
$totals = [
'individui' => $results['individui']->count(),
'gruppi' => $results['gruppi']->count(),
'eventi' => $results['eventi']->count(),
'documenti' => $results['documenti']->count(),
'mailingLists' => $results['mailingLists']->count(),
];
return view('ricerca.index', compact('selectedTags', 'tagSlugs', 'results', 'totals', 'allTags'));
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
class Tag extends Model
{
protected $table = 'tags';
protected $fillable = ['name', 'slug', 'color', 'order_column'];
protected $casts = [
'order_column' => 'integer',
];
protected static function boot(): void
{
parent::boot();
static::creating(function (self $tag) {
if (empty($tag->slug)) {
$tag->slug = str()->slug($tag->name);
}
});
}
public function individui(): MorphToMany
{
return $this->morphedByMany(Individuo::class, 'taggable');
}
public function gruppi(): MorphToMany
{
return $this->morphedByMany(Gruppo::class, 'taggable');
}
public function eventi(): MorphToMany
{
return $this->morphedByMany(Evento::class, 'taggable');
}
public function documenti(): MorphToMany
{
return $this->morphedByMany(Documento::class, 'taggable');
}
public function mailingLists(): MorphToMany
{
return $this->morphedByMany(MailingList::class, 'taggable');
}
public function getCountAttribute(): int
{
return $this->individui()->count()
+ $this->gruppi()->count()
+ $this->eventi()->count()
+ $this->documenti()->count()
+ $this->mailingLists()->count();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Traits;
use App\Models\Tag;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
trait HasTagsLight
{
public function tags(): MorphToMany
{
return $this->morphToMany(Tag::class, 'taggable');
}
public function scopeWithAllTags($query, array $tags)
{
foreach ($tags as $tag) {
$query->whereHas('tags', fn ($q) => $q->where('slug', $tag));
}
return $query;
}
public function scopeWithAnyTags($query, array $tags)
{
$query->whereHas('tags', fn ($q) => $q->whereIn('slug', $tags));
return $query;
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasTable('tags')) {
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('slug', 100)->unique();
$table->string('color', 7)->nullable();
$table->integer('order_column')->default(0);
$table->timestamps();
});
}
if (!Schema::hasTable('taggables')) {
Schema::create('taggables', function (Blueprint $table) {
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
$table->primary(['tag_id', 'taggable_id', 'taggable_type']);
});
}
}
public function down(): void
{
Schema::dropIfExists('taggables');
Schema::dropIfExists('tags');
}
};
@@ -0,0 +1,34 @@
@php
$filterTags = $filterTags ?? (\App\Models\Tag::orderBy('name')->get());
$activeTags = (array) request('tag', []);
@endphp
@if($filterTags->isNotEmpty())
<div class="tag-filter-bar px-3 py-2 bg-light border-bottom" style="display:flex;flex-wrap:wrap;gap:4px;align-items:center;">
<span class="small font-weight-bold text-muted mr-1"><i class="fas fa-tags mr-1"></i>Tag:</span>
@foreach($filterTags as $tag)
@php
$isActive = in_array($tag->slug, $activeTags);
$query = request()->query();
if ($isActive) {
$newTags = array_values(array_diff($activeTags, [$tag->slug]));
if (empty($newTags)) {
unset($query['tag']);
} else {
$query['tag'] = $newTags;
}
} else {
$query['tag'] = array_merge($activeTags, [$tag->slug]);
}
$url = request()->url() . '?' . http_build_query($query);
@endphp
<a href="{{ $url }}" class="badge tag-filter-badge" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;{{ $isActive ? '' : 'opacity:0.45;' }}font-size:0.8rem;padding:4px 10px;border-radius:12px;text-decoration:none;">
{{ $tag->name }}
</a>
@endforeach
@if(!empty($activeTags))
<a href="{{ request()->url() }}" class="badge badge-light" style="font-size:0.8rem;padding:4px 10px;border-radius:12px;text-decoration:none;">
<i class="fas fa-times mr-1"></i>Cancella filtri
</a>
@endif
</div>
@endif
@@ -0,0 +1,123 @@
@php
$allTags = $allTags ?? \App\Models\Tag::orderBy('name')->get();
$selectedTags = old('tags', $selectedTags ?? []);
$tagFieldName = $tagFieldName ?? 'tags';
$selectorId = 'tag-selector-' . md5($tagFieldName);
$selectedTagObjects = $allTags->whereIn('id', $selectedTags);
@endphp
<div class="form-group" id="{{ $selectorId }}">
<label>{{ $label ?? 'Tag' }}</label>
<div class="tag-selector-container" style="border:1px solid #ced4da;border-radius:.25rem;padding:8px;background:#fff;">
<div class="tag-selected d-flex flex-wrap mb-1" style="gap:4px;">
@foreach($selectedTagObjects as $tag)
<span class="tag-pill badge d-inline-flex align-items-center" data-tag-id="{{ $tag->id }}" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;padding:4px 10px;border-radius:12px;font-size:0.85rem;">
{{ $tag->name }}
<button type="button" class="tag-remove btn btn-xs p-0 ml-1 text-white" style="line-height:1;font-size:14px;background:none;border:none;" onclick="removeTag(event, '{{ $selectorId }}', {{ $tag->id }})">&times;</button>
</span>
@endforeach
</div>
<div class="tag-search-input">
<input type="text" class="form-control form-control-sm" placeholder="Cerca tag..." oninput="filterTags(event, '{{ $selectorId }}')" autocomplete="off">
</div>
<div class="tag-available mt-1" style="max-height:140px;overflow-y:auto;display:flex;flex-wrap:wrap;gap:4px;">
@foreach($allTags as $tag)
<button type="button" class="tag-btn btn btn-sm {{ in_array($tag->id, $selectedTags) ? 'tag-selected' : '' }}" data-tag-id="{{ $tag->id }}" data-tag-name="{{ $tag->name }}" data-tag-color="{{ $tag->color ?? '#6c757d' }}" style="background-color:{{ $tag->color ?? '#6c757d' }};color:#fff;border-radius:12px;cursor:pointer;{{ in_array($tag->id, $selectedTags) ? '' : 'opacity:0.5;' }}" onclick="toggleTag(event, '{{ $selectorId }}', {{ $tag->id }})">
{{ $tag->name }}
</button>
@endforeach
@if($allTags->isEmpty())
<p class="text-muted small mb-0 w-100">Nessun tag disponibile. <a href="{{ route('impostazioni.index') }}#tag">Crea i tag</a> nelle Impostazioni.</p>
@endif
<p class="tag-no-match text-muted small mb-0 w-100" style="display:none;">Nessun tag corrisponde alla ricerca.</p>
</div>
</div>
<div class="tag-hidden-inputs">
@foreach($selectedTags as $selectedId)
<input type="hidden" name="{{ $tagFieldName }}[]" value="{{ $selectedId }}">
@endforeach
</div>
</div>
@once
@push('scripts')
<script>
function toggleTag(e, selId, tagId) {
e.preventDefault();
var container = document.getElementById(selId);
var btn = container.querySelector('.tag-btn[data-tag-id="' + tagId + '"]');
if (!btn) return;
var isSelected = btn.classList.contains('tag-selected');
var selectedBox = container.querySelector('.tag-selected');
var hiddenContainer = container.querySelector('.tag-hidden-inputs');
if (isSelected) {
btn.classList.remove('tag-selected');
btn.style.opacity = '0.5';
var pill = container.querySelector('.tag-pill[data-tag-id="' + tagId + '"]');
if (pill) pill.remove();
var hidden = hiddenContainer.querySelector('input[value="' + tagId + '"]');
if (hidden) hidden.remove();
} else {
btn.classList.add('tag-selected');
btn.style.opacity = '1';
var name = btn.dataset.tagName;
var color = btn.dataset.tagColor || '#6c757d';
var pill = document.createElement('span');
pill.className = 'tag-pill badge d-inline-flex align-items-center';
pill.style.cssText = 'background-color:' + color + ';color:#fff;padding:4px 10px;border-radius:12px;font-size:0.85rem;';
pill.dataset.tagId = tagId;
pill.innerHTML = name + '<button type="button" class="tag-remove btn btn-xs p-0 ml-1 text-white" style="line-height:1;font-size:14px;background:none;border:none;" onclick="removeTag(event, \'' + selId + '\', ' + tagId + ')">&times;</button>';
selectedBox.appendChild(pill);
if (hiddenContainer.querySelector('input[value="' + tagId + '"]')) return;
var hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = '{{ $tagFieldName }}[]';
hidden.value = tagId;
hiddenContainer.appendChild(hidden);
}
}
function removeTag(e, selId, tagId) {
e.preventDefault();
var container = document.getElementById(selId);
var btn = container.querySelector('.tag-btn[data-tag-id="' + tagId + '"]');
if (btn) {
btn.classList.remove('tag-selected');
btn.style.opacity = '0.5';
}
var pill = container.querySelector('.tag-pill[data-tag-id="' + tagId + '"]');
if (pill) pill.remove();
var hiddenContainer = container.querySelector('.tag-hidden-inputs');
var hidden = hiddenContainer.querySelector('input[value="' + tagId + '"]');
if (hidden) hidden.remove();
}
function filterTags(e, selId) {
var container = document.getElementById(selId);
var q = e.target.value.toLowerCase();
var btns = container.querySelectorAll('.tag-btn');
var visibleCount = 0;
btns.forEach(function(btn) {
var name = btn.dataset.tagName.toLowerCase();
if (name.includes(q)) {
btn.style.display = '';
visibleCount++;
} else {
btn.style.display = 'none';
}
});
var noMatch = container.querySelector('.tag-no-match');
if (noMatch) {
noMatch.style.display = visibleCount === 0 ? 'block' : 'none';
}
}
</script>
@endpush
@endonce
+155
View File
@@ -0,0 +1,155 @@
@extends('layouts.adminlte')
@section('title', 'Ricerca per Tag')
@section('page_title', 'Ricerca per Tag')
@section('content')
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i>
@isset($selectedTags)
Risultati per:
@foreach($selectedTags as $t)
<span class="badge" style="background-color: {{ $t->color ?? '#6c757d' }}; color: #fff; font-size: 0.9rem; margin: 0 2px;">{{ $t->name }}</span>
@endforeach
@if(count($tagSlugs) > 1)
<small class="text-muted ml-1">(intersezione tutti i tag richiesti)</small>
@endif
@else
Seleziona uno o più tag
@endisset
</h3>
</div>
<div class="card-body">
@if(!isset($selectedTags) || $selectedTags->isEmpty())
<p class="text-muted">Clicca sui tag per selezionarli (Ctrl+click per più tag).</p>
<form method="GET" action="{{ route('ricerca.index') }}" id="tag-form">
<div style="display:flex;flex-wrap:wrap;gap:8px;">
@foreach($allTags as $t)
<label class="btn btn-sm" style="background-color: {{ $t->color ?? '#6c757d' }}; color: #fff; border-radius: 14px; cursor:pointer; opacity: 0.7;">
<input type="checkbox" name="tag[]" value="{{ $t->slug }}" class="tag-checkbox" style="display:none;" onchange="this.closest('label').style.opacity = this.checked ? '1' : '0.7'">
{{ $t->name }}
</label>
@endforeach
@if($allTags->isEmpty())
<p class="text-muted">Nessun tag disponibile. <a href="{{ route('impostazioni.index') }}#tag">Crea i tag</a> nelle Impostazioni.</p>
@endif
</div>
@if($allTags->isNotEmpty())
<div class="mt-3">
<button type="submit" class="btn btn-primary"><i class="fas fa-search mr-1"></i> Cerca</button>
</div>
@endif
</form>
@else
<div class="row">
@php
$indexRoutes = [
'individui' => 'individui.index',
'gruppi' => 'gruppi.index',
'eventi' => 'eventi.index',
'documenti' => 'documenti.index',
'mailingLists' => 'mailing-liste.index',
];
$tagParams = collect($tagSlugs)->map(fn($s) => "tag[]={$s}")->implode('&');
@endphp
@foreach([
['key' => 'individui', 'color' => 'info', 'icon' => 'fa-user', 'label' => 'Individui'],
['key' => 'gruppi', 'color' => 'success', 'icon' => 'fa-users', 'label' => 'Gruppi'],
['key' => 'eventi', 'color' => 'warning', 'icon' => 'fa-calendar-alt', 'label' => 'Eventi'],
['key' => 'documenti', 'color' => 'secondary', 'icon' => 'fa-file', 'label' => 'Documenti'],
['key' => 'mailingLists', 'color' => 'purple', 'icon' => 'fa-list', 'label' => 'Mailing List'],
] as $box)
<div class="col-md-6 col-lg-3 mb-4">
<div class="info-box bg-{{ $box['color'] }}">
<span class="info-box-icon"><i class="fas {{ $box['icon'] }}"></i></span>
<div class="info-box-content">
<span class="info-box-text">{{ $box['label'] }}</span>
<span class="info-box-number">{{ $totals[$box['key']] }}</span>
@if($totals[$box['key']] > 0 && isset($indexRoutes[$box['key']]))
<a href="{{ route($indexRoutes[$box['key']]) . '?' . $tagParams }}" class="small" style="color:#fff;">Vedi tutti &raquo;</a>
@endif
</div>
</div>
</div>
@endforeach
</div>
@php
$sections = [
'individui' => ['color' => 'info', 'icon' => 'fa-user', 'title' => 'Individui', 'cols' => ['Cognome', 'Nome', 'Email'], 'fields' => ['cognome', 'nome', 'email_primaria']],
'gruppi' => ['color' => 'success', 'icon' => 'fa-users', 'title' => 'Gruppi', 'cols' => ['Nome', 'Descrizione'], 'fields' => ['nome', 'descrizione_limit']],
'eventi' => ['color' => 'warning', 'icon' => 'fa-calendar-alt', 'title' => 'Eventi', 'cols' => ['Evento', 'Data', 'Ora'], 'fields' => ['nome_evento', 'data', 'ora']],
'documenti' => ['color' => 'secondary', 'icon' => 'fa-file', 'title' => 'Documenti', 'cols' => ['Nome File', 'Tipo'], 'fields' => ['nome_file', 'tipo']],
'mailingLists' => ['color' => 'purple', 'icon' => 'fa-list', 'title' => 'Mailing List', 'cols' => ['Nome', 'Contatti', 'Stato'], 'fields' => ['nome', 'contatti_count', 'stato']],
];
@endphp
@foreach($sections as $key => $section)
@if($totals[$key] > 0)
<div class="card card-{{ $section['color'] }} card-outline">
<div class="card-header">
<h5 class="card-title"><i class="fas {{ $section['icon'] }} mr-2"></i>{{ $section['title'] }} ({{ $totals[$key] }})</h5>
</div>
<div class="card-body p-0">
<table class="table table-bordered table-hover mb-0">
<thead><tr>
@foreach($section['cols'] as $col)<th>{{ $col }}</th>@endforeach
<th></th>
</tr></thead>
<tbody>
@foreach($results[$key] as $item)
<tr>
@if($key === 'individui')
<td>{{ $item->cognome }}</td>
<td>{{ $item->nome }}</td>
<td>{{ $item->email_primaria ?? '-' }}</td>
<td><a href="{{ route('individui.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'gruppi')
<td>{{ $item->nome }}</td>
<td>{{ Str::limit($item->descrizione, 80) ?? '-' }}</td>
<td><a href="{{ route('gruppi.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'eventi')
<td>{{ $item->nome_evento }}</td>
<td>{{ $item->data_specifica ? $item->data_specifica->format('d/m/Y') : ($item->tipo_recorrenza ?? '-') }}</td>
<td>{{ $item->ora_inizio ? $item->ora_inizio->format('H:i') : '-' }}</td>
<td><a href="{{ route('eventi.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'documenti')
<td>{{ $item->nome_file }}</td>
<td><span class="badge badge-info">{{ $item->tipo ?? '-' }}</span></td>
<td><a href="{{ url('/documenti/' . $item->id . '/edit') }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@elseif($key === 'mailingLists')
<td>{{ $item->nome }}</td>
<td><span class="badge badge-info">{{ $item->contatti->count() }}</span></td>
<td>
@if($item->attiva)
<span class="badge badge-success">Attiva</span>
@else
<span class="badge badge-secondary">Disattiva</span>
@endif
</td>
<td><a href="{{ route('mailing-liste.show', $item) }}" class="btn btn-xs btn-primary"><i class="fas fa-eye"></i></a></td>
@endif
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@endforeach
@if(array_sum($totals) === 0)
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i> Nessun elemento trovato con tutti i tag selezionati.
</div>
@endif
<div class="mt-3">
<a href="{{ route('ricerca.index') }}" class="btn btn-secondary"><i class="fas fa-arrow-left mr-1"></i> Nuova ricerca</a>
</div>
@endisset
</div>
</div>
@endsection