'Persons', 'App\Models\Organization' => 'Organizations', 'App\Models\Bank' => 'Banks', 'App\Models\Admin' => 'Admins', ]; // Data properties public $onlineReactedProfiles = []; public $totalCount = 0; public $countByType = []; // Logout modal properties public $showLogoutModal = false; public $selectedProfileId = null; public $selectedProfileType = null; public $selectedProfileName = null; public function mount( $reactionTypes = null, $guards = null, $showCount = true, $showAvatars = true, $maxDisplay = 20, $header = true, $headerText = null, $groupByModel = false, $lastSeen = false ) { // Setup reaction types if ($reactionTypes !== null) { if (is_string($reactionTypes)) { $this->reactionTypes = array_map('trim', explode(',', $reactionTypes)); } else { $this->reactionTypes = is_array($reactionTypes) ? $reactionTypes : [$reactionTypes]; } } else { // Keep as null to show all online users without filtering $this->reactionTypes = null; } // Setup guards to check if ($guards) { $this->guards = is_array($guards) ? $guards : [$guards]; } else { // Check all guards by default $this->guards = ['web', 'organization', 'bank']; } // Determine current auth guard $this->authGuard = session('active_guard') ?? 'web'; // Set display options $this->showCount = $showCount; $this->showAvatars = $showAvatars; $this->maxDisplay = $maxDisplay; $this->groupByModel = $groupByModel; $this->lastSeen = $lastSeen; // Load initial data $this->loadOnlineReactedProfiles(); // Set header text if required if ($header) { if ($headerText) { $this->headerText = $headerText; } else { $this->headerText = $this->getHeaderText($this->reactionTypes); } } else { $this->headerText = null; } } /** * Get the header text based on reaction types. * If no reaction types, return a default message. */ public function getHeaderText($reactionTypes) { if ($this->reactionTypes === null) { $isAdmin = session('activeProfileType') === 'App\Models\Admin'; $key = $isAdmin ? 'messages.profiles_online' : 'messages.reactions_contacts_online'; return trans_choice($key, $this->totalCount, ['count' => $this->totalCount]); } elseif (!empty($reactionTypes)) { if (count($reactionTypes) === 1) { return trans_choice('messages.' . strtolower($reactionTypes[0]) . '_contacts_online', $this->totalCount, ['count' => $this->totalCount]); } elseif (count($reactionTypes) > 1) { return trans_choice('messages.reactions_contacts_online', $this->totalCount, ['count' => $this->totalCount]); } } return 'Error: no reaction types found.'; } public function loadOnlineReactedProfiles() { try { // Get authenticated user/profile $authUser = auth($this->authGuard)->user(); if (!$authUser) { $this->onlineReactedProfiles = []; $this->totalCount = 0; return; } // Only require viaLoveReacter if filtering by reactions if ($this->reactionTypes !== null && !method_exists($authUser, 'viaLoveReacter')) { $this->onlineReactedProfiles = []; $this->totalCount = 0; return; } // Collect all online users from specified guards $allOnlineProfiles = collect(); $locationRelations = [ 'locations.city.translations', 'locations.division.translations', 'locations.country.translations', ]; foreach ($this->guards as $guard) { try { $presenceService = app(PresenceService::class); $onlineUsers = $presenceService->getOnlineUsers($guard); // Batch-load all models for this guard in one query $modelClass = $this->getModelClassForGuard($guard); if (!$modelClass || $onlineUsers->isEmpty()) continue; $ids = $onlineUsers->map(fn($u) => is_array($u) ? ($u['id'] ?? null) : ($u->id ?? null))->filter(); $modelsById = $modelClass::with($locationRelations)->whereIn('id', $ids)->get()->keyBy('id'); foreach ($onlineUsers as $userData) { $userId = is_array($userData) ? ($userData['id'] ?? null) : ($userData->id ?? null); if (!$userId) continue; $model = $modelsById->get($userId); if ($model && ($this->reactionTypes === null || method_exists($model, 'viaLoveReactant'))) { $profileData = $this->buildProfileData($model, $userData, $guard); if ($profileData) { $allOnlineProfiles->push($profileData); } } } } catch (\Exception $e) { \Log::error('Error loading online profiles for guard ' . $guard . ': ' . $e->getMessage()); } } // Exclude the current active profile from results $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $allOnlineProfiles = $allOnlineProfiles->filter(function ($profile) use ($activeProfileType, $activeProfileId) { return !($profile['model_type'] === $activeProfileType && $profile['id'] === $activeProfileId); }); // Filter profiles based on active profile type $activeProfileType = session('activeProfileType'); if ($activeProfileType === 'App\Models\Admin') { // Admins see all online profiles $reactedProfiles = $allOnlineProfiles; } else { // Non-admins see only their contacts (reactions + transactions + conversations) $contactKeys = $this->getContactProfileKeys(); $reactedProfiles = $allOnlineProfiles->filter(function ($profile) use ($contactKeys) { $key = $profile['model_type'] . '_' . $profile['id']; return in_array($key, $contactKeys); }); } // Apply display limit $limitedProfiles = $reactedProfiles->take($this->maxDisplay); // Group by model if requested if ($this->groupByModel) { $this->onlineReactedProfiles = $limitedProfiles ->groupBy('model_type') ->toArray(); } else { $this->onlineReactedProfiles = $limitedProfiles->values()->toArray(); } // Update counts $this->totalCount = $reactedProfiles->count(); $this->countByType = $reactedProfiles->groupBy('model_type')->map->count()->toArray(); // Dispatch event for other components $this->dispatch('online-reacted-profiles-updated', [ 'count' => $this->totalCount, 'profiles' => $this->onlineReactedProfiles ]); } catch (\Exception $e) { \Log::error('Error loading online reacted profiles: ' . $e->getMessage()); $this->onlineReactedProfiles = []; $this->totalCount = 0; } } protected function getModelFromUserData($userData, $guard) { try { // Get the model class for this guard $modelClass = $this->getModelClassForGuard($guard); if (!$modelClass) { return null; } // Handle both array and object formats $userId = is_array($userData) ? ($userData['id'] ?? null) : ($userData->id ?? null); if (!$userId) { return null; } return $modelClass::find($userId); } catch (\Exception $e) { \Log::error('Error getting model from user data: ' . $e->getMessage()); return null; } } protected function getModelClassForGuard($guard) { // Map guards to model classes $guardModelMap = [ 'web' => \App\Models\User::class, 'organization' => \App\Models\Organization::class, 'bank' => \App\Models\Bank::class, 'admin' => \App\Models\Admin::class, ]; return $guardModelMap[$guard] ?? null; } protected function buildProfileData($model, $userData, $guard) { try { // Get reactions for this profile $reactions = $this->getReactionsForProfile($model); // Build profile data array return [ 'id' => $model->id, 'model' => $model, 'model_type' => get_class($model), 'model_label' => $this->modelLabels[get_class($model)] ?? class_basename($model), 'guard' => $guard, 'name' => $model->name ?? 'Unknown', 'location' => $model->getLocationFirst()['name_short'] ?? '', 'avatar' => $model->profile_photo_path ?? null, 'last_seen' => is_array($userData) ? ($userData['last_seen'] ?? now()) : ($userData->last_seen ?? now()), 'reactions' => $reactions, 'profile_url' => route('profile.show_by_type_and_id', ['type' => __(strtolower(class_basename($model))), 'id' => $model->id]), 'is_online' => true, ]; } catch (\Exception $e) { \Log::error('Error building profile data: ' . $e->getMessage()); return null; } } protected function hasAuthUserReacted($model) { try { // If no reaction types specified, return true (show all) if ($this->reactionTypes === null) { return true; } if (!method_exists($model, 'viaLoveReactant')) { return false; } $reactantFacade = $model->viaLoveReactant(); // Get the authenticated user directly $authUser = auth($this->authGuard)->user(); if (!$authUser) { return false; } // Check if empty array (different from null) if (is_array($this->reactionTypes) && empty($this->reactionTypes)) { return false; } foreach ($this->reactionTypes as $reactionType) { if ($reactantFacade->isReactedBy($authUser, $reactionType)) { return true; } } return false; } catch (\Exception $e) { \Log::error('Error checking reactions: ' . $e->getMessage()); return false; } } protected function getReactionsForProfile($model) { try { if (!method_exists($model, 'viaLoveReactant')) { return []; } $reactantFacade = $model->viaLoveReactant(); $reactions = []; // Get the authenticated user directly $authUser = auth($this->authGuard)->user(); if (!$authUser) { return []; } if ($this->reactionTypes !== null) { foreach ($this->reactionTypes as $reactionType) { if ($reactantFacade->isReactedBy($authUser, $reactionType)) { $reactions[] = [ 'type' => $reactionType, ]; } } } return $reactions; } catch (\Exception $e) { \Log::error('Error getting reactions for profile: ' . $e->getMessage()); return []; } } public function toggleReaction($profileId, $modelType, $reactionType) { try { $authUser = auth($this->authGuard)->user(); if (!$authUser) { return; } $model = $modelType::find($profileId); if (!$model) { return; } $reacterFacade = $authUser->viaLoveReacter(); $reactantFacade = $model->viaLoveReactant(); if ($reactantFacade->isReactedBy($authUser, $reactionType)) { $reacterFacade->unreactTo($model, $reactionType); } else { $reacterFacade->reactTo($model, $reactionType); } $this->loadOnlineReactedProfiles(); } catch (\Exception $e) { \Log::error('Error toggling reaction: ' . $e->getMessage()); } } #[On('presence-updated')] #[On('echo-presence:presence-web-users,.UserPresenceUpdated')] public function refreshProfiles() { $this->loadOnlineReactedProfiles(); } #[On('user-activity')] public function onUserActivity() { $this->loadOnlineReactedProfiles(); } public function isCurrentUserProfile($profileId, $modelType) { // Get the active profile using getActiveProfile() helper $activeProfile = getActiveProfile(); if (!$activeProfile) { return false; } // Check if this is the current active profile (admin or any other type) if (get_class($activeProfile) === $modelType || get_class($activeProfile) === '\\' . $modelType) { if ($activeProfile->id === $profileId) { return true; } } // If the active profile is an Admin, check for related users if (get_class($activeProfile) === \App\Models\Admin::class) { if (method_exists($activeProfile, 'users')) { $relatedUsers = $activeProfile->users()->pluck('users.id')->toArray(); if ($modelType === 'App\Models\User' || $modelType === \App\Models\User::class) { return in_array($profileId, $relatedUsers); } } } return false; } public function openLogoutModal($profileId, $modelType) { // Verify admin access if (getActiveProfileType() !== 'Admin') { $this->dispatch('notify', [ 'type' => 'error', 'message' => 'Unauthorized action. Only administrators can log out users.' ]); return; } // Prevent logging out current admin or their related profiles if ($this->isCurrentUserProfile($profileId, $modelType)) { $this->dispatch('notify', [ 'type' => 'error', 'message' => 'You cannot log out your own profile or related user accounts.' ]); return; } $this->selectedProfileId = $profileId; $this->selectedProfileType = $modelType; // Get profile name for display try { $model = $modelType::find($profileId); $this->selectedProfileName = $model ? $model->name : null; } catch (\Exception $e) { $this->selectedProfileName = null; } $this->showLogoutModal = true; } public function closeLogoutModal() { $this->showLogoutModal = false; $this->selectedProfileId = null; $this->selectedProfileType = null; $this->selectedProfileName = null; } public function logoutUser() { // Verify admin access if (getActiveProfileType() !== 'Admin') { $this->dispatch('notify', [ 'type' => 'error', 'message' => 'Unauthorized action. Only administrators can log out users.' ]); $this->closeLogoutModal(); return; } try { if (!$this->selectedProfileId || !$this->selectedProfileType) { throw new \Exception('Invalid profile data'); } $model = $this->selectedProfileType::find($this->selectedProfileId); if (!$model) { throw new \Exception('User not found'); } // Get the guard for this model type $guard = $this->getGuardForModelType($this->selectedProfileType); if (!$guard) { throw new \Exception('Invalid guard'); } // Broadcast logout event via websocket - this will force the user's browser to logout broadcast(new \App\Events\UserForcedLogout($this->selectedProfileId, $guard)); // Delete all sessions for this user from the database \DB::connection(config('session.connection')) ->table(config('session.table', 'sessions')) ->where('user_id', $this->selectedProfileId) ->delete(); // Clear any cached authentication data \Cache::forget('auth_' . $guard . '_' . $this->selectedProfileId); // Clear presence cache \Cache::forget("presence_{$guard}_{$this->selectedProfileId}"); // Clear online users cache to force refresh \Cache::forget("online_users_{$guard}_" . \App\Services\PresenceService::ONLINE_THRESHOLD_MINUTES); // Set user offline in presence service $presenceService = app(\App\Services\PresenceService::class); $presenceService->setUserOffline($model, $guard); $this->dispatch('notify', [ 'type' => 'success', 'message' => __('User has been logged out successfully.') ]); // Refresh the online profiles list $this->loadOnlineReactedProfiles(); } catch (\Exception $e) { \Log::error('Error logging out user: ' . $e->getMessage()); $this->dispatch('notify', [ 'type' => 'error', 'message' => __('Failed to log out user. Please try again.') ]); } $this->closeLogoutModal(); } protected function getGuardForModelType($modelType) { // Map model types to guards $modelGuardMap = [ \App\Models\User::class => 'web', 'App\Models\User' => 'web', \App\Models\Organization::class => 'organization', 'App\Models\Organization' => 'organization', \App\Models\Bank::class => 'bank', 'App\Models\Bank' => 'bank', \App\Models\Admin::class => 'admin', 'App\Models\Admin' => 'admin', ]; return $modelGuardMap[$modelType] ?? null; } /** * Get profile keys (ModelClass_id) for all contacts of the active profile. * Mirrors the logic in Contacts.php: reactions (Star/Bookmark) + transactions + conversations. */ protected function getContactProfileKeys(): array { $activeProfile = getActiveProfile(); if (!$activeProfile) { return []; } $keys = collect(); $reacterId = $activeProfile->love_reacter_id; // 1. Reactions (Star + Bookmark) if ($reacterId) { $reacted = DB::table('love_reactions') ->join('love_reactants', 'love_reactions.reactant_id', '=', 'love_reactants.id') ->where('love_reactions.reacter_id', $reacterId) ->whereIn(DB::raw('SUBSTRING_INDEX(love_reactants.type, "\\\\", -1)'), ['User', 'Organization', 'Bank']) ->select('love_reactants.type as reactant_type', 'love_reactants.id as reactant_id') ->get(); foreach ($reacted as $row) { // Look up the profile id via the reactant $profileId = DB::table('love_reactants') ->join(DB::raw('(' . $this->buildProfileSubquery($row->reactant_type) . ') as p'), 'love_reactants.id', '=', 'p.love_reactant_id') ->where('love_reactants.id', $row->reactant_id) ->value('p.id'); if ($profileId) { $modelClass = $this->resolveModelClassFromReactantType($row->reactant_type); if ($modelClass) { $keys->push($modelClass . '_' . $profileId); } } } } // 2. Transactions $accountIds = DB::table('accounts') ->where('accountable_type', get_class($activeProfile)) ->where('accountable_id', $activeProfile->id) ->pluck('id'); if ($accountIds->isNotEmpty()) { $counterAccountIds = DB::table('transactions') ->where(function ($q) use ($accountIds) { $q->whereIn('from_account_id', $accountIds)->whereNotIn('to_account_id', $accountIds); }) ->orWhere(function ($q) use ($accountIds) { $q->whereIn('to_account_id', $accountIds)->whereNotIn('from_account_id', $accountIds); }) ->get() ->map(fn($t) => $accountIds->contains($t->from_account_id) ? $t->to_account_id : $t->from_account_id) ->unique(); DB::table('accounts') ->whereIn('id', $counterAccountIds) ->select('accountable_type', 'accountable_id') ->get() ->each(fn($a) => $keys->push($a->accountable_type . '_' . $a->accountable_id)); } // 3. WireChat private conversations $participantType = get_class($activeProfile); $participantId = $activeProfile->id; $myConversationIds = DB::table('wirechat_participants') ->join('wirechat_conversations', 'wirechat_participants.conversation_id', '=', 'wirechat_conversations.id') ->where('wirechat_participants.participantable_type', $participantType) ->where('wirechat_participants.participantable_id', $participantId) ->where('wirechat_conversations.type', ConversationType::PRIVATE->value) ->whereNull('wirechat_participants.deleted_at') ->pluck('wirechat_participants.conversation_id'); if ($myConversationIds->isNotEmpty()) { DB::table('wirechat_participants') ->whereIn('conversation_id', $myConversationIds) ->where(function ($q) use ($participantType, $participantId) { $q->where('participantable_type', '!=', $participantType) ->orWhere('participantable_id', '!=', $participantId); }) ->whereNull('deleted_at') ->select('participantable_type', 'participantable_id') ->get() ->each(fn($p) => $keys->push($p->participantable_type . '_' . $p->participantable_id)); } return $keys->unique()->values()->toArray(); } private function buildProfileSubquery(string $reactantType): string { $modelName = class_basename($reactantType); $table = strtolower($modelName) . 's'; return "SELECT id, love_reactant_id FROM {$table}"; } private function resolveModelClassFromReactantType(string $reactantType): ?string { $modelName = class_basename($reactantType); if (!in_array($modelName, ['User', 'Organization', 'Bank'])) { return null; } return "App\\Models\\{$modelName}"; } public function render() { return view('livewire.online-reacted-profiles'); } }