post = $post; $this->activeProfile = getActiveProfile(); $this->isGuest = !$this->activeProfile; // Ensure meeting is loaded if (!$this->post->relationLoaded('meeting')) { $this->post->load('meeting'); } // Get reservation count if ($this->post && method_exists($this->post, 'loveReactant')) { $reactant = $this->post->getLoveReactant(); if ($reactant) { $reactionType = \Cog\Laravel\Love\ReactionType\Models\ReactionType::fromName('Reserve'); if ($reactionType) { $this->reservationCount = $reactant->getReactionCounterOfType($reactionType)?->count ?? 0; } } } $this->checkReservationPermissions(); $this->checkIfReserved(); $this->loadReservations(); } private function loadReservations() { // Only load if current profile is the organizer of the meeting if (!$this->post || !isset($this->post->meeting)) { return; } $this->isOrganizer = session('activeProfileType') === $this->post->meeting->meetingable_type && session('activeProfileId') === $this->post->meeting->meetingable_id; if (!$this->isOrganizer) { return; } // Get all reacters for this post with Reserve reaction $reactionType = \Cog\Laravel\Love\ReactionType\Models\ReactionType::fromName('Reserve'); if (!$reactionType) { return; } $reactions = \Cog\Laravel\Love\Reaction\Models\Reaction::where('reactant_id', $this->post->love_reactant_id) ->where('reaction_type_id', $reactionType->getId()) ->get(); $groupedReactions = [ 'App\\Models\\User' => [], 'App\\Models\\Organization' => [], 'App\\Models\\Bank' => [], 'App\\Models\\Admin' => [], ]; foreach ($reactions as $reaction) { $reacter = $reaction->reacter; if ($reacter) { $reacterable = $reacter->reacterable; if ($reacterable) { $type = get_class($reacterable); if (isset($groupedReactions[$type])) { $groupedReactions[$type][] = $reacterable; } } } } $this->reservationsByType = $groupedReactions; } private function checkReservationPermissions() { // Check if reserve reaction is enabled in config if (!timebank_config('reactions.reserve.enabled', false)) { $this->canReserve = false; $this->disabledReason = 'reaction_disabled'; return; } // Admins cannot reserve if ($this->activeProfile instanceof \App\Models\Admin) { $this->canReserve = false; $this->disabledReason = 'admin_cannot_reserve'; return; } // Profile must be registered as love reacter if ( !$this->activeProfile || !method_exists($this->activeProfile, 'isRegisteredAsLoveReacter') || !$this->activeProfile->isRegisteredAsLoveReacter() ) { $this->canReserve = false; $this->disabledReason = 'not_registered_reacter'; return; } // Check if post has a meeting if (!$this->post || !isset($this->post->meeting)) { $this->canReserve = false; $this->disabledReason = 'no_meeting'; return; } $this->canReserve = true; $this->disabledReason = null; } private function checkIfReserved() { if (!$this->canReserve || !$this->activeProfile->isRegisteredAsLoveReacter()) { $this->hasReserved = false; return; } $this->hasReserved = $this->activeProfile ->viaLoveReacter() ->hasReactedTo($this->post, 'Reserve'); } public function redirectToLogin() { // Get the referrer URL (the page the user is viewing) $intendedUrl = request()->header('Referer'); // Store as intended URL if ($intendedUrl) { session(['url.intended' => $intendedUrl]); } return redirect()->route('login'); } public function openConfirmModal() { $this->showConfirmModal = true; } public function openCancelModal() { $this->showCancelModal = true; } public function openReservationsModal() { // Security check: only organizer can view if (!$this->isOrganizer) { return; } $this->showReservationsModal = true; } public function confirmReservation() { $this->showConfirmModal = false; if (!$this->canReserveSecurityCheck()) { return; } if ($this->hasReserved) { return; } try { $this->activeProfile->viaLoveReacter()->reactTo($this->post, 'Reserve'); // Update state $this->hasReserved = true; // Refresh count from database $reactionType = \Cog\Laravel\Love\ReactionType\Models\ReactionType::fromName('Reserve'); if ($reactionType) { $this->reservationCount = $this->post->getLoveReactant()->getReactionCounterOfType($reactionType)?->count ?? 0; } \Log::info('Reservation added. hasReserved: ' . ($this->hasReserved ? 'true' : 'false') . ', count: ' . $this->reservationCount); // Reload reservations list $this->loadReservations(); } catch (\Exception $e) { \Log::error('Failed to add reservation: ' . $e->getMessage()); session()->flash('error', __('Failed to add reservation. Please try again.')); } } public function confirmCancellation() { $this->showCancelModal = false; if (!$this->canReserveSecurityCheck()) { return; } if (!$this->hasReserved) { return; } try { $this->activeProfile->viaLoveReacter()->unReactTo($this->post, 'Reserve'); $this->reservationCount = max(0, $this->reservationCount - 1); $this->hasReserved = false; // Reload reservations list $this->loadReservations(); } catch (\Exception $e) { \Log::error('Failed to remove reservation: ' . $e->getMessage()); session()->flash('error', __('Failed to cancel reservation. Please try again.')); } } private function canReserveSecurityCheck() { if (!$this->post || !isset($this->post->meeting)) { return false; } if (!timebank_config('reactions.reserve.enabled', false)) { return false; } if ($this->activeProfile instanceof \App\Models\Admin) { return false; } if ( !method_exists($this->activeProfile, 'isRegisteredAsLoveReacter') || !$this->activeProfile->isRegisteredAsLoveReacter() ) { return false; } return true; } public function sendMessageToReserved() { // Security check: only organizer can send messages if (!$this->isOrganizer) { return; } // Validate the message $this->validate([ 'messageToReserved' => 'required|string|max:300', ]); // Strip HTML tags for security $cleanMessage = strip_tags($this->messageToReserved); // Get the send delay configuration (same as bulk mail) $sendDelay = timebank_config('bulk_mail.send_delay_seconds', 2); // Get all reserved participants $allReserved = []; foreach ($this->reservationsByType as $type => $reacters) { $allReserved = array_merge($allReserved, $reacters); } // Get the organizer profile $organizer = getActiveProfile(); // Create group chat with all participants // Get organizer's locale for group name $groupLocale = $organizer->lang_preference ?? config('app.fallback_locale'); // Get post title for group name with fallback logic $groupPostTranslation = $this->post->translations->where('locale', $groupLocale)->first(); if (!$groupPostTranslation) { $fallbackLocale = config('app.fallback_locale'); $groupPostTranslation = $this->post->translations->where('locale', $fallbackLocale)->first(); } if (!$groupPostTranslation) { $groupPostTranslation = $this->post->translations->first(); } $groupName = $groupPostTranslation ? $groupPostTranslation->title : 'Event ' . $this->post->id; // Create group conversation $groupConversation = $organizer->createGroup( name: $groupName, description: trans('messages.Reservation_update', [], $groupLocale) ); // Add all participants to the group (skip organizer as they're already added) foreach ($allReserved as $reacter) { // Skip if this is the organizer (already added when creating group) if (get_class($reacter) === get_class($organizer) && $reacter->id === $organizer->id) { continue; } $groupConversation->addParticipant($reacter); } // Construct the chat message using organizer's locale $chatMessage = $groupName . ' ' . __('update', [], $groupLocale) . ':' . PHP_EOL . PHP_EOL . $cleanMessage; // Send message to the group $message = $organizer->sendMessageTo($groupConversation, $chatMessage); // Broadcast to all participants for real-time notifications foreach ($allReserved as $reacter) { broadcast(new NotifyParticipant($reacter, $message)); } // Dispatch email jobs with delays $delay = 0; foreach ($allReserved as $reacter) { \App\Jobs\SendReservationUpdateMail::dispatch($reacter, $this->post, $cleanMessage, $organizer) ->delay(now()->addSeconds($delay)) ->onQueue('emails'); $delay += $sendDelay; } // Also send a copy to the organizer \App\Jobs\SendReservationUpdateMail::dispatch($organizer, $this->post, $cleanMessage, $organizer) ->delay(now()->addSeconds($delay)) ->onQueue('emails'); \Log::info('Reservation update messages dispatched', [ 'post_id' => $this->post->id, 'organizer_id' => $organizer->id, 'recipients_count' => count($allReserved) + 1, // +1 for organizer 'group_conversation_id' => $groupConversation->id ]); // Clear the message and show success $this->messageToReserved = ''; $this->showReservationsModal = false; $this->notification()->success( __('Reservation update sent!'), __('All participants have been notified by email and chat message.') ); } public function render() { return view('livewire.reserve-button'); } }