244 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			244 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						||
 | 
						||
namespace App\Http\Controllers;
 | 
						||
 | 
						||
use Illuminate\Http\Request;
 | 
						||
use Illuminate\Support\Str;
 | 
						||
use Carbon\Carbon;
 | 
						||
use Illuminate\Support\Facades\DB;
 | 
						||
use App\Models\Calendar;
 | 
						||
use App\Models\Event;
 | 
						||
use App\Models\EventMeta;
 | 
						||
 | 
						||
class EventController extends Controller
 | 
						||
{
 | 
						||
    /**
 | 
						||
     * create a new event page
 | 
						||
     */
 | 
						||
    public function create(Calendar $calendar)
 | 
						||
    {
 | 
						||
        $this->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 = <<<ICS
 | 
						||
BEGIN:VCALENDAR
 | 
						||
VERSION:2.0
 | 
						||
PRODID:-//Kithkin//Laravel CalDAV//EN
 | 
						||
BEGIN:VEVENT
 | 
						||
UID:$uid
 | 
						||
DTSTAMP:{$start->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   = <<<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);
 | 
						||
    }
 | 
						||
}
 |