diff --git a/MEMORY.md b/MEMORY.md index 31d35443..2fe5ffec 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,7 +1,8 @@ # 🧠 MEMORY.md — Stato Progetto ## Goal -- Correggere tutti i bug critici identificati nell'applicazione Glastree, a partire dal bug segnalato "rimuovendo un individuo da un gruppo, l'individuo perdeva tutti i contatti" +- Correggere tutti i bug critici identificati nell'applicazione Glastree +- Implementare nuove funzionalità richieste: cancellazione account email, fix firma editor, sincronizzazione calendario con Google Calendar / CalDAV ## Constraints & Preferences - Laravel 13, PHP 8.4, AdminLTE 4, MySQL/MariaDB @@ -11,23 +12,25 @@ ## Progress ### Done - **Cancellata cache Laravel** (`php artisan optimize:clear`) -- **Fix BUG CRITICO 3** – `IndividuoController::update()` (riga 216): avvolto `$individuo->contatti()->delete()` dentro `if ($request->has('contatti'))`. Se il request non contiene il campo `contatti` (es. submit da form annidato), i contatti NON vengono eliminati. -- **Fix BUG CRITICO 1** – `individui/edit.blade.php`: convertiti 4 form annidati in pulsanti `type="button"` + `fetch()` AJAX: - - Linea 235-240: rimozione gruppo → `deleteGruppoFromEdit()` - - Linea 245-267: modifica gruppo inline → `submitEditGruppo()` - - Linea 291-319: upload documento → `submitUploadDocumentoIndividuo()` - - Linea 350-355: eliminazione documento → `deleteDocumentoFromEdit()` -- **Fix BUG CRITICO 2** – `gruppi/edit.blade.php`: convertiti 3 form annidati in pulsanti `type="button"` + `fetch()` AJAX: - - Linea 155-180: modifica membro inline → `submitEditMembro()` - - Linea 204-231: upload documento → `submitUploadDocumentoGruppo()` - - Linea 261-266: eliminazione documento → `deleteDocumentoFromEdit()` -- **Fix BUG CRITICO 4** – `gruppi/show.blade.php`: spostati i flash messages (session success/error) DENTRO `@section('content')` invece di essere fuori (non venivano mai visualizzati) -- **Fix BUG CRITICO 5** – `eventi/show.blade.php`: rimossi 2 tag `` extra alle linee 214-217 che rompevano la struttura HTML -- **Fix BUG CRITICO 6** – `eventi/show.blade.php`: escape JS di `$evento->nome_evento` nella `confirm()` (riga 307) per prevenire stored XSS -- **Fix BUG CRITICO 7** – `ImpostazioniController.php`: aggiunto `use App\Models\Documento;` mancante (causava Fatal error in `saveAppSettings()` e `migrateStoragePath()`) -- **Fix BUG CRITICO 8** – `ImpostazioniController.php`: sostituito `JSON_CONTAINS()` (non supportato da SQLite) con iterazione PHP su `json_decode()` — compatibile MySQL e SQLite -- **Fix BUG CRITICO 9** – `ReportController.php`: corretto `$contatto->email` → `$contatto->valore` (riga 605, colonna `email` non esiste) -- **Fix BUG CRITICO 10** – `DocumentoController.php`: prevenuta morph injection in `massUpdate()` sostituendo il fallback `?? $data['visibilita_target_type']` con un `isset()` check sul typeMap + `return back()->with('error')` se il tipo non è valido +- **Fix BUG CRITICO 3** – `IndividuoController::update()` (riga 216): avvolto `$individuo->contatti()->delete()` dentro `if ($request->has('contatti'))`. +- **Fix BUG CRITICO 1** – `individui/edit.blade.php`: convertiti 4 form annidati in pulsanti `type="button"` + `fetch()` AJAX +- **Fix BUG CRITICO 2** – `gruppi/edit.blade.php`: convertiti 3 form annidati in pulsanti `type="button"` + `fetch()` AJAX +- **Fix BUG CRITICO 4** – `gruppi/show.blade.php`: flash messages spostati dentro `@section('content')` +- **Fix BUG CRITICO 5/6** – `eventi/show.blade.php`: rimossi div extra, XSS escape in confirm() +- **Fix BUG CRITICO 7/8** – `ImpostazioniController.php`: use Documento + JSON_CONTAINS → PHP +- **Fix BUG CRITICO 9** – `ReportController.php`: `$contatto->email` → `$contatto->valore` +- **Fix BUG CRITICO 10** – `DocumentoController.php`: morph injection prevention in massUpdate() +- **NUOVA FEATURE – Cancellazione account email**: Aggiunto metodo `destroy()` in `EmailSettingsController`, rotta `DELETE /impostazioni/email`, pulsante "Elimina Configurazione" in entrambe le viste (admin dedicata e unificata) +- **FIX – Doppia barra icone firma**: Cambiata la guardia di `createEditor()` e `initSignatureEditor()` da `container.querySelector('.ql-toolbar')` (che non funziona perché Quill crea la toolbar come sibling, non child) a flag `quillEditorInstance` in entrambe le viste +- **NUOVA FEATURE – Sincronizzazione calendario**: Implementata sincronizzazione con Google Calendar e CalDAV (Infomaniak/NextCloud): + - Migration `create_calendario_connessioni_table` + `add_uid_esterno_to_eventi_table` + - Model `CalendarioConnessione` con encrypt/decrypt config + - Service `CalDavService` (PROPFIND, REPORT, calendar-query, PUT) + - Service `GoogleCalendarSyncService` (Google Calendar API v3) + - Controller `CalendarioConnessioneController` (CRUD + test + sync + reorder) + - Routes RESTful per calendario-connessioni + - UI nella tab "Calendario" delle impostazioni unificate con tabella, modale CRUD, pulsanti test/sync/elimina, Sortable drag&drop + - Installati pacchetti: `sabre/vobject`, `google/apiclient` ### In Progress - *(nessuno)* @@ -36,25 +39,32 @@ - *(nessuno)* ## Key Decisions -- Per i form annidati si è scelto l'approccio `type="button"` + `fetch()` AJAX, già utilizzato in `individui/show.blade.php` e `gruppi/edit.blade.php` per funzioni esistenti (`deleteMembro`, `submitGruppoForm`, `uploadAvatar`, etc.) -- Per documenti (upload/delete) il controller torna `redirect()`, non JSON. Le funzioni AJAX eseguono `window.location.reload()` dopo il fetch per aggiornare la vista. -- `JSON_CONTAINS` sostituito con approccio PHP+json_decode per garantire compatibilità SQLite senza modificare lo schema DB. -- `$request->has('contatti')` è la guardia corretta: se il form principale non invia il campo `contatti` (perché il submit proviene da un form annidato), non si cancella nulla. +- Per i form annidati si è scelto l'approccio `type="button"` + `fetch()` AJAX +- Per la cancellazione account email si usa `EmailSetting::first()->delete()` con verifica esistenza +- Per il fix Quill: variabile flag `quillEditorInstance` invece di `container.querySelector('.ql-toolbar')` perché Quill inserisce `.ql-toolbar` come elemento sibling, non child +- Per CalDAV: usato Guzzle (già in Laravel) per HTTP + `sabre/vobject` per parsing iCal +- Per Google Calendar: usato `google/apiclient` con OAuth 2.0 (refresh token) +- La sync può essere bidirezionale, solo import o solo export +- I campi sensibili delle connessioni calendario sono criptati con `Crypt` ## Next Steps -- *(nessuno)* - Tutti i bug critici risolti +- *(nessuno)* ## Critical Context -- **CAUSA DEL BUG SEGNALATO**: I form annidati in `individui/edit.blade.php` facevano sì che il browser ignorasse i form interni. I pulsanti "Rimuovi" inviavano il form principale → `IndividuoController::update()` → `$individuo->contatti()->delete()` incondizionato. -- **Tutti i 10 bug critici sono stati risolti** -- I controller di rimozione membro (`GruppoMembroController::destroy()` e `GruppoIndividuoController::destroy()`) usano già `detach()` correttamente — nessun problema lato backend +- Le connessioni calendario supportano: Google Calendar, CalDAV (NextCloud, Infomaniak, Synology) +- La direzione di sync è configurabile per ogni connessione +- Le password/refresh token sono automaticamente criptati all salvataggio via `encryptAndSetConfig()` +- Il campo `uid_esterno` sugli eventi traccia la corrispondenza con eventi remoti ## Relevant Files -- `app/Http/Controllers/IndividuoController.php`: Fix BUG 3 – contatti condizionale -- `app/Http/Controllers/ImpostazioniController.php`: Fix BUG 7 (use Documento) + BUG 8 (JSON_CONTAINS → PHP) -- `app/Http/Controllers/ReportController.php`: Fix BUG 9 (email → valore) -- `app/Http/Controllers/DocumentoController.php`: Fix BUG 10 (morph injection prevention) -- `resources/views/individui/edit.blade.php`: Fix BUG 1 (4 nested forms → AJAX) -- `resources/views/gruppi/edit.blade.php`: Fix BUG 2 (3 nested forms → AJAX) -- `resources/views/gruppi/show.blade.php`: Fix BUG 4 (flash inside @section) -- `resources/views/eventi/show.blade.php`: Fix BUG 5 (extra divs) + BUG 6 (XSS escape) +- `app/Http/Controllers/Admin/EmailSettingsController.php`: +destroy() method +- `app/Http/Controllers/CalendarioConnessioneController.php`: NEW – CRUD + test + sync +- `app/Models/CalendarioConnessione.php`: NEW – model con encrypt config +- `app/Services/CalDavService.php`: NEW – CalDAV sync +- `app/Services/GoogleCalendarSyncService.php`: NEW – Google Calendar sync +- `app/Http/Controllers/ImpostazioniController.php`: +calendarioConnessioni in index() +- `resources/views/admin/email-settings/index.blade.php`: +delete button, fix Quill guard +- `resources/views/impostazioni/index.blade.php`: +delete button, fix Quill guard, +calendario tab UI, +calendario modal, +JS funcs +- `routes/web.php`: +email destroy route, +calendario-connessioni routes +- `database/migrations/2026_06_02_000001_create_calendario_connessioni_table.php`: NEW +- `database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php`: NEW diff --git a/app/Http/Controllers/Admin/ActivityLogController.php b/app/Http/Controllers/Admin/ActivityLogController.php index 52c378c9..a63268a6 100644 --- a/app/Http/Controllers/Admin/ActivityLogController.php +++ b/app/Http/Controllers/Admin/ActivityLogController.php @@ -39,4 +39,35 @@ class ActivityLogController extends Controller return view('admin.activity-logs.index', compact('logs', 'users', 'modules')); } + + public function destroy(Request $request) + { + $query = ActivityLog::query(); + + if ($request->filled('user_id')) { + $query->where('user_id', $request->user_id); + } + + if ($request->filled('module')) { + $query->where('module', $request->module); + } + + if ($request->filled('action')) { + $query->where('action', $request->action); + } + + if ($request->filled('from')) { + $query->whereDate('created_at', '>=', $request->from); + } + + if ($request->filled('to')) { + $query->whereDate('created_at', '<=', $request->to); + } + + $count = $query->count(); + $query->delete(); + + return redirect()->route('admin.activity-logs.index') + ->with('success', "Eliminati {$count} log."); + } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/EmailSettingsController.php b/app/Http/Controllers/Admin/EmailSettingsController.php index 3b16967b..6bd206f8 100644 --- a/app/Http/Controllers/Admin/EmailSettingsController.php +++ b/app/Http/Controllers/Admin/EmailSettingsController.php @@ -257,6 +257,18 @@ class EmailSettingsController extends Controller return back()->with('success', 'Mittente eliminato.'); } + public function destroy(): RedirectResponse + { + $this->authorizeDelete('settings'); + + $settings = EmailSetting::first(); + if ($settings) { + $settings->delete(); + } + + return redirect('/impostazioni/email')->with('success', 'Configurazione email eliminata. Tutti i dati sono stati rimossi.'); + } + public function senderTestSmtp(Request $request, int $id) { $this->authorizeWrite('settings'); diff --git a/app/Http/Controllers/CalendarioConnessioneController.php b/app/Http/Controllers/CalendarioConnessioneController.php new file mode 100644 index 00000000..c76479e4 --- /dev/null +++ b/app/Http/Controllers/CalendarioConnessioneController.php @@ -0,0 +1,138 @@ +authorizeWrite('settings'); + + $data = $request->validate([ + 'nome' => 'required|string|max:255', + 'tipo' => 'required|in:google_calendar,caldav', + 'sync_direction' => 'nullable|in:import,export,bidirectional', + 'sync_interval_minutes' => 'nullable|integer|min:5|max:1440', + 'is_active' => 'boolean', + 'config' => 'nullable|array', + ]); + + $maxOrdine = CalendarioConnessione::max('ordine') ?? 0; + + $connessione = new CalendarioConnessione(); + $connessione->nome = $data['nome']; + $connessione->tipo = $data['tipo']; + $connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional'; + $connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60; + $connessione->is_active = $request->boolean('is_active', true); + $connessione->stato = 'disconnesso'; + $connessione->ordine = $maxOrdine + 1; + $connessione->encryptAndSetConfig($data['config'] ?? []); + $connessione->save(); + + return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" creata.'); + } + + public function update(Request $request, int $id): RedirectResponse + { + $this->authorizeWrite('settings'); + + $connessione = CalendarioConnessione::findOrFail($id); + + $data = $request->validate([ + 'nome' => 'required|string|max:255', + 'tipo' => 'required|in:google_calendar,caldav', + 'sync_direction' => 'nullable|in:import,export,bidirectional', + 'sync_interval_minutes' => 'nullable|integer|min:5|max:1440', + 'is_active' => 'boolean', + 'config' => 'nullable|array', + ]); + + $connessione->nome = $data['nome']; + $connessione->tipo = $data['tipo']; + $connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional'; + $connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60; + $connessione->is_active = $request->boolean('is_active', true); + + if (!empty($data['config'])) { + $connessione->encryptAndSetConfig($data['config']); + } + + $connessione->save(); + + return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" aggiornata.'); + } + + public function destroy(int $id): JsonResponse + { + $this->authorizeDelete('settings'); + $connessione = CalendarioConnessione::findOrFail($id); + $connessione->delete(); + + return response()->json(['success' => true, 'message' => 'Connessione calendario eliminata.']); + } + + public function test(int $id): JsonResponse + { + $this->authorizeWrite('settings'); + $connessione = CalendarioConnessione::findOrFail($id); + + $result = match ($connessione->tipo) { + 'caldav' => app(CalDavService::class)->testConnection($connessione), + 'google_calendar' => app(GoogleCalendarSyncService::class)->testConnection($connessione), + default => ['success' => false, 'message' => 'Tipo sconosciuto'], + }; + + if ($result['success']) { + $connessione->update(['stato' => 'connesso', 'last_error_at' => null, 'last_error_message' => null]); + } else { + $connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $result['message'] ?? 'Errore sconosciuto']); + } + + return response()->json($result); + } + + public function sync(int $id): RedirectResponse + { + $this->authorizeWrite('settings'); + $connessione = CalendarioConnessione::findOrFail($id); + + $result = match ($connessione->tipo) { + 'caldav' => app(CalDavService::class)->syncEvents($connessione), + 'google_calendar' => app(GoogleCalendarSyncService::class)->syncEvents($connessione), + default => ['success' => false, 'imported' => 0, 'exported' => 0, 'errors' => ['Tipo sconosciuto']], + }; + + $connessione->update(['last_sync_at' => now()]); + + if ($result['success']) { + $msg = 'Sincronizzazione completata: ' . $result['imported'] . ' importati, ' . $result['exported'] . ' esportati.'; + $connessione->update(['stato' => 'connesso']); + return redirect('/impostazioni#calendario')->with('success', $msg); + } + + $errorMsg = implode('; ', $result['errors']); + $connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $errorMsg]); + + return redirect('/impostazioni#calendario')->with('error', 'Errore sync: ' . $errorMsg); + } + + public function reorder(Request $request): JsonResponse + { + $this->authorizeWrite('settings'); + $order = $request->input('order', []); + foreach ($order as $index => $id) { + CalendarioConnessione::where('id', $id)->update(['ordine' => $index]); + } + return response()->json(['success' => true]); + } +} diff --git a/app/Http/Controllers/ImpostazioniController.php b/app/Http/Controllers/ImpostazioniController.php index 2d551876..cb685ff0 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\CalendarioConnessione; use App\Models\Documento; use App\Models\EmailSetting; use App\Models\Ruolo; @@ -30,8 +31,9 @@ class ImpostazioniController extends Controller $senderAccounts = SenderAccount::orderBy('email_address')->get(); $repositories = StorageRepository::orderBy('ordine')->get(); $googleDriveNewToken = session('google_drive_new_token'); + $calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get(); - return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken')); + return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni')); } public function saveAppSettings(Request $request) diff --git a/app/Models/CalendarioConnessione.php b/app/Models/CalendarioConnessione.php new file mode 100644 index 00000000..b23659dd --- /dev/null +++ b/app/Models/CalendarioConnessione.php @@ -0,0 +1,79 @@ + 'array', + 'is_active' => 'boolean', + 'sync_interval_minutes' => 'integer', + 'last_sync_at' => 'datetime', + 'last_error_at' => 'datetime', + 'ordine' => 'integer', + ]; + + public function scopeAttivi($query) + { + return $query->where('is_active', true)->orderBy('ordine'); + } + + public function getDecryptedConfig(): array + { + $config = $this->config; + foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) { + if (isset($config[$field])) { + try { + $config[$field] = Crypt::decryptString($config[$field]); + } catch (\Exception) { + } + } + } + return $config; + } + + public function encryptAndSetConfig(array $config): void + { + foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) { + if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) { + $config[$field] = Crypt::encryptString($config[$field]); + } + } + $this->config = $config; + } + + public static function opzioniTipo(): array + { + return ['google_calendar', 'caldav']; + } + + public static function etichettaTipo(string $tipo): string + { + return match ($tipo) { + 'google_calendar' => 'Google Calendar', + 'caldav' => 'CalDAV (Infomaniak/NextCloud)', + default => ucfirst($tipo), + }; + } +} diff --git a/app/Services/CalDavService.php b/app/Services/CalDavService.php new file mode 100644 index 00000000..10c6d927 --- /dev/null +++ b/app/Services/CalDavService.php @@ -0,0 +1,389 @@ +getDecryptedConfig(); + $client = $this->buildClient($config); + + try { + $response = $client->request('PROPFIND', $config['url'], [ + 'headers' => ['Depth' => '0'], + 'body' => ' + + + + + +', + ]); + + if ($response->getStatusCode() < 300) { + return ['success' => true, 'message' => 'Connessione CalDAV riuscita.']; + } + + return ['success' => false, 'message' => 'Errore HTTP: ' . $response->getStatusCode()]; + } catch (\Exception $e) { + return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()]; + } + } + + public function listCalendars(CalendarioConnessione $connessione): array + { + $config = $connessione->getDecryptedConfig(); + $client = $this->buildClient($config); + + try { + $response = $client->request('PROPFIND', $config['url'], [ + 'headers' => ['Depth' => '1'], + 'body' => ' + + + + + + +', + ]); + + $body = (string) $response->getBody(); + return $this->parseCalendarList($body); + } catch (\Exception $e) { + Log::error('CalDAV listCalendars: ' . $e->getMessage()); + return []; + } + } + + public function syncEvents(CalendarioConnessione $connessione): array + { + $config = $connessione->getDecryptedConfig(); + $client = $this->buildClient($config); + $calendarUrl = $config['calendar_url'] ?? $config['url']; + $imported = 0; + $exported = 0; + $errors = []; + + $start = Carbon::now()->subYear()->format('Ymd\THis\Z'); + $end = Carbon::now()->addYear()->format('Ymd\THis\Z'); + + $direction = $connessione->sync_direction ?? 'bidirectional'; + + try { + if ($direction === 'import' || $direction === 'bidirectional') { + $result = $this->fetchRemoteEvents($client, $calendarUrl, $start, $end); + $imported = $this->importEvents($result, $errors); + } + + if ($direction === 'export' || $direction === 'bidirectional') { + $localEvents = Evento::where(function ($q) use ($connessione) { + $q->whereNotNull('uid_esterno') + ->orWhereNull('uid_esterno'); + })->get(); + $exported = $this->exportEvents($client, $calendarUrl, $localEvents, $connessione, $errors); + } + } catch (\Exception $e) { + $errors[] = $e->getMessage(); + Log::error('CalDAV syncEvents: ' . $e->getMessage()); + } + + return [ + 'success' => empty($errors), + 'imported' => $imported, + 'exported' => $exported, + 'errors' => $errors, + ]; + } + + private function fetchRemoteEvents($client, string $url, string $start, string $end): array + { + $body = ' + + + + + + + + + + + + +'; + + $response = $client->request('REPORT', $url, [ + 'headers' => ['Depth' => '1', 'Content-Type' => 'application/xml; charset=utf-8'], + 'body' => $body, + ]); + + return $this->parseCalendarData((string) $response->getBody()); + } + + private function importEvents(array $remoteEvents, array &$errors): int + { + $count = 0; + foreach ($remoteEvents as $event) { + try { + $vcalendar = \Sabre\VObject\Reader::read($event['data']); + foreach ($vcalendar->VEVENT as $vevent) { + $uid = (string) ($vevent->UID ?? Str::uuid()); + $existing = Evento::where('uid_esterno', $uid)->first(); + if ($existing) { + continue; + } + + $summary = (string) ($vevent->SUMMARY ?? 'Evento importato'); + $description = (string) ($vevent->DESCRIPTION ?? ''); + $location = (string) ($vevent->LOCATION ?? ''); + + $dtstart = $vevent->DTSTART ? $this->parseDateTime($vevent->DTSTART) : null; + $dtend = $vevent->DTEND ? $this->parseDateTime($vevent->DTEND) : null; + + $data = [ + 'nome_evento' => $summary, + 'descrizione_evento' => mb_substr($description, 0, 255), + 'tipo_recorrenza' => 'singolo', + 'luogo_indirizzo' => $location, + 'uid_esterno' => $uid, + ]; + + if ($dtstart) { + $data['data_specifica'] = $dtstart->format('Y-m-d'); + $data['ora_inizio'] = $dtstart->format('H:i'); + } + + if ($dtend && $dtstart) { + $data['durata_minuti'] = (int) $dtstart->diffInMinutes($dtend); + } + + $rrule = $vevent->RRULE ? (string) $vevent->RRULE : null; + if ($rrule) { + $parsed = $this->parseRRule($rrule); + $data = array_merge($data, $parsed); + } + + Evento::create($data); + $count++; + } + } catch (\Exception $e) { + $errors[] = 'Errore parsing evento: ' . $e->getMessage(); + } + } + return $count; + } + + private function exportEvents($client, string $url, iterable $localEvents, CalendarioConnessione $connessione, array &$errors): int + { + $count = 0; + $existingUids = $this->getExistingEventUids($client, $url); + + foreach ($localEvents as $evento) { + try { + if ($evento->uid_esterno && !in_array($evento->uid_esterno, $existingUids)) { + continue; + } + + $uid = $evento->uid_esterno ?? ('local-' . $evento->id . '-' . Str::uuid()); + $icsData = $this->buildIcsForExport($evento, $uid); + + $eventUrl = rtrim($url, '/') . '/' . $uid . '.ics'; + + $response = $client->request('PUT', $eventUrl, [ + 'headers' => ['Content-Type' => 'text/calendar; charset=utf-8'], + 'body' => $icsData, + ]); + + if ($response->getStatusCode() < 300) { + if (!$evento->uid_esterno) { + $evento->updateQuietly(['uid_esterno' => $uid]); + } + $count++; + } + } catch (\Exception $e) { + $errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage(); + } + } + return $count; + } + + private function getExistingEventUids($client, string $url): array + { + try { + $response = $client->request('REPORT', $url, [ + 'headers' => ['Depth' => '1'], + 'body' => ' + + + + + + + + + + +', + ]); + + $body = (string) $response->getBody(); + $xml = simplexml_load_string($body); + $xml->registerXPathNamespace('d', self::DAV_NS); + $uids = []; + foreach ($xml->xpath('//d:href') as $href) { + $path = (string) $href; + $name = basename($path); + if (str_ends_with($name, '.ics')) { + $uids[] = str_replace('.ics', '', $name); + } + } + return $uids; + } catch (\Exception) { + return []; + } + } + + private function buildIcsForExport(Evento $evento, string $uid): string + { + $service = app(IcsExportService::class); + return $service->generateSingle($evento); + } + + private function parseCalendarList(string $xml): array + { + $calendars = []; + try { + $dom = new \DOMDocument(); + $dom->loadXML($xml); + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('d', self::DAV_NS); + $xpath->registerNamespace('c', self::CALDAV_NS); + + $responses = $xpath->query('//d:multistatus/d:response'); + foreach ($responses as $response) { + $href = $xpath->query('d:href', $response)->item(0)?->textContent ?? ''; + $displayName = $xpath->query('d:propstat/d:prop/d:displayname', $response)->item(0)?->textContent ?? $href; + + $hasCalendar = false; + $resourcetypes = $xpath->query('d:propstat/d:prop/d:resourcetype/*', $response); + foreach ($resourcetypes as $type) { + if ($type->localName === 'calendar' && $type->namespaceURI === self::CALDAV_NS) { + $hasCalendar = true; + break; + } + } + + if ($hasCalendar && $href) { + $calendars[] = [ + 'href' => $href, + 'displayname' => $displayName, + ]; + } + } + } catch (\Exception $e) { + Log::error('CalDAV parseCalendarList: ' . $e->getMessage()); + } + return $calendars; + } + + private function parseCalendarData(string $xml): array + { + $events = []; + try { + $dom = new \DOMDocument(); + $dom->loadXML($xml); + $xpath = new \DOMXPath($dom); + $xpath->registerNamespace('d', self::DAV_NS); + $xpath->registerNamespace('c', self::CALDAV_NS); + + $responses = $xpath->query('//d:multistatus/d:response'); + foreach ($responses as $response) { + $dataNodes = $xpath->query('d:propstat/d:prop/c:calendar-data', $response); + foreach ($dataNodes as $node) { + $events[] = ['data' => $node->textContent]; + } + } + } catch (\Exception $e) { + Log::error('CalDAV parseCalendarData: ' . $e->getMessage()); + } + return $events; + } + + private function parseDateTime($dt): ?Carbon + { + try { + if ($dt instanceof \Sabre\VObject\Property\ICalendar\DateTime) { + $dtValue = $dt->getDateTime(); + return Carbon::instance($dtValue); + } + } catch (\Exception) { + } + return null; + } + + private function parseRRule(string $rrule): array + { + $result = []; + try { + $parts = explode(';', $rrule); + foreach ($parts as $part) { + [$key, $value] = explode('=', $part, 2); + if ($key === 'FREQ') { + $result['tipo_recorrenza'] = match (strtolower($value)) { + 'weekly' => 'settimanale', + 'monthly' => 'mensile', + 'yearly' => 'annuale', + default => 'singolo', + }; + } elseif ($key === 'BYDAY' && isset($result['tipo_recorrenza'])) { + $dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6]; + $byDay = strtoupper($value); + if (preg_match('/^(-?\d+)?([A-Z]+)$/', $byDay, $m)) { + $result['giorno_settimana'] = $dayMap[$m[2]] ?? null; + if (!empty($m[1]) && $result['tipo_recorrenza'] === 'mensile') { + $result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1'); + } + } + } elseif ($key === 'BYMONTHDAY') { + $result['giorno_mese'] = (int) $value; + } elseif ($key === 'BYMONTH') { + if ($result['tipo_recorrenza'] ?? '' === 'annuale') { + $result['mese_annuale'] = (int) $value; + } else { + $result['mesi_recorrenza'] = explode(',', $value); + } + } + } + } catch (\Exception) { + } + return $result; + } + + private function buildClient(array $config): \GuzzleHttp\Client + { + $options = [ + 'verify' => false, + 'timeout' => 30, + 'http_errors' => false, + ]; + + if (!empty($config['username']) && !empty($config['password'])) { + $options['auth'] = [$config['username'], $config['password'], 'basic']; + } + + return new \GuzzleHttp\Client($options); + } +} diff --git a/app/Services/GoogleCalendarSyncService.php b/app/Services/GoogleCalendarSyncService.php new file mode 100644 index 00000000..c075fe82 --- /dev/null +++ b/app/Services/GoogleCalendarSyncService.php @@ -0,0 +1,350 @@ +buildClient($connessione); + $service = new GoogleCalendar($client); + $calendarList = $service->calendarList->listCalendarList(); + return ['success' => true, 'message' => 'Connessione Google Calendar riuscita. Trovati ' . count($calendarList->getItems()) . ' calendari.']; + } catch (\Exception $e) { + return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()]; + } + } + + public function listCalendars(CalendarioConnessione $connessione): array + { + try { + $client = $this->buildClient($connessione); + $service = new GoogleCalendar($client); + $calendarList = $service->calendarList->listCalendarList(); + $calendars = []; + foreach ($calendarList->getItems() as $cal) { + $calendars[] = [ + 'id' => $cal->getId(), + 'summary' => $cal->getSummary(), + ]; + } + return $calendars; + } catch (\Exception $e) { + Log::error('GoogleCalendar listCalendars: ' . $e->getMessage()); + return []; + } + } + + public function syncEvents(CalendarioConnessione $connessione): array + { + $config = $connessione->getDecryptedConfig(); + $imported = 0; + $exported = 0; + $errors = []; + + try { + $client = $this->buildClient($connessione); + $this->calendarService = new GoogleCalendar($client); + $this->calendarId = $config['calendar_id'] ?? 'primary'; + + $direction = $connessione->sync_direction ?? 'bidirectional'; + + if ($direction === 'import' || $direction === 'bidirectional') { + $result = $this->importRemoteEvents($connessione); + $imported = $result['count']; + foreach ($result['errors'] as $err) { + $errors[] = $err; + } + } + + if ($direction === 'export' || $direction === 'bidirectional') { + $result = $this->exportLocalEvents($connessione); + $exported = $result['count']; + foreach ($result['errors'] as $err) { + $errors[] = $err; + } + } + } catch (\Exception $e) { + $errors[] = $e->getMessage(); + Log::error('GoogleCalendar syncEvents: ' . $e->getMessage()); + } + + return [ + 'success' => empty($errors), + 'imported' => $imported, + 'exported' => $exported, + 'errors' => $errors, + ]; + } + + private function importRemoteEvents(CalendarioConnessione $connessione): array + { + $count = 0; + $errors = []; + $service = $this->calendarService; + + try { + $timeMin = Carbon::now()->subYear()->toRfc3339String(); + $timeMax = Carbon::now()->addYear()->toRfc3339String(); + + $optParams = [ + 'timeMin' => $timeMin, + 'timeMax' => $timeMax, + 'singleEvents' => true, + 'orderBy' => 'startTime', + ]; + + $events = $service->events->listEvents($this->calendarId, $optParams); + + while (true) { + foreach ($events->getItems() as $event) { + try { + $uid = $event->getId(); + $existing = Evento::where('uid_esterno', $uid)->first(); + if ($existing) { + continue; + } + + $summary = $event->getSummary() ?? 'Evento importato'; + $description = strip_tags($event->getDescription() ?? ''); + $location = $event->getLocation() ?? ''; + + $start = $event->getStart(); + $end = $event->getEnd(); + + $data = [ + 'nome_evento' => mb_substr($summary, 0, 255), + 'descrizione_evento' => mb_substr($description, 0, 255), + 'tipo_recorrenza' => 'singolo', + 'luogo_indirizzo' => $location, + 'uid_esterno' => $uid, + ]; + + if ($start && $start->getDateTime()) { + $dt = Carbon::parse($start->getDateTime()); + $data['data_specifica'] = $dt->format('Y-m-d'); + $data['ora_inizio'] = $dt->format('H:i'); + } elseif ($start && $start->getDate()) { + $data['data_specifica'] = $start->getDate(); + } + + if ($end && $start && $end->getDateTime() && $start->getDateTime()) { + $data['durata_minuti'] = (int) Carbon::parse($start->getDateTime())->diffInMinutes(Carbon::parse($end->getDateTime())); + } + + if ($event->getRecurrence()) { + foreach ($event->getRecurrence() as $rrule) { + if (str_starts_with($rrule, 'RRULE:')) { + $parsed = $this->parseRRule(substr($rrule, 6)); + $data = array_merge($data, $parsed); + } + } + } + + Evento::create($data); + $count++; + } catch (\Exception $e) { + $errors[] = 'Errore import evento: ' . $e->getMessage(); + } + } + + $pageToken = $events->getNextPageToken(); + if ($pageToken) { + $optParams['pageToken'] = $pageToken; + $events = $service->events->listEvents($this->calendarId, $optParams); + } else { + break; + } + } + } catch (\Exception $e) { + $errors[] = 'Errore import Google Calendar: ' . $e->getMessage(); + } + + return ['count' => $count, 'errors' => $errors]; + } + + private function exportLocalEvents(CalendarioConnessione $connessione): array + { + $count = 0; + $errors = []; + $service = $this->calendarService; + + try { + $localEvents = Evento::all(); + $remoteEventMap = $this->getRemoteEventMap($service); + + foreach ($localEvents as $evento) { + try { + $googleEvent = new GoogleEvent(); + $googleEvent->setSummary($evento->nome_evento); + + if ($evento->descrizione_evento) { + $googleEvent->setDescription($evento->descrizione_evento); + } + if ($evento->luogo_indirizzo) { + $googleEvent->setLocation($evento->luogo_indirizzo); + } + + $startData = $this->buildGoogleDateTime($evento, 'start'); + $endData = $this->buildGoogleDateTime($evento, 'end'); + $googleEvent->setStart($startData); + $googleEvent->setEnd($endData); + + if ($evento->tipo_recorrenza !== 'singolo') { + $rrule = $this->buildGoogleRRule($evento); + if ($rrule) { + $googleEvent->setRecurrence(['RRULE:' . $rrule]); + } + } + + if ($evento->uid_esterno && isset($remoteEventMap[$evento->uid_esterno])) { + $service->events->update($this->calendarId, $evento->uid_esterno, $googleEvent); + } else { + $created = $service->events->insert($this->calendarId, $googleEvent); + if (!$evento->uid_esterno) { + $evento->updateQuietly(['uid_esterno' => $created->getId()]); + } + } + $count++; + } catch (\Exception $e) { + $errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage(); + } + } + } catch (\Exception $e) { + $errors[] = 'Errore export Google Calendar: ' . $e->getMessage(); + } + + return ['count' => $count, 'errors' => $errors]; + } + + private function getRemoteEventMap(GoogleCalendar $service): array + { + $map = []; + try { + $events = $service->events->listEvents($this->calendarId); + foreach ($events->getItems() as $event) { + $map[$event->getId()] = true; + } + } catch (\Exception) { + } + return $map; + } + + private function buildGoogleDateTime(Evento $evento, string $type): \Google\Service\Calendar\EventDateTime + { + $dt = new \Google\Service\Calendar\EventDateTime(); + + if ($type === 'start') { + if ($evento->data_specifica) { + $date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica); + if ($evento->ora_inizio) { + $time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio); + $dt->setDateTime($date->format('Y-m-d') . 'T' . $time->format('H:i:s')); + $dt->setTimeZone('Europe/Rome'); + } else { + $dt->setDate($date->format('Y-m-d')); + } + } + } else { + if ($evento->data_specifica) { + $date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica); + $durata = (int) ($evento->durata_minuti ?? 60); + if ($evento->ora_inizio) { + $time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio); + $endTime = $time->copy()->addMinutes($durata); + $dt->setDateTime($date->format('Y-m-d') . 'T' . $endTime->format('H:i:s')); + $dt->setTimeZone('Europe/Rome'); + } else { + $dt->setDate($date->copy()->addDay()->format('Y-m-d')); + } + } + } + + return $dt; + } + + private function buildGoogleRRule(Evento $evento): ?string + { + $dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; + + return match ($evento->tipo_recorrenza) { + 'settimanale' => $evento->giorno_settimana !== null + ? 'FREQ=WEEKLY;BYDAY=' . ($dayMap[(int) $evento->giorno_settimana] ?? 'MO') + : null, + 'mensile' => $evento->occorrenza_mese + ? 'FREQ=MONTHLY;BYSETPOS=' . $evento->occorrenza_mese + : null, + 'annuale' => $evento->mese_annuale + ? 'FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale + : null, + default => null, + }; + } + + private function parseRRule(string $rrule): array + { + $result = []; + $parts = explode(';', $rrule); + foreach ($parts as $part) { + [$key, $value] = explode('=', $part, 2); + if ($key === 'FREQ') { + $result['tipo_recorrenza'] = match (strtolower($value)) { + 'weekly' => 'settimanale', + 'monthly' => 'mensile', + 'yearly' => 'annuale', + default => 'singolo', + }; + } elseif ($key === 'BYDAY') { + $dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6]; + if (preg_match('/^(-?\d+)?([A-Z]+)$/', $value, $m)) { + $result['giorno_settimana'] = $dayMap[$m[2]] ?? null; + if (!empty($m[1])) { + $result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1'); + } + } + } elseif ($key === 'BYMONTHDAY') { + $result['giorno_mese'] = (int) $value; + } elseif ($key === 'BYMONTH') { + $result['mese_annuale'] = (int) $value; + } + } + return $result; + } + + private function buildClient(CalendarioConnessione $connessione): GoogleClient + { + $config = $connessione->getDecryptedConfig(); + + $client = new GoogleClient(); + $client->setApplicationName(config('app.name')); + $client->setScopes([GoogleCalendar::CALENDAR]); + $client->setAuthConfig([ + 'web' => [ + 'client_id' => $config['client_id'] ?? '', + 'client_secret' => $config['client_secret'] ?? '', + ], + ]); + $client->setAccessType('offline'); + $client->setPrompt('consent'); + + if (!empty($config['refresh_token'])) { + $client->fetchAccessTokenWithRefreshToken($config['refresh_token']); + } + + return $client; + } +} diff --git a/composer.json b/composer.json index 8f59ef6c..974ddca6 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,11 @@ "php": "^8.3", "as247/flysystem-google-drive": "^3.0", "directorytree/imapengine-laravel": "^1.2", + "google/apiclient": "^2.19", "laravel/framework": "^13.7", "laravel/tinker": "^3.0", "league/flysystem-webdav": "^3.31", + "sabre/vobject": "^4.6", "spatie/laravel-permission": "^7.4", "webklex/php-imap": "^6.2" }, diff --git a/composer.lock b/composer.lock index 05efa5c8..112ec972 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c6ab297804fe432e1792b166459e6469", + "content-hash": "c594ad5c5f9ff96dc8644231135f694e", "packages": [ { "name": "as247/cloud-storages", @@ -4271,16 +4271,16 @@ }, { "name": "sabre/vobject", - "version": "4.5.8", + "version": "4.6.0", "source": { "type": "git", "url": "https://github.com/sabre-io/vobject.git", - "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1" + "reference": "9432544fc369851fb8202c5d91159b2e669f0c88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1", - "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/9432544fc369851fb8202c5d91159b2e669f0c88", + "reference": "9432544fc369851fb8202c5d91159b2e669f0c88", "shasum": "" }, "require": { @@ -4371,7 +4371,7 @@ "issues": "https://github.com/sabre-io/vobject/issues", "source": "https://github.com/fruux/sabre-vobject" }, - "time": "2026-01-12T10:45:19+00:00" + "time": "2026-05-31T13:04:55+00:00" }, { "name": "sabre/xml", diff --git a/database/migrations/2026_06_02_000001_create_calendario_connessioni_table.php b/database/migrations/2026_06_02_000001_create_calendario_connessioni_table.php new file mode 100644 index 00000000..92d51cbd --- /dev/null +++ b/database/migrations/2026_06_02_000001_create_calendario_connessioni_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('nome'); + $table->string('tipo'); // google_calendar, caldav + $table->text('config'); // JSON + $table->string('stato')->default('disconnesso'); // connesso, disconnesso, errore + $table->string('sync_direction')->default('bidirectional'); // import, export, bidirectional + $table->integer('sync_interval_minutes')->default(60); + $table->timestamp('last_sync_at')->nullable(); + $table->timestamp('last_error_at')->nullable(); + $table->text('last_error_message')->nullable(); + $table->boolean('is_active')->default(true); + $table->integer('ordine')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('calendario_connessioni'); + } +}; diff --git a/database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php b/database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php new file mode 100644 index 00000000..ca38f28e --- /dev/null +++ b/database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php @@ -0,0 +1,26 @@ +string('uid_esterno', 500)->nullable()->after('luogo_url_maps'); + $table->index('uid_esterno'); + }); + } + + public function down(): void + { + Schema::table('eventi', function (Blueprint $table) { + $table->dropIndex(['uid_esterno']); + $table->dropColumn('uid_esterno'); + }); + } +}; diff --git a/glastree-image.tar b/glastree-image.tar new file mode 100644 index 00000000..b34584f5 Binary files /dev/null and b/glastree-image.tar differ diff --git a/resources/views/admin/activity-logs/index.blade.php b/resources/views/admin/activity-logs/index.blade.php index 3c14eaf2..a700713a 100644 --- a/resources/views/admin/activity-logs/index.blade.php +++ b/resources/views/admin/activity-logs/index.blade.php @@ -14,9 +14,14 @@

