Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,372 @@
<?php
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Lang;
use Livewire\Component;
use Namu\WireChat\Events\NotifyParticipant;
use WireUi\Traits\WireUiActions;
class ReserveButton extends Component
{
use WireUiActions;
public $post;
public $activeProfile;
public $hasReserved = false;
public $reservationCount = 0;
public $canReserve = false;
public $disabledReason = null;
public $showConfirmModal = false;
public $showCancelModal = false;
public $showReservationsModal = false;
public $reservationsByType = [];
public $isOrganizer = false;
public $messageToReserved = '';
public $isGuest = false;
public function mount($post)
{
$this->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');
}
}