diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 00000000..389fec3b --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":2,"defects":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":7},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.111,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.015,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.001,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":0.014,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.002,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.002,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":0.004}} \ No newline at end of file diff --git a/MEMORY.md b/MEMORY.md index e08a2b3c..a0db9bf5 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -21,6 +21,7 @@ Laravel 13 / PHP 8.4 project for parish management (Glastree). Full codebase exi ### ⚠️ IN COMPLETAMENTO - Sistema Email: funziona al 95%, SMTP potrebbe necessitare credenziali corrette +- Nuova implementazione: avatar gruppi, event types, email sync logging --- @@ -489,3 +490,30 @@ php artisan route:list --name=email - `app/Http/Controllers/ReportController.php`: righe 389-397 e 532, corretto nome colonna (Last updated: 25 Maggio 2026 - Fix report eventi_calendario, colonna data) + +### 25 Maggio 2026 - Fix Gruppi Table Hierarchy +- **Problema**: La visualizzazione a tabella dei gruppi non mostrava correttamente la gerarchia padre-figlio +- **Soluzione**: + - Aggiunto metodo `sortGruppiHierarchically()` in `GruppoController` per ordinare i gruppi in modo gerarchico + - La tabella ora mostra prima i gruppi root, poi i loro figli in ordine annidato + - Aggiunta eager loading `avatar` per supportare il futuro avatar dei gruppi +- **Files modificati**: + - `app/Http/Controllers/GruppoController.php`: aggiunto `sortGruppiHierarchically()`, `addChildrenRecursive()`, aggiunto 'avatar' a eager loading + +### 25 Maggio 2026 - Group Avatar Support +- **Funzionalità**: I gruppi ora possono avere un avatar/logo come gli individui +- **Implementazione**: + - Aggiunto metodo `avatar()` in `Gruppo.php` (relazione HasOne a Documento con tipologia 'avatar') + - Aggiunto accessor `getAvatarUrlAttribute()` per ottenere l'URL dell'avatar + - Eager loading `avatar` aggiunto in `GruppoController@index()` + - View `gruppi/index.blade.php` aggiornata per mostrare l'avatar nella tabella + - View `gruppi/show.blade.php` aggiornata per mostrare l'avatar del gruppo + - View `gruppi/create.blade.php` e `edit.blade.php` aggiornate per permettere upload avatar +- **Files modificati**: + - `app/Models/Gruppo.php`: aggiunto `avatar()`, `getAvatarUrlAttribute()` + - `app/Http/Controllers/GruppoController.php`: aggiunto eager loading `avatar` + - `resources/views/gruppi/index.blade.php`: colonna avatar nella tabella + - `resources/views/gruppi/show.blade.php`: avatar nella header card + - `resources/views/gruppi/create.blade.php` e `edit.blade.php`: upload avatar + +(Last updated: 25 Maggio 2026 - Fix gruppi hierarchy, group avatar support) diff --git a/app/Http/Controllers/DocumentoController.php b/app/Http/Controllers/DocumentoController.php index e040e095..1a47f76b 100644 --- a/app/Http/Controllers/DocumentoController.php +++ b/app/Http/Controllers/DocumentoController.php @@ -192,11 +192,19 @@ class DocumentoController extends Controller $fullPath = Storage::disk('local')->path($documento->file_path); $mimeType = $documento->mime_type; - if (str_starts_with($mimeType, 'image/')) { + $previewableMimeTypes = [ + 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', + 'application/pdf', + 'text/plain', 'text/html', 'text/csv', + ]; + + if (in_array($mimeType, $previewableMimeTypes)) { return response()->file($fullPath, ['Content-Type' => $mimeType]); } - return response()->file($fullPath, ['Content-Type' => $mimeType]); + return response()->view('documenti.preview-fallback', [ + 'documento' => $documento, + ])->header('X-Preview-Fallback', 'true'); } public function destroy($documento) diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 7798dcd8..658ad289 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -342,8 +342,11 @@ class EmailController extends Controller public function syncEmails() { + \Illuminate\Support\Facades\Log::info('Email sync started'); + $settings = EmailSetting::getActive(); if (!$settings) { + \Illuminate\Support\Facades\Log::warning('Email sync aborted: no active email configuration'); throw new \Exception('Nessuna configurazione email attiva'); } @@ -351,6 +354,8 @@ class EmailController extends Controller $mailbox = $this->connectImap($settings); + \Illuminate\Support\Facades\Log::info('IMAP connection established', ['host' => $settings->imap_host]); + $folderMappings = [ 'INBOX' => 'inbox', 'Sent' => 'sent', @@ -362,11 +367,19 @@ class EmailController extends Controller '[Gmail]/Trash' => 'trash', ]; + $totalProcessed = 0; + $totalNew = 0; + foreach (array_keys($folderMappings) as $imapFolderName) { try { $folder = $mailbox->folders()->find($imapFolderName); - if (!$folder) continue; + if (!$folder) { + \Illuminate\Support\Facades\Log::debug('IMAP folder not found', ['folder' => $imapFolderName]); + continue; + } + \Illuminate\Support\Facades\Log::info('Processing IMAP folder', ['folder' => $imapFolderName]); + $messages = $folder->messages() ->withHeaders() ->withBody() @@ -374,14 +387,31 @@ class EmailController extends Controller ->limit(100) ->get(); + \Illuminate\Support\Facades\Log::info('Messages fetched from folder', ['folder' => $imapFolderName, 'count' => count($messages)]); + foreach ($messages as $imapMessage) { + $totalProcessed++; + $messageId = $imapMessage->messageId() ?? uniqid('msg_'); + $existing = EmailMessage::where('message_id', $messageId)->first(); + + if ($existing) { + continue; + } + $this->processImapMessage($imapMessage, $imapFolderName); + $totalNew++; } } catch (\Exception $e) { + \Illuminate\Support\Facades\Log::error('Error processing IMAP folder', ['folder' => $imapFolderName, 'error' => $e->getMessage()]); continue; } } + \Illuminate\Support\Facades\Log::info('Email sync completed', [ + 'processed' => $totalProcessed, + 'new' => $totalNew, + ]); + return true; } diff --git a/app/Http/Controllers/EventoController.php b/app/Http/Controllers/EventoController.php index f3625dc4..2ee5ae59 100644 --- a/app/Http/Controllers/EventoController.php +++ b/app/Http/Controllers/EventoController.php @@ -67,14 +67,15 @@ class EventoController extends Controller '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|string', + '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|boolean', + 'is_incontro_gruppo' => 'nullable', 'gruppi' => 'nullable|array', 'gruppi.*' => 'exists:gruppi,id', 'responsabili' => 'nullable|array', @@ -144,14 +145,15 @@ class EventoController extends Controller '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|string', + '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|boolean', + 'is_incontro_gruppo' => 'nullable', 'gruppi' => 'nullable|array', 'gruppi.*' => 'exists:gruppi,id', 'responsabili' => 'nullable|array', @@ -237,40 +239,168 @@ class EventoController extends Controller foreach ($eventi as $evento) { if ($evento->tipo_recorrenza === 'singolo' || $evento->tipo_recorrenza === null) { if ($evento->data_specifica) { - $events[] = [ - 'id' => $evento->id, - 'title' => $evento->nome_evento, - 'start' => $evento->data_specifica->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), - ]; + $events[] = $this->buildCalendarEvent($evento, $evento->data_specifica); } } elseif ($evento->tipo_recorrenza === 'settimanale' && $evento->giorno_settimana !== null) { - $startDate = \Carbon\Carbon::parse($start); - $endDate = \Carbon\Carbon::parse($end); - - $current = $startDate->copy(); - while ($current <= $endDate) { - $nextOccurrence = $current->copy()->next($evento->giorno_settimana); - if ($nextOccurrence >= $startDate && $nextOccurrence <= $endDate) { - $events[] = [ - 'id' => $evento->id . '_' . $nextOccurrence->format('Ymd'), - 'title' => $evento->nome_evento, - 'start' => $nextOccurrence->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), - ]; - } - $current->addWeek(); - } + $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 = [ diff --git a/app/Http/Controllers/GruppoAvatarController.php b/app/Http/Controllers/GruppoAvatarController.php new file mode 100644 index 00000000..28b654c9 --- /dev/null +++ b/app/Http/Controllers/GruppoAvatarController.php @@ -0,0 +1,77 @@ +authorizeWrite('gruppi'); + + $request->validate([ + 'avatar' => 'required|image|mimes:jpeg,png,gif,webp|max:2048', + ]); + + $existing = $gruppo->avatar; + if ($existing) { + if ($existing->file_path && Storage::disk('local')->exists($existing->file_path)) { + Storage::disk('local')->delete($existing->file_path); + } + $existing->delete(); + } + + $file = $request->file('avatar'); + $path = $file->store('avatars/gruppi', 'local'); + + $documento = Documento::create([ + 'user_id' => auth()->id(), + 'nome_file' => $file->getClientOriginalName(), + 'file_path' => $path, + 'tipologia' => 'avatar', + 'visibilita' => 'gruppo', + 'visibilita_target_id' => $gruppo->id, + 'visibilita_target_type' => Gruppo::class, + 'mime_type' => $file->getMimeType(), + 'dimensione' => $file->getSize(), + ]); + + $gruppo->avatar()->save($documento); + + return response()->json(['success' => true, 'avatar_url' => $documento->file_path]); + } + + public function show(Gruppo $gruppo) + { + $this->authorizeRead('gruppi'); + + $avatar = $gruppo->avatar; + if (!$avatar || !$avatar->file_path || !Storage::disk('local')->exists($avatar->file_path)) { + abort(404); + } + + $fullPath = Storage::disk('local')->path($avatar->file_path); + return response()->file($fullPath, ['Content-Type' => $avatar->mime_type]); + } + + public function destroy(Gruppo $gruppo) + { + $this->authorizeWrite('gruppi'); + + $avatar = $gruppo->avatar; + if ($avatar) { + if ($avatar->file_path && Storage::disk('local')->exists($avatar->file_path)) { + Storage::disk('local')->delete($avatar->file_path); + } + $avatar->delete(); + } + + return response()->json(['success' => true]); + } +} diff --git a/app/Http/Controllers/GruppoController.php b/app/Http/Controllers/GruppoController.php index d6e53998..bd4c2303 100644 --- a/app/Http/Controllers/GruppoController.php +++ b/app/Http/Controllers/GruppoController.php @@ -1,5 +1,7 @@ orderBy('nome') - ->get() - ->map(function ($gruppo) { - $depth = 0; - $parent = $gruppo->parent; - while ($parent) { - $depth++; - $parent = $parent->parent; - } - $gruppo->depth = $depth; - return $gruppo; - }); - - if ($viewMode === 'table') { - $gruppi = $this->sortGruppiHierarchically($allGruppi); - } else { - $gruppi = $allGruppi; - } - - return view('gruppi.index', compact( - 'gruppi', - 'allColumns', - 'visibleColumns', - 'viewMode', - 'vista' - )); - } - - protected function sortGruppiHierarchically($gruppi) - { - $rootGroups = $gruppi->filter(fn($g) => $g->parent_id === null)->sortBy('nome'); - $sorted = collect(); - - foreach ($rootGroups as $root) { - $sorted->push($root); - $this->addChildrenRecursive($root, $gruppi, $sorted); - } - - return $sorted; - } - - protected function addChildrenRecursive($parent, $allGruppi, &$sorted) - { - $children = $allGruppi->filter(fn($g) => $g->parent_id === $parent->id)->sortBy('nome'); - - foreach ($children as $child) { - $sorted->push($child); - $this->addChildrenRecursive($child, $allGruppi, $sorted); - } - } - - $allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui']) - ->orderBy('nome') - ->get() - ->map(function ($gruppo) { - $depth = 0; - $parent = $gruppo->parent; - while ($parent) { - $depth++; - $parent = $parent->parent; - } - $gruppo->depth = $depth; - return $gruppo; - }); - - if ($viewMode === 'table') { - $gruppi = $this->sortGruppiHierarchically($allGruppi); - } else { - $gruppi = $allGruppi; - } - - return view('gruppi.index', compact( - 'gruppi', - 'allColumns', - 'visibleColumns', - 'viewMode', - 'vista' - )); - } - - protected function sortGruppiHierarchically($gruppi) - { - $rootGroups = $gruppi->filter(fn($g) => $g->parent_id === null)->sortBy('nome'); - $sorted = collect(); - - foreach ($rootGroups as $root) { - $sorted->push($root); - $this->addChildrenRecursive($root, $gruppi, $sorted); - } - - return $sorted; - } - - protected function addChildrenRecursive($parent, $allGruppi, &$sorted) - { - $children = $allGruppi->filter(fn($g) => $g->parent_id === $parent->id)->sortBy('nome'); - - foreach ($children as $child) { - $sorted->push($child); - $this->addChildrenRecursive($child, $allGruppi, $sorted); - } - } - - $allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui']) + $allGruppi = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar']) ->orderBy('nome') ->get() ->map(function ($gruppo) { @@ -233,31 +131,31 @@ class GruppoController extends Controller return redirect('/gruppi/' . $gruppo->id)->with('success', 'Gruppo creato con successo.'); } - public function show($gruppo) + public function show($id) { $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'); - }, 'eventi' => function ($q) { + }, 'avatar', 'eventi' => function ($q) { $q->where('is_incontro_gruppo', true); - }, 'eventi.responsabili.contatti'])->findOrFail($gruppo); + }, 'eventi.responsabili.contatti'])->findOrFail($id); return view('gruppi.show', compact('gruppo')); } - public function edit($gruppo) + public function edit($id) { $this->authorizeWrite('gruppi'); - $gruppo = Gruppo::with(['individui.contatti', 'individui.documenti'])->findOrFail($gruppo); + $gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar'])->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')); } - public function update(Request $request, $gruppo) + public function update(Request $request, $id) { $this->authorizeWrite('gruppi'); - $gruppo = Gruppo::findOrFail($gruppo); + $gruppo = Gruppo::findOrFail($id); $data = $request->validate([ 'nome' => 'required|string|max:255', 'descrizione' => 'nullable|string', @@ -295,10 +193,10 @@ class GruppoController extends Controller return redirect('/gruppi/' . $gruppo->id)->with('success', 'Gruppo aggiornato.'); } - public function destroy($gruppo) + public function destroy($id) { $this->authorizeDelete('gruppi'); - $gruppo = Gruppo::with('children', 'individui')->findOrFail($gruppo); + $gruppo = Gruppo::with('children', 'individui')->findOrFail($id); $hasChildren = $gruppo->children()->count() > 0; $isSuperAdmin = auth()->user()->isSuperAdmin(); @@ -393,13 +291,13 @@ class GruppoController extends Controller return response()->json(['success' => true, 'vista_id' => $vista->id]); } - public function deleteVista($vista) + public function deleteVista($id) { $vista = \App\Models\VistaReport::where('user_id', auth()->id()) - ->where('id', $vista) + ->where('id', $id) ->firstOrFail(); $vista->delete(); return redirect()->route('gruppi.index')->with('success', 'Vista eliminata.'); } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 27defde1..6d703fee 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -121,6 +121,47 @@ class ReportController extends Controller return redirect()->route('report.index')->with('success', 'Report personalizzato eliminato.'); } + public function printPreview(Request $request) + { + if (!auth()->user()->canAccess('report')) { + abort(403); + } + + $reportType = $request->input('report'); + $customReportId = $request->input('custom_id'); + + if ($customReportId) { + $report = \App\Models\ReportCustom::findOrFail($customReportId); + if ($report->user_id !== auth()->id()) { + abort(403); + } + $data = $this->runCustomReport($report); + return view('report.print-preview', compact('reportType', 'data', 'report')); + } + + $data = match ($reportType) { + 'individui_per_genere' => $this->reportIndividuiPerGenere(), + 'individui_per_eta' => $this->reportIndividuiPerEta(), + 'gruppi_gerarchia' => $this->reportGruppiGerarchia(), + 'gruppi_membri' => $this->reportGruppiMembri(), + 'eventi_calendario' => $this->reportEventiCalendario(), + 'documenti_per_tipo' => $this->reportDocumentiPerTipo(), + 'contatti_per_tipo' => $this->reportContattiPerTipo(), + 'mailing_list_dettaglio' => $this->reportMailingListDettaglio(), + 'individui_senza_contatti' => $this->reportIndividuiSenzaContatti(), + 'individui_senza_gruppo' => $this->reportIndividuiSenzaGruppo(), + 'eventi_per_gruppo' => $this->reportEventiPerGruppo(), + 'documenti_orfani' => $this->reportDocumentiOrfani(), + default => null, + }; + + if (!$data) { + return redirect()->route('report.index')->with('error', 'Report non trovato.'); + } + + return view('report.print-preview', compact('reportType', 'data')); + } + public function exportCSV(Request $request) { if (!auth()->user()->canAccess('report')) { diff --git a/app/Models/Evento.php b/app/Models/Evento.php index 7eaa5371..27fc004f 100644 --- a/app/Models/Evento.php +++ b/app/Models/Evento.php @@ -127,7 +127,15 @@ class Evento extends Model public function getOccorrenzeMensiliList(): array { - $giorni = ['Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica']; + $giorni = [ + 0 => 'Domenica', + 1 => 'Lunedì', + 2 => 'Martedì', + 3 => 'Mercoledì', + 4 => 'Giovedì', + 5 => 'Venerdì', + 6 => 'Sabato', + ]; $list = []; for ($occ = 1; $occ <= 4; $occ++) { foreach ($giorni as $idx => $giorno) { @@ -153,7 +161,15 @@ class Evento extends Model $occorrenza = (int) $parts[0]; $giorno = (int) $parts[1]; - $giorni = ['Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato', 'Domenica']; + $giorni = [ + 0 => 'Domenica', + 1 => 'Lunedì', + 2 => 'Martedì', + 3 => 'Mercoledì', + 4 => 'Giovedì', + 5 => 'Venerdì', + 6 => 'Sabato', + ]; return $occorrenza . '° ' . ($giorni[$giorno] ?? '-'); } @@ -199,7 +215,13 @@ class Evento extends Model } $mesiLabels = $this->getMesiList(); - return $mesiLabels[$this->mese_annuale]['label'] ?? '-'; + $label = $mesiLabels[$this->mese_annuale]['label'] ?? '-'; + + if ($this->giorno_mese) { + $label .= ' (giorno ' . $this->giorno_mese . ')'; + } + + return $label; } public function getPeriodicitaLabelAttribute(): string diff --git a/app/Models/Gruppo.php b/app/Models/Gruppo.php index 2483ae8b..2470270d 100644 --- a/app/Models/Gruppo.php +++ b/app/Models/Gruppo.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Gruppo extends Model @@ -48,24 +49,47 @@ class Gruppo extends Model ->withTimestamps(); } + public function avatar(): HasOne + { + return $this->hasOne(Documento::class, 'visibilita_target_id') + ->where('tipologia', 'avatar') + ->where('visibilita_target_type', self::class); + } + + public function getAvatarUrlAttribute(): ?string + { + if ($this->avatar && $this->avatar->file_path) { + return url('/gruppi/' . $this->id . '/avatar-file'); + } + return null; + } + + public function getResponsabili() + { + if (empty($this->responsabile_ids)) { + return collect(); + } + + $ids = is_array($this->responsabile_ids) + ? $this->responsabile_ids + : json_decode($this->responsabile_ids, true); + + if (empty($ids)) { + return collect(); + } + + return Individuo::whereIn('id', $ids)->get(); + } + public function getResponsabiliIds(): array { if (empty($this->responsabile_ids)) { return []; } - if (is_array($this->responsabile_ids)) { - return $this->responsabile_ids; - } - return json_decode($this->responsabile_ids, true) ?? []; - } - public function getResponsabili(): \Illuminate\Database\Eloquent\Collection - { - $ids = $this->getResponsabiliIds(); - if (empty($ids)) { - return new \Illuminate\Database\Eloquent\Collection(); - } - return Individuo::whereIn('id', $ids)->get(); + return is_array($this->responsabile_ids) + ? $this->responsabile_ids + : json_decode($this->responsabile_ids, true) ?? []; } public function eventi(): BelongsToMany diff --git a/database/migrations/2026_05_26_082316_change_occorrenza_mese_to_string_in_eventi.php b/database/migrations/2026_05_26_082316_change_occorrenza_mese_to_string_in_eventi.php new file mode 100644 index 00000000..492c242e --- /dev/null +++ b/database/migrations/2026_05_26_082316_change_occorrenza_mese_to_string_in_eventi.php @@ -0,0 +1,22 @@ +string('occorrenza_mese', 10)->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('eventi', function (Blueprint $table) { + $table->unsignedTinyInteger('occorrenza_mese')->nullable()->change(); + }); + } +}; diff --git a/resources/views/documenti/preview-fallback.blade.php b/resources/views/documenti/preview-fallback.blade.php new file mode 100644 index 00000000..eff0d190 --- /dev/null +++ b/resources/views/documenti/preview-fallback.blade.php @@ -0,0 +1,86 @@ + + +
+ + +| {{ $header }} | + @endforeach +
|---|
| {{ is_array($cell) ? json_encode($cell) : $cell }} | + @endforeach +
| Nome | -Diocesi | -Ruolo | -Data Adesione | -Azioni | -
|---|---|---|---|---|
| - - nome); ?> - | -diocesi?->nome ?: '-'); ?> | -- getRuoliForGruppo($gruppo->id) ?> - count() > 0): ?> - addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> - nome); ?> - popLoop(); $loop = $__env->getLastLoop(); ?> - - - - - | -pivot->data_adesione ? \Carbon\Carbon::parse($gruppo->pivot->data_adesione)->format('d/m/Y') : '-'); ?> | -- - - | -
Nessun gruppo associato
-| Nome | -Tipologia | -Dimensione | -Data Upload | -Azioni | -
|---|---|---|---|---|
| - - - nome_file); ?> - - - | -tipologia))); ?> | -dimensione / 1024, 1)); ?> KB | -created_at->format('d/m/Y')); ?> | -- file_path): ?> - - - - | -
Nessun documento
-| - | - | - - - - Destinatario - - - - - Mittente - - - - | -- - Oggetto - - - | -- - Data - - - | -
|---|---|---|---|---|
|
-
- Nessuna email - |
- ||||
| Nome | - - -Descrizione | - - -Diocesi | - - -Livello | - - -Gruppo Padre | - - -Membri | - - -Responsabili | - - -Indirizzo | - - -Città | - - -Telefono | - - -Azioni | -|
|---|---|---|---|---|---|---|---|---|---|---|---|
| - - - nome); ?> - - | - - -descrizione, 50) ?: '-'); ?> | - - -diocesi?->nome ?? '-'); ?> | - - -depth ?? 0); ?> | - - -parent?->nome ?? '-'); ?> | - - -individui->count()); ?> | - - -- getResponsabili() ?> - count() > 0): ?> - pluck('cognome')->implode(', ')); ?> - - - - - - | - - -indirizzo_incontro ?: '-'); ?> | - - -città_incontro ?: '-'); ?> | - - -- | - - -- | - -- - - - - - - - - - - - | -
Nessun gruppo presente. - - Crea il primo gruppo - -
-Scarica il template CSV per l'importazione:
- - Scarica Template - -cognomenomedata_nascita (formato YYYY-MM-DD)indirizzocapcittàsigla_provincia (2 lettere)genere (M o F)tipo_documento (carta_identita, patente)numero_documentoscadenza_documento (YYYY-MM-DD)notecontatto_1_tipo (email, telefono, cellulare)contatto_1_valorecontatto_1_etichetta (personale, lavoro)contatto_2_* (secondo contatto)Codice: codice_id); ?>
-Data di nascita: data_nascita?->format('d/m/Y') ?: '-'); ?>
-Genere: genere === 'M' ? 'Maschio' : ($individuo->genere === 'F' ? 'Femmina' : '-')); ?>
-Indirizzo: indirizzo ?: '-'); ?>
-CAP: cap ?: '-'); ?>
-Città: città ?: '-'); ?>
-Provincia: sigla_provincia ?: '-'); ?>
-Tipo: tipo_documento ? ucfirst(str_replace('_', ' ', $individuo->tipo_documento)) : '-'); ?>
-Numero: numero_documento ?: '-'); ?>
-Scadenza: scadenza_documento?->format('d/m/Y') ?: '-'); ?>
- hasDocumentoScaduto()): ?> -Documento scaduto da giorni_scadenza_documento); ?> giorni
- hasDocumentoScadeEntroGiorni(30)): ?> -Scade tra giorni_scadenza_documento); ?> giorni
- -note)); ?>
-| Tipo | -Valore | -Etichetta | -Primario | -Azioni | -
|---|---|---|---|---|
| tipo); ?> | -- tipo === 'email'): ?> - valore); ?> - tipo, ['telefono', 'cellulare', 'whatsapp'])): ?> - valore); ?> - - valore); ?> - - valore); ?> - - - | -etichetta ?: '-'); ?> | -- is_primary): ?> - Sì - - No - - | -- - - | -
Nessun contatto
-| Nome | -Diocesi | -Responsabile | -Ruolo | -Data Adesione | -Azioni | -
|---|---|---|---|---|---|
| - - nome); ?> - | -diocesi?->nome ?: '-'); ?> | -getResponsabili()->isNotEmpty() ? $gruppo->getResponsabili()->first()->nome_completo : '-'); ?> | -- pivot->ruolo_ids ? json_decode($gruppo->pivot->ruolo_ids, true) : []; - $ruoli = \App\Models\Ruolo::findByIds($ruoloIds); - ?> - count() > 0): ?> - addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> - nome); ?> - popLoop(); $loop = $__env->getLastLoop(); ?> - - - - - | -pivot->data_adesione ? \Carbon\Carbon::parse($gruppo->pivot->data_adesione)->format('d/m/Y') : '-'); ?> | -- - - | -
Nessun gruppo associato
-| Nome | -Tipologia | -Dimensione | -Data Upload | -Azioni | -
|---|---|---|---|---|
| - - - nome_file); ?> - - - | -tipologia))); ?> | -dimensione / 1024, 1)); ?> KB | -created_at->format('d/m/Y')); ?> | -- file_path): ?> - - - - | -
Nessun documento
-smtp.gmail.com sulla porta 587 con TLS
- e genera una App Password.
- | Data/Ora | -Utente | -Azione | -Modulo | -Descrizione | -IP | -
|---|---|---|---|---|---|
| created_at->format('d/m/Y H:i:s')); ?> | -user->name ?? 'Sistema'); ?> | -- - action); ?> - - - | -module); ?> | -description); ?> | -ip_address); ?> | -
| Nessun log presente | -|||||
|
-
-
-
-
-
-
- |
- Nome | -Descrizione | -Contatti | -Stato | -Creata il | -Azioni | -
|---|---|---|---|---|---|---|
|
-
-
-
-
-
-
- |
- - nome); ?> - | -descrizione ?: '-'); ?> | -- contatti->count()); ?> - | -- attiva): ?> - Attiva - - Disattiva - - | -created_at->format('d/m/Y')); ?> | -
-
-
-
-
-
-
-
- |
-
Nessuna mailing list
- - Crea la prima lista - -| Individuo | -Codice | -Contatti | -Ruolo | -Data Adesione | -- |
|---|
Nessun membro aggiunto. Clicca su "Aggiungi Membro" per iniziare.
-Nessuna notifica
- -Benvenuto in .
- -Dal menu laterale puoi navigare tra le sezioni:
-