From e07a2386cfe6777ab559bbe35ee48972589eb789 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 18 Dec 2025 20:16:37 +0700 Subject: [PATCH] feat: Implement loyalty reward management with dedicated routes, model, UI, and permissions. --- .../Datatable/Loyalty/RewardsTable.php | 65 +++++++++++++ .../Forms/Studio/Loyalty/RewardForm.php | 91 +++++++++++++++++++ app/Livewire/Studio/Loyalty/Reward.php | 87 ++++++++++++++++++ app/Models/Reward.php | 25 +++++ app/Providers/ViewServiceProvider.php | 7 ++ ...2025_12_18_193031_create_rewards_table.php | 32 +++++++ database/seeders/RolePermissionSeeder.php | 14 +++ .../livewire/studio/loyalty/rewards.blade.php | 90 ++++++++++++++++++ routes/pages/studio.php | 5 + 9 files changed, 416 insertions(+) create mode 100644 app/Livewire/Datatable/Loyalty/RewardsTable.php create mode 100644 app/Livewire/Forms/Studio/Loyalty/RewardForm.php create mode 100644 app/Livewire/Studio/Loyalty/Reward.php create mode 100644 app/Models/Reward.php create mode 100644 database/migrations/2025_12_18_193031_create_rewards_table.php create mode 100644 resources/views/livewire/studio/loyalty/rewards.blade.php diff --git a/app/Livewire/Datatable/Loyalty/RewardsTable.php b/app/Livewire/Datatable/Loyalty/RewardsTable.php new file mode 100644 index 0000000..eac28d6 --- /dev/null +++ b/app/Livewire/Datatable/Loyalty/RewardsTable.php @@ -0,0 +1,65 @@ +searchable() + ->sortable(), + + Column::make('Poin', 'points') + ->format(fn ($value) => formatCurrencyNumber($value)) + ->searchable() + ->sortable(), + + Column::make('Stok', 'stock') + ->format(fn ($value) => $value === null ? 'Tidak Terbatas' : formatCurrencyNumber($value)) + ->searchable() + ->sortable(), + + Column::make('Aksi') + ->label(function ($row) { + $actions = ''; + + if (auth()->user()->can('update reward')) { + $actions .= view('components.actions.table.edit-modal', [ + 'id' => $row->hash, + 'method' => 'update', + 'modalTitle' => 'Ubah Hadiah', + ])->render(); + } + + if (auth()->user()->can('delete reward')) { + $actions .= view('components.actions.table.delete', [ + 'id' => $row->hash, + ])->render(); + } + + return $actions; + }) + ->html() + ->hideIf(auth()->user()->cannot('update reward') && auth()->user()->cannot('delete reward')), + ]; + } + + public function builder(): Builder + { + return Reward::select('id', 'name', 'points', 'stock'); + } +} diff --git a/app/Livewire/Forms/Studio/Loyalty/RewardForm.php b/app/Livewire/Forms/Studio/Loyalty/RewardForm.php new file mode 100644 index 0000000..6fe759b --- /dev/null +++ b/app/Livewire/Forms/Studio/Loyalty/RewardForm.php @@ -0,0 +1,91 @@ + ['required', 'string', 'max:100'], + 'points' => ['required', new UnsignedInteger], + 'stock' => ['nullable', new UnsignedInteger], + 'how_to_get' => ['required', 'string'], + 'thumbnail' => 'array', + ]; + } + + public function validationAttributes(): array + { + return [ + 'name' => 'nama', + 'points' => 'poin', + 'stock' => 'stok', + 'how_to_get' => 'cara mendapatkan', + ]; + } + + public function setReward(Reward $reward): void + { + $this->reward = $reward; + + $this->name = $reward->name; + $this->points = formatCurrencyNumber($reward->points); + $this->stock = formatCurrencyNumber($reward->stock); + $this->how_to_get = $reward->how_to_get ?? ''; + $this->thumbnail = $this->mapMediaCollection($reward->getMedia('thumbnail')); + } + + public function store(): void + { + $this->validate(); + + DB::transaction(function () { + $reward = Reward::create([ + 'name' => $this->name, + 'points' => parseRupiahToInt($this->points), + 'stock' => parseRupiahToInt($this->stock), + 'how_to_get' => $this->how_to_get, + ]); + + $this->uploadMedia($this->thumbnail, $reward, 'thumbnail'); + }); + } + + public function update(): void + { + $this->validate(); + + DB::transaction(function () { + $this->reward->update([ + 'name' => $this->name, + 'points' => parseRupiahToInt($this->points), + 'stock' => $this->stock ? parseRupiahToInt($this->stock) : null, + 'how_to_get' => $this->how_to_get, + ]); + + $this->syncMedia($this->thumbnail, $this->reward, 'thumbnail'); + $this->uploadMedia($this->thumbnail, $this->reward, 'thumbnail'); + }); + } +} diff --git a/app/Livewire/Studio/Loyalty/Reward.php b/app/Livewire/Studio/Loyalty/Reward.php new file mode 100644 index 0000000..1015f5a --- /dev/null +++ b/app/Livewire/Studio/Loyalty/Reward.php @@ -0,0 +1,87 @@ +canOrAbort('create reward'); + + $this->form->store(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Hadiah berhasil ditambahkan.'); + + Flux::modals()->close(); + } + + public function update(): void + { + $this->canOrAbort('update reward'); + + $this->form->update(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Hadiah berhasil diperbarui.'); + + Flux::modals()->close(); + } + + public function delete(RewardModel $reward): void + { + $this->canOrAbort('delete reward'); + + $reward->delete(); + + $this->dispatch('refreshDatatable'); + + $this->toast('Hadiah berhasil dihapus.'); + + Flux::modals()->close(); + } + + #[On('modal:open')] + public function openModal(string $method, string $modalTitle, ?string $id = null): void + { + $this->resetValidation(); + $this->resetErrorBag(); + + $this->method = $method; + $this->modalTitle = $modalTitle; + + $id && $this->form->setReward(RewardModel::byHashOrFail($id)); + } + + public function render(): View + { + return view('livewire.studio.loyalty.rewards', [ + 'pageTitle' => 'Hadiah', + ]); + } +} diff --git a/app/Models/Reward.php b/app/Models/Reward.php new file mode 100644 index 0000000..8a8730e --- /dev/null +++ b/app/Models/Reward.php @@ -0,0 +1,25 @@ + 'int', + 'stock' => 'int', + ]; + } +} diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index 736165d..1b4ac96 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -89,6 +89,13 @@ public function boot(): void 'match' => 'studio.loyalty.voucher.*', 'can' => 'view voucher', ], + [ + 'label' => 'Hadiah', + 'icon' => 'gift', + 'route' => 'studio.loyalty.reward.index', + 'match' => 'studio.loyalty.reward.*', + 'can' => 'view reward', + ], [ 'label' => 'Customer', 'icon' => 'user-plus', diff --git a/database/migrations/2025_12_18_193031_create_rewards_table.php b/database/migrations/2025_12_18_193031_create_rewards_table.php new file mode 100644 index 0000000..ef7a0e3 --- /dev/null +++ b/database/migrations/2025_12_18_193031_create_rewards_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->text('how_to_get'); + $table->unsignedInteger('points'); + $table->unsignedInteger('stock')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rewards'); + } +}; diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 846168a..f1a7e4e 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -62,6 +62,11 @@ public function run(): void 'update voucher', 'delete voucher', + 'view reward', + 'create reward', + 'update reward', + 'delete reward', + 'view customer', 'create customer', 'update customer', @@ -224,6 +229,10 @@ public function run(): void 'update voucher', 'delete voucher', + 'create reward', + 'update reward', + 'delete reward', + 'create perfume', 'update perfume', 'delete perfume', @@ -320,6 +329,11 @@ public function run(): void 'update voucher', 'delete voucher', + 'view reward', + 'create reward', + 'update reward', + 'delete reward', + 'view customer', 'create customer', 'update customer', diff --git a/resources/views/livewire/studio/loyalty/rewards.blade.php b/resources/views/livewire/studio/loyalty/rewards.blade.php new file mode 100644 index 0000000..f98e5b9 --- /dev/null +++ b/resources/views/livewire/studio/loyalty/rewards.blade.php @@ -0,0 +1,90 @@ + +
+
+ {{ $pageTitle }} +
+
+ @can('create reward') + + Tambah + + + @endcan +
+
+ +
+ +
+ + @include('components.modals.confirmation', [ + 'modalName' => 'delete-confirmation', + 'modalTitle' => 'Apakah Anda yakin?', + 'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.', + 'buttonVariant' => 'primary', + 'buttonColor' => 'danger', + 'buttonText' => 'Ya, Hapus', + ]) + +
+ {{ $modalTitle }} + + + Nama * + + + + + + Poin * + + + + + + Stok + Kosongkan jika tidak ada batas maksimum. + + + + + + Cara Mendapatkan * + + + + +
+