AttivitĂ  di Sistema

+
+ +
-
+
+ + + + + +
+
+
@endsection @section('scripts') @@ -1392,6 +1621,11 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { if (target === '#tipologie-eventi') { sortableTipologieEventi.refresh(); } + if (target === '#calendario') { + if (typeof sortableCalendario !== 'undefined') { + sortableCalendario.refresh(); + } + } }); // ===== MITTENTI AGGIUNTIVI ===== @@ -1539,9 +1773,11 @@ function testSmtp() { }); } +var quillEditorInstance = null; + function createEditor() { var container = document.querySelector('#signature-editor'); - if (!container || container.querySelector('.ql-toolbar')) return; + if (!container || quillEditorInstance) return; var quill = new Quill('#signature-editor', { theme: 'snow', @@ -1590,10 +1826,12 @@ function createEditor() { signatureInput.value = quill.root.innerHTML; } }); + + quillEditorInstance = quill; } function initSignatureEditor() { - if (document.querySelector('#signature-editor .ql-toolbar')) return; + if (quillEditorInstance) return; if (typeof Quill === 'undefined') { var script = document.createElement('script'); @@ -1771,7 +2009,122 @@ function testRepo(id, btn) { }); } +// ===== CONNESSIONI CALENDARIO ===== +var calendarioConnessioni = @json($calendarioConnessioni ?? []); + +function resetCalendarioForm() { + document.getElementById('calendarioForm').action = '{{ route('calendario-connessioni.store') }}'; + document.getElementById('calendarioMethod').value = 'POST'; + document.getElementById('calendarioModalTitle').textContent = 'Nuova Connessione Calendario'; + document.getElementById('calendarioForm').reset(); + document.getElementById('calendarioId').value = ''; + document.getElementById('cal_is_active').checked = true; + document.getElementById('cal_caldav_password').placeholder = ''; + document.getElementById('cal_gc_client_secret').placeholder = ''; + toggleCalendarioConfigFields(); +} + +function toggleCalendarioConfigFields() { + const tipo = document.getElementById('cal_tipo').value; + document.getElementById('caldavFields').style.display = tipo === 'caldav' ? '' : 'none'; + document.getElementById('googleCalendarFields').style.display = tipo === 'google_calendar' ? '' : 'none'; +} + +function editCalendario(id) { + const cal = calendarioConnessioni.find(c => c.id === id); + if (!cal) return; + + document.getElementById('calendarioForm').action = '{{ route('calendario-connessioni.update', '__ID__') }}'.replace('__ID__', id); + document.getElementById('calendarioMethod').value = 'PUT'; + document.getElementById('calendarioModalTitle').textContent = 'Modifica Connessione - ' + cal.nome; + document.getElementById('calendarioId').value = cal.id; + document.getElementById('cal_nome').value = cal.nome; + document.getElementById('cal_tipo').value = cal.tipo; + document.getElementById('cal_sync_direction').value = cal.sync_direction || 'bidirectional'; + document.getElementById('cal_sync_interval').value = cal.sync_interval_minutes || 60; + document.getElementById('cal_is_active').checked = cal.is_active; + + const cfg = cal.config || {}; + document.getElementById('cal_caldav_url').value = cfg.url || ''; + document.getElementById('cal_caldav_calendar_url').value = cfg.calendar_url || ''; + document.getElementById('cal_caldav_username').value = cfg.username || ''; + document.getElementById('cal_caldav_password').value = ''; + document.getElementById('cal_caldav_password').placeholder = cfg.password ? '......' : ''; + document.getElementById('cal_gc_client_id').value = cfg.client_id || ''; + document.getElementById('cal_gc_client_secret').value = ''; + document.getElementById('cal_gc_client_secret').placeholder = cfg.client_secret ? '......' : ''; + document.getElementById('cal_gc_refresh_token').value = cfg.refresh_token || ''; + document.getElementById('cal_gc_calendar_id').value = cfg.calendar_id || 'primary'; + + toggleCalendarioConfigFields(); + $('#calendarioModal').modal('show'); +} + +function deleteCalendario(id, nome) { + if (!confirm('Eliminare la connessione "' + nome + '"?')) return; + var url = '{{ route('calendario-connessioni.destroy', '__ID__') }}'.replace('__ID__', id); + fetch(url, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'X-HTTP-Method-Override': 'DELETE' + } + }).then(function(r) { return r.json(); }).then(function(data) { + if (data.success) { + location.reload(); + } else { + alert('❌ ' + (data.message || 'Errore')); + } + }).catch(function() { + alert('❌ Errore di rete'); + }); +} + +function testCalendario(id, btn) { + btn.disabled = true; + btn.innerHTML = ' Test...'; + + $.ajax({ + url: '{{ route('calendario-connessioni.test', '__ID__') }}'.replace('__ID__', id), + method: 'POST', + headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }, + timeout: 30000, + success: function(response) { + btn.disabled = false; + btn.innerHTML = ''; + alert(response.success ? '✅ ' + response.message : '❌ ' + response.message); + }, + error: function(xhr) { + btn.disabled = false; + btn.innerHTML = ''; + const msg = xhr.responseJSON?.message || 'Errore di connessione'; + alert('❌ ' + msg); + } + }); +} + $(document).ready(function() { + if (document.getElementById('calendarioSortable')) { + var sortableCalendario = Sortable.create(document.getElementById('calendarioSortable'), { + handle: '.handle', + animation: 150, + onEnd: function(evt) { + var order = []; + $('#calendarioSortable tr').each(function() { + order.push($(this).data('id')); + }); + fetch('{{ route('calendario-connessioni.reorder') }}', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ order: order }) + }); + } + }); + } + if (document.getElementById('repoSortable')) { var sortableRepo = Sortable.create(document.getElementById('repoSortable'), { handle: '.handle', diff --git a/routes/web.php b/routes/web.php index cdc9390c..d2a3304b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -23,6 +23,7 @@ use App\Http\Controllers\Admin\ActivityLogController; use App\Http\Controllers\Admin\EmailSettingsController; use App\Http\Controllers\EmailController; use App\Http\Controllers\StorageRepositoryController; +use App\Http\Controllers\CalendarioConnessioneController; use App\Http\Middleware\AdminOnly; use App\Http\Middleware\CheckPermission; @@ -183,13 +184,22 @@ Route::post('/impostazioni/migra-percorso', [ImpostazioniController::class, 'mig Route::get('/impostazioni/email', [EmailSettingsController::class, 'index'])->middleware('auth')->name('impostazioni.email.index'); Route::post('/impostazioni/email', [EmailSettingsController::class, 'save'])->middleware('auth')->name('impostazioni.email.save'); Route::post('/impostazioni/email/test', [EmailSettingsController::class, 'testConnection'])->middleware('auth')->name('impostazioni.email.test'); - Route::post('/impostazioni/email/test-smtp', [EmailSettingsController::class, 'testSmtp'])->middleware('auth')->name('impostazioni.email.testSmtp'); +Route::post('/impostazioni/email/test-smtp', [EmailSettingsController::class, 'testSmtp'])->middleware('auth')->name('impostazioni.email.testSmtp'); Route::get('/impostazioni/email/sync', [EmailSettingsController::class, 'syncNow'])->middleware('auth')->name('impostazioni.email.sync'); +Route::delete('/impostazioni/email', [EmailSettingsController::class, 'destroy'])->middleware('auth')->name('impostazioni.email.destroy'); Route::post('/impostazioni/sender', [EmailSettingsController::class, 'senderStore'])->middleware('auth')->name('impostazioni.sender.store'); Route::put('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderUpdate'])->middleware('auth')->name('impostazioni.sender.update'); Route::delete('/impostazioni/sender/{id}', [EmailSettingsController::class, 'senderDestroy'])->middleware('auth')->name('impostazioni.sender.destroy'); Route::post('/impostazioni/sender/{id}/test', [EmailSettingsController::class, 'senderTestSmtp'])->middleware('auth')->name('impostazioni.sender.test'); +// Calendar Connections (CalDAV / Google Calendar) +Route::post('/calendario-connessioni', [App\Http\Controllers\CalendarioConnessioneController::class, 'store'])->middleware('auth')->name('calendario-connessioni.store'); +Route::put('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'update'])->middleware('auth')->name('calendario-connessioni.update'); +Route::delete('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'destroy'])->middleware('auth')->name('calendario-connessioni.destroy'); +Route::post('/calendario-connessioni/{id}/test', [App\Http\Controllers\CalendarioConnessioneController::class, 'test'])->middleware('auth')->name('calendario-connessioni.test'); +Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\CalendarioConnessioneController::class, 'sync'])->middleware('auth')->name('calendario-connessioni.sync'); +Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder'); + // Storage Repositories (Opzione B) Route::post('/storage-repositories', [StorageRepositoryController::class, 'store'])->middleware('auth')->name('storage-repositories.store'); Route::put('/storage-repositories/{storage_repository}', [StorageRepositoryController::class, 'update'])->middleware('auth')->name('storage-repositories.update'); @@ -235,6 +245,7 @@ Route::prefix('admin')->middleware(['auth', AdminOnly::class])->group(function ( Route::delete('/ruoli/{id}', [RuoloController::class, 'destroy'])->name('admin.ruoli.destroy'); Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('admin.activity-logs.index'); + Route::delete('/activity-logs', [ActivityLogController::class, 'destroy'])->name('admin.activity-logs.destroy'); // Backup Route::get('/backup', [\App\Http\Controllers\Admin\BackupController::class, 'index'])->name('admin.backup.index'); diff --git a/storage/framework/views/b7fb0777954b7384c283d9ea1db9ba0f.php b/storage/framework/views/b7fb0777954b7384c283d9ea1db9ba0f.php new file mode 100755 index 00000000..627e590e --- /dev/null +++ b/storage/framework/views/b7fb0777954b7384c283d9ea1db9ba0f.php @@ -0,0 +1,165 @@ +startSection('title', 'Dashboard'); ?> +startSection('page_title', 'Dashboard'); ?> + +startSection('content'); ?> +
+
+
+
+

