feat: Allow vouchers to be assigned to specific loyalty tiers.
This commit is contained in:
parent
cc735e461d
commit
d25dd7b9c3
@ -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('<span class="text-xs text-gray-400 border border-gray-400 rounded px-1">' . $value . '</span>'))
|
||||
->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('
|
||||
<div class="flex items-center space-x-2 bg-gray-50 border border-gray-200 rounded-lg px-2 py-1 text-xs">
|
||||
<span class="font-medium text-gray-700">{{ $value["name"] }}</span>
|
||||
</div>', ['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'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
12
app/Models/TierVoucher.php
Normal file
12
app/Models/TierVoucher.php
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TierVoucher extends Model
|
||||
{
|
||||
protected $table = 'tier_voucher';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@ -53,6 +53,15 @@ public function upcoming(Builder $query): void
|
||||
$query->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);
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tier_voucher', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
<span class="w-2 h-2 rounded-full bg-home-primary"></span>
|
||||
<span
|
||||
class="text-xs font-bold text-home-primary uppercase tracking-wider">{{ $voucher->tags }}</span>
|
||||
class="text-xs font-bold text-home-primary uppercase tracking-wider">{{ $voucher->tags }}</span>
|
||||
@if ($voucher->tiers->isNotEmpty())
|
||||
<span class="w-1 h-1 rounded-full bg-home-primary/30"></span>
|
||||
<span class="text-[10px] font-bold text-home-primary/60 uppercase tracking-wider">
|
||||
Khusus {{ $voucher->tiers->take(2)->pluck('name')->join(', ') }}{{ $voucher->tiers->count() > 2 ? ' +'.($voucher->tiers->count() - 2) : '' }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h3
|
||||
|
||||
@ -30,7 +30,7 @@ class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap
|
||||
</flux:heading>
|
||||
|
||||
<flux:text class="mb-1">
|
||||
Min. Belanja {{ $voucher['min_purchase'] }}
|
||||
Min. Belanja {{ $voucher['min_purchase'] }} • {{ $voucher['tier_name'] }}
|
||||
</flux:text>
|
||||
|
||||
<div x-data="{ textToCopy: '{{ $voucher['code'] }}', copied: false }"
|
||||
|
||||
@ -41,22 +41,22 @@
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Min. Belanja</flux:label>
|
||||
<flux:input.group>
|
||||
<flux:input.group.prefix>Rp</flux:input.group.prefix>
|
||||
<flux:input placeholder="10.000"
|
||||
x-mask:dynamic="$money($input, ',')"
|
||||
wire:model="form.min_purchase" autocomplete="off" />
|
||||
</flux:input.group>
|
||||
<flux:error name="form.min_purchase" />
|
||||
</flux:field>
|
||||
<flux:label>Min. Belanja</flux:label>
|
||||
<flux:input.group>
|
||||
<flux:input.group.prefix>Rp</flux:input.group.prefix>
|
||||
<flux:input placeholder="10.000"
|
||||
x-mask:dynamic="$money($input, ',')"
|
||||
wire:model="form.min_purchase" autocomplete="off" />
|
||||
</flux:input.group>
|
||||
<flux:error name="form.min_purchase" />
|
||||
</flux:field>
|
||||
|
||||
<flux:input label="Kuota" placeholder="100" wire:model="form.quota"
|
||||
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
|
||||
<flux:input label="Kuota" placeholder="100" wire:model="form.quota"
|
||||
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
|
||||
|
||||
<flux:input label="Batas Per Customer" placeholder="1"
|
||||
wire:model="form.limit_per_user" x-mask:dynamic="$money($input, ',')"
|
||||
autocomplete="off" />
|
||||
<flux:input label="Batas Per Customer" placeholder="1"
|
||||
wire:model="form.limit_per_user" x-mask:dynamic="$money($input, ',')"
|
||||
autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
<flux:card class="space-y-2 p-6">
|
||||
<flux:field>
|
||||
<flux:label>Jumlah Diskon <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:radio.group wire:model="form.type" variant="buttons" class="w-full *:flex-1">
|
||||
<flux:radio.group wire:model.live="form.type" variant="buttons" class="w-full *:flex-1">
|
||||
@foreach (\App\Enums\VoucherType::cases() as $item)
|
||||
<flux:radio value="{{ $item->value }}">
|
||||
{{ $item->label() }}
|
||||
@ -142,6 +142,18 @@
|
||||
</flux:select>
|
||||
<flux:error name="form.outlet_ids" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Tier</flux:label>
|
||||
<flux:description>Jika tidak memilih, maka voucher berlaku tanpa membatasi tier.</flux:description>
|
||||
<flux:select placeholder="Pilih Tier" wire:model="form.tier_ids" multiple searchable variant="listbox">
|
||||
@foreach ($tiers as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="form.tier_ids" />
|
||||
</flux:field>
|
||||
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user