From d25dd7b9c3319b0fbc6d469facf91d81447d0acc Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 18 Dec 2025 14:18:12 +0700 Subject: [PATCH] feat: Allow vouchers to be assigned to specific loyalty tiers. --- .../Datatable/Loyalty/VouchersTable.php | 29 ++++++++----- .../Forms/Studio/Loyalty/VoucherForm.php | 12 +++++- .../Studio/Loyalty/Voucher/Create.php | 4 ++ app/Livewire/Studio/Loyalty/Voucher/Edit.php | 4 ++ app/Models/Tier.php | 6 +++ app/Models/TierVoucher.php | 12 ++++++ app/Models/Voucher.php | 14 +++++++ ...12_18_134500_create_tier_voucher_table.php | 30 +++++++++++++ database/seeders/VoucherSeeder.php | 17 +++++++- .../views/livewire/home/vouchers.blade.php | 8 +++- .../views/livewire/member/voucher.blade.php | 2 +- .../studio/loyalty/voucher/form.blade.php | 42 ++++++++++++------- 12 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 app/Models/TierVoucher.php create mode 100644 database/migrations/2025_12_18_134500_create_tier_voucher_table.php diff --git a/app/Livewire/Datatable/Loyalty/VouchersTable.php b/app/Livewire/Datatable/Loyalty/VouchersTable.php index bdd215a..006012d 100644 --- a/app/Livewire/Datatable/Loyalty/VouchersTable.php +++ b/app/Livewire/Datatable/Loyalty/VouchersTable.php @@ -41,43 +41,50 @@ public function columns(): array ->sortable(), Column::make('Min. Belanja', 'min_purchase') - ->format(fn ($value) => formatCurrencyNumber($value, 'Rp')) + ->format(fn($value) => formatCurrencyNumber($value, 'Rp')) ->searchable() ->sortable(), + ArrayColumn::make('Tier') + ->data( + fn($value, $row) => $row->tiers->pluck('name')->toArray() ?: ['Semua Tier'] + ) + ->outputFormat(fn($index, $value) => Blade::render('' . $value . '')) + ->flexRow(['class' => 'gap-2 flex-wrap']), + Column::make('Jumlah Diskon', 'discount_amount') - ->label(fn ($row, $column) => formatDiscount($row->discount_amount, $row->type)) + ->label(fn($row, $column) => formatDiscount($row->discount_amount, $row->type)) ->searchable() ->sortable(), Column::make('Maks. Diskon') - ->label(fn ($row, $column) => formatCurrencyNumber($row->max_discount, 'Rp')) + ->label(fn($row, $column) => formatCurrencyNumber($row->max_discount, 'Rp')) ->searchable() ->sortable(), Column::make('Kuota', 'quota') - ->format(fn ($value) => $value ? formatCurrencyNumber($value) : 'Tidak Terbatas') + ->format(fn($value) => $value ? formatCurrencyNumber($value) : 'Tidak Terbatas') ->searchable() ->sortable(), Column::make('Sisa Voucher', 'available_count') - ->format(fn ($value) => $value ? formatCurrencyNumber($value) : 'Tidak Terbatas') + ->format(fn($value) => $value ? formatCurrencyNumber($value) : 'Tidak Terbatas') ->searchable() ->sortable(), Column::make('Batas Per Pembali', 'limit_per_user') - ->format(fn ($value) => $value ? $value : 'Tidak Terbatas') + ->format(fn($value) => $value ? $value : 'Tidak Terbatas') ->searchable() ->sortable(), ArrayColumn::make('Outlet') ->data( - fn ($value, $row) => $row->outlets->map(fn ($outlet) => [ + fn($value, $row) => $row->outlets->map(fn($outlet) => [ 'name' => $outlet->name, 'stock' => formatCurrencyNumber($outlet->pivot->stock, ''), ])->toArray() ) - ->outputFormat(fn ($index, $value) => Blade::render(' + ->outputFormat(fn($index, $value) => Blade::render('
{{ $value["name"] }}
', ['value' => $value])) @@ -134,11 +141,11 @@ public function columns(): array public function builder(): Builder { - return Voucher::select('id', 'name', 'code', 'tags', 'min_purchase', 'discount_amount', 'max_discount', 'quota', 'available_count', 'limit_per_user', 'start_date', 'end_date', 'type', 'created_at') - ->with('outlets') + return Voucher::select('vouchers.id', 'vouchers.name', 'code', 'tags', 'min_purchase', 'discount_amount', 'max_discount', 'quota', 'available_count', 'limit_per_user', 'start_date', 'end_date', 'type', 'vouchers.created_at') + ->with(['outlets', 'tiers']) ->whereHas( 'outlets', - fn ($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id')) + fn($q) => $q->whereIn('outlets.id', auth()->user()->outlets()->pluck('outlets.id')) ); } } diff --git a/app/Livewire/Forms/Studio/Loyalty/VoucherForm.php b/app/Livewire/Forms/Studio/Loyalty/VoucherForm.php index 1f2d9bc..a5c71f4 100644 --- a/app/Livewire/Forms/Studio/Loyalty/VoucherForm.php +++ b/app/Livewire/Forms/Studio/Loyalty/VoucherForm.php @@ -37,6 +37,8 @@ class VoucherForm extends Form public ?string $end_date = null; + public array $tier_ids = []; + public array $outlet_ids = []; public function rules(): array @@ -73,6 +75,8 @@ public function rules(): array 'summary' => ['required', 'string', 'max:200'], 'start_date' => ['required', 'date', 'after:yesterday'], 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'], + 'tier_ids' => ['nullable', 'array'], + 'tier_ids.*' => Rule::exists('tiers', 'id'), 'outlet_ids' => ['required', 'array', 'min:1'], 'outlet_ids.*' => Rule::exists('outlets', 'id'), ]; @@ -92,6 +96,8 @@ public function validationAttributes(): array 'discount_amount' => 'Jumlah Diskon', 'start_date' => 'tanggal mulai', 'end_date' => 'tanggal selesai', + 'tier_ids' => 'tier', + 'tier_ids.*' => 'tier', 'outlet_ids' => 'outlet', 'outlet_ids.*' => 'outlet', ]; @@ -99,7 +105,7 @@ public function validationAttributes(): array public function setVoucher(Voucher $voucher): void { - $voucher->load('outlets'); + $voucher->load(['outlets', 'tiers']); $this->voucher = $voucher; @@ -115,6 +121,8 @@ public function setVoucher(Voucher $voucher): void $this->summary = $this->voucher->summary; $this->start_date = $this->voucher->start_date; $this->end_date = $this->voucher->end_date; + + $this->tier_ids = $this->voucher->tiers->pluck('id')->toArray(); $this->outlet_ids = $this->voucher->outlets->pluck('id')->toArray(); } @@ -135,6 +143,7 @@ public function store(): void $voucher = Voucher::create($data); $voucher->outlets()->attach($this->outlet_ids); + $voucher->tiers()->attach($this->tier_ids); }); } @@ -149,6 +158,7 @@ public function update(): void $this->voucher->update($data); $this->voucher->outlets()->sync($this->outlet_ids); + $this->voucher->tiers()->sync($this->tier_ids); }); } diff --git a/app/Livewire/Studio/Loyalty/Voucher/Create.php b/app/Livewire/Studio/Loyalty/Voucher/Create.php index 35a9075..41a64a2 100644 --- a/app/Livewire/Studio/Loyalty/Voucher/Create.php +++ b/app/Livewire/Studio/Loyalty/Voucher/Create.php @@ -6,6 +6,7 @@ use App\Livewire\Forms\Studio\Loyalty\VoucherForm; use App\Models\Outlet; use App\Models\PushNotification; +use App\Models\Tier; use App\Traits\Authorization\WithAuthorization; use App\Traits\Components\WithOutletSelector; use App\Traits\Components\WithToast; @@ -23,9 +24,12 @@ class Create extends Component public array $outlets; + public array $tiers; + public function mount(): void { $this->outlets = Outlet::pluck('name', 'id')->toArray(); + $this->tiers = Tier::pluck('name', 'id')->toArray(); } public function save(): void diff --git a/app/Livewire/Studio/Loyalty/Voucher/Edit.php b/app/Livewire/Studio/Loyalty/Voucher/Edit.php index d5839bb..90b48d5 100644 --- a/app/Livewire/Studio/Loyalty/Voucher/Edit.php +++ b/app/Livewire/Studio/Loyalty/Voucher/Edit.php @@ -4,6 +4,7 @@ use App\Livewire\Forms\Studio\Loyalty\VoucherForm; use App\Models\Outlet; +use App\Models\Tier; use App\Models\Voucher; use App\Traits\Authorization\WithAuthorization; use App\Traits\Components\WithOutletSelector; @@ -22,11 +23,14 @@ class Edit extends Component public array $outlets; + public array $tiers; + public function mount(Voucher $voucher): void { $this->form->setVoucher($voucher); $this->outlets = Outlet::pluck('name', 'id')->toArray(); + $this->tiers = Tier::pluck('name', 'id')->toArray(); } public function save(): void diff --git a/app/Models/Tier.php b/app/Models/Tier.php index c9b4973..a1ab30c 100644 --- a/app/Models/Tier.php +++ b/app/Models/Tier.php @@ -5,6 +5,7 @@ use Dyrynda\Database\Support\CascadeSoftDeletes; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Veelasky\LaravelHashId\Eloquent\HashableId; @@ -48,6 +49,11 @@ public function rewards(): HasMany return $this->hasMany(TierReward::class); } + public function vouchers(): BelongsToMany + { + return $this->belongsToMany(Voucher::class); + } + public static function getHighestTier() { return self::orderByDesc('min_points')->first(); diff --git a/app/Models/TierVoucher.php b/app/Models/TierVoucher.php new file mode 100644 index 0000000..7574904 --- /dev/null +++ b/app/Models/TierVoucher.php @@ -0,0 +1,12 @@ +whereDate('start_date', '>', now()); } + #[Scope] + public function forTier(Builder $query, ?int $tierId): void + { + $query->where(function ($q) use ($tierId) { + $q->whereDoesntHave('tiers') + ->when($tierId, fn ($query) => $query->orWhereHas('tiers', fn ($q) => $q->where('tiers.id', $tierId))); + }); + } + public function outlets(): BelongsToMany { return $this->belongsToMany(Outlet::class); @@ -63,6 +72,11 @@ public function orders(): HasMany return $this->hasMany(Order::class); } + public function tiers(): BelongsToMany + { + return $this->belongsToMany(Tier::class); + } + public function users(): BelongsToMany { return $this->belongsToMany(User::class); diff --git a/database/migrations/2025_12_18_134500_create_tier_voucher_table.php b/database/migrations/2025_12_18_134500_create_tier_voucher_table.php new file mode 100644 index 0000000..de6fb7f --- /dev/null +++ b/database/migrations/2025_12_18_134500_create_tier_voucher_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('tier_id')->constrained()->cascadeOnDelete(); + $table->foreignId('voucher_id')->constrained()->cascadeOnDelete(); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tier_voucher'); + } +}; diff --git a/database/seeders/VoucherSeeder.php b/database/seeders/VoucherSeeder.php index 9427ce3..5887130 100644 --- a/database/seeders/VoucherSeeder.php +++ b/database/seeders/VoucherSeeder.php @@ -4,6 +4,7 @@ use App\Enums\VoucherType; use App\Models\Outlet; +use App\Models\Tier; use App\Models\Voucher; use Illuminate\Database\Seeder; @@ -11,12 +12,16 @@ class VoucherSeeder extends Seeder { public function run(): void { + $tiers = Tier::all(); + $tierIds = $tiers->pluck('id')->toArray(); + $vouchers = [ [ 'name' => 'Diskon 10%', 'code' => 'DISC10', 'type' => VoucherType::PERCENTAGE, 'tags' => 'DISC 10%', + 'tier_ids' => [], // Voucher umum untuk semua tier 'summary' => 'Diskon 10% untuk pembelian minimal Rp50.000', 'discount_amount' => 10, 'min_purchase' => 50000, @@ -32,6 +37,7 @@ public function run(): void 'code' => 'DISC20', 'type' => VoucherType::PERCENTAGE, 'tags' => 'DISC 20%', + 'tier_ids' => isset($tierIds[0]) ? [$tierIds[0]] : [], // Voucher khusus tier pertama 'summary' => 'Diskon 20% untuk pembelian minimal Rp100.000', 'discount_amount' => 20, 'min_purchase' => 100000, @@ -44,9 +50,10 @@ public function run(): void ], [ 'name' => 'Potongan Rp25K', - 'code' => 'SAVE 25K', + 'code' => 'SAVE25K', 'type' => VoucherType::FIXED, 'tags' => 'SAVE 25K', + 'tier_ids' => [], // Voucher umum 'summary' => 'Potongan Rp25K untuk pembelian minimal Rp100.000', 'discount_amount' => 25000, 'min_purchase' => 100000, @@ -62,6 +69,7 @@ public function run(): void 'code' => 'SAVE50K', 'type' => VoucherType::FIXED, 'tags' => 'SAVE 50K', + 'tier_ids' => isset($tierIds[1]) ? [$tierIds[1]] : [], // Voucher khusus tier kedua 'summary' => 'Potongan Rp50K untuk pembelian minimal Rp200.000', 'discount_amount' => 50000, 'min_purchase' => 200000, @@ -77,6 +85,7 @@ public function run(): void 'code' => 'ONGKIR', 'type' => VoucherType::FIXED, 'tags' => 'GRATIS ONGKIR', + 'tier_ids' => [], // Voucher umum 'summary' => 'Gratis Ongkir untuk pembelian minimal Rp50.000', 'discount_amount' => 15000, 'min_purchase' => 50000, @@ -92,6 +101,7 @@ public function run(): void 'code' => 'FLASH30', 'type' => VoucherType::PERCENTAGE, 'tags' => 'FLASH 30%', + 'tier_ids' => isset($tierIds[2]) ? [$tierIds[2]] : [], // Voucher khusus tier ketiga 'summary' => 'Diskon 30% untuk pembelian minimal Rp75.000', 'discount_amount' => 30, 'min_purchase' => 75000, @@ -107,6 +117,7 @@ public function run(): void 'code' => 'MEMBER15', 'type' => VoucherType::PERCENTAGE, 'tags' => 'Serial Member', + 'tier_ids' => [], // Voucher umum untuk semua member 'summary' => 'Diskon 15% untuk pembelian minimal Rp50.000', 'discount_amount' => 15, 'min_purchase' => 50000, @@ -119,8 +130,12 @@ public function run(): void ]; foreach ($vouchers as $voucher) { + $tierIdsToAttach = $voucher['tier_ids']; + unset($voucher['tier_ids']); + $newVoucher = Voucher::create($voucher); + $newVoucher->tiers()->attach($tierIdsToAttach); $newVoucher->outlets()->attach(Outlet::inRandomOrder()->first()->id); } } diff --git a/resources/views/livewire/home/vouchers.blade.php b/resources/views/livewire/home/vouchers.blade.php index 2fee8fb..b8571ec 100644 --- a/resources/views/livewire/home/vouchers.blade.php +++ b/resources/views/livewire/home/vouchers.blade.php @@ -67,7 +67,13 @@ class="absolute top-0 right-0 w-24 h-24 bg-home-primary/10 rounded-full blur-2xl class="inline-flex items-center gap-2 bg-white px-3 py-1 rounded-full border border-home-primary/10 shadow-sm mb-4"> {{ $voucher->tags }} + class="text-xs font-bold text-home-primary uppercase tracking-wider">{{ $voucher->tags }} + @if ($voucher->tiers->isNotEmpty()) + + + Khusus {{ $voucher->tiers->take(2)->pluck('name')->join(', ') }}{{ $voucher->tiers->count() > 2 ? ' +'.($voucher->tiers->count() - 2) : '' }} + + @endif

- Min. Belanja {{ $voucher['min_purchase'] }} + Min. Belanja {{ $voucher['min_purchase'] }} • {{ $voucher['tier_name'] }}
- Min. Belanja - - Rp - - - - + Min. Belanja + + Rp + + + + - + - +
@@ -88,7 +88,7 @@ Jumlah Diskon * - + @foreach (\App\Enums\VoucherType::cases() as $item) {{ $item->label() }} @@ -142,6 +142,18 @@ + + + Tier + Jika tidak memilih, maka voucher berlaku tanpa membatasi tier. + + @foreach ($tiers as $key => $value) + {{ $value }} + @endforeach + + + +