+

Individui

+
+
+ +
+ Visualizza +
+
+
+
+
+

+

Gruppi

+
+
+ +
+ Visualizza +
+
+
+
+
+

+

Documenti

+
+
+ +
+ Visualizza +
+
+
+
+
+

+

Eventi

+
+
+ +
+ Visualizza +
+
+
+ +
+
+
+
+

+

Notifiche

+
+
+ +
+ Visualizza +
+
+
+
+
+

+

Nuove Email

+
+
+ +
+ Visualizza +
+
+
+
+
+

+

Mailing List

+
+
+ +
+ Visualizza +
+
+ canManage('settings')): ?> +
+
+
+

+

Backup

+
+
+ +
+ Visualizza +
+
+ +
+ +
+
+
+
+

Ultime Notifiche

+
+
+ count() > 0): ?> +
    + addLoop($__currentLoopData); foreach($__currentLoopData as $notifica): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
  • + titolo); ?> + created_at->diffForHumans()); ?> +
  • + popLoop(); $loop = $__env->getLastLoop(); ?> +
+ +

Nessuna notifica

+ +
+
+
+
+
+
+

Benvenuto

+
+
+ + +

+ +

Benvenuto in .

+ +

Dal menu laterale puoi navigare tra le sezioni:

+
    +
  • Individui: Gestione delle persone registrate
  • +
  • Gruppi: Gestione della struttura organizzativa
  • +
  • Documenti: Archivio documentale
  • +
  • Eventi: Calendario eventi
  • +
  • Mailing: Comunicazioni email
  • +
