'Ostermontag', ]; protected $queryString = [ 'view' => ['except' => 'month'], 'date' => ['except' => ''], 'search' => ['except' => ''], 'from' => ['except' => ''], 'to' => ['except' => ''], ]; protected $listeners = ['eventCreated' => 'loadEvents']; public function mount() { $tz = auth()->user()->timezone; $this->date = $this->date ? Carbon::parse($this->date, $tz) : now($tz); $this->loadEvents(); } public function updatedView() { $this->loadEvents(); } public function setView($view) { $this->view = $view; $this->loadEvents(); } public function loadEvents() { $tz = auth()->user()->timezone; $user = auth()->user(); // πŸ”₯ VIEW RANGE if ($this->view === 'month') { $start = $this->date->copy()->startOfMonth()->startOfWeek(); $end = $this->date->copy()->endOfMonth()->endOfWeek(); } elseif ($this->view === 'week') { $start = $this->date->copy()->startOfWeek(); $end = $this->date->copy()->endOfWeek(); } else { $start = $this->date->copy()->startOfDay(); $end = $this->date->copy()->endOfDay(); } $query = $user->events(); // πŸ”₯ SEARCH if ($this->search) { $query->where('title', 'like', '%' . $this->search . '%'); } // πŸ”₯ FILTER (richtig fΓΌr Multi-Day!) if ($this->from) { $from = Carbon::parse($this->from)->startOfDay(); $query->where(function ($q) use ($from) { $q->where('starts_at', '>=', $from) ->orWhere('ends_at', '>=', $from); }); } if ($this->to) { $to = Carbon::parse($this->to)->endOfDay(); $query->where(function ($q) use ($to) { $q->where('starts_at', '<=', $to) ->orWhere('ends_at', '<=', $to); }); } // πŸ”₯ RANGE (WICHTIG) $rawEvents = $query->where(function ($q) use ($start, $end) { $q->whereBetween('starts_at', [$start, $end]) ->orWhereBetween('ends_at', [$start, $end]) ->orWhere(function ($q2) use ($start, $end) { $q2->where('starts_at', '<=', $start) ->where('ends_at', '>=', $end); }); })->get(); $this->rawEvents = $rawEvents; // πŸ”₯ SPLIT EVENTS IN DAYS $events = []; foreach ($rawEvents as $event) { $startDate = $event->starts_at ->copy() ->setTimezone($tz) ->startOfDay(); $endDate = $event->ends_at ? $event->ends_at->copy()->setTimezone($tz)->startOfDay() : $startDate; for ($date = $startDate->copy(); $date <= $endDate; $date->addDay()) { $events[$date->format('Y-m-d')][] = $event; } } $this->events = collect($events)->map(fn($g) => collect($g)); } public function updatedSearch() { $this->loadEvents(); } public function updatedFrom() { $this->loadEvents(); } public function updatedTo() { $this->loadEvents(); } public function updateEventTime($eventId, $start, $end) { $event = \App\Models\Event::findOrFail($eventId); $tz = auth()->user()->timezone ?? 'UTC'; $event->update([ 'starts_at' => Carbon::parse($start, $tz)->utc(), 'ends_at' => $end ? Carbon::parse($end, $tz)->utc() : null, ]); $this->loadEvents(); } public function moveEventToDate($eventId, $date) { $event = \App\Models\Event::findOrFail($eventId); $tz = auth()->user()->timezone ?? 'UTC'; $start = $event->starts_at->copy()->timezone($tz); $end = $event->ends_at?->copy()->timezone($tz); /* |-------------------------------------------------------------------------- | πŸ”₯ FALL 1: ALL DAY ODER MULTI DAY |-------------------------------------------------------------------------- */ $isMultiDay = $end && $start->toDateString() !== $end->toDateString(); if ($event->is_all_day || $isMultiDay) { // πŸ‘‰ Tage-Differenz behalten (NICHT Sekunden!) $daySpan = $end ? $start->startOfDay()->diffInDays($end->startOfDay()) : 0; // πŸ‘‰ neuer Start (immer 00:00 bei allday) $newStart = Carbon::parse($date, $tz)->startOfDay(); // πŸ‘‰ neuer End $newEnd = $end ? $newStart->copy()->addDays($daySpan)->endOfDay() : null; } else { /* |-------------------------------------------------------------------------- | πŸ”₯ FALL 2: NORMALER TERMIN |-------------------------------------------------------------------------- */ $duration = $end ? $start->diffInSeconds($end) : 3600; $newStart = Carbon::parse( $date . ' ' . $start->format('H:i'), $tz ); $newEnd = $end ? $newStart->copy()->addSeconds($duration) : null; } /* |-------------------------------------------------------------------------- | SAVE (UTC!) |-------------------------------------------------------------------------- */ $event->update([ 'starts_at' => $newStart->utc(), 'ends_at' => $newEnd?->utc(), ]); $this->loadEvents(); } // public function moveEventToDate($eventId, $date) // { // $event = \App\Models\Event::findOrFail($eventId); // // $tz = auth()->user()->timezone ?? 'UTC'; // // // aktuelle Zeiten holen (in User TZ) // $start = $event->starts_at->copy()->timezone($tz); // $end = $event->ends_at?->copy()->timezone($tz); // // // πŸ”₯ Dauer berechnen (wichtig fΓΌr Multi-Day) // $duration = $end // ? $start->diffInSeconds($end) // : 3600; // // // πŸ”₯ neues Datum + gleiche Uhrzeit // $newStart = \Carbon\Carbon::parse( // $date . ' ' . $start->format('H:i'), // $tz // )->utc(); // // $newEnd = $end // ? $newStart->copy()->addSeconds($duration) // : null; // // $event->update([ // 'starts_at' => $newStart, // 'ends_at' => $newEnd, // ]); // // $this->loadEvents(); // } public function goToToday() { $this->date = now(auth()->user()->timezone); $this->loadEvents(); } public function next() { if ($this->view === 'month') $this->date->addMonth(); if ($this->view === 'week') $this->date->addWeek(); if ($this->view === 'day') $this->date->addDay(); $this->loadEvents(); } public function prev() { if ($this->view === 'month') $this->date->subMonth(); if ($this->view === 'week') $this->date->subWeek(); if ($this->view === 'day') $this->date->subDay(); $this->loadEvents(); } public function moveEventToDateTime($eventId, $date, $time) { $event = Event::findOrFail($eventId); $tz = auth()->user()->timezone ?? config('app.timezone'); if (!preg_match('/^\d{2}:\d{2}$/', $time)) { return; } [$hour, $minute] = explode(':', $time); $hour = min(max((int)$hour, 0), 23); $minute = min(max((int)$minute, 0), 59); // πŸ”₯ WICHTIG: alte Zeiten in USER TZ holen $oldStart = $event->starts_at->copy()->setTimezone($tz); $oldEnd = $event->ends_at?->copy()->setTimezone($tz); $duration = $oldEnd ? $oldStart->diffInMinutes($oldEnd) : 60; if ($duration <= 0) { $duration = 60; } // πŸ”₯ neue Zeit in USER TZ setzen $newStart = Carbon::parse($date, $tz)->setTime($hour, $minute); $newEnd = $newStart->copy()->addMinutes($duration); // πŸ”₯ zurΓΌck in UTC speichern $event->starts_at = $newStart->copy()->utc(); $event->ends_at = $newEnd->copy()->utc(); $event->save(); $this->loadEvents(); } public function updateEventTimeAndDate($eventId, $date, $start, $end) { $event = Event::findOrFail($eventId); $tz = auth()->user()->timezone ?? config('app.timezone'); $start = Carbon::parse($date.' '.$start, $tz)->utc(); $end = Carbon::parse($date.' '.$end, $tz)->utc(); if ($end->lessThanOrEqualTo($start)) { $end = $start->copy()->addHour(); } $event->update([ 'starts_at' => $start, 'ends_at' => $end, ]); $this->loadEvents(); } public function updateEventRange($id, $startDate, $endDate, $startTime, $endTime) { $event = Event::findOrFail($id); $tz = auth()->user()->timezone; $start = Carbon::parse("$startDate $startTime", $tz)->utc(); $end = Carbon::parse("$endDate $endTime", $tz)->utc(); if ($end->lessThanOrEqualTo($start)) { $end = $start->copy()->addHour(); } $event->update([ 'starts_at' => $start, 'ends_at' => $end, ]); $this->loadEvents(); } private function transformAllDayEvents($events) { return $events->map(function ($event) { $start = $event->starts_at->copy(); $end = $event->ends_at?->copy() ?? $start; $startDay = $start->startOfDay(); $endDay = $end->startOfDay(); return [ 'id' => $event->id, 'title' => $event->title, 'color' => $event->color, 'start_day' => $startDay, 'end_day' => $endDay, 'duration_days' => $startDay->diffInDays($endDay) + 1, ]; }); } private function mapAllDayToWeek($events, $weekStart) { return $events->map(function ($event) use ($weekStart) { $startOffset = $event['start_day']->diffInDays($weekStart, false); $endOffset = $event['end_day']->diffInDays($weekStart, false); // clamp auf Woche $start = max($startOffset, 0); $end = min($endOffset, 6); $span = ($end - $start) + 1; return [ ...$event, 'left' => ($start / 7) * 100, 'width' => ($span / 7) * 100, ]; }); } public function getCalendarDaysProperty(): \Illuminate\Support\Collection { $tz = auth()->user()->timezone; return collect(range(0, 6))->map(function ($i) use ($tz) { $day = $this->date->copy()->startOfWeek()->addDays($i); $key = $day->format('Y-m-d'); $events = collect($this->events[$key] ?? []); /* |-------------------------------------------------------------------------- | πŸ”₯ ALLDAY + MULTIDAY RAUSFILTERN (WICHTIG!) |-------------------------------------------------------------------------- */ $filtered = $events->reject(function ($event) use ($tz) { $start = $event->starts_at->copy()->setTimezone($tz); $end = $event->ends_at?->copy()->setTimezone($tz) ?? $start; // πŸ‘‰ gleiche Logik wie in getAllDayEventsProperty $isMultiDay = $end->startOfDay()->gt($start->startOfDay()); return $event->is_all_day || $isMultiDay; }); /* |-------------------------------------------------------------------------- | πŸ”₯ TIMED EVENTS TRANSFORMIEREN |-------------------------------------------------------------------------- */ $mapped = $filtered ->map(fn($event) => $this->transformEvent($event, $day, $tz)) ->sortBy('start') ->values(); /* |-------------------------------------------------------------------------- | πŸ”₯ OVERLAP FIX |-------------------------------------------------------------------------- */ $timed = $mapped->map(function ($event) use ($mapped) { $overlapping = $mapped->filter(function ($e) use ($event) { return $e['start'] < $event['end'] && $e['end'] > $event['start']; })->values(); $count = max($overlapping->count(), 1); $index = $overlapping->pluck('id')->search($event['id']); $index = $index === false ? 0 : $index; $event['count'] = $count; $event['index'] = $index; return $event; }); return [ 'date' => $day, 'key' => $key, 'timed' => $timed, ]; }); } // public function getCalendarDaysProperty(): \Illuminate\Support\Collection // { // $tz = auth()->user()->timezone; // // return collect(range(0, 6))->map(function ($i) use ($tz) { // // $day = $this->date->copy()->startOfWeek()->addDays($i); // $key = $day->format('Y-m-d'); // // $events = collect($this->events[$key] ?? []); // // /* // |-------------------------------------------------------------------------- // | πŸ”₯ NUR ALLDAY RAUS // |-------------------------------------------------------------------------- // */ // $mapped = $events // ->reject(fn($e) => $e->is_all_day) // ->map(fn($event) => $this->transformEvent($event, $day, $tz)) // ->sortBy('start') // ->values(); // // /* // |-------------------------------------------------------------------------- // | OVERLAP // |-------------------------------------------------------------------------- // */ // $timed = $mapped->map(function ($event) use ($mapped) { // // $overlapping = $mapped->filter(function ($e) use ($event) { // return $e['start'] < $event['end'] // && $e['end'] > $event['start']; // })->values(); // // $count = max($overlapping->count(), 1); // // $index = $overlapping->pluck('id')->search($event['id']); // $index = $index === false ? 0 : $index; // // $event['count'] = $count; // $event['index'] = $index; // // return $event; // }); // // return [ // 'date' => $day, // 'key' => $key, // 'timed' => $timed, // ]; // }); // } // public function getCalendarDaysProperty(): \Illuminate\Support\Collection // { // $tz = auth()->user()->timezone; // // return collect(range(0, 6))->map(function ($i) use ($tz) { // // $day = $this->date->copy()->startOfWeek()->addDays($i); // $key = $day->format('Y-m-d'); // // $events = collect($this->events[$key] ?? []); // // // πŸ‘‰ AllDay Events // $allDay = $events->filter(fn($e) => $e->is_all_day); // // // πŸ‘‰ Timed Events transformieren // $mapped = $events // ->reject(fn($e) => $e->is_all_day) // ->map(fn($event) => $this->transformEvent($event, $day, $tz)) // ->sortBy('start') // πŸ”₯ wichtig fΓΌr stabile Reihenfolge // ->values(); // // // πŸ‘‰ Overlap berechnen // $timed = $mapped->map(function ($event) use ($mapped) { // // $overlapping = $mapped->filter(function ($e) use ($event) { // return $e['start'] < $event['end'] // && $e['end'] > $event['start']; // })->values(); // // $count = max($overlapping->count(), 1); // // $index = $overlapping->pluck('id')->search($event['id']); // $index = $index === false ? 0 : $index; // // $event['count'] = $count; // $event['index'] = $index; // // return $event; // }); // // return [ // 'date' => $day, // 'key' => $key, // 'allDay' => $allDay, // 'timed' => $timed, // ]; // }); // } // public function getAllDayEventsProperty() // { // $tz = auth()->user()->timezone; // // $weekStart = $this->date->copy()->startOfWeek(); // $weekEnd = $this->date->copy()->endOfWeek(); // // return collect($this->rawEvents) // ->filter(fn($event) => $event->is_all_day == true) // ->map(function ($event) use ($weekStart, $weekEnd, $tz) { // // $start = $event->starts_at->copy()->setTimezone($tz); // $end = $event->ends_at // ? $event->ends_at->copy()->setTimezone($tz) // : $start; // // if ($end->lt($weekStart) || $start->gt($weekEnd)) { // return null; // } // // $startClamped = $start->copy()->max($weekStart)->startOfDay(); // $endClamped = $end->copy()->min($weekEnd)->startOfDay(); // // $startOffset = $weekStart->diffInDays($startClamped); // $endOffset = $weekStart->diffInDays($endClamped); // // $span = max(($endOffset - $startOffset) + 1, 1); // // return [ // 'id' => $event->id, // 'title' => $event->title, // 'color' => $event->color, // // // πŸ”₯ DAS HAT GEFEHLT // 'start' => $startClamped, // 'end' => $endClamped, // // 'left' => ($startOffset / 7) * 100, // 'width' => ($span / 7) * 100, // ]; // }) // ->filter() // ->values(); // } // public function getAllDayEventsProperty() // { // $tz = auth()->user()->timezone; // // $weekStart = $this->date->copy()->startOfWeek()->startOfDay(); // $weekEnd = $this->date->copy()->endOfWeek()->endOfDay(); // // $events = collect($this->rawEvents) // // ->filter(function ($event) use ($tz) { // // $start = $event->starts_at->copy()->setTimezone($tz); // $end = $event->ends_at?->copy()->setTimezone($tz) ?? $start; // // // πŸ‘‰ echtes AllDay oder MultiDay // return $event->is_all_day // || $end->startOfDay()->gt($start->startOfDay()); // }) // // ->map(function ($event) use ($tz, $weekStart, $weekEnd) { // // $start = $event->starts_at->copy()->setTimezone($tz); // $end = $event->ends_at // ? $event->ends_at->copy()->setTimezone($tz) // : $start; // // /* // |-------------------------------------------------------------------------- // | ❌ außerhalb β†’ raus // |-------------------------------------------------------------------------- // */ // if ($end->lt($weekStart) || $start->gt($weekEnd)) { // return null; // } // // /* // |-------------------------------------------------------------------------- // | πŸ”₯ CLAMP AUF WOCHE // |-------------------------------------------------------------------------- // */ // $startClamped = $start->lt($weekStart) // ? $weekStart->copy() // : $start->copy(); // // $endClamped = $end->gt($weekEnd) // ? $weekEnd->copy() // : $end->copy(); // // /* // |-------------------------------------------------------------------------- // | πŸ”₯ WICHTIG: START/END auf DAY LEVEL // |-------------------------------------------------------------------------- // */ // $startDay = $startClamped->copy()->startOfDay(); // $endDay = $endClamped->copy()->startOfDay(); // // /* // |-------------------------------------------------------------------------- // | πŸ”₯ OFFSET BERECHNUNG (STABIL) // |-------------------------------------------------------------------------- // */ // $startOffset = $weekStart->diffInDays($startDay); // $endOffset = $weekStart->diffInDays($endDay); // // $span = max(1, ($endOffset - $startOffset) + 1); // // return [ // 'id' => $event->id, // 'title' => $event->title, // 'color' => $event->color, // // 'start' => $startDay, // 'end' => $endDay, // // 'left' => ($startOffset / 7) * 100, // 'width' => ($span / 7) * 100, // ]; // }) // // ->filter() // ->sortBy('start') // ->values(); // // return $this->stackAllDayEvents($events); // } public function getAllDayEventsProperty() { $tz = auth()->user()->timezone; $weekStart = $this->date->copy()->startOfWeek()->startOfDay(); $weekEnd = $this->date->copy()->endOfWeek()->endOfDay(); $events = collect($this->rawEvents) ->filter(function ($event) use ($tz) { $start = $event->starts_at->copy()->setTimezone($tz); $end = $event->ends_at?->copy()->setTimezone($tz) ?? $start; $isMultiDay = $end->startOfDay()->gt($start->startOfDay()); return $event->is_all_day || $isMultiDay; }) ->map(function ($event) use ($tz, $weekStart, $weekEnd) { $start = $event->starts_at->copy()->setTimezone($tz)->startOfDay(); $end = $event->ends_at ? $event->ends_at->copy()->setTimezone($tz)->startOfDay() : $start; if ($end->lt($weekStart) || $start->gt($weekEnd)) { return null; } $startClamped = $start->lt($weekStart) ? $weekStart->copy() : $start; $endClamped = $end->gt($weekEnd) ? $weekEnd->copy() : $end; $startOffset = $weekStart->diffInDays($startClamped); $endOffset = $weekStart->diffInDays($endClamped); $span = ($endOffset - $startOffset) + 1; return [ 'id' => $event->id, 'title' => $event->title, 'color' => $event->color, 'start' => $startClamped, 'end' => $endClamped, 'col' => $startOffset, 'span' => $span, ]; }) ->filter() ->sortBy('start') ->values(); return $this->stackAllDayEvents($events); } private function stackAllDayEvents($events) { $rows = []; foreach ($events as $event) { $placed = false; foreach ($rows as &$row) { $collision = collect($row)->first(function ($e) use ($event) { $aStart = $event['col']; $aEnd = $event['col'] + $event['span'] - 1; $bStart = $e['col']; $bEnd = $e['col'] + $e['span'] - 1; return !($aEnd < $bStart || $aStart > $bEnd); }); if (!$collision) { $row[] = $event; $placed = true; break; } } if (!$placed) { $rows[] = [$event]; } } return collect($rows)->map(function ($row, $rowIndex) { return collect($row)->map(function ($event) use ($rowIndex) { return [ ...$event, 'row' => $rowIndex ]; }); })->flatten(1); } private function transformEvent($event, $day, $tz) { $start = $event->starts_at->copy()->setTimezone($tz); $end = $event->ends_at?->copy()->setTimezone($tz); $dayStart = $day->copy()->startOfDay(); $dayEnd = $day->copy()->endOfDay(); // πŸ‘‰ Seminar / MultiDay Fix if ($end && $start->diffInDays($end) > 0 && !$event->is_all_day) { $effectiveStart = $dayStart->copy()->setTime($start->hour, $start->minute); $effectiveEnd = $dayStart->copy()->setTime($end->hour, $end->minute); // πŸ‘‰ Exceptions $exceptions = is_array($event->exceptions) ? $event->exceptions : json_decode($event->exceptions ?? '[]', true); $exception = collect($exceptions) ->firstWhere('date', $day->format('Y-m-d')); if ($exception) { if (!empty($exception['start'])) { [$h, $m] = explode(':', $exception['start']); $effectiveStart->setTime($h, $m); } if (!empty($exception['end'])) { [$h, $m] = explode(':', $exception['end']); $effectiveEnd->setTime($h, $m); } } } else { $effectiveStart = $start; $effectiveEnd = $end ?? $start->copy()->addHour(); } $hourHeight = 64; $top = ($effectiveStart->hour * $hourHeight) + (($effectiveStart->minute / 60) * $hourHeight); $height = max($effectiveStart->diffInMinutes($effectiveEnd), 1) / 60 * $hourHeight; return [ 'model' => $event, 'id' => $event->id, 'title' => $event->title, 'top' => $top, 'height' => $height, 'start' => $effectiveStart, 'end' => $effectiveEnd, ]; } public function colorToRgba($color, $opacity = 1): string { if (!$color) return 'rgba(99,102,241,0.8)'; // fallback $color = ltrim($color, '#'); $r = hexdec(substr($color, 0, 2)); $g = hexdec(substr($color, 2, 2)); $b = hexdec(substr($color, 4, 2)); return "rgba($r, $g, $b, $opacity)"; } public function render() { return view('livewire.calendar.index') ->layout('layouts.app'); } }