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)