diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 389fec3b..1a5d358c 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +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 +{"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.034,"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.025,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.001,"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 55336cc9..5508b2bb 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -548,4 +548,94 @@ php artisan route:list --name=email - `resources/views/eventi/show.blade.php` - `resources/views/eventi/index.blade.php` -(Last updated: 26 Maggio 2026 - Event Types system) +### 26 Maggio 2026 - ACL Fixes & Form Modules Sync +- **Problema**: Form admin utenti (create/edit) aveva lista moduli hardcoded e mancante di `'report'`; user 4 (fbarachino) non aveva permesso `report` né `is_admin` +- **Fix form edit/create**: Sostituito `@foreach(['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste'] as $module)` con `@foreach(\App\Models\User::MODULES as $module)` in entrambe le view (`edit.blade.php:76`, `create.blade.php:66`) — ora la lista è sempre allineata alla costante `User::MODULES` +- **Fix user 4**: + - Aggiunto `'report' => 2` alle permissions (full access) + - Settato `is_admin = true` → `isSuperAdmin()` = true, bypass totale ACL + - `canAccess('report')` = true, tutti i menu sidebar ora visibili +- **Nuovo comando**: `app/Console/Commands/SyncUserPermissions.php` — `php artisan users:sync-permissions` + - Itera tutti gli utenti (inclusi superadmin) e aggiunge con default 0 ogni modulo mancante in `User::MODULES` + - Previene discrepanze future tra permissions salvate e MODULES costante +- **Files modificati**: + - `resources/views/admin/utenti/edit.blade.php`: linea 76, hardcoded → `User::MODULES` + - `resources/views/admin/utenti/create.blade.php`: linea 66, hardcoded → `User::MODULES` +- **File creato**: + - `app/Console/Commands/SyncUserPermissions.php`: nuovo comando artisan +- **Stato sidebar user 4**: `isSuperAdmin()` = true → tutti i menu visibili, `canAccess('report')` = true + +### 26 Maggio 2026 - ACL 'settings' Module (Impostazioni/Admin Permission) +- **Problema**: Non esisteva un permesso ACL specifico per accedere al menu Impostazioni e Admin. L'accesso era legato esclusivamente a `isSuperAdmin()` bypass totale. Inoltre l'`ImpostazioniController` usava `authorizeWrite('viste')` come piggyback scorretto. +- **Soluzione**: Nuovo modulo `settings` separato, slegato da `viste` e da `isSuperAdmin()`. +- **Modifiche**: + - `User.php`: aggiunto `'settings'` a `MODULES` (costante linea 36) + - `adminlte.blade.php` sidebar: `@if(Auth::user()->isSuperAdmin())` → `@if(Auth::user()->canManage('settings'))` per menu Impostazioni e Admin (linee 205-226) + - `AdminOnly.php` middleware: `isSuperAdmin()` → `isSuperAdmin() || canManage('settings')` — permette accesso a `/admin/*` anche a non-superadmin con permesso settings:2 + - `ImpostazioniController.php`: rimpiazzate tutte le 16 occorrenze di `authorizeWrite('viste')` e `authorizeDelete('viste')` con `'settings'` + - `EmailSettingsController.php`: aggiunto `$this->authorizeWrite('settings')` in tutti i 5 metodi pubblici (index, save, testConnection, testSmtp, syncNow) — prima non avevano alcuna autorizzazione esplicita + - `SyncUserPermissions.php` aggiornato: ora include anche superadmin nella sync (per consistenza dati) + - Eseguito `php artisan users:sync-permissions` → user 1 e 4 aggiornati con `settings: 0` +- **Comportamento risultante**: + - `isSuperAdmin()` bypassa sempre tutto (base Controller.php) + - Utente normale con `settings:2` (Completo) → vede menu Impostazioni+Admin, può operare + - Utente normale con `settings:0` → menu nascosto, 403 su accesso diretto URL + - `settings:1` (Lettura) non basta per operazioni di scrittura (sidebar non mostra il menu perché richiede `canManage`) +- **Files modificati**: + - `app/Models/User.php`: MODULES + 'settings' + - `resources/views/layouts/adminlte.blade.php`: isSuperAdmin → canManage('settings') + - `app/Http/Middleware/AdminOnly.php`: isSuperAdmin || canManage('settings') + - `app/Http/Controllers/ImpostazioniController.php`: viste → settings in 16 authorize calls + - `app/Http/Controllers/Admin/EmailSettingsController.php`: aggiunte 5 authorizeWrite('settings') + - `app/Console/Commands/SyncUserPermissions.php`: superadmin inclusi nella sync + +### 26 Maggio 2026 - Fix Salvataggio is_admin (Checkbox Superadmin) +- **Problema**: Non era possibile togliere il flag "Superadmin (bypass ACL)" dall'interfaccia admin. Il flag rimaneva sempre true dopo il salvataggio. +- **Cause** (3 bug concatenati): + 1. **`url()` assoluto** nell'action del form edit (`url('/admin/utenti/' . $user->id)`) → da accesso remoto generava `http://localhost/admin/utenti/4`, il POST falliva silenziosamente + 2. **Nessun hidden field** per unchecked checkbox → quando la checkbox veniva deselezionata, il campo `is_admin` non veniva inviato nella richiesta. Anche se il controller aveva `?? false`, il combinato col bug #1 impediva il salvataggio + 3. **Mancanza `old()`** per lo stato checkbox → su validation error redirect, la checkbox mostrava il valore DB invece del valore inviato +- **Fix applicati**: + - `edit.blade.php`: `url(...)` → path relativo `/admin/utenti/...` (linea 20); aggiunto hidden `` prima della checkbox; `old()` per stato checkbox e status/role_preset_id + - `create.blade.php`: aggiunto hidden field come sopra; default per `settings` = `'0'` (Nessuno) invece di `'1'` (Lettura) per nuovi utenti + - `UtenteController.php` (store + update): rimosso `?? false` da `$user->is_admin` — ora usa direttamente `$validated['is_admin']` (sempre presente grazie all'hidden field) + - `AuthController.php`: aggiunti `'report' => 0` e `'settings' => 0` ai default permissions per nuove registrazioni (sia first-user che non) +- **Files modificati**: + - `resources/views/admin/utenti/edit.blade.php`: form action, hidden field, old() + - `resources/views/admin/utenti/create.blade.php`: hidden field, settings default 0 + - `app/Http/Controllers/Admin/UtenteController.php`: removed ?? false + - `app/Http/Controllers/Auth/AuthController.php`: added report/settings defaults + +(Last updated: 26 Maggio 2026 - Fix Salvataggio is_admin) + +### 26 Maggio 2026 - Export ICS/iCal Eventi +- **Nuovo Service**: `app/Services/IcsExportService.php` — generazione file ICS RFC 5545 + - `generateSingle(Evento)` — esporta un singolo evento + - `generateCollection(iterable)` — esporta multipli eventi in un unico file ICS + - Supporto **RRULE** completo per eventi ricorrenti: + - `settimanale` → `FREQ=WEEKLY;BYDAY=MO` + - `mensile` → `FREQ=MONTHLY;BYSETPOS=1;BYDAY=SU` (con opzionale `BYMONTH`) + - `annuale` → `FREQ=YEARLY;BYMONTH=6;BYMONTHDAY=15` + - `altro` → `FREQ=WEEKLY;BYDAY=MO` + - Timezone `Europe/Rome` per eventi con ora, DATE (all-day) per eventi senza ora + - LOCATION, DESCRIPTION, SUMMARY, UID univoci + - Line folding a 75 caratteri RFC 5545 + - Escape di `;`, `,`, `\`, `\n` +- **Nuovi metodi in `EventoController`**: + - `exportIcs(int $id, IcsExportService $ics)` — StreamedResponse download singolo evento + - `exportSelectedIcs(Request $request, IcsExportService $ics)` — StreamedResponse con filtro per ID selezionati o ricerca corrente +- **Routes**: + - `GET /eventi/{evento}/export-ics` → `eventi.export-ics` + - `POST /eventi/export-ics` → `eventi.export-selected-ics` +- **View updates**: + - `show.blade.php`: pulsante "Esporta ICS" nella toolbar azioni + - `index.blade.php`: pulsante "Esporta ICS" nella card-tools; se ci sono righe selezionate esporta solo quelle, altrimenti esporta tutti i risultati della ricerca corrente + - `calendar.blade.php`: pulsante "Esporta ICS" che esporta tutti gli eventi +- **Files creati**: + - `app/Services/IcsExportService.php` +- **Files modificati**: + - `app/Http/Controllers/EventoController.php` + - `routes/web.php` + - `resources/views/eventi/show.blade.php` + - `resources/views/eventi/index.blade.php` + - `resources/views/eventi/calendar.blade.php` diff --git a/app/Console/Commands/SyncUserPermissions.php b/app/Console/Commands/SyncUserPermissions.php new file mode 100644 index 00000000..a2121986 --- /dev/null +++ b/app/Console/Commands/SyncUserPermissions.php @@ -0,0 +1,42 @@ +permissions ?? []; + $changed = false; + + foreach (User::MODULES as $module) { + if (!array_key_exists($module, $permissions)) { + $permissions[$module] = User::LEVEL_NONE; + $changed = true; + } + } + + if ($changed) { + $user->permissions = $permissions; + $user->save(); + $this->info("Synced permissions for user ID {$user->id} ({$user->email})"); + $count++; + } + } + + $this->info("Done. {$count} users updated."); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Admin/EmailSettingsController.php b/app/Http/Controllers/Admin/EmailSettingsController.php index dd741672..77941307 100644 --- a/app/Http/Controllers/Admin/EmailSettingsController.php +++ b/app/Http/Controllers/Admin/EmailSettingsController.php @@ -11,12 +11,14 @@ class EmailSettingsController extends Controller { public function index() { + $this->authorizeWrite('settings'); $settings = EmailSetting::first() ?? new EmailSetting(); return view('admin.email-settings.index', compact('settings')); } public function save(Request $request) { + $this->authorizeWrite('settings'); $validated = $request->validate([ 'imap_host' => 'required|string|max:255', 'imap_port' => 'required|integer|min:1|max:65535', @@ -62,6 +64,7 @@ class EmailSettingsController extends Controller public function testConnection(Request $request) { + $this->authorizeWrite('settings'); $settings = EmailSetting::first(); if (!$settings) { @@ -87,6 +90,7 @@ class EmailSettingsController extends Controller public function testSmtp(Request $request) { + $this->authorizeWrite('settings'); $request->validate([ 'email' => 'required|email', ]); @@ -164,6 +168,7 @@ class EmailSettingsController extends Controller public function syncNow() { + $this->authorizeWrite('settings'); $settings = EmailSetting::getActive(); if (!$settings) { diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index fe666baa..d5c66f42 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -67,6 +67,8 @@ class AuthController extends Controller 'documenti' => 1, 'mailing' => 1, 'viste' => 1, + 'report' => 0, + 'settings' => 0, ]; if ($isFirstUser) { @@ -75,9 +77,11 @@ class AuthController extends Controller 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, - 'mailing' => 2, - 'viste' => 2, - ]; + 'mailing' => 2, + 'viste' => 2, + 'report' => 0, + 'settings' => 0, + ]; } $user = User::create([ diff --git a/app/Http/Controllers/EventoController.php b/app/Http/Controllers/EventoController.php index 884e8de2..38fe7e2f 100644 --- a/app/Http/Controllers/EventoController.php +++ b/app/Http/Controllers/EventoController.php @@ -6,7 +6,9 @@ use App\Models\Evento; use App\Models\Gruppo; use App\Models\Individuo; use App\Models\TipologiaEvento; +use App\Services\IcsExportService; use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\StreamedResponse; class EventoController extends Controller { @@ -219,6 +221,55 @@ class EventoController extends Controller 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'); diff --git a/app/Http/Controllers/ImpostazioniController.php b/app/Http/Controllers/ImpostazioniController.php index 7d23116e..7afaa2b0 100644 --- a/app/Http/Controllers/ImpostazioniController.php +++ b/app/Http/Controllers/ImpostazioniController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\AppSetting; +use App\Models\EmailSetting; use App\Models\Ruolo; use App\Models\TipologiaDocumento; use App\Models\TipologiaEvento; @@ -13,19 +14,20 @@ class ImpostazioniController extends Controller { public function index() { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $tipologie = TipologiaDocumento::orderBy('ordine')->get(); $tipologieEventi = TipologiaEvento::orderBy('ordine')->get(); $ruoli = Ruolo::orderBy('ordine')->get(); $appSettings = AppSetting::first(); + $emailSettings = EmailSetting::first() ?? new EmailSetting(); - return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings')); + return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings')); } public function saveAppSettings(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $validated = $request->validate([ 'nome_applicazione' => 'nullable|string|max:100', @@ -52,7 +54,7 @@ class ImpostazioniController extends Controller public function tipologieStore(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $validated = $request->validate([ 'nome' => 'required|string|max:50|unique:tipologie_documenti,nome', @@ -73,7 +75,7 @@ class ImpostazioniController extends Controller public function tipologieUpdate(Request $request, $id) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $tipologia = TipologiaDocumento::findOrFail($id); @@ -90,7 +92,7 @@ class ImpostazioniController extends Controller public function tipologieDestroy($id) { - $this->authorizeDelete('viste'); + $this->authorizeDelete('settings'); $tipologia = TipologiaDocumento::findOrFail($id); $count = $tipologia->documenti()->count(); @@ -105,7 +107,7 @@ class ImpostazioniController extends Controller public function tipologieReorder(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $order = $request->input('order', []); @@ -118,7 +120,7 @@ class ImpostazioniController extends Controller public function ruoliStore(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $validated = $request->validate([ 'nome' => 'required|string|max:50|unique:ruoli,nome', @@ -139,7 +141,7 @@ class ImpostazioniController extends Controller public function ruoliUpdate(Request $request, $id) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $ruolo = Ruolo::findOrFail($id); @@ -156,7 +158,7 @@ class ImpostazioniController extends Controller public function ruoliDestroy($id) { - $this->authorizeDelete('viste'); + $this->authorizeDelete('settings'); $ruolo = Ruolo::findOrFail($id); @@ -176,7 +178,7 @@ class ImpostazioniController extends Controller public function ruoliReorder(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $order = $request->input('order', []); @@ -189,7 +191,7 @@ class ImpostazioniController extends Controller public function tipologieEventiStore(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $validated = $request->validate([ 'nome' => 'required|string|max:50|unique:tipologie_eventi,nome', @@ -210,7 +212,7 @@ class ImpostazioniController extends Controller public function tipologieEventiUpdate(Request $request, $id) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $tipologia = TipologiaEvento::findOrFail($id); @@ -227,7 +229,7 @@ class ImpostazioniController extends Controller public function tipologieEventiDestroy($id) { - $this->authorizeDelete('viste'); + $this->authorizeDelete('settings'); $tipologia = TipologiaEvento::findOrFail($id); $count = $tipologia->eventi()->count(); @@ -242,7 +244,7 @@ class ImpostazioniController extends Controller public function tipologieEventiReorder(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $order = $request->input('order', []); @@ -255,7 +257,7 @@ class ImpostazioniController extends Controller public function uploadLogo(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $request->validate([ 'logo' => 'nullable|image|mimes:png,jpg,jpeg,gif,svg|max:2048', @@ -285,7 +287,7 @@ class ImpostazioniController extends Controller public function removeLogo(Request $request) { - $this->authorizeWrite('viste'); + $this->authorizeWrite('settings'); $type = $request->input('type'); diff --git a/app/Http/Middleware/AdminOnly.php b/app/Http/Middleware/AdminOnly.php index 6a0c1daf..d0b4b924 100644 --- a/app/Http/Middleware/AdminOnly.php +++ b/app/Http/Middleware/AdminOnly.php @@ -14,7 +14,7 @@ class AdminOnly return redirect('/login'); } - if (!auth()->user()->isSuperAdmin()) { + if (!auth()->user()->isSuperAdmin() && !auth()->user()->canManage('settings')) { abort(403, 'Accesso riservato agli amministratori.'); } diff --git a/app/Models/User.php b/app/Models/User.php index 3029ce5c..c6316714 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -33,7 +33,7 @@ class User extends Authenticatable public const STATUS_SUSPENDED = 'suspended'; public const STATUS_PENDING = 'pending'; - public const MODULES = ['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste', 'report']; + public const MODULES = ['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste', 'report', 'settings']; public const LEVEL_NONE = 0; public const LEVEL_READ = 1; public const LEVEL_FULL = 2; diff --git a/app/Services/IcsExportService.php b/app/Services/IcsExportService.php new file mode 100644 index 00000000..ca51ad2e --- /dev/null +++ b/app/Services/IcsExportService.php @@ -0,0 +1,212 @@ + 'SU', + 1 => 'MO', + 2 => 'TU', + 3 => 'WE', + 4 => 'TH', + 5 => 'FR', + 6 => 'SA', + ]; + + public function generateSingle(Evento $evento): string + { + return $this->generateCollection(collect([$evento])); + } + + public function generateCollection(iterable $eventi): string + { + $lines = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Glastree//Eventi//IT', + 'CALSCALE:GREGORIAN', + 'METHOD:PUBLISH', + 'X-WR-CALNAME:' . config('app.name') . ' - Eventi', + ]; + + foreach ($eventi as $evento) { + array_push($lines, ...$this->buildVEvent($evento)); + } + + $lines[] = 'END:VCALENDAR'; + + return $this->fold(implode("\r\n", $lines)); + } + + private function buildVEvent(Evento $evento): array + { + $uid = 'evento-' . $evento->id . '@' . preg_replace('/[^a-z0-9]/', '', strtolower(config('app.name'))); + $lines = [ + 'BEGIN:VEVENT', + 'UID:' . $uid, + 'DTSTAMP:' . now()->format(self::DATETIME_FORMAT) . 'Z', + ]; + + if ($evento->isSingolo() && $evento->data_specifica) { + $date = $evento->data_specifica instanceof Carbon + ? $evento->data_specifica + : Carbon::parse($evento->data_specifica); + + if ($evento->ora_inizio) { + $start = $date->copy()->setTimeFrom($evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio)); + $durata = $evento->durata_minuti ?? 60; + $end = $start->copy()->addMinutes((int) $durata); + $lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $start->format(self::DATETIME_FORMAT); + $lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $end->format(self::DATETIME_FORMAT); + } else { + $lines[] = 'DTSTART;VALUE=DATE:' . $date->format(self::DATE_FORMAT); + $lines[] = 'DTEND;VALUE=DATE:' . $date->copy()->addDay()->format(self::DATE_FORMAT); + } + } else { + $rrule = $this->buildRRule($evento); + if ($rrule) { + $lines[] = $rrule; + } + + if ($evento->ora_inizio) { + $time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio); + $lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $time->format('His'); + $durata = $evento->durata_minuti ?? 60; + $endTime = $time->copy()->addMinutes((int) $durata); + $lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $endTime->format('His'); + } + } + + $lines[] = 'SUMMARY:' . $this->escapeText($evento->nome_evento); + + if ($evento->descrizione_evento || $evento->descrizione) { + $desc = $evento->descrizione_evento + ? ($evento->descrizione ? $evento->descrizione_evento . '\n\n' . $evento->descrizione : $evento->descrizione_evento) + : $evento->descrizione; + $lines[] = 'DESCRIPTION:' . $this->escapeText($desc); + } + + if ($evento->luogo_indirizzo) { + $lines[] = 'LOCATION:' . $this->escapeText($evento->luogo_indirizzo); + } + + $lines[] = 'END:VEVENT'; + + return $lines; + } + + private function buildRRule(Evento $evento): ?string + { + return match ($evento->tipo_recorrenza) { + 'settimanale' => $this->rruleSettimanale($evento), + 'mensile' => $this->rruleMensile($evento), + 'annuale' => $this->rruleAnnuale($evento), + 'altro' => $this->rruleAltro($evento), + default => null, + }; + } + + private function rruleSettimanale(Evento $evento): ?string + { + if ($evento->giorno_settimana === null) { + return null; + } + return 'RRULE:FREQ=WEEKLY;BYDAY=' . (self::DAY_MAP[(int) $evento->giorno_settimana] ?? 'MO'); + } + + private function rruleMensile(Evento $evento): ?string + { + if (!$evento->occorrenza_mese) { + return null; + } + + $parts = explode(',', $evento->occorrenza_mese); + if (count($parts) !== 2) { + return null; + } + + $occorrenza = (int) $parts[0]; + $giorno = (int) $parts[1]; + $byDay = self::DAY_MAP[$giorno] ?? 'MO'; + + $rrule = 'RRULE:FREQ=MONTHLY;BYSETPOS=' . $occorrenza . ';BYDAY=' . $byDay; + + if ($evento->mesi_recorrenza) { + $mesi = explode(',', $evento->mesi_recorrenza); + $rrule .= ';BYMONTH=' . implode(',', $mesi); + } + + return $rrule; + } + + private function rruleAnnuale(Evento $evento): ?string + { + if (!$evento->mese_annuale) { + return null; + } + + $rrule = 'RRULE:FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale; + + $giorno = (int) ($evento->giorno_mese ?? 1); + $rrule .= ';BYMONTHDAY=' . $giorno; + + return $rrule; + } + + private function rruleAltro(Evento $evento): ?string + { + return $this->rruleSettimanale($evento); + } + + private function escapeText(?string $text): string + { + if ($text === null) { + return ''; + } + + $text = str_replace( + ['\\', ';', ',', "\n"], + ['\\\\', '\\;', '\\,', '\\n'], + $text + ); + + return $text; + } + + private function fold(string $ics): string + { + $lines = explode("\r\n", $ics); + $folded = []; + + foreach ($lines as $line) { + while (mb_strlen($line) > 75) { + $folded[] = mb_substr($line, 0, 75); + $line = ' ' . mb_substr($line, 75); + } + $folded[] = $line; + } + + return implode("\r\n", $folded); + } + + public function getFilename(Evento $evento): string + { + $name = str_replace(['/', '\\', ' '], '_', $evento->nome_evento); + return $name . '.ics'; + } + + public function getCollectionFilename(): string + { + return 'eventi_' . now()->format('Ymd_His') . '.ics'; + } +} diff --git a/resources/views/admin/utenti/create.blade.php b/resources/views/admin/utenti/create.blade.php index 56d60e06..a20ae8a0 100644 --- a/resources/views/admin/utenti/create.blade.php +++ b/resources/views/admin/utenti/create.blade.php @@ -44,9 +44,10 @@ @endforeach +
- +
@@ -63,12 +64,12 @@ - @foreach(['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste'] as $module) + @foreach(\App\Models\User::MODULES as $module) {{ ucfirst($module) }} - - - + + + @endforeach diff --git a/resources/views/admin/utenti/edit.blade.php b/resources/views/admin/utenti/edit.blade.php index 2976a2a1..59d6556d 100644 --- a/resources/views/admin/utenti/edit.blade.php +++ b/resources/views/admin/utenti/edit.blade.php @@ -17,7 +17,7 @@

Dati Utente

-
+ @csrf @method('PUT')
@@ -38,9 +38,9 @@
@@ -48,15 +48,16 @@
+
- isSuperAdmin() ? 'checked' : '' }}> + isSuperAdmin() ? '1' : '0') === '1' ? 'checked' : '' }}>
@@ -73,7 +74,7 @@ - @foreach(['individui', 'gruppi', 'eventi', 'documenti', 'mailing', 'viste'] as $module) + @foreach(\App\Models\User::MODULES as $module) {{ ucfirst($module) }} permissions[$module] ?? 0) == 0 ? 'checked' : '' }}> diff --git a/resources/views/eventi/calendar.blade.php b/resources/views/eventi/calendar.blade.php index 7731155e..b37ccbf6 100644 --- a/resources/views/eventi/calendar.blade.php +++ b/resources/views/eventi/calendar.blade.php @@ -17,6 +17,12 @@ Nuovo Evento + + @csrf + +
diff --git a/resources/views/eventi/index.blade.php b/resources/views/eventi/index.blade.php index 8c842c8c..457b86d1 100644 --- a/resources/views/eventi/index.blade.php +++ b/resources/views/eventi/index.blade.php @@ -23,6 +23,9 @@ $canDeleteEventi = Auth::user()->canDelete('eventi'); Calendario + @if($canDeleteEventi)
+ + Esporta ICS + Torna all'elenco diff --git a/resources/views/impostazioni/index.blade.php b/resources/views/impostazioni/index.blade.php index 0d140ced..49806638 100644 --- a/resources/views/impostazioni/index.blade.php +++ b/resources/views/impostazioni/index.blade.php @@ -23,20 +23,27 @@ Logo + + Email + + + Calendario + Tipologie Documenti - - Tipi di Ruolo - Tipologie Eventi + + Tipi di Ruolo +
+ {{-- === APPLICAZIONE === --}}
@@ -51,12 +58,11 @@
@csrf - @method('POST')
- Nome mostrato nel titolo, sidebar e login @@ -65,7 +71,7 @@
- Nome organizzazione mostrato sotto il logo nel login @@ -75,7 +81,7 @@
- Lascia vuoto per usare il formato predefinito: © {anno} {nome_applicazione} @@ -85,7 +91,7 @@
- Versione mostrata nel footer @@ -118,6 +124,7 @@
+ {{-- === LOGO === --}}
+{{-- ===== MODALI TIPOLOGIE DOCUMENTI ===== --}}