session()->get('user.business_id'); if (! auth()->user()->can('communication.access')) { abort(403, 'Unauthorized action.'); } return (int) $business_id; } public function index(Request $request) { $businessId = $this->authorizeModule(); $this->ensureMeetingTablesExist(); $userId = auth()->id(); $query = Meeting::where('business_id', $businessId) ->where(function ($q) use ($userId) { $q->where('created_by', $userId)->orWhereHas('participants', fn ($p) => $p->where('user_id', $userId)); }); if ($request->filled('status') && in_array($request->status, ['scheduled', 'active', 'ended'], true)) { $query->where('status', $request->status); } $meetings = $query->with(['creator:id,first_name,last_name', 'participants.user:id,first_name,last_name']) ->orderByDesc('created_at') ->paginate(20); $counts = [ 'all' => Meeting::where('business_id', $businessId) ->where(function ($q) use ($userId) { $q->where('created_by', $userId)->orWhereHas('participants', fn ($p) => $p->where('user_id', $userId)); })->count(), 'scheduled' => Meeting::where('business_id', $businessId) ->where('status', 'scheduled') ->where(function ($q) use ($userId) { $q->where('created_by', $userId)->orWhereHas('participants', fn ($p) => $p->where('user_id', $userId)); })->count(), 'active' => Meeting::where('business_id', $businessId) ->where('status', 'active') ->where(function ($q) use ($userId) { $q->where('created_by', $userId)->orWhereHas('participants', fn ($p) => $p->where('user_id', $userId)); })->count(), 'ended' => Meeting::where('business_id', $businessId) ->where('status', 'ended') ->where(function ($q) use ($userId) { $q->where('created_by', $userId)->orWhereHas('participants', fn ($p) => $p->where('user_id', $userId)); })->count(), ]; if ($request->ajax()) { return response()->json([ 'meetings' => $meetings, 'counts' => $counts, ]); } return view('communication::spa'); } public function store(Request $request) { $businessId = $this->authorizeModule(); $this->ensureMeetingTablesExist(); $validated = $request->validate([ 'title' => 'required|string|max:255', 'scheduled_at' => 'nullable|date', 'participant_ids' => 'sometimes|array', 'participant_ids.*' => 'integer|exists:users,id', 'start_now' => 'sometimes|boolean', ]); $participantIds = array_unique(array_map('intval', $validated['participant_ids'] ?? [])); foreach ($participantIds as $participantId) { $this->chatRoomService->assertColleague($businessId, $participantId); } $startNow = filter_var($validated['start_now'] ?? true, FILTER_VALIDATE_BOOLEAN); $scheduledAt = ! empty($validated['scheduled_at']) ? $this->util->uf_date($validated['scheduled_at'], true) : null; $meeting = Meeting::create([ 'business_id' => $businessId, 'title' => $validated['title'], 'livekit_room_name' => 'biz-'.$businessId.'-meet-'.Str::uuid(), 'status' => $startNow ? 'active' : 'scheduled', 'created_by' => auth()->id(), 'scheduled_at' => $scheduledAt, 'started_at' => $startNow ? now() : null, ]); MeetingParticipant::create([ 'meeting_id' => $meeting->id, 'user_id' => auth()->id(), 'role' => 'host', 'joined_at' => $startNow ? now() : null, ]); foreach ($participantIds as $participantId) { if ($participantId == auth()->id()) { continue; } MeetingParticipant::create([ 'meeting_id' => $meeting->id, 'user_id' => $participantId, 'role' => 'participant', ]); event(new MeetingInvite($meeting, $participantId, $businessId, auth()->user()->user_full_name)); } return response()->json($meeting->load(['creator', 'participants.user'])); } public function show(int $id) { $businessId = $this->authorizeModule(); $this->ensureMeetingTablesExist(); return response()->json($this->findAccessibleMeeting($businessId, $id, auth()->id())->load(['creator', 'participants.user'])); } public function room(int $id) { $businessId = $this->authorizeModule(); $this->ensureMeetingTablesExist(); $meeting = $this->findAccessibleMeeting($businessId, $id, auth()->id()); return view('communication::meetings.room', compact('meeting')); } public function token(Request $request, int $id) { $businessId = $this->authorizeModule(); $this->ensureMeetingTablesExist(); $meeting = $this->findAccessibleMeeting($businessId, $id, auth()->id()); if ($meeting->status === 'ended') { return response()->json(['success' => false, 'msg' => __('communication::lang.meeting_ended')], 400); } if ($meeting->status === 'scheduled' && $meeting->created_by !== auth()->id()) { return response()->json(['success' => false, 'msg' => __('communication::lang.meeting_not_started')], 400); } if ($meeting->status === 'scheduled' && $meeting->created_by === auth()->id()) { $meeting->update(['status' => 'active', 'started_at' => now()]); } $participant = MeetingParticipant::where('meeting_id', $meeting->id)->where('user_id', auth()->id())->first(); if ($participant && ! $participant->joined_at) { $participant->update(['joined_at' => now()]); } $isHost = ($participant?->role === 'host') || $meeting->created_by === auth()->id(); $translationEnabled = config('communication.translation_enabled', true) && filter_var($request->input('translation_enabled', true), FILTER_VALIDATE_BOOLEAN); $language = $this->liveKitTokenService->normalizeLanguage($request->input('language')); return response()->json([ 'success' => true, 'token' => $this->liveKitTokenService->createToken( auth()->user(), $meeting->livekit_room_name, $isHost, $language, $translationEnabled, ), 'url' => $this->liveKitTokenService->getUrl(), 'room_name' => $meeting->livekit_room_name, 'language' => $language, 'translation_enabled' => $translationEnabled, ]); } public function end(int $id) { $businessId = $this->authorizeModule(); $meeting = Meeting::where('business_id', $businessId)->findOrFail($id); if ($meeting->created_by !== auth()->id()) { abort(403, 'Unauthorized action.'); } $meeting->update(['status' => 'ended', 'ended_at' => now()]); return response()->json(['success' => true]); } private function findAccessibleMeeting(int $businessId, int $meetingId, int $userId): Meeting { return Meeting::where('business_id', $businessId) ->where(function ($q) use ($userId) { $q->where('created_by', $userId)->orWhereHas('participants', fn ($p) => $p->where('user_id', $userId)); }) ->findOrFail($meetingId); } private function ensureMeetingTablesExist(): void { if (! Schema::hasTable('meetings') || ! Schema::hasTable('meeting_participants')) { abort(503, __('communication::lang.meeting_tables_missing')); } } }