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 @@
+@endsection
+
+@section('scripts')
+
@endsection
\ No newline at end of file
diff --git a/resources/views/admin/email-settings/index.blade.php b/resources/views/admin/email-settings/index.blade.php
index 3e878d26..80eac2c0 100644
--- a/resources/views/admin/email-settings/index.blade.php
+++ b/resources/views/admin/email-settings/index.blade.php
@@ -377,7 +377,7 @@
-
@@ -628,9 +634,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',
@@ -679,10 +687,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');
diff --git a/resources/views/impostazioni/index.blade.php b/resources/views/impostazioni/index.blade.php
index 9f5b70a7..efacb352 100644
--- a/resources/views/impostazioni/index.blade.php
+++ b/resources/views/impostazioni/index.blade.php
@@ -709,7 +709,7 @@
-
@@ -730,19 +736,104 @@
-
Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV, ecc.).
-
-
-
Prossimamente: Potrai collegare calendari Google, Infomaniak o altri provider CalDAV per sincronizzare gli eventi del calendario.
+
Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV come Infomaniak/NextCloud).
+
+ @if(session('success'))
+
{{ session('success') }}
+ @endif
+ @if(session('error'))
+
{{ session('error') }}
+ @endif
+
+ @php
+ $calendarioConnessioni = \App\Models\CalendarioConnessione::orderBy('ordine')->get();
+ @endphp
+
+ @if($calendarioConnessioni->count() > 0)
+
+
+
+
+ #
+ Nome
+ Tipo
+ Direzione Sync
+ Stato
+ Ultimo Sync
+ Azioni
+
+
+
+ @foreach($calendarioConnessioni as $cal)
+
+
+ {{ $cal->nome }}
+
+
+
+ {{ \App\Models\CalendarioConnessione::etichettaTipo($cal->tipo) }}
+
+
+
+ @switch($cal->sync_direction)
+ @case('import') Solo Import @break
+ @case('export') Solo Export @break
+ @default Bidirezionale
+ @endswitch
+
+
+ @if($cal->stato === 'connesso')
+ Connesso
+ @elseif($cal->stato === 'errore')
+ Errore
+ @else
+ {{ ucfirst($cal->stato) }}
+ @endif
+
+
+ @if($cal->last_sync_at)
+ {{ $cal->last_sync_at->format('d/m/Y H:i') }}
+ @else
+ Mai
+ @endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @endforeach
+
+
-
-
-
Nessun calendario remoto configurato.
-
La funzionalitĂ sarĂ disponibile in un prossimo aggiornamento.
+ @else
+
+
+
Nessun calendario remoto configurato.
+
Clicca "Nuova Connessione" per collegare Google Calendar o un server CalDAV (Infomaniak, NextCloud).
+ @endif
@@ -1277,6 +1368,144 @@
+
+{{-- ===== MODALE CONNESSIONE CALENDARIO ===== --}}
+
@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'); ?>
+
+
+
+
+
+
+ canManage('settings')): ?>
+
+
+
+
+
+
+
+
+
+ count() > 0): ?>
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $notifica): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+
+ titolo); ?>
+ created_at->diffForHumans()); ?>
+
+ popLoop(); $loop = $__env->getLastLoop(); ?>
+
+
+
Nessuna notifica
+
+
+
+
+
+
+
+
+
+
+
+
+
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'); ?>
+ Dashboard
+ Impostazioni
+stopSection(); ?>
+
+startSection('content'); ?>
+
+
+
+
+
+
+
+
+
+
Configura il nome e il testo visualizzato dall'applicazione.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Configura repository remoti (WebDAV, Google Drive) per archiviare documenti. Ogni repository appare come root separata nell'albero delle cartelle in Gestione Documenti.
+
+
+
+
+
+
+
+
+ count() > 0): ?>
+
+
+
+
+ #
+ Nome
+ Tipo
+ Stato
+ Azioni
+
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $repo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+
+
+ nome); ?>
+
+
+
+ tipo)); ?>
+
+
+
+
+ is_active): ?>
+ Attivo
+
+ Disattivo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+
+
Nessun repository remoto configurato.
+
Clicca "Nuovo Repository" per aggiungere un server WebDAV o Google Drive.
+
+
+
+
+
+ count() > 0): ?>
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
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);
+ ?>
+
+
+
+
+
+
+ Disco di archiviazione
+
+ >Locale (storage/app/private)
+ >Pubblico (storage/app/public)
+
+ Il disco predefinito per salvare i documenti
+
+
+
+
+ Percorso relativo
+
+ 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 ://.
+
+
+
+
+
+
+
+ Sposta file nel nuovo percorso
+
+
+ Ignora
+
+
+
+
Nessun file da spostare.
+
+
+
+
+
+ Salva Impostazioni
+
+
+
+
+
+
+
+
+
+
+
+
Carica il logo dell'applicazione. VerrĂ visualizzato nella pagina di login e nella sidebar.
+
+
+
+
+
+
+
+
+
+ Salva Logo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Host IMAP
+
+ Gmail: imap.gmail.com | Outlook: outlook.office365.com
+
+
+
+
+
+ Crittografia
+
+ imap_encryption ?? 'ssl') == 'ssl' ? 'selected' : ''); ?>>SSL
+ imap_encryption ?? '') == 'tls' ? 'selected' : ''); ?>>TLS
+ imap_encryption ?? '') == 'none' ? 'selected' : ''); ?>>Nessuna
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 .
+
+
+
+
+
+
+ Crittografia
+
+ smtp_encryption ?? 'tls') == 'tls' ? 'selected' : ''); ?>>TLS
+ smtp_encryption ?? '') == 'ssl' ? 'selected' : ''); ?>>SSL
+ smtp_encryption ?? '') == 'none' ? 'selected' : ''); ?>>Nessuna
+
+
+
+
+
+
+
+ Username SMTP
+
+ Generalmente uguale all'account email
+
+
+
+
+ Password SMTP / App Password
+
+ Lascia vuoto per usare la password IMAP
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Username/Email
+
+ Per Gmail usa l'indirizzo completo
+
+
+
+
+
+
+
+
+ Email Indirizzo
+
+
+
+
+
+ Nome Visualizzato
+
+
+
+
+
+
+
+ Email Risposta (Reply-To)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Intervallo Sync (minuti)
+
+ sync_interval_minutes ?? 5) == 1 ? 'selected' : ''); ?>>1 minuto
+ sync_interval_minutes ?? 5) == 5 ? 'selected' : ''); ?>>5 minuti
+ sync_interval_minutes ?? 5) == 15 ? 'selected' : ''); ?>>15 minuti
+ sync_interval_minutes ?? 5) == 30 ? 'selected' : ''); ?>>30 minuti
+ sync_interval_minutes ?? 5) == 60 ? 'selected' : ''); ?>>1 ora
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ count() > 0): ?>
+
+
+
+
+ Email
+ Nome
+ SMTP
+ Reply-To
+ Verify
+ Stato
+ Azioni
+
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $sender): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+
+ email_address); ?>
+ email_name ?: '-'); ?>
+ smtp_host ?: '-'); ?>:smtp_port ?: '-'); ?>
+ reply_to ?: '-'); ?>
+ verify_email ?: '-'); ?>
+
+ is_active): ?>
+ Attivo
+
+ Disattivo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+
+
Nessun mittente aggiuntivo configurato.
+
Usa il pulsante "Nuovo Mittente" per aggiungere un account di invio dedicato (es. Newsletter).
+
+
+
+
+
+ count() > 0): ?>
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Configura la sincronizzazione con calendari esterni (Google Calendar, CalDAV come Infomaniak/NextCloud).
+
+
+
+
+
+
+
+
+ get();
+ ?>
+
+ count() > 0): ?>
+
+
+
+
+ #
+ Nome
+ Tipo
+ Direzione Sync
+ Stato
+ Ultimo Sync
+ Azioni
+
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $cal): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+
+
Nessun calendario remoto configurato.
+
Clicca "Nuova Connessione" per collegare Google Calendar o un server CalDAV (Infomaniak, NextCloud).
+
+
+
+
+
+
+
+
+
+
+
+
Gestisci le tipologie disponibili per i documenti caricati nel sistema.
+
+
+
+
+
+
+
+
+
+
+ Nuova Tipologia
+
+
+
+
+
+
+ Ordine
+ Nome
+ Descrizione
+ Stato
+ Azioni
+
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $tipologia): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+ nome); ?>
+
+ descrizione ?? '-'); ?>
+
+ attiva): ?>
+ Attiva
+
+ Disattivata
+
+
+
+
+
+
+ documenti()->count() == 0): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
Gestisci le tipologie disponibili per gli eventi del calendario.
+
+
+
+
+
+
+
+
+
+
+ Nuova Tipologia
+
+
+
+
+
+
+ Ordine
+ Nome
+ Descrizione
+ Stato
+ Azioni
+
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $tipologia): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+ nome); ?>
+
+ descrizione ?? '-'); ?>
+
+ attiva): ?>
+ Attiva
+
+ Disattivata
+
+
+
+
+
+
+ eventi()->count() == 0): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
Gestisci i ruoli che possono essere assegnati agli individui nei gruppi.
+
+
+
+
+
+
+
+
+
+
+ Nuovo Ruolo
+
+
+
+
+
+
+ Ordine
+ Nome
+ Descrizione
+ Stato
+ Azioni
+
+
+
+ addLoop($__currentLoopData); foreach($__currentLoopData as $ruolo): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+ nome); ?>
+
+ descrizione ?? '-'); ?>
+
+ attiva): ?>
+ Attivo
+
+ Disattivato
+
+
+
+
+
+
+ getMembriCount() ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ popLoop(); $loop = $__env->getLastLoop(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nome *
+
+
+
+ Descrizione
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nome *
+
+
+
+ Descrizione
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nome *
+
+
+
+ Descrizione
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nome visualizzato
+
+
+
+
+
+
Configurazione SMTP
+
+
+
+
+
+ Crittografia
+
+ TLS
+ SSL
+ Nessuna
+
+
+
+
+
+
+
+
+ Password SMTP
+
+ Lascia vuoto per non cambiare
+
+
+
+
+
+
+
+ Reply-To (no-reply)
+
+ Indirizzo di risposta per le email inviate
+
+
+
+
+ Verify Email (report)
+
+ Riceve report riepilogo dopo invii massivi
+
+
+
+
+ Nota interna
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tipo *
+
+ CalDAV (Infomaniak/NextCloud)
+ Google Calendar
+
+
+
+
+
+
+
+ Direzione Sync
+
+ Bidirezionale
+ Solo Import (da remoto a locale)
+ Solo Export (da locale a remoto)
+
+
+
+
+
+ Intervallo Sync (minuti)
+
+ 5 minuti
+ 15 minuti
+ 30 minuti
+ 1 ora
+ 6 ore
+ 24 ore
+
+
+
+
+
+
+
+
Configurazione
+
+
+
+
+ URL Server CalDAV *
+
+ URL principale del server CalDAV (es. NextCloud, Infomaniak, Synology)
+
+
+ URL Calendario (opzionale)
+
+ URL specifico del calendario, se diverso dall'URL del server
+
+
+
+
+
+ Password / App Password
+
+
+
+
+
+
+
+
+
+
+
+
+ Client Secret *
+
+
+
+
+
+ Refresh Token
+
+ Dopo aver configurato Client ID e Client Secret, salva la connessione e usa il test per verificare.
+
+
+ Calendar ID
+
+ ID del calendario Google (default: primary per il calendario principale)
+
+
+
+
Google Calendar: Crea il progetto in
Google Cloud Console
+ → API & Services → Credentials → Crea OAuth Client ID (Web Application).
+ Abilita
Google Calendar API e configura l'URI di redirect.
+
+
+
+
+
+
+
+
+stopSection(); ?>
+
+startSection('scripts'); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tipo *
+
+ WebDAV
+ Google Drive
+
+
+
+
+
+
+
+
Configurazione
+
+
+
+
+
+
+ URL Base WebDAV *
+
+
+
+
+
+ Root path
+
+ Percorso base nel server
+
+
+
+
+
+
+
+
+ Auth Type
+
+ Basic
+ Digest
+ NTLM
+
+
+
+
+
+
+
+
+
+
+
+
+ Client Secret *
+
+
+
+
+
+
+
+
+
Autorizzazione Google
+
+ Non autorizzato
+
+
+ Autorizza con Google
+
+
+ Dopo aver cliccato, verrai reindirizzato a Google per autorizzare l'app.
+ Al termine, il Refresh Token verrĂ salvato automaticamente.
+
+
+
+
+
+ Root Folder ID
+
+ ID della cartella Google Drive, default: root
+
+
+
+
+
+
Prima volta? Crea il progetto in
Google Cloud Console
+ → API & Services → Credentials → Crea OAuth Client ID (Web Application).
+ Aggiungi
come
Authorized Redirect URI .
+
+
+
+
+
+
+
+
+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 @@
+
+
+
+
+
+
+
+ yieldContent('title', e($appName)); ?>
+
+
+
+ yieldContent('styles'); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ yieldContent('content'); ?>
+
+
+
+
+
+
+
+
+
+
+
+ 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',