+ is_admin): ?> +
+ Amministratore: Hai accesso completo al sistema. +
+ +
+
+
+
+stopSection(); ?> +make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/b8b9b66a1d90ca24f6e19777b0838604.php b/storage/framework/views/b8b9b66a1d90ca24f6e19777b0838604.php new file mode 100755 index 00000000..ecc1f329 --- /dev/null +++ b/storage/framework/views/b8b9b66a1d90ca24f6e19777b0838604.php @@ -0,0 +1,2315 @@ +startSection('title', 'Impostazioni'); ?> + +startSection('page_title', 'Impostazioni'); ?> + +startSection('breadcrumbs'); ?> + + +stopSection(); ?> + +startSection('content'); ?> +
+ +
+
+ +
+
+
+

Configurazione Applicazione

+
+
+

Configura il nome e il testo visualizzato dall'applicazione.

+ + +
+ + +
+ +
+
+
+ + + Nome mostrato nel titolo, sidebar e login +
+
+
+
+ + + Nome organizzazione mostrato sotto il logo nel login +
+
+
+ +
+ + + Lascia vuoto per usare il formato predefinito: © {anno} {nome_applicazione} +
+ +
+
+
+ + + Versione mostrata nel footer +
+
+
+
+ +
+ show_version ?? false) ? 'checked' : ''); ?>> + +
+
+
+
+ +
+ + + Messaggio mostrato nella dashboard. Lascia vuoto per usare il testo predefinito. +
+ + +
+
+
+
+ + +
+
+
+

