kithkin/app/Http/Controllers/CalendarController.php

405 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Models\Calendar;
use App\Models\Event;
use App\Services\Calendar\CreateCalendar;
use App\Services\Calendar\CalendarRangeResolver;
use App\Services\Calendar\CalendarViewBuilder;
use App\Services\Calendar\CalendarSettingsPersister;
use App\Services\Event\EventRecurrence;
class CalendarController extends Controller
{
/**
* Consolidated calendar dashboard.
*
* Query params:
* view = month | week | four (default: month)
* date = Y-m-d anchor date (default: today, in user TZ)
*
* The view receives a `$payload` array:
* ├─ view current view name
* ├─ range ['start' => 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 shouldnt
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
*/
}