From 158b7b650ac90685530805198c4deb827aaef78c Mon Sep 17 00:00:00 2001 From: isaacclad Date: Wed, 1 Jul 2026 20:29:16 +0000 Subject: [PATCH] Allow adding conference speakers after creation on details page. Hosts can assign team speakers and invite panelists by email from the conference show page, with presenter refs synced to live sessions. Co-authored-by: Cursor --- .../Controllers/Meet/ConferenceController.php | 77 +++++++++++++++++- app/Services/Meet/ConferenceService.php | 22 +++++ .../views/meet/conferences/show.blade.php | 80 +++++++++++++++---- routes/web.php | 1 + tests/Feature/MeetWebTest.php | 49 ++++++++++++ 5 files changed, 210 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/Meet/ConferenceController.php b/app/Http/Controllers/Meet/ConferenceController.php index 39aecff..c71eebd 100644 --- a/app/Http/Controllers/Meet/ConferenceController.php +++ b/app/Http/Controllers/Meet/ConferenceController.php @@ -7,7 +7,9 @@ use App\Http\Controllers\Meet\Concerns\ScopesToAccount; use App\Models\Member; use App\Models\Room; use App\Models\Template; +use App\Models\User; use App\Services\Meet\CalendarService; +use App\Services\Meet\ConferenceService; use App\Services\Meet\InvitationService; use App\Services\Meet\RoomService; use App\Services\Meet\SessionService; @@ -28,6 +30,7 @@ class ConferenceController extends Controller protected InvitationService $invitations, protected CalendarService $calendar, protected TownHallService $townHalls, + protected ConferenceService $conferences, ) {} public function index(Request $request): View @@ -152,13 +155,83 @@ class ConferenceController extends Controller $room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles']); - $presenterRefs = (array) $room->setting('presenter_refs', []); + $presenterRefs = $this->conferences->presenterRefs($room); $speakers = Member::owned($this->ownerRef($request)) ->where('organization_id', $room->organization_id) ->whereIn('user_ref', $presenterRefs) ->get(); - return view('meet.conferences.show', compact('room', 'speakers')); + $speakerUsers = User::query() + ->whereIn('public_id', $presenterRefs) + ->get() + ->keyBy('public_id'); + + $members = Member::owned($this->ownerRef($request)) + ->where('organization_id', $room->organization_id) + ->where('user_ref', '!=', $this->ownerRef($request)) + ->orderBy('user_ref') + ->get(); + + $memberUsers = User::query() + ->whereIn('public_id', $members->pluck('user_ref')) + ->get() + ->keyBy('public_id'); + + $canManageSpeakers = app(\App\Services\Meet\MeetPermissions::class) + ->can($this->member($request), 'meetings.manage'); + + return view('meet.conferences.show', compact( + 'room', + 'speakers', + 'speakerUsers', + 'members', + 'memberUsers', + 'presenterRefs', + 'canManageSpeakers', + )); + } + + public function updateSpeakers(Request $request, Room $room): RedirectResponse + { + $this->authorizeAbility($request, 'meetings.manage'); + $this->authorizeOwner($request, $room); + abort_unless($room->isConference(), 404); + + $validated = $request->validate([ + 'presenter_refs' => ['nullable', 'array'], + 'presenter_refs.*' => ['string', 'max:64'], + 'invite_emails' => ['nullable', 'string', 'max:2000'], + ]); + + $allowedRefs = Member::owned($this->ownerRef($request)) + ->where('organization_id', $room->organization_id) + ->pluck('user_ref') + ->all(); + + $presenterRefs = collect($validated['presenter_refs'] ?? []) + ->filter() + ->unique() + ->intersect($allowedRefs) + ->values() + ->all(); + + $this->conferences->syncPresenterRefs($room, $presenterRefs); + + $inviteCount = 0; + if ($emails = trim((string) ($validated['invite_emails'] ?? ''))) { + $invites = collect(preg_split('/[\s,;]+/', $emails)) + ->filter() + ->map(fn ($email) => ['email' => $email, 'role' => 'panelist']) + ->all(); + $inviteCount = count($invites); + $this->invitations->inviteToRoom($room, $invites, $request->user()); + } + + $message = $inviteCount > 0 + ? 'Speakers updated and '.$inviteCount.' invitation(s) sent.' + : 'Speakers updated.'; + + return redirect()->route('meet.conferences.show', $room)->with('success', $message); } public function start(Request $request, Room $room): RedirectResponse diff --git a/app/Services/Meet/ConferenceService.php b/app/Services/Meet/ConferenceService.php index ec4e539..2b5ecce 100644 --- a/app/Services/Meet/ConferenceService.php +++ b/app/Services/Meet/ConferenceService.php @@ -10,6 +10,28 @@ use App\Models\User; class ConferenceService { + /** + * @param list $presenterRefs + */ + public function syncPresenterRefs(Room $room, array $presenterRefs): Room + { + $presenterRefs = array_values(array_unique(array_filter($presenterRefs))); + $settings = array_merge($room->settings ?? [], ['presenter_refs' => $presenterRefs]); + $room->update(['settings' => $settings]); + + $session = $room->activeSession(); + if ($session) { + $merged = collect($session->presenter_refs ?? []) + ->merge($presenterRefs) + ->unique() + ->values() + ->all(); + $session->update(['presenter_refs' => $merged]); + } + + return $room->fresh(); + } + /** * @return list */ diff --git a/resources/views/meet/conferences/show.blade.php b/resources/views/meet/conferences/show.blade.php index 36d5be2..61ea0c1 100644 --- a/resources/views/meet/conferences/show.blade.php +++ b/resources/views/meet/conferences/show.blade.php @@ -54,27 +54,73 @@ @if ($room->setting('green_room'))