Repository Remoti

+
+ +
+
+
+

Configura repository remoti (WebDAV, Google Drive) per archiviare documenti. Ogni repository appare come root separata nell'albero delle cartelle in Gestione Documenti.

+ + +
+ + +
+ + + count() > 0): ?> +
+ + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $repo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
#NomeTipoStatoAzioni
nome); ?> + + + tipo)); ?> + + + + is_active): ?> + Attivo + + Disattivo + + + + + +
+
+ +
+ +

Nessun repository remoto configurato.

+

Clicca "Nuovo Repository" per aggiungere un server WebDAV o Google Drive.

+
+ +
+
+ + count() > 0): ?> +
+
+

Come funziona

+
+
+
    +
  • Ogni repository configurato apparirĂ  come root separata nella sidebar Gestione Documenti, sotto "Repository Remoti".
  • +
  • Puoi navigare i file e le cartelle di ogni repository remoto direttamente dall'interfaccia documenti.
  • +
  • WebDAV: supporta server che parlano il protocollo WebDAV (es. NextCloud, ownCloud, Synology).
  • +
  • Google Drive: richiede OAuth 2.0 con client ID, client secret e refresh token configurati nella Google Cloud Console.
  • +
  • Usa il pulsante Test per verificare la connettivitĂ  dopo la configurazione.
  • +
