Carbon, 'end' => Carbon] * ├─ calendars keyed by calendar id (for the left-hand toggle list) * └─ events flat list of VEVENTs in that range */ public function index( Request $request, EventRecurrence $recurrence, CalendarRangeResolver $rangeResolver, CalendarViewBuilder $viewBuilder, CalendarSettingsPersister $settingsPersister ) { /** * * manage parameters and core variables */ // set the calendar key $principal = auth()->user()->principal_uri; // user settings $user = $request->user(); $tz = $user->timezone ?? config('app.timezone'); $timeFormat = $user->getSetting('app.time_format', '12'); // settings $defaults = $settingsPersister->defaults($user, $tz); $weekStart = $defaults['week_start']; $weekEnd = $defaults['week_end']; // get the view and time range [$view, $range] = $rangeResolver->resolveRange( $request, $tz, $weekStart, $weekEnd, $defaults['view'], $defaults['date'] ); $density = $settingsPersister->resolveDensity($request, $defaults['density']); $stepMinutes = $density['step']; $labelEvery = $density['label_every']; $daytimeHoursEnabled = $settingsPersister->resolveDaytimeHours($request, $defaults['daytime_hours']); $daytimeHoursRange = $settingsPersister->daytimeHoursRange(); $daytimeHoursRows = $daytimeHoursEnabled ? intdiv((($daytimeHoursRange['end'] - $daytimeHoursRange['start']) * 60), 15) : 96; $daytimeHoursForView = ($daytimeHoursEnabled && in_array($view, ['day', 'week', 'four'], true)) ? $daytimeHoursRange : null; // date range span and controls $span = $rangeResolver->gridSpan($view, $range, $weekStart, $weekEnd); $nav = $rangeResolver->navDates($view, $range['start'], $tz); // get the user's visible calendars from the left bar $visible = collect($request->query('c', [])); // keep a stable anchor date for forms that aren't the nav buttons $anchorDate = $request->query('date', now($tz)->toDateString()); // persist settings $settingsPersister->persist( $user, $request, $view, $range['start'], $stepMinutes, $daytimeHoursEnabled ); /** * * calendars */ $calendars = Calendar::query() ->dashboardForPrincipal($principal) ->get(); $calendars = $calendars->map(function ($cal) use ($visible) { $cal->visible = $visible->isEmpty() || $visible->contains($cal->slug); return $cal; }); $calendar_map = $calendars->keyBy('id'); /** * * get events for calendars in range */ // get all the events in one query $events = Event::forCalendarsInRange( $calendars->pluck('id'), $span['start'], $span['end'] ); // build event payload $events = $viewBuilder->buildEventPayloads( $events, $calendar_map, $timeFormat, $view, $range, $tz, $recurrence, $span, $daytimeHoursForView, ); /** * * mini calendar */ // create the mini calendar grid based on the mini cal controls $mini_anchor = $request->query('mini', $range['start']->toDateString()); $mini_anchor_date = $rangeResolver->safeDate($mini_anchor, $tz, $range['start']->toDateString()); $mini_start = $mini_anchor_date->copy()->startOfMonth(); $mini_nav = [ 'prev' => $mini_start->copy()->subMonth()->toDateString(), 'next' => $mini_start->copy()->addMonth()->toDateString(), 'today' => Carbon::today($tz)->startOfMonth()->toDateString(), 'label' => $mini_start->format('F Y'), ]; $mini_headers = $viewBuilder->weekdayHeaders($tz, $weekStart); // compute the mini's 42-day span (Mon..Sun, 6 rows) $mini_grid_start = $mini_start->copy()->startOfWeek($weekStart); $mini_grid_end = $mini_start->copy()->endOfMonth()->endOfWeek($weekEnd); if ($mini_grid_start->diffInDays($mini_grid_end) + 1 < 42) { $mini_grid_end->addWeek(); } // fetch events specifically for the mini-span $mini_events = Event::forCalendarsInRange( $calendars->pluck('id'), $mini_grid_start, $mini_grid_end ); $mini_events = $viewBuilder->buildEventPayloads( $mini_events, $calendar_map, $timeFormat, $view, ['start' => $mini_grid_start, 'end' => $mini_grid_end], $tz, $recurrence, ['start' => $mini_grid_start, 'end' => $mini_grid_end], null, ); // now build the mini from mini_events (not from $events) $mini = $viewBuilder->buildMiniGrid($mini_start, $mini_events, $tz, $weekStart, $weekEnd); /** * * main calendar grid */ // create the calendar grid of days $grid = $viewBuilder->buildCalendarGrid($view, $range, $events, $tz, $span); // get the title $header = $rangeResolver->headerTitle($view, $range['start'], $range['end']); // format the data for the frontend, including separate arrays for events specifically and the big grid $payload = [ 'view' => $view, 'range' => $range, 'nav' => $nav, 'header' => $header, 'week_start' => $weekStart, 'hgroup' => $viewBuilder->viewHeaders($view, $range, $tz, $weekStart), 'events' => $events, // keyed by occurrence 'grid' => $grid, // day objects hold only ID-sets 'mini' => $mini, // mini calendar days with events for indicators 'mini_nav' => $mini_nav, // separate mini calendar navigation 'mini_headers' => $mini_headers, 'active' => [ 'date' => $range['start']->toDateString(), 'year' => $range['start']->format('Y'), 'month' => $range['start']->format("F"), 'day' => $range['start']->format("d"), ], 'daytime_hours' => [ 'enabled' => $daytimeHoursEnabled, 'start' => $daytimeHoursRange['start'], 'end' => $daytimeHoursRange['end'], 'rows' => $daytimeHoursRows, ], 'timezone' => $tz, 'calendars' => $calendars->mapWithKeys(function ($cal) { $color = $cal->meta_color ?? $cal->calendarcolor ?? default_calendar_color(); $colorFg = $cal->meta_color_fg ?? contrast_text_color($color); return [ $cal->id => [ 'id' => $cal->id, 'slug' => $cal->slug, 'name' => $cal->displayname, 'color' => $color, 'color_fg' => $colorFg, 'visible' => $cal->visible, 'is_remote' => $cal->is_remote, ], ]; }), ]; // time-based payload values $timeBased = in_array($view, ['day', 'week', 'four'], true); if ($timeBased) { // create the time gutter if we're in a time-based view $payload['slots'] = $viewBuilder->timeSlots( $range['start'], $tz, $timeFormat, $daytimeHoursEnabled ? $daytimeHoursRange : null ); $payload['time_format'] = $timeFormat; // optional, if the blade cares // add the now indicator $payload['now'] = $viewBuilder->nowIndicator( $view, $range, $tz, 15, 1, $daytimeHoursEnabled ? $daytimeHoursRange : null ); } // send the density array always, even though it doesn't matter for month $payload['density'] = [ 'step' => $stepMinutes, // 15|30|60 'label_every' => $labelEvery, // 1|2|4 'anchor' => $anchorDate, ]; return view('calendar.index', $payload); } /** * create sabre calendar + meta */ public function store(Request $request, CreateCalendar $creator) { $data = $request->validate([ 'name' => 'required|string|max:100', 'description' => 'nullable|string|max:255', 'timezone' => 'required|string|max:64', 'color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/', 'redirect' => 'nullable|string', // where to go after creating ]); $creator->create($request->user(), $data); $redirect = $data['redirect'] ?? route('calendar.index'); return redirect($redirect)->with('toast', [ 'message' => __('Calendar created!'), 'type' => 'success', ]); } /** * show calendar details */ public function show(Calendar $calendar) { $this->authorize('view', $calendar); $calendar->load([ 'meta', 'instances' => fn ($q) => $q->where('principaluri', auth()->user()->uri), ]); /* grab the single instance for convenience in the view */ $instance = $calendar->instances->first(); $caldavUrl = $instance?->caldavUrl(); // null-safe /* events + meta, newest first */ $events = $calendar->events() ->with('meta') ->orderByDesc('lastmodified') ->get(); return view( 'calendar.show', compact('calendar', 'instance', 'events', 'caldavUrl') ); } /** * edit calendar page */ public function edit(Calendar $calendar) { $this->authorize('update', $calendar); $calendar->load([ 'meta', 'instances' => fn ($q) => $q->where('principaluri', auth()->user()->uri), ]); $instance = $calendar->instances->first(); // may be null but shouldn’t return view('calendar.edit', compact('calendar', 'instance')); } /** * update sabre + meta records */ public function update(Request $request, Calendar $calendar) { $this->authorize('update', $calendar); $data = $request->validate([ 'name' => 'required|string|max:100', 'description' => 'nullable|string|max:255', 'timezone' => 'required|string', 'color' => 'nullable|regex:/^#[0-9A-Fa-f]{6}$/', ]); // update the instance row $calendar->instances() ->where('principaluri', auth()->user()->uri) ->update([ 'displayname' => $data['name'], 'description' => $data['description'] ?? '', 'calendarcolor' => $data['color'] ?? null, 'timezone' => $data['timezone'], ]); // bump synctoken on master calendar row $calendar->increment('synctoken'); // update calendar meta (our table) $color = calendar_color($data); $calendar->meta()->updateOrCreate([], [ 'color' => $color, 'color_fg' => contrast_text_color($color), ]); return redirect() ->route('calendar.show', $calendar) ->with('toast', __('Calendar saved successfully!')); } /** * * Delete calendar * @todo */ public function destroy(Calendar $calendar) { $this->authorize('delete', $calendar); $calendar->delete(); // cascades to meta via FK return redirect()->route('calendar.index'); } /** * * Private helpers */ }