Thumbnail

+
+ + @error('form.thumbnail') + + @enderror +
+
+ +
+ + + Simpan + +
+
+
+
diff --git a/routes/pages/studio.php b/routes/pages/studio.php index b91cb64..28a528f 100644 --- a/routes/pages/studio.php +++ b/routes/pages/studio.php @@ -25,6 +25,7 @@ use App\Livewire\Studio\Information\Faq as FaqComponent; use App\Livewire\Studio\Information\PriceRequest as PriceRequestComponent; use App\Livewire\Studio\Loyalty\Customer as CustomerComponent; +use App\Livewire\Studio\Loyalty\Reward as RewardComponent; use App\Livewire\Studio\Loyalty\Tier as TierComponent; use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate; use App\Livewire\Studio\Loyalty\Voucher\Edit as VoucherEdit; @@ -94,6 +95,10 @@ Route::get('/create', VoucherCreate::class)->name('create')->middleware('can:create voucher'); Route::get('/{voucher}/edit', VoucherEdit::class)->name('edit')->middleware('can:update voucher'); }); + + Route::prefix('rewards')->name('reward.')->group(function () { + Route::get('/', RewardComponent::class)->name('index')->middleware('can:view reward'); + }); }); Route::prefix('catalog')->name('studio.catalog.')->group(function () {