authorize('update', $calendar); $instance = $calendar->instanceForUser(); $event = new Event; $event->meta = (object) [ 'title' => '', 'description' => '', 'location' => '', 'start_at' => null, 'end_at' => null, 'all_day' => false, 'category' => '', ]; $start = $event->start_at; $end = $event->end_at; return view('event.form', compact('calendar', 'instance', 'event', 'start', 'end')); } /** * edit event page */ public function edit(Calendar $calendar, Event $event) { $this->authorize('update', $calendar); $instance = $calendar->instanceForUser(); $timezone = $instance?->timezone ?? 'UTC'; $start = optional($event->meta?->start_at) ?->timezone($timezone) ?->format('Y-m-d\TH:i'); $end = optional($event->meta?->end_at) ?->timezone($timezone) ?->format('Y-m-d\TH:i'); return view('event.form', compact('calendar', 'instance', 'event', 'start', 'end')); } /** * single event view handling * * URL: /calendar/{uuid}/event/{event_id} */ public function show(Request $request, Calendar $calendar, Event $event) { // ensure the event really belongs to the parent calendar if ((int) $event->calendarid !== (int) $calendar->id) { abort(Response::HTTP_NOT_FOUND); } // authorize $this->authorize('view', $event); // eager-load meta so the view has everything $event->load('meta'); // check for HTML; it sends `HX-Request: true` on every AJAX call $isHtmx = $request->header('HX-Request') === 'true'; // convert Sabre timestamps if meta is missing $start = $event->meta->start_at ?? Carbon::createFromTimestamp($event->firstoccurence); $end = $event->meta->end_at ?? ($event->lastoccurence ? Carbon::createFromTimestamp($event->lastoccurence) : $start); $data = compact('calendar', 'event', 'start', 'end'); return $isHtmx ? view('event.partials.details', $data) // tiny fragment for the modal : view('event.show', $data); // full-page fallback } /** * BACKEND METHODS * */ /** * insert vevent into sabre’s calendarobjects + meta row */ public function store(Request $req, Calendar $calendar) { $this->authorize('update', $calendar); $data = $req->validate([ 'title' => 'required|string|max:200', 'start_at' => 'required|date', 'end_at' => 'required|date|after:start_at', 'description' => 'nullable|string', 'location' => 'nullable|string', 'all_day' => 'sometimes|boolean', 'category' => 'nullable|string|max:50', ]); // prepare payload $uid = Str::uuid() . '@' . parse_url(config('app.url'), PHP_URL_HOST); // store events as UTC in the database; convert to calendar time in the view $client_timezone = $calendar->timezone ?? 'UTC'; $start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $client_timezone) ->setTimezone('UTC'); $end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $client_timezone) ->setTimezone('UTC'); // prepare strings $description = $data['description'] ?? ''; $location = $data['location'] ?? ''; $description = str_replace("\n", '\\n', $description); $location = str_replace("\n", '\\n', $location); /* build minimal iCalendar payload */ $ical = <<utc()->format('Ymd\\THis\\Z')} DTSTART:{$start->format('Ymd\\THis')} DTEND:{$end->format('Ymd\\THis')} SUMMARY:{$data['title']} DESCRIPTION:$description LOCATION:$location END:VEVENT END:VCALENDAR ICS; $event = Event::create([ 'calendarid' => $calendar->id, 'uri' => Str::uuid() . '.ics', 'lastmodified' => time(), 'etag' => md5($ical), 'size' => strlen($ical), 'componenttype' => 'VEVENT', 'uid' => $uid, 'calendardata' => $ical, ]); $event->meta()->create([ 'title' => $data['title'], 'description' => $data['description'] ?? null, 'location' => $data['location'] ?? null, 'all_day' => $data['all_day'] ?? false, 'category' => $data['category'] ?? null, 'start_at' => $start, 'end_at' => $end, ]); return redirect()->route('calendar.show', $calendar); } /** * update vevent + meta */ public function update(Request $req, Calendar $calendar, Event $event) { $this->authorize('update', $calendar); $data = $req->validate([ 'title' => 'required|string|max:200', 'start_at' => 'required|date', 'end_at' => 'required|date|after:start_at', 'description' => 'nullable|string', 'location' => 'nullable|string', 'category' => 'nullable|string|max:50', ]); // rebuild the icalendar payload $calendar_timezone = $calendar->timezone ?? 'UTC'; $start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $calendar_timezone)->setTimezone($calendar_timezone); $end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $calendar_timezone)->setTimezone($calendar_timezone); // prepare strings $description = $data['description'] ?? ''; $location = $data['location'] ?? ''; $description = str_replace("\n", '\\n', $description); $location = str_replace("\n", '\\n', $location); // note: keep the UID stable (CalDAV relies on it!) $uid = $event->uid; $ical = <<utc()->format('Ymd\\THis\\Z')} DTSTART;TZID={$calendar_timezone}:{$start->format('Ymd\\THis')} DTEND;TZID={$calendar_timezone}:{$end->format('Ymd\\THis')} SUMMARY:{$data['title']} DESCRIPTION:$description LOCATION:$location END:VEVENT END:VCALENDAR ICS; // persist changes $event->update([ 'calendardata' => $ical, 'etag' => md5($ical), 'lastmodified' => time(), ]); $event->meta()->updateOrCreate([], [ 'title' => $data['title'], 'description' => $data['description'] ?? null, 'location' => $data['location'] ?? null, 'all_day' => $data['all_day'] ?? false, 'category' => $data['category'] ?? null, 'start_at' => $start, 'end_at' => $end, ]); return redirect()->route('calendar.show', $calendar); } }