'SU', 1 => 'MO', 2 => 'TU', 3 => 'WE', 4 => 'TH', 5 => 'FR', 6 => 'SA', ]; public function generateSingle(Evento $evento): string { return $this->generateCollection(collect([$evento])); } public function generateCollection(iterable $eventi): string { $lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Glastree//Eventi//IT', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'X-WR-CALNAME:' . config('app.name') . ' - Eventi', ]; foreach ($eventi as $evento) { array_push($lines, ...$this->buildVEvent($evento)); } $lines[] = 'END:VCALENDAR'; return $this->fold(implode("\r\n", $lines)); } private function buildVEvent(Evento $evento): array { $uid = 'evento-' . $evento->id . '@' . preg_replace('/[^a-z0-9]/', '', strtolower(config('app.name'))); $lines = [ 'BEGIN:VEVENT', 'UID:' . $uid, 'DTSTAMP:' . now()->format(self::DATETIME_FORMAT) . 'Z', ]; if ($evento->isSingolo() && $evento->data_specifica) { $date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica); if ($evento->ora_inizio) { $start = $date->copy()->setTimeFrom($evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio)); $durata = $evento->durata_minuti ?? 60; $end = $start->copy()->addMinutes((int) $durata); $lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $start->format(self::DATETIME_FORMAT); $lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $end->format(self::DATETIME_FORMAT); } else { $lines[] = 'DTSTART;VALUE=DATE:' . $date->format(self::DATE_FORMAT); $lines[] = 'DTEND;VALUE=DATE:' . $date->copy()->addDay()->format(self::DATE_FORMAT); } } else { $rrule = $this->buildRRule($evento); if ($rrule) { $lines[] = $rrule; } if ($evento->ora_inizio) { $time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio); $lines[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $time->format('His'); $durata = $evento->durata_minuti ?? 60; $endTime = $time->copy()->addMinutes((int) $durata); $lines[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $endTime->format('His'); } } $lines[] = 'SUMMARY:' . $this->escapeText($evento->nome_evento); if ($evento->descrizione_evento || $evento->descrizione) { $desc = $evento->descrizione_evento ? ($evento->descrizione ? $evento->descrizione_evento . '\n\n' . $evento->descrizione : $evento->descrizione_evento) : $evento->descrizione; $lines[] = 'DESCRIPTION:' . $this->escapeText($desc); } if ($evento->luogo_indirizzo) { $lines[] = 'LOCATION:' . $this->escapeText($evento->luogo_indirizzo); } $lines[] = 'END:VEVENT'; return $lines; } private function buildRRule(Evento $evento): ?string { return match ($evento->tipo_recorrenza) { 'settimanale' => $this->rruleSettimanale($evento), 'mensile' => $this->rruleMensile($evento), 'annuale' => $this->rruleAnnuale($evento), 'altro' => $this->rruleAltro($evento), default => null, }; } private function rruleSettimanale(Evento $evento): ?string { if ($evento->giorno_settimana === null) { return null; } return 'RRULE:FREQ=WEEKLY;BYDAY=' . (self::DAY_MAP[(int) $evento->giorno_settimana] ?? 'MO'); } private function rruleMensile(Evento $evento): ?string { if (!$evento->occorrenza_mese) { return null; } $parts = explode(',', $evento->occorrenza_mese); if (count($parts) !== 2) { return null; } $occorrenza = (int) $parts[0]; $giorno = (int) $parts[1]; $byDay = self::DAY_MAP[$giorno] ?? 'MO'; $rrule = 'RRULE:FREQ=MONTHLY;BYSETPOS=' . $occorrenza . ';BYDAY=' . $byDay; if ($evento->mesi_recorrenza) { $mesi = explode(',', $evento->mesi_recorrenza); $rrule .= ';BYMONTH=' . implode(',', $mesi); } return $rrule; } private function rruleAnnuale(Evento $evento): ?string { if (!$evento->mese_annuale) { return null; } $rrule = 'RRULE:FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale; $giorno = (int) ($evento->giorno_mese ?? 1); $rrule .= ';BYMONTHDAY=' . $giorno; return $rrule; } private function rruleAltro(Evento $evento): ?string { return $this->rruleSettimanale($evento); } private function escapeText(?string $text): string { if ($text === null) { return ''; } $text = str_replace( ['\\', ';', ',', "\n"], ['\\\\', '\\;', '\\,', '\\n'], $text ); return $text; } private function fold(string $ics): string { $lines = explode("\r\n", $ics); $folded = []; foreach ($lines as $line) { while (mb_strlen($line) > 75) { $folded[] = mb_substr($line, 0, 75); $line = ' ' . mb_substr($line, 75); } $folded[] = $line; } return implode("\r\n", $folded); } public function getFilename(Evento $evento): string { $name = str_replace(['/', '\\', ' '], '_', $evento->nome_evento); return $name . '.ics'; } public function getCollectionFilename(): string { return 'eventi_' . now()->format('Ymd_His') . '.ics'; } }