+
+
+ +
+ + +
+
+
+

Repository Documenti

+
+
+

Configura il repository filesystem dove vengono salvati i documenti caricati.

+ + documenti_storage_disk ?? 'local'; + $currentPath = $appSettings->documenti_storage_path ?? 'documenti'; + $absPath = storage_path('app/' . ($currentDisk === 'local' ? 'private/' : '') . $currentPath); + ?> + +
+ +
+
+
+ + + Il disco predefinito per salvare i documenti +
+
+
+
+ + + Sottocartella all'interno del disco +
+
+
+ +
+ + Percorso assoluto calcolato: + +
+ I nuovi documenti verranno salvati in questa directory. +
+ + + +
+ + Percorso modificato! +

La nuova directory :// è stata creata. + 0): ?> + Ci sono file nella vecchia posizione ://.

+ + + + + + + + + Ignora + + + +

Nessun file da spostare.

+ +
+ + + + +
+
+
+ + + + + +
+
+
+

Sezioni Email

+
+ +
+ +
+ + + +
+
+
+
+

Configurazione Server IMAP

+
+
+
+
+
+ + + Gmail: imap.gmail.com | Outlook: outlook.office365.com +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+

Configurazione Server SMTP (per invio email)

+
+
+
+ + Nota: Per inviare email devi configurare il server SMTP. + Per Gmail, usa smtp.gmail.com sulla porta 587 con TLS + e genera una App Password. +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + + Generalmente uguale all'account email +
+
+
+
+ + + Lascia vuoto per usare la password IMAP +
+
+
+
+
+
+ +
+
+
+

Credenziali Account

+
+
+
+
+
+ + + Per Gmail usa l'indirizzo completo +
+
+
+
+ + + + Per Gmail: genera una App Password + +
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+

Sincronizzazione

+
+
+
+
+
+ + +
+
+
+
+ +
+ last_sync_at): ?> + + Ultimo sync: last_sync_at->format('d/m/Y H:i')); ?> + + + + + Mai sincronizzato + + +
+
+
+
+
+
+ is_active ?? false) ? 'checked' : ''); ?>> + +
+
+
+
+
+ +
+
+
+

Firma Email

+
+
+
+
+ signature_enabled ?? true) ? 'checked' : ''); ?>> + +
+ Quando attivo, la firma verrĂ  aggiunta in fondo a ogni email inviata +
+
+
+ +
+ + Usa il riquadro sopra per comporre la tua firma. HTML supportato. +
+
+
+
+ +
+
+
+

Mittenti Aggiuntivi

+
+ +
+
+
+ +
+ + +
+ + + count() > 0): ?> +
+ + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $sender): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
EmailNomeSMTPReply-ToVerifyStatoAzioni
email_address); ?>email_name ?: '-'); ?>smtp_host ?: '-'); ?>:smtp_port ?: '-'); ?>reply_to ?: '-'); ?>verify_email ?: '-'); ?> + is_active): ?> + Attivo + + Disattivo + + + + + +
+
+ +
+ +

Nessun mittente aggiuntivo configurato.

+

Usa il pulsante "Nuovo Mittente" per aggiungere un account di invio dedicato (es. Newsletter).

+
+ +
+
+ + count() > 0): ?> +
+
+

Come funziona

+
+
+
    +
  • I mittenti aggiuntivi sono account solo invio (SMTP) senza IMAP.
  • +
  • Puoi selezionarli nel compose email o negli invii mailing.
  • +
  • Il campo Reply-To imposta l'indirizzo di risposta (es. no-reply@parrocchia.it).
  • +
  • Il campo Verify Email riceve un report di riepilogo dopo ogni invio massivo con l'esito (ok/falliti).
  • +
  • Se Reply-To non è impostato, viene usato Verify Email come fallback.
  • +
+
+
+ +
+
+ +
+ +
+ + +
+
+
+

Calendari Remoti

+
+ +
+
+
+

Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV come Infomaniak/NextCloud).

+ + +
+ + +
+ + + get(); + ?> + + count() > 0): ?> +
+ + + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $cal): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
#NomeTipoDirezione SyncStatoUltimo SyncAzioni
nome); ?> + + + tipo)); ?> + + + + sync_direction): + case ('import'): ?> Solo Import + Solo Export + Bidirezionale + + + stato === 'connesso'): ?> + Connesso + stato === 'errore'): ?> + Errore + + stato)); ?> + + + last_sync_at): ?> + last_sync_at->format('d/m/Y H:i')); ?> + + Mai + + + + +
+ + +
+ +
+
+ +
+ +

Nessun calendario remoto configurato.

+

Clicca "Nuova Connessione" per collegare Google Calendar o un server CalDAV (Infomaniak, NextCloud).

+
+ +
+
+
+ + +
+
+
+

Tipologie Documenti

+
+
+

Gestisci le tipologie disponibili per i documenti caricati nel sistema.

+ + +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $tipologia): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
OrdineNomeDescrizioneStatoAzioni
+ + + nome); ?> + descrizione ?? '-'); ?> + attiva): ?> + Attiva + + Disattivata + + + + documenti()->count() == 0): ?> +
+ + + +
+ + + +
+
+
+
+ + +
+
+
+

Tipologie Eventi

+
+
+

Gestisci le tipologie disponibili per gli eventi del calendario.

+ + +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $tipologia): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
OrdineNomeDescrizioneStatoAzioni
+ + + nome); ?> + descrizione ?? '-'); ?> + attiva): ?> + Attiva + + Disattivata + + + + eventi()->count() == 0): ?> +
+ + + +
+ + + +
+
+
+
+ + +
+
+
+

Tipi di Ruolo

+
+
+

Gestisci i ruoli che possono essere assegnati agli individui nei gruppi.

+ + +
+ + +
+ + +
+ +
+ + + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
OrdineNomeDescrizioneStatoAzioni
+ + + nome); ?> + descrizione ?? '-'); ?> + attiva): ?> + Attivo + + Disattivato + + + + getMembriCount() ?> + +
+ + + +
+ + + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +stopSection(); ?> + +startSection('scripts'); ?> + + + + + +stopSection(); ?> + +make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php b/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php new file mode 100755 index 00000000..8362c51d --- /dev/null +++ b/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php @@ -0,0 +1,301 @@ + + + + + + + + <?php echo $__env->yieldContent('title', e($appName)); ?> + + + + yieldContent('styles'); ?> + + +
+ + + + + +
+
+
+
+
+

yieldContent('page_title', 'Dashboard'); ?>

