Files
glastree/app/Http/Controllers/EventoController.php
T
2026-06-10 14:03:43 +02:00

633 lines
25 KiB
PHP

<?php
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;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
class EventoController extends Controller
{
public function index(Request $request)
{
$this->authorizeRead('eventi');
$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 . '%');
}
if ($request->filled('gruppo_id')) {
$query->whereHas('gruppi', fn($q) => $q->where('gruppi.id', $request->gruppo_id));
}
if ($request->filled('tipo')) {
$query->where('tipo_recorrenza', $request->tipo);
}
$sortBy = $request->get('sort', 'created_at');
$sortDir = $request->get('direction', 'desc');
$allowedSorts = ['nome_evento', 'descrizione_evento', 'tipo_evento', 'tipo_recorrenza', 'data_specifica', 'created_at'];
if ($sortBy === 'gruppi') {
$query->withCount('gruppi')->orderBy('gruppi_count', $sortDir === 'asc' ? 'asc' : 'desc');
} elseif ($sortBy === 'responsabili') {
$query->withCount('responsabili')->orderBy('responsabili_count', $sortDir === 'asc' ? 'asc' : 'desc');
} elseif (in_array($sortBy, $allowedSorts)) {
$query->orderBy($sortBy, $sortDir === 'asc' ? 'asc' : 'desc');
} else {
$query->orderByDesc('created_at');
}
$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();
$vista = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'eventi')
->where('is_default', true)
->first();
$userVistas = \App\Models\VistaReport::where('user_id', auth()->id())
->where('tipo', 'eventi')
->orderBy('is_default', 'desc')
->orderBy('nome')
->get();
$allColumnDefs = [
['key' => 'nome_evento', 'label' => 'Nome Evento'],
['key' => 'descrizione_evento', 'label' => 'Descrizione'],
['key' => 'tipo_evento', 'label' => 'Tipologia'],
['key' => 'tipo_recorrenza', 'label' => 'Ricorrenza'],
['key' => 'gruppi', 'label' => 'Gruppi'],
['key' => 'responsabili', 'label' => 'Responsabili'],
['key' => 'tag', 'label' => 'Tag'],
['key' => 'data_specifica', 'label' => 'Data'],
['key' => 'created_at', 'label' => 'Creato'],
];
$defaultVisible = ['nome_evento', 'descrizione_evento', 'tipo_evento', 'tipo_recorrenza', 'gruppi', 'responsabili', 'tag', 'created_at'];
$visibleColumns = $vista && !empty($vista->colonne_visibili)
? $vista->colonne_visibili
: $defaultVisible;
$tableColumns = array_map(fn($col) => [
'key' => $col['key'],
'label' => $col['label'],
'visible' => in_array($col['key'], $visibleColumns),
], $allColumnDefs);
$entityType = 'eventi';
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
return view('eventi.index', compact('eventi', 'gruppi', 'allTags', 'vista', 'visibleColumns', 'tableColumns', 'entityType', 'columnWidths', 'userVistas'));
}
public function create(Request $request)
{
$this->authorizeWrite('eventi');
$gruppi = Gruppo::orderBy('nome')->get();
$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', 'tags'));
}
public function store(Request $request)
{
$this->authorizeWrite('eventi');
$data = $request->validate([
'nome_evento' => 'required|string|max:255',
'descrizione_evento' => 'nullable|string|max:255',
'tipo_evento' => 'nullable|exists:tipologie_eventi,nome',
'tipo_recorrenza' => 'nullable|in:singolo,settimanale,mensile,annuale,altro',
'giorno_settimana' => 'nullable|integer|min:0|max:6',
'giorno_mese' => 'nullable|integer|min:1|max:31',
'occorrenza_mese' => 'nullable|string|max:10',
'mesi_recorrenza' => 'nullable|array',
'mesi_recorrenza.*' => 'integer|min:1|max:12',
'mese_annuale' => 'nullable|integer|min:1|max:12',
'ora_inizio' => 'nullable|date_format:H:i',
'data_specifica' => 'nullable|date',
'durata_minuti' => 'nullable|integer|min:1',
'descrizione' => 'nullable|string',
'note' => 'nullable|string',
'is_incontro_gruppo' => 'nullable',
'gruppi' => 'nullable|array',
'gruppi.*' => 'exists:gruppi,id',
'responsabili' => 'nullable|array',
'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([
'nome_evento' => $data['nome_evento'],
'descrizione_evento' => $data['descrizione_evento'] ?? null,
'tipo_evento' => $data['tipo_evento'] ?? null,
'tipo_recorrenza' => $data['tipo_recorrenza'] ?? 'singolo',
'giorno_settimana' => $data['giorno_settimana'] ?? null,
'giorno_mese' => $data['giorno_mese'] ?? null,
'occorrenza_mese' => $data['occorrenza_mese'] ?? null,
'mesi_recorrenza' => is_array($request->mesi_recorrenza) ? implode(',', $request->mesi_recorrenza) : ($data['mesi_recorrenza'] ?? null),
'mese_annuale' => $data['mese_annuale'] ?? null,
'ora_inizio' => $data['ora_inizio'] ?? null,
'data_specifica' => $data['data_specifica'] ?? null,
'durata_minuti' => $data['durata_minuti'] ?? null,
'descrizione' => $data['descrizione'] ?? null,
'note' => $data['note'] ?? null,
'is_incontro_gruppo' => $request->boolean('is_incontro_gruppo'),
'luogo_indirizzo' => $data['luogo_indirizzo'] ?? null,
'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']);
}
if (!empty($data['responsabili'])) {
$evento->responsabili()->attach($data['responsabili']);
}
return redirect('/eventi/' . $evento->id)->with('success', 'Evento creato con successo.');
}
public function show($evento)
{
$this->authorizeRead('eventi');
$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', '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', 'tags', 'selectedTags'));
}
public function update(Request $request, $evento)
{
$this->authorizeWrite('eventi');
$evento = Evento::findOrFail($evento);
$data = $request->validate([
'nome_evento' => 'required|string|max:255',
'descrizione_evento' => 'nullable|string|max:255',
'tipo_evento' => 'nullable|exists:tipologie_eventi,nome',
'tipo_recorrenza' => 'nullable|in:singolo,settimanale,mensile,annuale,altro',
'giorno_settimana' => 'nullable|integer|min:0|max:6',
'giorno_mese' => 'nullable|integer|min:1|max:31',
'occorrenza_mese' => 'nullable|string|max:10',
'mesi_recorrenza' => 'nullable|array',
'mesi_recorrenza.*' => 'integer|min:1|max:12',
'mese_annuale' => 'nullable|integer|min:1|max:12',
'ora_inizio' => 'nullable|date_format:H:i',
'data_specifica' => 'nullable|date',
'durata_minuti' => 'nullable|integer|min:1',
'descrizione' => 'nullable|string',
'note' => 'nullable|string',
'is_incontro_gruppo' => 'nullable',
'gruppi' => 'nullable|array',
'gruppi.*' => 'exists:gruppi,id',
'responsabili' => 'nullable|array',
'responsabili.*' => 'exists:individui,id',
'luogo_indirizzo' => 'nullable|string|max:500',
'luogo_url_maps' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$evento->update([
'nome_evento' => $data['nome_evento'],
'descrizione_evento' => $data['descrizione_evento'] ?? null,
'tipo_evento' => $data['tipo_evento'] ?? null,
'tipo_recorrenza' => $data['tipo_recorrenza'] ?? 'singolo',
'giorno_settimana' => $data['giorno_settimana'] ?? null,
'giorno_mese' => $data['giorno_mese'] ?? null,
'occorrenza_mese' => $data['occorrenza_mese'] ?? null,
'mesi_recorrenza' => is_array($request->mesi_recorrenza) ? implode(',', $request->mesi_recorrenza) : ($data['mesi_recorrenza'] ?? null),
'mese_annuale' => $data['mese_annuale'] ?? null,
'ora_inizio' => $data['ora_inizio'] ?? null,
'data_specifica' => $data['data_specifica'] ?? null,
'durata_minuti' => $data['durata_minuti'] ?? null,
'descrizione' => $data['descrizione'] ?? null,
'note' => $data['note'] ?? null,
'is_incontro_gruppo' => $request->boolean('is_incontro_gruppo'),
'luogo_indirizzo' => $data['luogo_indirizzo'] ?? null,
'luogo_url_maps' => $data['luogo_url_maps'] ?? null,
]);
$evento->tags()->sync($data['tags'] ?? []);
$evento->gruppi()->sync($data['gruppi'] ?? []);
$evento->responsabili()->sync($data['responsabili'] ?? []);
return redirect('/eventi/' . $evento->id)->with('success', 'Evento aggiornato.');
}
public function destroy($evento)
{
$this->authorizeDelete('eventi');
$evento = Evento::findOrFail($evento);
$evento->delete();
return redirect('/eventi')->with('success', 'Evento eliminato.');
}
public function massElimina(Request $request)
{
$this->authorizeDelete('eventi');
$ids = $request->input('ids', []);
if (!is_array($ids) || empty($ids)) {
return redirect('/eventi')->with('error', 'Nessun evento selezionato.');
}
$eventi = Evento::whereIn('id', $ids)->get();
foreach ($eventi as $evento) {
$evento->gruppi()->detach();
$evento->responsabili()->detach();
$evento->delete();
}
return redirect('/eventi')->with('success', count($eventi) . ' eventi eliminati con successo.');
}
public function exportIcs(int $id, IcsExportService $ics): StreamedResponse
{
$this->authorizeRead('eventi');
$evento = Evento::findOrFail($id);
$content = $ics->generateSingle($evento);
return response()->streamDownload(function () use ($content) {
echo $content;
}, $ics->getFilename($evento), [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Length' => strlen($content),
]);
}
public function exportSelectedIcs(Request $request, IcsExportService $ics): StreamedResponse
{
$this->authorizeRead('eventi');
$ids = $request->input('ids', []);
if (!empty($ids) && is_array($ids)) {
$eventi = Evento::whereIn('id', $ids)->get();
} else {
$query = Evento::query();
if ($request->filled('search')) {
$query->where('nome_evento', 'like', '%' . $request->search . '%');
}
if ($request->filled('gruppo_id')) {
$query->whereHas('gruppi', fn($q) => $q->where('gruppi.id', $request->gruppo_id));
}
if ($request->filled('tipo')) {
$query->where('tipo_recorrenza', $request->tipo);
}
$eventi = $query->get();
}
if ($eventi->isEmpty()) {
return redirect('/eventi')->with('error', 'Nessun evento da esportare.');
}
$content = $ics->generateCollection($eventi);
return response()->streamDownload(function () use ($content) {
echo $content;
}, $ics->getCollectionFilename(), [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Length' => strlen($content),
]);
}
public function calendar()
{
$this->authorizeRead('eventi');
return view('eventi.calendar');
}
public function calendarEvents(Request $request)
{
$this->authorizeRead('eventi');
$start = $request->input('start');
$end = $request->input('end');
$baseUrl = $request->getSchemeAndHttpHost();
$query = Evento::with(['gruppi', 'responsabili']);
$eventi = $query->get();
$events = [];
foreach ($eventi as $evento) {
if ($evento->tipo_recorrenza === 'singolo' || $evento->tipo_recorrenza === null) {
if ($evento->data_specifica) {
$events[] = $this->buildCalendarEvent($evento, $evento->data_specifica);
}
} elseif ($evento->tipo_recorrenza === 'settimanale' && $evento->giorno_settimana !== null) {
$events = array_merge($events, $this->generateSettimanaleEvents($evento, $start, $end, $baseUrl));
} elseif ($evento->tipo_recorrenza === 'mensile' && $evento->occorrenza_mese) {
$events = array_merge($events, $this->generateMensileEvents($evento, $start, $end, $baseUrl));
} elseif ($evento->tipo_recorrenza === 'annuale' && $evento->mese_annuale) {
$events = array_merge($events, $this->generateAnnualeEvents($evento, $start, $end, $baseUrl));
} elseif ($evento->tipo_recorrenza === 'altro' && $evento->giorno_settimana !== null) {
$events = array_merge($events, $this->generateAltroEvents($evento, $start, $end, $baseUrl));
}
}
return response()->json($events);
}
protected function buildCalendarEvent(Evento $evento, $date, ?string $baseUrl = null): array
{
if ($date instanceof \Carbon\Carbon) {
$carbon = $date;
} elseif (is_string($date)) {
$carbon = \Carbon\Carbon::parse($date);
} else {
$carbon = $date;
}
$baseUrl = $baseUrl ?? request()->getSchemeAndHttpHost();
return [
'id' => $evento->id . '_' . $carbon->format('Ymd'),
'title' => $evento->nome_evento,
'start' => $carbon->format('Y-m-d') . ($evento->ora_inizio ? 'T' . $evento->ora_inizio->format('H:i') : ''),
'url' => $baseUrl . '/eventi/' . $evento->id,
'backgroundColor' => $this->getEventoColor($evento),
'borderColor' => $this->getEventoColor($evento),
];
}
protected function generateSettimanaleEvents(Evento $evento, $start, $end, $baseUrl): array
{
$startDate = \Carbon\Carbon::parse($start);
$endDate = \Carbon\Carbon::parse($end);
$events = [];
$current = $startDate->copy()->startOfWeek();
$targetDay = $evento->giorno_settimana;
while ($current <= $endDate) {
$dayOfWeek = (int) $current->format('w');
if ($dayOfWeek === $targetDay && $current >= $startDate && $current <= $endDate) {
$events[] = $this->buildCalendarEvent($evento, $current->copy(), $baseUrl);
}
$current->addDay();
}
return $events;
}
protected function generateMensileEvents(Evento $evento, $start, $end, $baseUrl): array
{
$startDate = \Carbon\Carbon::parse($start);
$endDate = \Carbon\Carbon::parse($end);
$events = [];
$parts = explode(',', $evento->occorrenza_mese);
if (count($parts) !== 2) {
return $events;
}
$occorrenza = (int) $parts[0];
$giornoSettimana = (int) $parts[1];
$mesiSelezionati = [];
if ($evento->mesi_recorrenza) {
$mesiSelezionati = array_map('intval', explode(',', $evento->mesi_recorrenza));
}
$currentMonth = $startDate->copy()->startOfMonth();
$endMonth = $endDate->copy()->startOfMonth();
while ($currentMonth <= $endMonth) {
$monthNumber = (int) $currentMonth->format('n');
if (empty($mesiSelezionati) || in_array($monthNumber, $mesiSelezionati)) {
$occurrenceDate = $this->findNthWeekdayOfMonth($currentMonth, $occorrenza, $giornoSettimana);
if ($occurrenceDate && $occurrenceDate >= $startDate && $occurrenceDate <= $endDate) {
$events[] = $this->buildCalendarEvent($evento, $occurrenceDate, $baseUrl);
}
}
$currentMonth->addMonth();
}
return $events;
}
protected function findNthWeekdayOfMonth(\Carbon\Carbon $month, int $occurrence, int $targetDayOfWeek): ?\Carbon\Carbon
{
$firstOfMonth = $month->copy()->startOfMonth();
$firstDayOfWeek = (int) $firstOfMonth->format('w');
$daysUntilTarget = ($targetDayOfWeek - $firstDayOfWeek + 7) % 7;
$firstOccurrence = $firstOfMonth->copy()->addDays($daysUntilTarget);
$targetDate = $firstOccurrence->copy()->addWeeks($occurrence - 1);
if ((int) $targetDate->format('n') !== (int) $month->format('n')) {
return null;
}
return $targetDate;
}
protected function generateAnnualeEvents(Evento $evento, $start, $end, $baseUrl): array
{
$startDate = \Carbon\Carbon::parse($start);
$endDate = \Carbon\Carbon::parse($end);
$events = [];
$year = $startDate->year;
$endYear = $endDate->year;
while ($year <= $endYear + 1) {
$day = $evento->giorno_mese ?? 1;
$eventDate = \Carbon\Carbon::create($year, $evento->mese_annuale, $day);
$daysInMonth = $eventDate->daysInMonth;
if ($evento->giorno_mese && $evento->giorno_mese > $daysInMonth) {
$eventDate = $eventDate->endOfMonth();
}
if ($eventDate >= $startDate && $eventDate <= $endDate) {
$events[] = $this->buildCalendarEvent($evento, $eventDate, $baseUrl);
}
$year++;
}
return $events;
}
protected function generateAltroEvents(Evento $evento, $start, $end, $baseUrl): array
{
$startDate = \Carbon\Carbon::parse($start);
$endDate = \Carbon\Carbon::parse($end);
$events = [];
$current = $startDate->copy()->startOfWeek();
$targetDay = $evento->giorno_settimana;
while ($current <= $endDate) {
$dayOfWeek = (int) $current->format('w');
if ($dayOfWeek === $targetDay && $current >= $startDate && $current <= $endDate) {
$events[] = $this->buildCalendarEvent($evento, $current->copy(), $baseUrl);
}
$current->addDay();
}
return $events;
}
private function getEventoColor(Evento $evento): string
{
$colors = [
'primary' => '#007bff',
'secondary' => '#6c757d',
'success' => '#28a745',
'danger' => '#dc3545',
'warning' => '#ffc107',
'info' => '#17a2b8',
'dark' => '#343a40',
];
if ($evento->isIncroGruppo()) {
return $colors['info'];
}
if ($evento->isRicorrente()) {
return $colors['success'];
}
return $colors['primary'];
}
public function importIcs()
{
$this->authorizeWrite('eventi');
return view('eventi.import');
}
public function importIcsStore(Request $request)
{
$this->authorizeWrite('eventi');
$request->validate([
'file' => 'required|file|mimes:ics,text/calendar',
]);
$file = $request->file('file');
$content = file_get_contents($file->path());
try {
$vcalendar = \Sabre\VObject\Reader::read($content);
} catch (\Exception $e) {
Log::warning('Errore lettura file ICS: ' . $e->getMessage());
return back()->with('error', 'Il file ICS non è valido: ' . $e->getMessage());
}
$imported = 0;
$skipped = 0;
$errors = [];
foreach ($vcalendar->VEVENT as $vevent) {
try {
$uid = (string) ($vevent->UID ?? '');
if (!empty($uid)) {
$existing = Evento::where('uid_esterno', $uid)->first();
if ($existing) {
$skipped++;
continue;
}
}
$nomeEvento = (string) ($vevent->SUMMARY ?? '');
if (empty($nomeEvento)) {
$skipped++;
continue;
}
$dtStart = $vevent->DTSTART ?? null;
$dtEnd = $vevent->DTEND ?? null;
$dataSpecifica = null;
$oraInizio = null;
$durataMinuti = null;
if ($dtStart) {
$dtStartDateTime = new \DateTime((string) $dtStart);
$dataSpecifica = $dtStartDateTime->format('Y-m-d');
$oraInizio = $dtStartDateTime->format('H:i');
if ($dtEnd) {
$dtEndDateTime = new \DateTime((string) $dtEnd);
$durataMinuti = (int) ($dtEndDateTime->getTimestamp() - $dtStartDateTime->getTimestamp()) / 60;
if ($durataMinuti < 1) {
$durataMinuti = null;
}
}
}
$descrizione = (string) ($vevent->DESCRIPTION ?? '');
$luogo = (string) ($vevent->LOCATION ?? '');
Evento::create([
'nome_evento' => $nomeEvento,
'descrizione' => !empty($descrizione) ? $descrizione : null,
'tipo_recorrenza' => 'singolo',
'data_specifica' => $dataSpecifica,
'ora_inizio' => $oraInizio,
'durata_minuti' => $durataMinuti,
'luogo_indirizzo' => !empty($luogo) ? $luogo : null,
'uid_esterno' => !empty($uid) ? $uid : null,
]);
$imported++;
} catch (\Exception $e) {
Log::warning('Errore import evento ICS: ' . $e->getMessage());
$errors[] = 'Errore: ' . $e->getMessage();
}
}
$message = 'Importati ' . $imported . ' eventi.';
if ($skipped > 0) {
$message .= ' Saltati ' . $skipped . ' eventi (già presenti o senza nome).';
}
if (count($errors) > 0) {
return back()->with('error', $message . ' Errori: ' . implode('; ', $errors));
}
return redirect('/eventi')->with('success', $message);
}
}