Add kiosk join/leave pages, waiting room, and meeting feedback.
Deploy Ladill Meet / deploy (push) Successful in 47s

Redesign public join flows on the Frontdesk kiosk template, enforce host
admission when waiting room is enabled, and collect star ratings after leave.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-01 13:33:48 +00:00
co-authored by Cursor
parent 5e34d71de0
commit 7ae26a467c
26 changed files with 865 additions and 124 deletions
+96 -4
View File
@@ -3,11 +3,14 @@
namespace App\Http\Controllers\Meet;
use App\Http\Controllers\Controller;
use App\Models\Participant;
use App\Models\Room;
use App\Models\Session;
use App\Services\Meet\InvitationService;
use App\Services\Meet\SecurityPolicyService;
use App\Services\Meet\SessionService;
use App\Services\Meet\WebinarService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -23,8 +26,13 @@ class JoinController extends Controller
public function show(Request $request, Room $room): View|RedirectResponse
{
$room->loadMissing('organization');
if ($room->status === 'cancelled') {
return view('meet.join.cancelled', compact('room'));
return view('meet.join.cancelled', [
'room' => $room,
'organization' => $room->organization,
]);
}
if ($room->isWebinar() && $request->query('token')) {
@@ -33,15 +41,39 @@ class JoinController extends Controller
[$allowed, $reason] = $this->security->canJoin($room, $request->user(), $request->user()?->email);
if (! $allowed && $reason !== 'passcode_required') {
return view('meet.join.denied', compact('room', 'reason'));
return view('meet.join.denied', [
'room' => $room,
'organization' => $room->organization,
'reason' => $reason,
]);
}
if ($room->passcode && ! $request->session()->get("meet.passcode.{$room->uuid}")) {
return view('meet.join.passcode', compact('room'));
return view('meet.join.passcode', [
'room' => $room,
'organization' => $room->organization,
]);
}
$session = $room->activeSession();
if ($session) {
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
$participant = $participantUuid
? Participant::where('uuid', $participantUuid)->where('session_id', $session->id)->first()
: null;
if ($participant?->status === 'waiting') {
return redirect()->route('meet.join.waiting', $room);
}
if ($participant?->status === 'joined') {
return redirect()->route('meet.room', $session);
}
}
return view('meet.join.show', [
'room' => $room,
'organization' => $room->organization,
'user' => $request->user(),
'isWebinar' => $room->isWebinar(),
]);
@@ -130,12 +162,72 @@ class JoinController extends Controller
$displayName = $reg->display_name;
}
$participant = $this->sessions->joinParticipant($session, $user, $role, $displayName, $email);
$joinStatus = $room->requiresWaitingRoom($user, $role) ? 'waiting' : 'joined';
$participant = $this->sessions->joinParticipant($session, $user, $role, $displayName, $email, $joinStatus);
app(InvitationService::class)->markAcceptedOnJoin($room, $user, $email);
$this->sessions->rememberParticipantSession($request, $participant, $session);
if ($participant->status === 'waiting') {
return redirect()->route('meet.join.waiting', $room);
}
return redirect()->route('meet.room', $session);
}
public function waiting(Request $request, Room $room): View|RedirectResponse
{
$room->loadMissing('organization');
$session = $room->activeSession();
abort_unless($session?->isLive(), 404);
$participant = $this->participantFromSession($request, $session);
if (! $participant || $participant->status === 'removed') {
return redirect()->route('meet.join', $room);
}
if ($participant->status === 'joined') {
return redirect()->route('meet.room', $session);
}
return view('meet.join.waiting', [
'room' => $room,
'session' => $session,
'organization' => $room->organization,
'participant' => $participant,
]);
}
public function waitingStatus(Request $request, Room $room): JsonResponse
{
$session = $room->activeSession();
abort_unless($session, 404);
$participant = $this->participantFromSession($request, $session);
return response()->json([
'status' => $participant?->status ?? 'unknown',
'redirect' => ($participant && $participant->status === 'joined')
? route('meet.room', $session)
: null,
'denied' => $participant?->status === 'removed',
]);
}
protected function participantFromSession(Request $request, Session $session): ?Participant
{
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
if (! $participantUuid) {
return null;
}
return Participant::query()
->where('uuid', $participantUuid)
->where('session_id', $session->id)
->first();
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Meet;
use App\Http\Controllers\Controller;
use App\Models\Participant;
use App\Models\Room;
use App\Models\Session;
use App\Models\SessionFeedback;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class LeaveController extends Controller
{
public function show(Request $request, Session $session): View|RedirectResponse
{
$session->loadMissing('room.organization');
abort_unless($session->room, 404);
$feedback = (array) $request->session()->get('meet.left.feedback', []);
if (($feedback['session_uuid'] ?? null) !== $session->uuid) {
return redirect()->route('meet.join', $session->room);
}
return view('meet.left.show', [
'session' => $session,
'room' => $session->room,
'organization' => $session->room->organization,
'feedback' => $feedback,
]);
}
public function store(Request $request, Session $session): RedirectResponse
{
$feedback = (array) $request->session()->get('meet.left.feedback', []);
abort_unless(($feedback['session_uuid'] ?? null) === $session->uuid, 403);
$validated = $request->validate([
'rating' => ['required', 'integer', 'min:1', 'max:5'],
'audio_rating' => ['nullable', 'integer', 'min:1', 'max:5'],
'video_rating' => ['nullable', 'integer', 'min:1', 'max:5'],
'comment' => ['nullable', 'string', 'max:1000'],
]);
SessionFeedback::updateOrCreate(
[
'session_id' => $session->id,
'participant_uuid' => $feedback['participant_uuid'] ?? null,
],
[
'display_name' => $feedback['display_name'] ?? null,
'user_ref' => $feedback['user_ref'] ?? null,
'rating' => $validated['rating'],
'audio_rating' => $validated['audio_rating'] ?? null,
'video_rating' => $validated['video_rating'] ?? null,
'comment' => $validated['comment'] ?? null,
],
);
$request->session()->forget('meet.left.feedback');
return redirect()
->route('meet.left.thanks', $session)
->with('rated', true);
}
public function thanks(Request $request, Session $session): View
{
$session->loadMissing('room.organization');
return view('meet.left.thanks', [
'session' => $session,
'room' => $session->room,
'organization' => $session->room->organization,
]);
}
}
@@ -39,6 +39,12 @@ class MeetingRoomController extends Controller
->where('session_id', $session->id)
->firstOrFail();
if ($participant->status === 'waiting') {
return redirect()->route('meet.join.waiting', $session->room);
}
abort_unless($participant->status === 'joined', 403, 'You are not in this meeting.');
$token = $this->media->isConfigured()
? $this->sessions->generateMediaToken($participant)
: '';
@@ -81,11 +87,18 @@ class MeetingRoomController extends Controller
if ($participant) {
$this->sessions->leaveParticipant($participant);
$request->session()->put('meet.left.feedback', [
'session_uuid' => $session->uuid,
'participant_uuid' => $participant->uuid,
'display_name' => $participant->display_name,
'user_ref' => $participant->user_ref,
]);
}
$request->session()->forget("meet.participant.{$session->uuid}");
return redirect()->route('meet.dashboard');
return redirect()->route('meet.left', $session);
}
public function raiseHand(Request $request, Session $session): JsonResponse
@@ -145,7 +158,7 @@ class MeetingRoomController extends Controller
{
$this->currentParticipant($request, $session);
$session->load(['participants' => fn ($q) => $q->where('status', 'joined')]);
$session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]);
$avatars = ParticipantPresenter::avatarMap($session->participants, $session->room->host_user_ref);
@@ -167,6 +180,7 @@ class MeetingRoomController extends Controller
'participants' => $session->participants->map(
fn ($p) => ParticipantPresenter::toArray($p, $avatars, $session->room->host_user_ref)
),
'waiting_count' => $session->participants->where('status', 'waiting')->count(),
'messages' => $messages->map(fn ($m) => [
'uuid' => $m->uuid,
'sender_name' => $m->sender_name,
@@ -271,6 +285,28 @@ class MeetingRoomController extends Controller
return response()->json(['ok' => true]);
}
public function admit(Request $request, Session $session, Participant $participant): JsonResponse
{
$current = $this->currentParticipant($request, $session);
abort_unless($current->isHost(), 403);
abort_unless($participant->session_id === $session->id, 404);
$this->sessions->admitParticipant($participant);
return response()->json(['ok' => true, 'status' => 'joined']);
}
public function deny(Request $request, Session $session, Participant $participant): JsonResponse
{
$current = $this->currentParticipant($request, $session);
abort_unless($current->isHost(), 403);
abort_unless($participant->session_id === $session->id, 404);
$this->sessions->denyParticipant($participant);
return response()->json(['ok' => true, 'status' => 'removed']);
}
protected function currentParticipant(Request $request, Session $session): Participant
{
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");