Route webinar and conference registration through Ladill Events.
Deploy Ladill Meet / deploy (push) Successful in 1m26s

Add Events linking UI and service client, remove Meet-native registration and invitation flows for these room types, and redirect attendees to Events registration when joining.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-02 07:38:50 +00:00
co-authored by Cursor
parent 8162827957
commit 34d0a9c504
20 changed files with 595 additions and 286 deletions
@@ -1,7 +1,7 @@
<x-app-layout title="Schedule conference">
<div class="mx-auto max-w-xl">
<h1 class="text-xl font-semibold text-slate-900">Schedule a conference</h1>
<p class="mt-1 text-sm text-slate-500">Large-format events with assigned hosts and speakers. Optional green room for presenters before going live.</p>
<p class="mt-1 text-sm text-slate-500">After creating the conference, link it to a virtual or hybrid event in Ladill Events for registration, speakers, invitations, and QR codes. Optional green room for presenters before going live.</p>
<form method="POST" action="{{ route('meet.conferences.store') }}" class="mt-6 space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
@@ -36,26 +36,6 @@
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Invite speakers by email (comma-separated)</label>
<input type="text" name="invite_emails" value="{{ old('invite_emails') }}" placeholder="speaker@example.com" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
@if ($members->isNotEmpty())
<div>
<label class="block text-sm font-medium text-slate-700">Assigned speakers</label>
<p class="mt-0.5 text-xs text-slate-500">Team members who can speak on stage. You are always the host.</p>
<div class="mt-2 max-h-40 space-y-2 overflow-y-auto rounded-lg border border-slate-200 p-3">
@foreach ($members as $member)
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="presenter_refs[]" value="{{ $member->user_ref }}" @checked(in_array($member->user_ref, old('presenter_refs', []))) class="rounded border-slate-300">
<span>{{ $member->user_ref }}</span>
</label>
@endforeach
</div>
</div>
@endif
<div>
<label class="block text-sm font-medium text-slate-700">Passcode (optional)</label>
<input type="text" name="passcode" value="{{ old('passcode') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
+14 -73
View File
@@ -4,11 +4,6 @@
<p class="text-xs font-semibold uppercase tracking-wide text-violet-600">Conference</p>
<h1 class="text-2xl font-semibold text-slate-900">{{ $room->title }}</h1>
<p class="mt-1 text-sm text-slate-500">{{ config('meet.room_statuses')[$room->status] ?? $room->status }}</p>
@if(! empty($eventsEventUrl))
<a href="{{ $eventsEventUrl }}" target="_blank" rel="noopener" class="mt-2 inline-flex text-sm font-medium text-violet-600 hover:text-violet-800">
View in Ladill Events
</a>
@endif
</div>
@if ($errors->any())
@@ -17,6 +12,14 @@
</div>
@endif
@if (session('success'))
<div class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
{{ session('success') }}
</div>
@endif
@include('meet.partials.events-integration', ['room' => $room])
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-medium text-slate-700">Join link</h2>
<div class="mt-2 flex gap-2">
@@ -39,11 +42,9 @@
</form>
@endif
<a href="{{ route('meet.rooms.ical', $room) }}" class="btn-secondary btn-secondary-sm">Download iCal</a>
<a href="{{ route('meet.rooms.qr', $room) }}" target="_blank" class="btn-secondary btn-secondary-sm">QR code</a>
@if ($room->sessions->isNotEmpty())
<a href="{{ route('meet.rooms.attendance', $room) }}" class="btn-secondary btn-secondary-sm">Attendance CSV</a>
@endif
<a href="{{ route('meet.invitations.index', $room) }}" class="btn-secondary btn-secondary-sm">Manage invitations</a>
</div>
@if ($room->passcode)
@@ -59,78 +60,18 @@
@if ($room->setting('green_room'))
<p class="mt-3 text-sm text-violet-700">Green room is on when you start, you and assigned speakers rehearse before going live to the audience.</p>
@endif
</div>
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-medium text-slate-700">Speakers</h2>
<p class="mt-1 text-xs text-slate-500">Team members on stage and external speakers invited by email. You are always the host.</p>
@if ($speakers->isNotEmpty() || $room->invitations->where('role', 'panelist')->isNotEmpty())
<div class="mt-4 space-y-3">
@if ($speakers->isNotEmpty())
<div>
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500">Assigned speakers</h3>
<ul class="mt-2 space-y-1 text-sm text-slate-600">
@foreach ($speakers as $speaker)
@php $user = $speakerUsers->get($speaker->user_ref); @endphp
<li>{{ $user?->name ?? $speaker->user_ref }}</li>
@endforeach
</ul>
</div>
@endif
@if ($room->invitations->where('role', 'panelist')->isNotEmpty())
<div>
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500">Invited speakers</h3>
<ul class="mt-2 space-y-1 text-sm text-slate-600">
@foreach ($room->invitations->where('role', 'panelist') as $invite)
<li>
{{ $invite->display_name ?? $invite->email }}
<span class="text-slate-400">· {{ ucfirst($invite->status) }}</span>
</li>
@endforeach
</ul>
</div>
@endif
</div>
@else
<p class="mt-3 text-sm text-slate-500">No speakers assigned yet.</p>
@endif
@if ($canManageSpeakers)
<form method="POST" action="{{ route('meet.conferences.speakers.update', $room) }}" class="mt-5 space-y-4 border-t border-slate-100 pt-5">
@if ($room->status === 'scheduled')
<form method="POST" action="{{ route('meet.conferences.speakers.update', $room) }}" class="mt-6 border-t border-slate-100 pt-6">
@csrf
<div>
<label class="block text-sm font-medium text-slate-700">Invite speakers by email</label>
<input type="text" name="invite_emails" value="{{ old('invite_emails') }}" placeholder="speaker@example.com, another@example.com"
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<p class="mt-1 text-xs text-slate-500">Comma-separated. Invitations are sent as speaker (panelist) roles.</p>
</div>
@if ($members->isNotEmpty())
<div>
<label class="block text-sm font-medium text-slate-700">Assigned speakers</label>
<div class="mt-2 max-h-40 space-y-2 overflow-y-auto rounded-lg border border-slate-200 p-3">
@foreach ($members as $member)
@php $user = $memberUsers->get($member->user_ref); @endphp
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="presenter_refs[]" value="{{ $member->user_ref }}"
@checked(in_array($member->user_ref, old('presenter_refs', $presenterRefs))) class="rounded border-slate-300">
<span>{{ $user?->name ?? $member->user_ref }}</span>
</label>
@endforeach
</div>
</div>
@endif
<label class="flex items-center gap-2 text-sm text-slate-700">
<h2 class="text-sm font-medium text-slate-700">Stage layout</h2>
<label class="mt-3 flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" name="panel_discussions" value="1"
@checked(old('panel_discussions', $room->setting('panel_discussions'))) class="rounded border-slate-300">
Enable panel discussions (multi-speaker stage layout)
</label>
<button type="submit" class="btn-primary btn-primary-sm">Save speakers</button>
<input type="hidden" name="presenter_refs" value="">
<button type="submit" class="btn-primary btn-primary-sm mt-3">Save settings</button>
</form>
@endif
</div>
@@ -0,0 +1,61 @@
<x-app-layout :title="'Link to Ladill Events · '.$room->title">
<div class="mx-auto max-w-xl">
<a href="{{ $room->isWebinar() ? route('meet.webinars.show', $room) : route('meet.conferences.show', $room) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800"> Back</a>
<h1 class="mt-4 text-xl font-semibold text-slate-900">Link to Ladill Events</h1>
<p class="mt-1 text-sm text-slate-500">
Choose a virtual or hybrid event. Registration, bulk invitations, speakers, and QR codes will be managed in Events.
</p>
@if ($errors->any())
<div class="mt-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{{ $errors->first() }}
</div>
@endif
@if ($events === [])
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6 text-sm text-slate-600">
<p>No virtual or hybrid events found in your Ladill Events account.</p>
<a href="{{ $eventsAppUrl }}/qr-codes/create" target="_blank" rel="noopener" class="mt-4 inline-flex btn-primary btn-primary-sm">Create event in Events</a>
</div>
@else
<form method="POST" action="{{ route('meet.events.link.store', $room) }}" class="mt-6 space-y-5">
@csrf
<div class="rounded-2xl border border-slate-200 bg-white p-6">
<label class="block text-sm font-medium text-slate-700">Event</label>
<select name="event_id" required class="mt-2 w-full rounded-lg border-slate-300 text-sm">
<option value="">Select an event…</option>
@foreach ($events as $event)
<option value="{{ $event['id'] }}" @selected(old('event_id') == $event['id'])>
{{ $event['title'] }} ({{ ucfirst($event['format']) }})
</option>
@endforeach
</select>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-6">
<label class="block text-sm font-medium text-slate-700">Virtual session slot (optional)</label>
<p class="mt-1 text-xs text-slate-500">Pick an empty session matching this {{ $room->isConference() ? 'conference' : 'webinar' }}, or leave blank to attach to the first available {{ $expectedType === 'town_hall' ? 'conference' : 'webinar' }} slot.</p>
<select name="virtual_session_id" class="mt-2 w-full rounded-lg border-slate-300 text-sm">
<option value="">Auto-assign slot</option>
@foreach ($events as $event)
@foreach ($event['virtual_sessions'] ?? [] as $session)
@php
$typeMatch = ($session['meet_room_type'] ?? '') === $expectedType;
$available = empty($session['meet_room_uuid']);
@endphp
@if ($typeMatch)
<option value="{{ $session['id'] }}" data-event="{{ $event['id'] }}" @selected(old('virtual_session_id') === $session['id'])>
[{{ $event['title'] }}] {{ $session['title'] ?: 'Untitled session' }}{{ $available ? '' : ' (linked)' }}
</option>
@endif
@endforeach
@endforeach
</select>
</div>
<button type="submit" class="btn-primary btn-primary-lg w-full">Link event</button>
</form>
@endif
</div>
</x-app-layout>
@@ -0,0 +1,79 @@
@php
use App\Support\EventsSourceLink;
$eventsUrls = EventsSourceLink::urls($room);
$isLinked = EventsSourceLink::isLinked($room);
$eventTitle = EventsSourceLink::eventTitle($room);
$eventsBase = EventsSourceLink::baseUrl();
$kind = $room->isConference() ? 'conference' : 'webinar';
@endphp
<div class="mt-6 rounded-2xl border {{ $isLinked ? 'border-indigo-200 bg-indigo-50/40' : 'border-amber-200 bg-amber-50/50' }} p-6">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="text-sm font-medium text-slate-900">Ladill Events</h2>
@if ($isLinked && $eventTitle)
<p class="mt-1 text-sm text-slate-600">Linked to <span class="font-medium">{{ $eventTitle }}</span></p>
@elseif ($isLinked)
<p class="mt-1 text-sm text-slate-600">Linked to event #{{ EventsSourceLink::eventId($room) }}</p>
@else
<p class="mt-1 text-sm text-slate-600">
Registration, invitations, speakers, and QR codes for this {{ $kind }} are managed in Ladill Events.
Link a virtual or hybrid event to enable attendee registration.
</p>
@endif
</div>
@if ($isLinked)
<form method="POST" action="{{ route('meet.events.link.destroy', $room) }}" onsubmit="return confirm('Remove the Events link? Registration settings stay in Events but this room will no longer show as linked.');">
@csrf
@method('DELETE')
<button type="submit" class="text-xs font-medium text-slate-500 hover:text-slate-700">Unlink</button>
</form>
@endif
</div>
@if ($isLinked)
<div class="mt-4 grid gap-2 sm:grid-cols-2">
@if ($eventsUrls['registration'])
<div class="rounded-lg border border-white/80 bg-white p-3">
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Registration page</p>
<div class="mt-2 flex gap-2">
<input type="text" readonly value="{{ $eventsUrls['registration'] }}" class="min-w-0 flex-1 rounded border-slate-200 bg-slate-50 text-xs font-mono">
<x-copy-button :text="$eventsUrls['registration']" class="btn-secondary btn-secondary-sm shrink-0" />
</div>
</div>
@endif
@if ($eventsUrls['admin'])
<a href="{{ $eventsUrls['admin'] }}" target="_blank" rel="noopener" class="rounded-lg border border-white/80 bg-white p-3 text-sm font-medium text-indigo-700 hover:bg-indigo-50">
Attendees & check-in
</a>
@endif
@if ($eventsUrls['edit'])
<a href="{{ $eventsUrls['edit'] }}" target="_blank" rel="noopener" class="rounded-lg border border-white/80 bg-white p-3 text-sm font-medium text-indigo-700 hover:bg-indigo-50">
Event settings (tickets, branding)
</a>
@endif
@if ($eventsUrls['comms'])
<a href="{{ $eventsUrls['comms'] }}" target="_blank" rel="noopener" class="rounded-lg border border-white/80 bg-white p-3 text-sm font-medium text-indigo-700 hover:bg-indigo-50">
Bulk invitations & comms
</a>
@endif
@if ($eventsUrls['qr'])
<a href="{{ $eventsUrls['qr'] }}" target="_blank" rel="noopener" class="rounded-lg border border-white/80 bg-white p-3 text-sm font-medium text-indigo-700 hover:bg-indigo-50">
Event QR code
</a>
@endif
@if ($eventsUrls['badges'])
<a href="{{ $eventsUrls['badges'] }}" target="_blank" rel="noopener" class="rounded-lg border border-white/80 bg-white p-3 text-sm font-medium text-indigo-700 hover:bg-indigo-50">
Badges & printing
</a>
@endif
</div>
<p class="mt-3 text-xs text-slate-500">Programme speakers and panelists are configured on the event programme in Events and sync to this session when linked.</p>
@else
<div class="mt-4 flex flex-wrap gap-3">
<a href="{{ route('meet.events.link', $room) }}" class="btn-primary btn-primary-sm">Link to Ladill Events</a>
<a href="{{ $eventsBase }}/qr-codes/create" target="_blank" rel="noopener" class="btn-secondary btn-secondary-sm">Create event in Events</a>
</div>
@endif
</div>
@@ -1,7 +1,7 @@
<x-app-layout title="Schedule webinar">
<div class="mx-auto max-w-xl">
<h1 class="text-xl font-semibold text-slate-900">Schedule a webinar</h1>
<p class="mt-1 text-sm text-slate-500">Attendees register in advance. Billed at GHS {{ number_format(config('meet.billing.price_per_participant_ghs', 0.30), 2) }} per participant when the session ends.</p>
<p class="mt-1 text-sm text-slate-500">After creating the webinar, link it to a virtual or hybrid event in Ladill Events for registration, invitations, and QR codes. Billed at GHS {{ number_format(config('meet.billing.price_per_participant_ghs', 0.30), 2) }} per participant when the session ends.</p>
<form method="POST" action="{{ route('meet.webinars.store') }}" class="mt-6 space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
@@ -36,11 +36,6 @@
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Invite panelists by email (comma-separated)</label>
<input type="text" name="invite_emails" value="{{ old('invite_emails') }}" placeholder="panelist@example.com" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Passcode (optional)</label>
<input type="text" name="passcode" value="{{ old('passcode') }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@@ -52,14 +47,13 @@
<input type="checkbox" name="waiting_room" value="1" @checked(old('waiting_room', $defaultSettings['waiting_room'] ?? true)) class="mt-0.5 rounded border-slate-300">
<span>
<span class="font-medium text-slate-800">Waiting room</span>
<span class="mt-0.5 block text-xs text-slate-500">Hold registrants until you admit them.</span>
<span class="mt-0.5 block text-xs text-slate-500">Hold attendees until you admit them.</span>
</span>
</label>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="join_before_host" value="1" @checked(old('join_before_host')) class="rounded border-slate-300"> Allow join before host</label>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="mute_on_join" value="1" @checked(old('mute_on_join', $defaultSettings['mute_on_join'] ?? true)) class="rounded border-slate-300"> Mute attendees on join</label>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="auto_record" value="1" @checked(old('auto_record')) class="rounded border-slate-300"> Auto-record</label>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="live_captions" value="1" @checked(old('live_captions')) class="rounded border-slate-300"> Live captions</label>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="webinar_auto_approve" value="1" @checked(old('webinar_auto_approve')) class="rounded border-slate-300"> Auto-approve registrations</label>
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="panel_discussions" value="1" @checked(old('panel_discussions')) class="rounded border-slate-300"> Enable panel discussions (multi-speaker stage layout)</label>
</fieldset>
+3 -37
View File
@@ -5,11 +5,6 @@
<p class="text-xs font-semibold uppercase tracking-wide text-indigo-600">Webinar</p>
<h1 class="text-2xl font-semibold text-slate-900">{{ $room->title }}</h1>
<p class="mt-1 text-sm text-slate-500">{{ config('meet.room_statuses')[$room->status] ?? $room->status }}</p>
@if(! empty($eventsEventUrl))
<a href="{{ $eventsEventUrl }}" target="_blank" rel="noopener" class="mt-2 inline-flex text-sm font-medium text-indigo-600 hover:text-indigo-800">
View in Ladill Events
</a>
@endif
</div>
@if ($room->canRestart())
<form method="POST" action="{{ route('meet.webinars.start', $room) }}">
@@ -35,14 +30,10 @@
Billed at <span class="font-medium">GHS {{ number_format(config('meet.billing.price_per_participant_ghs', 0.30), 2) }} per participant</span> (peak attendance when the session ends).
</div>
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-medium text-slate-700">Registration link</h2>
<div class="mt-2 flex gap-2">
<input type="text" readonly value="{{ route('meet.webinar.register', $room) }}" class="flex-1 rounded-lg border-slate-300 bg-slate-50 text-sm font-mono">
<x-copy-button :text="route('meet.webinar.register', $room)" class="btn-secondary btn-secondary-sm shrink-0" />
</div>
@include('meet.partials.events-integration', ['room' => $room])
<h2 class="mt-6 text-sm font-medium text-slate-700">Join link (hosts & panelists)</h2>
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-medium text-slate-700">Join link (hosts & panelists)</h2>
<div class="mt-2 flex gap-2">
<input type="text" readonly value="{{ $room->joinUrl() }}" class="flex-1 rounded-lg border-slate-300 bg-slate-50 text-sm font-mono">
<x-copy-button :text="$room->joinUrl()" class="btn-secondary btn-secondary-sm shrink-0" />
@@ -50,19 +41,15 @@
<div class="btn-group mt-4">
<a href="{{ route('meet.rooms.ical', $room) }}" class="btn-secondary btn-secondary-sm">Download iCal</a>
<a href="{{ route('meet.rooms.qr', $room) }}" target="_blank" class="btn-secondary btn-secondary-sm">QR code</a>
@if ($room->sessions->isNotEmpty())
<a href="{{ route('meet.rooms.attendance', $room) }}" class="btn-secondary btn-secondary-sm">Attendance CSV</a>
@endif
<a href="{{ route('meet.invitations.index', $room) }}" class="btn-secondary btn-secondary-sm">Manage invitations</a>
@if ($room->canRestart() && ($room->status === 'ended' || $room->sessions->isNotEmpty()))
<form method="POST" action="{{ route('meet.webinars.start', $room) }}" class="inline">
@csrf
<button type="submit" class="btn-secondary btn-secondary-sm">{{ $room->restartLabel() }}</button>
</form>
@endif
<a href="{{ route('meet.webinar.registrations', $room) }}" class="btn-secondary btn-secondary-sm">Registrations</a>
<a href="{{ route('meet.webinar.register', $room) }}" target="_blank" class="btn-secondary btn-secondary-sm">Registration page</a>
</div>
@if ($room->passcode)
@@ -122,26 +109,5 @@
</ul>
</div>
@endif
@if ($room->sessions->isNotEmpty())
<div class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-medium text-slate-700">Sessions</h2>
<ul class="mt-3 divide-y divide-slate-100 text-sm">
@foreach ($room->sessions as $session)
<li class="flex items-center justify-between py-2">
<span>{{ $session->started_at?->format('M j, Y g:i A') ?? 'Pending' }}</span>
<span class="text-slate-500">{{ $session->peak_participants }} participants · {{ config('meet.session_statuses')[$session->status] ?? $session->status }}</span>
</li>
@endforeach
</ul>
</div>
@endif
@if ($room->status === 'scheduled')
<form method="POST" action="{{ route('meet.rooms.cancel', $room) }}" class="mt-6" onsubmit="return confirm('Cancel this webinar?')">
@csrf
<button type="submit" class="text-sm text-red-600 hover:underline">Cancel webinar</button>
</form>
@endif
</div>
</x-app-layout>