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; } }