diff --git a/app/Livewire/Auth/Dashboard.php b/app/Livewire/Auth/Dashboard.php index 0ab86141a0b..88489d484c4 100644 --- a/app/Livewire/Auth/Dashboard.php +++ b/app/Livewire/Auth/Dashboard.php @@ -48,6 +48,8 @@ class Dashboard extends Component public bool $showClaimConfirm = false; + public array $selectedRewardIds = []; + /** * Sanitize showCreateForm input to prevent array injection attacks. */ @@ -590,6 +592,103 @@ public function claimReward(): void } } + /** + * Toggle select/deselect all rewards for a game account. + */ + public function toggleSelectAll(int $gameAccountId): void + { + $gameAccount = GameAccount::where('id', $gameAccountId) + ->where('user_id', auth()->id()) + ->first(); + + if (! $gameAccount) { + return; + } + + $isXileRetro = $gameAccount->server === GameAccount::SERVER_XILERETRO; + $rewardService = app(DonationRewardService::class); + $pendingRewards = $rewardService->getUserPendingRewards(auth()->user()) + ->filter(function ($reward) use ($isXileRetro) { + return $isXileRetro ? $reward->is_xileretro : $reward->is_xilero; + }); + + $allRewardIds = $pendingRewards->pluck('id')->toArray(); + + // If all are selected, deselect all. Otherwise, select all. + $allSelected = ! empty($allRewardIds) && count(array_intersect($this->selectedRewardIds, $allRewardIds)) === count($allRewardIds); + + if ($allSelected) { + // Deselect all + $this->selectedRewardIds = array_values(array_diff($this->selectedRewardIds, $allRewardIds)); + } else { + // Select all + $this->selectedRewardIds = array_values(array_unique(array_merge($this->selectedRewardIds, $allRewardIds))); + } + } + + /** + * Claim multiple selected rewards at once. + */ + public function claimSelectedRewards(int $gameAccountId): void + { + if (empty($this->selectedRewardIds)) { + session()->flash('error', 'Please select at least one reward to claim.'); + + return; + } + + $gameAccount = GameAccount::where('id', $gameAccountId) + ->where('user_id', auth()->id()) + ->first(); + + if (! $gameAccount) { + session()->flash('error', 'Game account not found.'); + + return; + } + + $rewards = DonationRewardClaim::with(['tier', 'item']) + ->whereIn('id', $this->selectedRewardIds) + ->where('user_id', auth()->id()) + ->get(); + + if ($rewards->isEmpty()) { + session()->flash('error', 'No valid rewards found.'); + $this->selectedRewardIds = []; + + return; + } + + $rewardService = app(DonationRewardService::class); + $successCount = 0; + $errors = []; + + foreach ($rewards as $reward) { + if (! $reward->canBeClaimedBy($gameAccount)) { + $errors[] = "{$reward->item->name} cannot be claimed on this account."; + + continue; + } + + try { + $rewardService->claimReward($reward, $gameAccount); + $successCount++; + } catch (Exception $e) { + $errors[] = "Failed to claim {$reward->item->name}: {$e->getMessage()}"; + } + } + + $this->selectedRewardIds = []; + + if ($successCount > 0) { + session()->flash('success', "Successfully claimed {$successCount} reward(s)! Items will be delivered to {$gameAccount->userid} on next login."); + } + + if (! empty($errors)) { + session()->flash('error', implode(' ', $errors)); + } + } + public function render() { $gameAccounts = auth()->user()->gameAccounts() diff --git a/app/Livewire/DonateShop.php b/app/Livewire/DonateShop.php index af6ced40538..facaf315c43 100644 --- a/app/Livewire/DonateShop.php +++ b/app/Livewire/DonateShop.php @@ -45,6 +45,8 @@ class DonateShop extends Component public bool $showPurchaseConfirm = false; + public int $purchaseQuantity = 1; + /** * Sanitize category input to prevent array injection attacks. */ @@ -232,12 +234,41 @@ public function selectItem(?int $itemId): void { $this->selectedItemId = $itemId; $this->showPurchaseConfirm = false; + $this->purchaseQuantity = 1; // Reset quantity when selecting new item if ($itemId) { UberShopItem::where('id', $itemId)->increment('views'); } } + /** + * Increment purchase quantity. + */ + public function incrementQuantity(): void + { + $item = $this->selectedItem(); + if (! $item) { + return; + } + + // If item has stock limit, don't exceed it + if ($item->stock !== null && $this->purchaseQuantity >= $item->stock) { + return; + } + + $this->purchaseQuantity++; + } + + /** + * Decrement purchase quantity. + */ + public function decrementQuantity(): void + { + if ($this->purchaseQuantity > 1) { + $this->purchaseQuantity--; + } + } + /** * Validate game account selection when updated via wire:model. */ @@ -629,15 +660,18 @@ public function purchase(): void $user->refresh(); $currentBalance = $user->uber_balance; + // Calculate total cost based on quantity + $totalCost = $item->uber_cost * $this->purchaseQuantity; + // Validate sufficient balance - if ($currentBalance < $item->uber_cost) { - session()->flash('error', 'Insufficient Ubers. You need '.($item->uber_cost - $currentBalance).' more.'); + if ($currentBalance < $totalCost) { + session()->flash('error', 'Insufficient Ubers. You need '.($totalCost - $currentBalance).' more.'); return; } try { - DB::transaction(function () use ($user, $gameAccount, $item, $currentBalance) { + DB::transaction(function () use ($user, $gameAccount, $item, $currentBalance, $totalCost) { // Lock the item row to prevent race conditions with stock $lockedItem = UberShopItem::with('item')->where('id', $item->id)->lockForUpdate()->first(); @@ -646,8 +680,13 @@ public function purchase(): void throw new Exception('Item is no longer available.'); } + // Re-check stock for the requested quantity + if ($lockedItem->stock !== null && $lockedItem->stock < $this->purchaseQuantity) { + throw new Exception('Insufficient stock. Only '.$lockedItem->stock.' available.'); + } + // Calculate new balance - $newBalance = $currentBalance - $lockedItem->uber_cost; + $newBalance = $currentBalance - $totalCost; // Deduct ubers from user's balance $user->uber_balance = $newBalance; @@ -655,38 +694,44 @@ public function purchase(): void // Decrement stock if item has limited stock if ($lockedItem->stock !== null) { - $lockedItem->decrement('stock'); + $lockedItem->decrement('stock', $this->purchaseQuantity); } - // Create purchase record - $purchase = UberShopPurchase::create([ - 'account_id' => $gameAccount->ragnarok_account_id, - 'account_name' => $gameAccount->userid, - 'shop_item_id' => $lockedItem->id, - 'item_id' => $lockedItem->item->item_id, - 'item_name' => $lockedItem->item->name, - 'refine_level' => $lockedItem->refine_level, - 'quantity' => $lockedItem->quantity, - 'uber_cost' => $lockedItem->uber_cost, - 'uber_balance_after' => $newBalance, - 'status' => UberShopPurchase::STATUS_PENDING, - 'is_xileretro' => $gameAccount->server === 'xileretro', - 'purchased_at' => now(), - ]); - - // Send purchase confirmation email - $user->notify(new UberShopPurchaseNotification( - $purchase, - $gameAccount->userid, - $gameAccount->serverName() - )); + // Create purchase records (one for each quantity) + for ($i = 0; $i < $this->purchaseQuantity; $i++) { + $purchase = UberShopPurchase::create([ + 'account_id' => $gameAccount->ragnarok_account_id, + 'account_name' => $gameAccount->userid, + 'shop_item_id' => $lockedItem->id, + 'item_id' => $lockedItem->item->item_id, + 'item_name' => $lockedItem->item->name, + 'refine_level' => $lockedItem->refine_level, + 'quantity' => $lockedItem->quantity, + 'uber_cost' => $lockedItem->uber_cost, + 'uber_balance_after' => $newBalance, + 'status' => UberShopPurchase::STATUS_PENDING, + 'is_xileretro' => $gameAccount->server === 'xileretro', + 'purchased_at' => now(), + ]); + + // Send purchase confirmation email for first purchase only + if ($i === 0) { + $user->notify(new UberShopPurchaseNotification( + $purchase, + $gameAccount->userid, + $gameAccount->serverName() + )); + } + } }); - session()->flash('success', "Successfully purchased {$item->display_name} for {$item->uber_cost} Ubers! The item will be delivered to {$gameAccount->userid} on next login."); + $quantityText = $this->purchaseQuantity > 1 ? " (x{$this->purchaseQuantity})" : ''; + session()->flash('success', "Successfully purchased {$item->display_name}{$quantityText} for {$totalCost} Ubers! The item will be delivered to {$gameAccount->userid} on next login."); // Reset state $this->selectedItemId = null; $this->showPurchaseConfirm = false; + $this->purchaseQuantity = 1; } catch (Exception $e) { session()->flash('error', 'Purchase failed: '.$e->getMessage()); diff --git a/resources/views/livewire/auth/partials/game-account-card.blade.php b/resources/views/livewire/auth/partials/game-account-card.blade.php index b146a4fb280..57172363bfb 100644 --- a/resources/views/livewire/auth/partials/game-account-card.blade.php +++ b/resources/views/livewire/auth/partials/game-account-card.blade.php @@ -115,18 +115,58 @@ class="px-3 py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-l {{-- Pending Rewards --}} @if($hasPendingRewards)
-

Pending Rewards

+
+

Pending Rewards

+
+ {{-- Select/Deselect All Button --}} + + + {{-- Claim Selected Button --}} + @if($this->selectedRewardIds && count($this->selectedRewardIds) > 0) + + @endif +
+
@foreach($pendingRewards as $reward) -
-
-
+
+ @endforeach
diff --git a/resources/views/livewire/donate-shop.blade.php b/resources/views/livewire/donate-shop.blade.php index 492fc8048e3..83756e02e8a 100644 --- a/resources/views/livewire/donate-shop.blade.php +++ b/resources/views/livewire/donate-shop.blade.php @@ -540,6 +540,61 @@ class="max-h-full max-w-full object-contain"

@endif + {{-- Quantity Selector --}} + @if ($selectedItem->is_available && $canPurchase) +
+ +
+ + +
+ + @if ($selectedItem->stock !== null) + + / {{ $selectedItem->stock }} + + @endif +
+ + +
+ + {{-- Total Cost Display --}} + @if ($purchaseQuantity > 1) +
+
+ + {{ $selectedItem->uber_cost }} Ubers × {{ $purchaseQuantity }} + + + = {{ $selectedItem->uber_cost * $purchaseQuantity }} Ubers + +
+
+ @endif +
+ @endif + @if ($isPurchasingRestricted && ! $canPurchase)

@@ -553,10 +608,10 @@ class="max-h-full max-w-full object-contain" - @elseif ($userBalance < $selectedItem->uber_cost) + @elseif ($userBalance < ($selectedItem->uber_cost * $purchaseQuantity))

- You need {{ $selectedItem->uber_cost - $userBalance }} more Ubers. + You need {{ ($selectedItem->uber_cost * $purchaseQuantity) - $userBalance }} more Ubers.

@@ -591,10 +647,10 @@ class="flex-1 px-4 py-2.5 bg-green-600 hover:bg-green-500 disabled:bg-gray-600 t wire:click="confirmPurchase" class="w-full px-4 py-2.5 bg-amber-500 hover:bg-amber-400 text-gray-900 font-bold rounded-lg transition-colors" > - Purchase for {{ $selectedItem->uber_cost }} {{ Str::plural('Uber', $selectedItem->uber_cost) }} + Purchase for {{ $selectedItem->uber_cost * $purchaseQuantity }} {{ Str::plural('Uber', $selectedItem->uber_cost * $purchaseQuantity) }}

- Balance after: {{ number_format($userBalance - $selectedItem->uber_cost) }} Ubers + Balance after: {{ number_format($userBalance - ($selectedItem->uber_cost * $purchaseQuantity)) }} Ubers

@endif @else