'Voucher', ])] class Voucher extends Component { use WithPagination, WithToast; public $outlets; public string $search = ''; public ?string $outlet_id = null; public ?string $sort = null; public ?VoucherModel $selectedVoucher = null; public bool $showDetailModal = false; public function mount(): void { $this->outlets = Outlet::query()->operational()->pluck('name', 'id'); } public function updatedSearch(): void { $this->resetPage(); } public function updatedOutletId(): void { $this->resetPage(); } public function updatedSort(): void { $this->resetPage(); } public function showDetail(string $hashId): void { $this->selectedVoucher = VoucherModel::byHash($hashId); $this->selectedVoucher->load('outlets'); $this->selectedVoucher->min_purchase_formatted = formatCurrencyNumber($this->selectedVoucher->min_purchase, 'Rp'); $this->selectedVoucher->end_date_formatted = formatDateLocalized($this->selectedVoucher->end_date); $this->showDetailModal = true; } public function closeDetail(): void { $this->showDetailModal = false; $this->selectedVoucher = null; } public function claim(string $hashId): void { if (! auth()->check()) { $this->redirect(route('login'), navigate: true); return; } $voucher = VoucherModel::byHash($hashId); $user = auth()->user(); // Basic checks if (! $voucher) { $this->toast('Voucher tidak ditemukan.', 'Gagal', 'danger'); return; } if ($voucher->quota !== null && $voucher->available_count >= $voucher->quota) { $this->toast('Klaim gagal! Kuota voucher sudah habis.', 'Gagal', 'danger'); return; } // Check if user already reached limit $claimedCount = $user->vouchers()->where('voucher_id', $voucher->id)->exists(); if ($voucher->limit_per_user !== null) { $claimedCount = $user->vouchers()->where('voucher_id', $voucher->id)->count(); if ($claimedCount >= $voucher->limit_per_user) { $this->toast('Anda sudah mencapai batas klaim untuk voucher ini.', 'Peringatan', 'warning'); return; } } elseif ($claimedCount) { // If limit_per_user is null, assume it can only be claimed once? // Or can it be claimed multiple times? // Usually vouchers are one-time claim per user unless specified. // Based on common logic, if limit_per_user is null, maybe it's unlimited? // But usually it's 1. Let's stick to the limit_per_user check. } // Check if voucher is for specific tier $userTierId = $user->membership?->tier_id; if ($voucher->tiers()->exists() && ! $voucher->tiers()->where('tiers.id', $userTierId)->exists()) { $this->toast('Voucher ini tidak tersedia untuk tier Anda.', 'Gagal', 'danger'); return; } try { // Claim with transaction DB::transaction(function () use ($user, $voucher) { // Re-check quota inside transaction $v = VoucherModel::where('id', $voucher->id)->lockForUpdate()->first(); if ($v->quota !== null && $v->available_count >= $v->quota) { throw new \Exception('Kuota voucher sudah habis.'); } $user->vouchers()->attach($v->id); $v->increment('available_count'); }); $this->toast('Voucher berhasil diklaim! Silakan cek di menu Voucher Saya.', 'Berhasil', 'success'); } catch (\Throwable $e) { $this->toast($e->getMessage(), 'Gagal', 'danger'); } } public function render(): View { $vouchers = VoucherModel::where(function ($query) { $query->where('name', 'like', '%'.$this->search.'%') ->orWhere('code', 'like', '%'.$this->search.'%'); }) ->when($this->outlet_id, function ($query) { $query->whereHas('outlets', function ($q) { $q->where('outlets.id', $this->outlet_id); }); }) ->when($this->sort, function ($query) { match ($this->sort) { 'latest' => $query->orderBy('created_at', 'desc'), 'biggest' => $query->orderBy('discount_amount', 'desc'), 'ending-soon' => $query->orderByRaw('end_date IS NULL') ->orderBy('end_date', 'asc'), }; }) ->active() ->latest() ->paginate(3) ->through(function (VoucherModel $voucher) { $voucher->min_purchase_formatted = formatCurrencyNumber($voucher->min_purchase, 'Rp'); $voucher->end_date = formatDateLocalized($voucher->end_date); return $voucher; }); return view('livewire.home.vouchers', [ 'vouchers' => $vouchers, ]); } }