kithkin/app/Http/Controllers/EventController.php

335 lines
12 KiB
PHP
Raw 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 App\Models\Calendar;
use App\Models\Event;
use App\Models\EventMeta;
use App\Models\Location;
use App\Services\Location\Geocoder;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class EventController extends Controller
{
/**
*
* create a new event page
*/
public function create(Calendar $calendar, Request $request)
{
// authorize access to this calendar
$this->authorize('update', $calendar);
// the instance for the signed-in user (provides the uri/slug)
$instance = $calendar->instanceForUser();
$slug = $instance?->uri ?? $calendar->id; // fallback just in case
// build a fresh event "shell" with meta defaults (keeps your view happy)
$event = new Event;
$event->meta = (object) [
'title' => '',
'description' => '',
'location' => '',
'start_at' => null,
'end_at' => null,
'all_day' => false,
'category' => '',
];
// choose a timezone and derive defaults for start/end
$tz = auth()->user()->timezone ?? config('app.timezone', 'UTC');
// if ?date=YYYY-MM-DD is present, start that day at 9am; otherwise "now"
$anchor = $request->query('date')
? Carbon::parse($request->query('date'), $tz)->startOfDay()->addHours(9)
: Carbon::now($tz);
$anchor->second(0);
$start_carbon = $anchor->copy();
$end_carbon = $anchor->copy()->addHour();
// format for <input type="datetime-local">
$start = $start_carbon->format('Y-m-d\TH:i');
$end = $end_carbon->format('Y-m-d\TH:i');
return view('event.form', compact(
'calendar', // bound model (so route() can take the model directly)
'instance', // convenience in the view
'slug', // if you prefer passing just the slug into route()
'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 metadata so the view has everything
$event->load('meta');
$event->load('meta.venue');
// 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 sabres calendarobjects + meta row
*/
public function store(Request $req, Calendar $calendar, Geocoder $geocoder)
{
$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',
// normalized fields from the suggestions ui (all optional)
'loc_display_name' => 'nullable|string',
'loc_place_name' => 'nullable|string', // optional if you add this hidden input
'loc_street' => 'nullable|string',
'loc_city' => 'nullable|string',
'loc_state' => 'nullable|string',
'loc_postal' => 'nullable|string',
'loc_country' => 'nullable|string',
'loc_lat' => 'nullable',
'loc_lon' => 'nullable',
]);
$uid = Str::uuid() . '@' . parse_url(config('app.url'), PHP_URL_HOST);
// parse local -> utc
$clientTz = $calendar->timezone ?? 'UTC';
$start = Carbon::createFromFormat('Y-m-d\TH:i', $data['start_at'], $clientTz)->utc();
$end = Carbon::createFromFormat('Y-m-d\TH:i', $data['end_at'], $clientTz)->utc();
// normalize description/location for ics
$description = str_replace("\n", '\\n', $data['description'] ?? '');
$locationStr = str_replace("\n", '\\n', $data['location'] ?? '');
// write dtstart/dtend as utc with "Z"
$ical = <<<ICS
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Kithkin//Laravel CalDAV//EN
BEGIN:VEVENT
UID:$uid
DTSTAMP:{$start->format('Ymd\THis\Z')}
DTSTART:{$start->format('Ymd\THis\Z')}
DTEND:{$end->format('Ymd\THis\Z')}
SUMMARY:{$data['title']}
DESCRIPTION:$description
LOCATION:$locationStr
END:VEVENT
END:VCALENDAR
ICS;
// create sabre object
$event = Event::create([
'calendarid' => $calendar->id,
'uri' => Str::uuid() . '.ics',
'lastmodified' => time(),
'etag' => md5($ical),
'size' => strlen($ical),
'componenttype' => 'VEVENT',
'uid' => $uid,
'calendardata' => $ical,
]);
// resolve a location_id
$locationId = null;
$raw = $data['location'] ?? null;
// did the user pick a suggestion (hidden normalized fields present)?
$hasNormHints = $req->filled('loc_display_name') ||
$req->filled('loc_place_name') ||
$req->filled('loc_street') ||
$req->filled('loc_city') ||
$req->filled('loc_state') ||
$req->filled('loc_postal') ||
$req->filled('loc_country') ||
$req->filled('loc_lat') ||
$req->filled('loc_lon');
if ($raw) {
if ($hasNormHints) {
$norm = [
'display_name' => $req->input('loc_display_name') ?: $raw,
'place_name' => $req->input('loc_place_name'), // fine if null
'raw_address' => $raw,
'street' => $req->input('loc_street'),
'city' => $req->input('loc_city'),
'state' => $req->input('loc_state'),
'postal' => $req->input('loc_postal'),
'country' => $req->input('loc_country'),
'lat' => $req->filled('loc_lat') ? (float) $req->input('loc_lat') : null,
'lon' => $req->filled('loc_lon') ? (float) $req->input('loc_lon') : null,
];
$loc = Location::findOrCreateNormalized($norm, $raw);
$locationId = $loc->id;
} else {
// no hints: try geocoding the free-form string
$norm = $geocoder->forward($raw);
if ($norm) {
$loc = Location::findOrCreateNormalized($norm, $raw);
$locationId = $loc->id;
} else {
// label-only fallback so the event still links to a location row
$loc = Location::labelOnly($raw);
$locationId = $loc->id;
}
}
}
// meta row (store raw string and link to normalized location if we have one)
$event->meta()->create([
'title' => $data['title'],
'description' => $data['description'] ?? null,
'location' => $raw,
'location_id' => $locationId,
'all_day' => (bool) ($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 = <<<ICS
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Kithkin//Laravel CalDAV//EN
BEGIN:VEVENT
UID:$uid
DTSTAMP:{$start->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);
}
}