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(); return view('eventi.index', compact('eventi', 'gruppi', 'allTags')); } 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); } }