+
+
+ +
+
+
+
+ +
+
+ + + yieldContent('content'); ?> +
+
+
+ +
+ + + v + +
+
+ + + + + + yieldContent('scripts'); ?> + + \ No newline at end of file diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 6f12fd88..a5d00a3f 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -6,9 +6,12 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( + 'App\\Console\\Commands\\BackupRunCommand' => $baseDir . '/app/Console/Commands/BackupRunCommand.php', + 'App\\Console\\Commands\\ExportHelpPdf' => $baseDir . '/app/Console/Commands/ExportHelpPdf.php', 'App\\Console\\Commands\\SyncUserPermissions' => $baseDir . '/app/Console/Commands/SyncUserPermissions.php', 'App\\Helpers\\ReportColumnRegistry' => $baseDir . '/app/Helpers/ReportColumnRegistry.php', 'App\\Http\\Controllers\\Admin\\ActivityLogController' => $baseDir . '/app/Http/Controllers/Admin/ActivityLogController.php', + 'App\\Http\\Controllers\\Admin\\BackupController' => $baseDir . '/app/Http/Controllers/Admin/BackupController.php', 'App\\Http\\Controllers\\Admin\\EmailSettingsController' => $baseDir . '/app/Http/Controllers/Admin/EmailSettingsController.php', 'App\\Http\\Controllers\\Admin\\RuoloController' => $baseDir . '/app/Http/Controllers/Admin/RuoloController.php', 'App\\Http\\Controllers\\Admin\\UtenteController' => $baseDir . '/app/Http/Controllers/Admin/UtenteController.php', @@ -25,12 +28,14 @@ return array( 'App\\Http\\Controllers\\GruppoController' => $baseDir . '/app/Http/Controllers/GruppoController.php', 'App\\Http\\Controllers\\GruppoIndividuoController' => $baseDir . '/app/Http/Controllers/GruppoIndividuoController.php', 'App\\Http\\Controllers\\GruppoMembroController' => $baseDir . '/app/Http/Controllers/GruppoMembroController.php', + 'App\\Http\\Controllers\\HelpController' => $baseDir . '/app/Http/Controllers/HelpController.php', 'App\\Http\\Controllers\\HomeController' => $baseDir . '/app/Http/Controllers/HomeController.php', 'App\\Http\\Controllers\\ImpostazioniController' => $baseDir . '/app/Http/Controllers/ImpostazioniController.php', 'App\\Http\\Controllers\\IndividuoController' => $baseDir . '/app/Http/Controllers/IndividuoController.php', 'App\\Http\\Controllers\\MailingController' => $baseDir . '/app/Http/Controllers/MailingController.php', 'App\\Http\\Controllers\\MailingListController' => $baseDir . '/app/Http/Controllers/MailingListController.php', 'App\\Http\\Controllers\\ReportController' => $baseDir . '/app/Http/Controllers/ReportController.php', + 'App\\Http\\Controllers\\StorageRepositoryController' => $baseDir . '/app/Http/Controllers/StorageRepositoryController.php', 'App\\Http\\Controllers\\VistaReportController' => $baseDir . '/app/Http/Controllers/VistaReportController.php', 'App\\Http\\Middleware\\AdminOnly' => $baseDir . '/app/Http/Middleware/AdminOnly.php', 'App\\Http\\Middleware\\CheckPermission' => $baseDir . '/app/Http/Middleware/CheckPermission.php', @@ -57,6 +62,7 @@ return array( 'App\\Models\\RolePreset' => $baseDir . '/app/Models/RolePreset.php', 'App\\Models\\Ruolo' => $baseDir . '/app/Models/Ruolo.php', 'App\\Models\\SenderAccount' => $baseDir . '/app/Models/SenderAccount.php', + 'App\\Models\\StorageRepository' => $baseDir . '/app/Models/StorageRepository.php', 'App\\Models\\Tenant' => $baseDir . '/app/Models/Tenant.php', 'App\\Models\\TipologiaDocumento' => $baseDir . '/app/Models/TipologiaDocumento.php', 'App\\Models\\TipologiaEvento' => $baseDir . '/app/Models/TipologiaEvento.php', @@ -64,7 +70,9 @@ return array( 'App\\Models\\VistaReport' => $baseDir . '/app/Models/VistaReport.php', 'App\\Providers\\AppServiceProvider' => $baseDir . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AuthServiceProvider' => $baseDir . '/app/Providers/AuthServiceProvider.php', + 'App\\Services\\BackupService' => $baseDir . '/app/Services/BackupService.php', 'App\\Services\\IcsExportService' => $baseDir . '/app/Services/IcsExportService.php', + 'App\\Services\\StorageRepositoryService' => $baseDir . '/app/Services/StorageRepositoryService.php', 'As247\\CloudStorages\\Cache\\PathCache' => $vendorDir . '/as247/cloud-storages/src/Cache/PathCache.php', 'As247\\CloudStorages\\Cache\\Stores\\ArrayStore' => $vendorDir . '/as247/cloud-storages/src/Cache/Stores/ArrayStore.php', 'As247\\CloudStorages\\Cache\\Stores\\GoogleDrivePersistentStore' => $vendorDir . '/as247/cloud-storages/src/Cache/Stores/GoogleDrivePersistentStore.php', @@ -289,6 +297,7 @@ return array( 'Database\\Seeders\\ComuniSeeder' => $baseDir . '/database/seeders/ComuniSeeder.php', 'Database\\Seeders\\DatabaseSeeder' => $baseDir . '/database/seeders/DatabaseSeeder.php', 'Database\\Seeders\\DiocesiSeeder' => $baseDir . '/database/seeders/DiocesiSeeder.php', + 'Database\\Seeders\\DockerBaseDataSeeder' => $baseDir . '/database/seeders/DockerBaseDataSeeder.php', 'Database\\Seeders\\RolePresetSeeder' => $baseDir . '/database/seeders/RolePresetSeeder.php', 'Database\\Seeders\\RoleSeeder' => $baseDir . '/database/seeders/RoleSeeder.php', 'DeepCopy\\DeepCopy' => $vendorDir . '/myclabs/deep-copy/src/DeepCopy/DeepCopy.php', @@ -41213,8 +41222,10 @@ return array( 'Termwind\\ValueObjects\\Styles' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Styles.php', 'Tests\\Feature\\CalendarEventsTest' => $baseDir . '/tests/Feature/CalendarEventsTest.php', 'Tests\\Feature\\ExampleTest' => $baseDir . '/tests/Feature/ExampleTest.php', + 'Tests\\Feature\\StorageRepositoryTest' => $baseDir . '/tests/Feature/StorageRepositoryTest.php', 'Tests\\TestCase' => $baseDir . '/tests/TestCase.php', 'Tests\\Unit\\ExampleTest' => $baseDir . '/tests/Unit/ExampleTest.php', + 'Tests\\Unit\\StorageRepositoryServiceTest' => $baseDir . '/tests/Unit/StorageRepositoryServiceTest.php', 'TheSeer\\Tokenizer\\Exception' => $vendorDir . '/theseer/tokenizer/src/Exception.php', 'TheSeer\\Tokenizer\\NamespaceUri' => $vendorDir . '/theseer/tokenizer/src/NamespaceUri.php', 'TheSeer\\Tokenizer\\NamespaceUriException' => $vendorDir . '/theseer/tokenizer/src/NamespaceUriException.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index c2a1d079..234024b8 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -673,9 +673,12 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56 ); public static $classMap = array ( + 'App\\Console\\Commands\\BackupRunCommand' => __DIR__ . '/../..' . '/app/Console/Commands/BackupRunCommand.php', + 'App\\Console\\Commands\\ExportHelpPdf' => __DIR__ . '/../..' . '/app/Console/Commands/ExportHelpPdf.php', 'App\\Console\\Commands\\SyncUserPermissions' => __DIR__ . '/../..' . '/app/Console/Commands/SyncUserPermissions.php', 'App\\Helpers\\ReportColumnRegistry' => __DIR__ . '/../..' . '/app/Helpers/ReportColumnRegistry.php', 'App\\Http\\Controllers\\Admin\\ActivityLogController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/ActivityLogController.php', + 'App\\Http\\Controllers\\Admin\\BackupController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/BackupController.php', 'App\\Http\\Controllers\\Admin\\EmailSettingsController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/EmailSettingsController.php', 'App\\Http\\Controllers\\Admin\\RuoloController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/RuoloController.php', 'App\\Http\\Controllers\\Admin\\UtenteController' => __DIR__ . '/../..' . '/app/Http/Controllers/Admin/UtenteController.php', @@ -692,12 +695,14 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56 'App\\Http\\Controllers\\GruppoController' => __DIR__ . '/../..' . '/app/Http/Controllers/GruppoController.php', 'App\\Http\\Controllers\\GruppoIndividuoController' => __DIR__ . '/../..' . '/app/Http/Controllers/GruppoIndividuoController.php', 'App\\Http\\Controllers\\GruppoMembroController' => __DIR__ . '/../..' . '/app/Http/Controllers/GruppoMembroController.php', + 'App\\Http\\Controllers\\HelpController' => __DIR__ . '/../..' . '/app/Http/Controllers/HelpController.php', 'App\\Http\\Controllers\\HomeController' => __DIR__ . '/../..' . '/app/Http/Controllers/HomeController.php', 'App\\Http\\Controllers\\ImpostazioniController' => __DIR__ . '/../..' . '/app/Http/Controllers/ImpostazioniController.php', 'App\\Http\\Controllers\\IndividuoController' => __DIR__ . '/../..' . '/app/Http/Controllers/IndividuoController.php', 'App\\Http\\Controllers\\MailingController' => __DIR__ . '/../..' . '/app/Http/Controllers/MailingController.php', 'App\\Http\\Controllers\\MailingListController' => __DIR__ . '/../..' . '/app/Http/Controllers/MailingListController.php', 'App\\Http\\Controllers\\ReportController' => __DIR__ . '/../..' . '/app/Http/Controllers/ReportController.php', + 'App\\Http\\Controllers\\StorageRepositoryController' => __DIR__ . '/../..' . '/app/Http/Controllers/StorageRepositoryController.php', 'App\\Http\\Controllers\\VistaReportController' => __DIR__ . '/../..' . '/app/Http/Controllers/VistaReportController.php', 'App\\Http\\Middleware\\AdminOnly' => __DIR__ . '/../..' . '/app/Http/Middleware/AdminOnly.php', 'App\\Http\\Middleware\\CheckPermission' => __DIR__ . '/../..' . '/app/Http/Middleware/CheckPermission.php', @@ -724,6 +729,7 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56 'App\\Models\\RolePreset' => __DIR__ . '/../..' . '/app/Models/RolePreset.php', 'App\\Models\\Ruolo' => __DIR__ . '/../..' . '/app/Models/Ruolo.php', 'App\\Models\\SenderAccount' => __DIR__ . '/../..' . '/app/Models/SenderAccount.php', + 'App\\Models\\StorageRepository' => __DIR__ . '/../..' . '/app/Models/StorageRepository.php', 'App\\Models\\Tenant' => __DIR__ . '/../..' . '/app/Models/Tenant.php', 'App\\Models\\TipologiaDocumento' => __DIR__ . '/../..' . '/app/Models/TipologiaDocumento.php', 'App\\Models\\TipologiaEvento' => __DIR__ . '/../..' . '/app/Models/TipologiaEvento.php', @@ -731,7 +737,9 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56 'App\\Models\\VistaReport' => __DIR__ . '/../..' . '/app/Models/VistaReport.php', 'App\\Providers\\AppServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AppServiceProvider.php', 'App\\Providers\\AuthServiceProvider' => __DIR__ . '/../..' . '/app/Providers/AuthServiceProvider.php', + 'App\\Services\\BackupService' => __DIR__ . '/../..' . '/app/Services/BackupService.php', 'App\\Services\\IcsExportService' => __DIR__ . '/../..' . '/app/Services/IcsExportService.php', + 'App\\Services\\StorageRepositoryService' => __DIR__ . '/../..' . '/app/Services/StorageRepositoryService.php', 'As247\\CloudStorages\\Cache\\PathCache' => __DIR__ . '/..' . '/as247/cloud-storages/src/Cache/PathCache.php', 'As247\\CloudStorages\\Cache\\Stores\\ArrayStore' => __DIR__ . '/..' . '/as247/cloud-storages/src/Cache/Stores/ArrayStore.php', 'As247\\CloudStorages\\Cache\\Stores\\GoogleDrivePersistentStore' => __DIR__ . '/..' . '/as247/cloud-storages/src/Cache/Stores/GoogleDrivePersistentStore.php', @@ -956,6 +964,7 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56 'Database\\Seeders\\ComuniSeeder' => __DIR__ . '/../..' . '/database/seeders/ComuniSeeder.php', 'Database\\Seeders\\DatabaseSeeder' => __DIR__ . '/../..' . '/database/seeders/DatabaseSeeder.php', 'Database\\Seeders\\DiocesiSeeder' => __DIR__ . '/../..' . '/database/seeders/DiocesiSeeder.php', + 'Database\\Seeders\\DockerBaseDataSeeder' => __DIR__ . '/../..' . '/database/seeders/DockerBaseDataSeeder.php', 'Database\\Seeders\\RolePresetSeeder' => __DIR__ . '/../..' . '/database/seeders/RolePresetSeeder.php', 'Database\\Seeders\\RoleSeeder' => __DIR__ . '/../..' . '/database/seeders/RoleSeeder.php', 'DeepCopy\\DeepCopy' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/DeepCopy.php', @@ -41880,8 +41889,10 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56 'Termwind\\ValueObjects\\Styles' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Styles.php', 'Tests\\Feature\\CalendarEventsTest' => __DIR__ . '/../..' . '/tests/Feature/CalendarEventsTest.php', 'Tests\\Feature\\ExampleTest' => __DIR__ . '/../..' . '/tests/Feature/ExampleTest.php', + 'Tests\\Feature\\StorageRepositoryTest' => __DIR__ . '/../..' . '/tests/Feature/StorageRepositoryTest.php', 'Tests\\TestCase' => __DIR__ . '/../..' . '/tests/TestCase.php', 'Tests\\Unit\\ExampleTest' => __DIR__ . '/../..' . '/tests/Unit/ExampleTest.php', + 'Tests\\Unit\\StorageRepositoryServiceTest' => __DIR__ . '/../..' . '/tests/Unit/StorageRepositoryServiceTest.php', 'TheSeer\\Tokenizer\\Exception' => __DIR__ . '/..' . '/theseer/tokenizer/src/Exception.php', 'TheSeer\\Tokenizer\\NamespaceUri' => __DIR__ . '/..' . '/theseer/tokenizer/src/NamespaceUri.php', 'TheSeer\\Tokenizer\\NamespaceUriException' => __DIR__ . '/..' . '/theseer/tokenizer/src/NamespaceUriException.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 23b4784b..9beff94c 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -5767,17 +5767,17 @@ }, { "name": "sabre/vobject", - "version": "4.5.8", - "version_normalized": "4.5.8.0", + "version": "4.6.0", + "version_normalized": "4.6.0.0", "source": { "type": "git", "url": "https://github.com/sabre-io/vobject.git", - "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1" + "reference": "9432544fc369851fb8202c5d91159b2e669f0c88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1", - "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/9432544fc369851fb8202c5d91159b2e669f0c88", + "reference": "9432544fc369851fb8202c5d91159b2e669f0c88", "shasum": "" }, "require": { @@ -5794,7 +5794,7 @@ "suggest": { "hoa/bench": "If you would like to run the benchmark scripts" }, - "time": "2026-01-12T10:45:19+00:00", + "time": "2026-05-31T13:04:55+00:00", "bin": [ "bin/vobject", "bin/generate_vcards" diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 7a04693e..7f908d1f 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'laravel/laravel', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '3471befb1af62975088d115a46aa180e13652a0d', + 'reference' => 'aa4e582925ec155fa08dbeb9bf4e3a97b4751df1', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -481,7 +481,7 @@ 'laravel/laravel' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '3471befb1af62975088d115a46aa180e13652a0d', + 'reference' => 'aa4e582925ec155fa08dbeb9bf4e3a97b4751df1', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -1010,9 +1010,9 @@ 'dev_requirement' => false, ), 'sabre/vobject' => array( - 'pretty_version' => '4.5.8', - 'version' => '4.5.8.0', - 'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1', + 'pretty_version' => '4.6.0', + 'version' => '4.6.0.0', + 'reference' => '9432544fc369851fb8202c5d91159b2e669f0c88', 'type' => 'library', 'install_path' => __DIR__ . '/../sabre/vobject', 'aliases' => array(), diff --git a/vendor/sabre/vobject/lib/Component/VCard.php b/vendor/sabre/vobject/lib/Component/VCard.php index 82fab82b..e2b44081 100644 --- a/vendor/sabre/vobject/lib/Component/VCard.php +++ b/vendor/sabre/vobject/lib/Component/VCard.php @@ -430,6 +430,34 @@ class VCard extends VObject\Document } } + /** + * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL). + * + * This function will return null if the exact property list does not exist. + * + * For example to get the property of `TEL;TYPE=HOME,CELL` + * you would call `getByTypes('TEL', ['HOME', 'CELL'])` + * + * @param string[] $types + * + * @return \ArrayAccess|array|null + */ + public function getByTypes(string $propertyName, array $types) + { + $types = array_map('strtolower', $types); + foreach ($this->select($propertyName) as $field) { + if (isset($field['TYPE'])) { + $parts = array_map('strtolower', $field['TYPE']->getParts()); + + if (!array_diff($types, $parts) && !array_diff($parts, $types)) { + return $field; + } + } + } + + return null; + } + /** * This method should return a list of default property values. * diff --git a/vendor/sabre/vobject/lib/ITip/Broker.php b/vendor/sabre/vobject/lib/ITip/Broker.php index e100c146..e89b7a4e 100644 --- a/vendor/sabre/vobject/lib/ITip/Broker.php +++ b/vendor/sabre/vobject/lib/ITip/Broker.php @@ -685,7 +685,7 @@ class Broker // We only need to do that though, if the master event is not declined. if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { foreach ($eventInfo['exdate'] as $exDate) { - if (!in_array($exDate, $oldEventInfo['exdate'])) { + if (!in_array($exDate, $oldEventInfo['exdate'] ?? [])) { if (isset($instances[$exDate])) { $instances[$exDate]['newstatus'] = 'DECLINED'; } else { diff --git a/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php b/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php index b52ba6a1..799283e0 100644 --- a/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php +++ b/vendor/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php @@ -18,11 +18,18 @@ class FindFromTimezoneMap implements TimezoneFinder '/^\((UTC|GMT)(\+|\-)[\d]{2}\.[\d]{2}\) (.*)/', ]; + /** + * @throws void + */ public function find(string $tzid, bool $failIfUncertain = false): ?DateTimeZone { // Next, we check if the tzid is somewhere in our tzid map. if ($this->hasTzInMap($tzid)) { - return new DateTimeZone($this->getTzFromMap($tzid)); + try { + return new DateTimeZone($this->getTzFromMap($tzid)); + } catch (\Exception $e) { + return null; + } } // Some Microsoft products prefix the offset first, so let's strip that off @@ -34,7 +41,11 @@ class FindFromTimezoneMap implements TimezoneFinder } $tzidAlternate = $matches[3]; if ($this->hasTzInMap($tzidAlternate)) { - return new DateTimeZone($this->getTzFromMap($tzidAlternate)); + try { + return new DateTimeZone($this->getTzFromMap($tzidAlternate)); + } catch (\Exception $e) { + return null; + } } } diff --git a/vendor/sabre/vobject/lib/Version.php b/vendor/sabre/vobject/lib/Version.php index decd4247..72391f59 100644 --- a/vendor/sabre/vobject/lib/Version.php +++ b/vendor/sabre/vobject/lib/Version.php @@ -14,5 +14,5 @@ class Version /** * Full version number. */ - public const VERSION = '4.5.8'; + public const VERSION = '4.6.0'; } diff --git a/vendor/sabre/vobject/lib/timezonedata/exchangezones.php b/vendor/sabre/vobject/lib/timezonedata/exchangezones.php index 3e7eace1..f42944b3 100644 --- a/vendor/sabre/vobject/lib/timezonedata/exchangezones.php +++ b/vendor/sabre/vobject/lib/timezonedata/exchangezones.php @@ -40,12 +40,12 @@ return [ 'Kabul' => 'Asia/Kabul', 'Ekaterinburg' => 'Asia/Yekaterinburg', 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', - 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Kolkata', 'Kathmandu, Nepal' => 'Asia/Kathmandu', 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', 'Astana, Dhaka' => 'Asia/Dhaka', 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', - 'Rangoon' => 'Asia/Rangoon', + 'Rangoon' => 'Asia/Yangon', 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', 'Krasnoyarsk' => 'Asia/Krasnoyarsk', 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', @@ -72,7 +72,7 @@ return [ 'Mid-Atlantic' => 'America/Noronha', 'Brasilia' => 'America/Sao_Paulo', // Best guess 'Buenos Aires' => 'America/Argentina/Buenos_Aires', - 'Greenland' => 'America/Godthab', + 'Greenland' => 'America/Nuuk', 'Newfoundland' => 'America/St_Johns', 'Atlantic Time (Canada)' => 'America/Halifax', 'Caracas, La Paz' => 'America/Caracas', diff --git a/vendor/sabre/vobject/lib/timezonedata/lotuszones.php b/vendor/sabre/vobject/lib/timezonedata/lotuszones.php index 4b50808f..08c37afd 100644 --- a/vendor/sabre/vobject/lib/timezonedata/lotuszones.php +++ b/vendor/sabre/vobject/lib/timezonedata/lotuszones.php @@ -34,7 +34,7 @@ return [ 'Newfoundland' => 'America/St_Johns', 'Argentina' => 'America/Argentina/Buenos_Aires', 'E. South America' => 'America/Belem', - 'Greenland' => 'America/Godthab', + 'Greenland' => 'America/Nuuk', 'Montevideo' => 'America/Montevideo', 'SA Eastern' => 'America/Belem', // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. @@ -71,12 +71,12 @@ return [ 'Ekaterinburg' => 'Asia/Yekaterinburg', 'Pakistan' => 'Asia/Karachi', 'West Asia' => 'Asia/Tashkent', - 'India' => 'Asia/Calcutta', + 'India' => 'Asia/Kolkata', 'Sri Lanka' => 'Asia/Colombo', 'Nepal' => 'Asia/Kathmandu', 'Central Asia' => 'Asia/Dhaka', 'N. Central Asia' => 'Asia/Almaty', - 'Myanmar' => 'Asia/Rangoon', + 'Myanmar' => 'Asia/Yangon', 'North Asia' => 'Asia/Krasnoyarsk', 'SE Asia' => 'Asia/Bangkok', 'China' => 'Asia/Shanghai', diff --git a/vendor/sabre/vobject/lib/timezonedata/windowszones.php b/vendor/sabre/vobject/lib/timezonedata/windowszones.php index 2049a95c..7ff61b52 100644 --- a/vendor/sabre/vobject/lib/timezonedata/windowszones.php +++ b/vendor/sabre/vobject/lib/timezonedata/windowszones.php @@ -20,7 +20,7 @@ return [ 'Arab Standard Time' => 'Asia/Riyadh', 'Arabian Standard Time' => 'Asia/Dubai', 'Arabic Standard Time' => 'Asia/Baghdad', - 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Argentina Standard Time' => 'America/Argentina/Buenos_Aires', 'Astrakhan Standard Time' => 'Europe/Astrakhan', 'Atlantic Standard Time' => 'America/Halifax', 'Aus Central W. Standard Time' => 'Australia/Eucla', @@ -55,16 +55,16 @@ return [ 'Eastern Standard Time (Mexico)' => 'America/Cancun', 'Egypt Standard Time' => 'Africa/Cairo', 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', - 'FLE Standard Time' => 'Europe/Kiev', + 'FLE Standard Time' => 'Europe/Kyiv', 'Fiji Standard Time' => 'Pacific/Fiji', 'GMT Standard Time' => 'Europe/London', 'GTB Standard Time' => 'Europe/Bucharest', 'Georgian Standard Time' => 'Asia/Tbilisi', - 'Greenland Standard Time' => 'America/Godthab', + 'Greenland Standard Time' => 'America/Nuuk', 'Greenwich Standard Time' => 'Atlantic/Reykjavik', 'Haiti Standard Time' => 'America/Port-au-Prince', 'Hawaiian Standard Time' => 'Pacific/Honolulu', - 'India Standard Time' => 'Asia/Calcutta', + 'India Standard Time' => 'Asia/Kolkata', 'Iran Standard Time' => 'Asia/Tehran', 'Israel Standard Time' => 'Asia/Jerusalem', 'Jordan Standard Time' => 'Asia/Amman', @@ -82,10 +82,10 @@ return [ 'Morocco Standard Time' => 'Africa/Casablanca', 'Mountain Standard Time' => 'America/Denver', 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', - 'Myanmar Standard Time' => 'Asia/Rangoon', + 'Myanmar Standard Time' => 'Asia/Yangon', 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', 'Namibia Standard Time' => 'Africa/Windhoek', - 'Nepal Standard Time' => 'Asia/Katmandu', + 'Nepal Standard Time' => 'Asia/Kathmandu', 'New Zealand Standard Time' => 'Pacific/Auckland', 'Newfoundland Standard Time' => 'America/St_Johns', 'Norfolk Standard Time' => 'Pacific/Norfolk', @@ -127,7 +127,7 @@ return [ 'Transbaikal Standard Time' => 'Asia/Chita', 'Turkey Standard Time' => 'Europe/Istanbul', 'Turks And Caicos Standard Time' => 'America/Grand_Turk', - 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Eastern Standard Time' => 'America/Indiana/Indianapolis', 'US Mountain Standard Time' => 'America/Phoenix', 'UTC' => 'Etc/GMT', 'UTC+12' => 'Etc/GMT-12',