Green room is on — when you start, you and assigned speakers rehearse before going live to the audience.

@endif + - @if ($speakers->isNotEmpty()) -
-

Assigned speakers

-
    - @foreach ($speakers as $speaker) -
  • {{ $speaker->user_ref }}
  • - @endforeach -
+
+

Speakers

+

Team members on stage and external speakers invited by email. You are always the host.

+ + @if ($speakers->isNotEmpty() || $room->invitations->where('role', 'panelist')->isNotEmpty()) +
+ @if ($speakers->isNotEmpty()) +
+

Assigned speakers

+
    + @foreach ($speakers as $speaker) + @php $user = $speakerUsers->get($speaker->user_ref); @endphp +
  • {{ $user?->name ?? $speaker->user_ref }}
  • + @endforeach +
+
+ @endif + + @if ($room->invitations->where('role', 'panelist')->isNotEmpty()) +
+

Invited speakers

+
    + @foreach ($room->invitations->where('role', 'panelist') as $invite) +
  • + {{ $invite->display_name ?? $invite->email }} + · {{ ucfirst($invite->status) }} +
  • + @endforeach +
+
+ @endif
+ @else +

No speakers assigned yet.

@endif - @if ($room->invitations->where('role', 'panelist')->isNotEmpty()) -
-

Invited speakers

-
    - @foreach ($room->invitations->where('role', 'panelist') as $invite) -
  • {{ $invite->email }}
  • - @endforeach -
-
+ @if ($canManageSpeakers) +
+ @csrf + +
+ + +

Comma-separated. Invitations are sent as speaker (panelist) roles.

+
+ + @if ($members->isNotEmpty()) +
+ +
+ @foreach ($members as $member) + @php $user = $memberUsers->get($member->user_ref); @endphp + + @endforeach +
+
+ @endif + + +
@endif
diff --git a/routes/web.php b/routes/web.php index c11d11d..a5c8452 100644 --- a/routes/web.php +++ b/routes/web.php @@ -138,6 +138,7 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/conferences/create', [ConferenceController::class, 'create'])->name('meet.conferences.create'); Route::post('/conferences', [ConferenceController::class, 'store'])->name('meet.conferences.store'); Route::get('/conferences/{room}', [ConferenceController::class, 'show'])->name('meet.conferences.show'); + Route::post('/conferences/{room}/speakers', [ConferenceController::class, 'updateSpeakers'])->name('meet.conferences.speakers.update'); Route::post('/conferences/{room}/start', [ConferenceController::class, 'start'])->name('meet.conferences.start'); Route::get('/rooms', [SpaceController::class, 'index'])->name('meet.spaces.index'); diff --git a/tests/Feature/MeetWebTest.php b/tests/Feature/MeetWebTest.php index 6594856..1e7558f 100644 --- a/tests/Feature/MeetWebTest.php +++ b/tests/Feature/MeetWebTest.php @@ -723,6 +723,55 @@ class MeetWebTest extends TestCase ->assertSee('NextGen Summit'); } + public function test_host_can_add_speakers_after_conference_created(): void + { + $speaker = User::create([ + 'public_id' => 'speaker-user-001', + 'name' => 'Keynote Speaker', + 'email' => 'speaker@example.com', + ]); + + Member::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $speaker->public_id, + 'role' => 'member', + ]); + + $room = Room::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'host_user_ref' => $this->user->public_id, + 'title' => 'Product launch', + 'type' => 'town_hall', + 'status' => 'scheduled', + 'scheduled_at' => now()->addDay(), + 'timezone' => 'UTC', + 'settings' => array_merge(config('meet.default_settings'), [ + 'green_room' => true, + 'stage_mode' => true, + 'presenter_refs' => [], + ]), + ]); + + $this->actingAs($this->user) + ->post(route('meet.conferences.speakers.update', $room), [ + 'presenter_refs' => [$speaker->public_id], + 'invite_emails' => 'guest-speaker@example.com', + ]) + ->assertRedirect(route('meet.conferences.show', $room)) + ->assertSessionHas('success'); + + $room->refresh(); + $this->assertContains($speaker->public_id, $room->setting('presenter_refs')); + + $this->assertDatabaseHas('meet_invitations', [ + 'room_id' => $room->id, + 'email' => 'guest-speaker@example.com', + 'role' => 'panelist', + ]); + } + public function test_host_can_create_space_room(): void { $this->actingAs($this->user)