feat(tier reward): implementasi crud untuk hadiah tier

This commit is contained in:
Yoga Pangestu 2025-11-04 21:27:20 +07:00
parent 2f62af0f76
commit be4ff665df
8 changed files with 347 additions and 5 deletions

View File

@ -0,0 +1,76 @@
<?php
namespace App\Livewire\Forms\Studio\Loyalty\Tier;
use App\Models\TierReward;
use App\Rules\UnsignedInteger;
use Illuminate\Validation\Rule;
use Livewire\Form;
class RewardForm extends Form
{
public ?TierReward $tierReward = null;
public string $tier_id = '';
public string $name = '';
public string $value = '';
public ?string $how_to_get = null;
public function rules(): array
{
return [
'tier_id' => ['required', Rule::exists('tiers', 'id')],
'name' => ['required', 'string', 'max:100'],
'value' => ['required', new UnsignedInteger],
'how_to_get' => 'nullable',
];
}
public function validationAttributes(): array
{
return [
'name' => 'nama',
'value' => 'nilia',
'how_to_get' => 'cara mendapatkan',
];
}
public function setRewards(TierReward $tierReward)
{
$this->tierReward = $tierReward;
$this->tier_id = $tierReward->tier_id;
$this->name = $tierReward->name;
$this->value = currency($tierReward->value);
$this->how_to_get = $tierReward->how_to_get;
}
public function store()
{
$this->validate();
return TierReward::create($this->prepareSavedData());
}
public function update()
{
$this->validate();
$this->tierReward->update($this->prepareSavedData());
return $this->tierReward;
}
private function prepareSavedData()
{
return [
'tier_id' => $this->tier_id,
'name' => $this->name,
'value' => replaceCurrency($this->value),
'how_to_get' => $this->how_to_get,
];
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace App\Livewire\Studio\Loyalty\Tier;
use App\Livewire\Forms\Studio\Loyalty\Tier\RewardForm;
use App\Models\TierReward;
use App\Traits\WithAuthorization;
use App\Traits\WithCloseModal;
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Flux\Flux;
use Livewire\Attributes\On;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Hadiah')]
class Reward extends Component
{
use WithAuthorization, WithCloseModal, WithConfirmation, WithToast, WithUpdatedData;
public RewardForm $form;
public string $modalTitle = '';
public string $method = '';
public array $rewards = [];
#[On('modal:open:reward')]
public function openModal(string $method, string $modalTitle, ?string $tier_id = null, ?string $id = null)
{
$this->resetValidation();
$this->resetErrorBag();
$this->method = $method;
$this->modalTitle = $modalTitle;
$this->form->tier_id = $tier_id;
$this->rewards = TierReward::where('tier_id', $tier_id)
->get()
->map(fn (TierReward $reward) => [
'hash' => $reward->hash,
'tier_id' => $reward->tier_id,
'name' => $reward->name,
'value' => currency($reward->value, 'Rp'),
])
->toArray();
if ($id) {
$this->form->setRewards(TierReward::byHashOrFail($id));
}
}
public function create()
{
$this->canOrAbort('create tier reward');
$this->form->store();
$this->toast('Hadiah berhasil ditambahkan.');
Flux::modals()->close();
}
public function update()
{
$this->canOrAbort('update tier reward');
$this->form->update();
$this->toast('Hadiah berhasil diperbarui.');
Flux::modals()->close();
}
public function delete(TierReward $reward)
{
$reward->delete();
$this->toast('Hadiah berhasil dihapus.');
Flux::modal('delete')->close();
$this->rewards = array_values(array_filter($this->rewards, fn ($item) => $item['hash'] !== $reward->hash));
}
public function render()
{
return view('livewire.studio.loyalty.tier.reward', [
'pageTitle' => 'Hadiah',
]);
}
}

View File

@ -29,4 +29,9 @@ public function memberships(): HasMany
{ {
return $this->hasMany(Membership::class); return $this->hasMany(Membership::class);
} }
public function rewards(): HasMany
{
return $this->hasMany(TierReward::class);
}
} }

20
app/Models/TierReward.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Veelasky\LaravelHashId\Eloquent\HashableId;
class TierReward extends Model
{
use HashableId, SoftDeletes;
protected $guarded = ['id'];
public function tier(): BelongsTo
{
return $this->belongsTo(Tier::class);
}
}

View File

@ -0,0 +1,33 @@
<?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_rewards', function (Blueprint $table) {
$table->id();
$table->foreignId('tier_id')->constrained()->cascadeOnDelete();
$table->string('name', 100);
$table->unsignedInteger('value');
$table->text('how_to_get')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tier_rewards');
}
};

View File

@ -49,6 +49,11 @@ public function run(): void
'update tier', 'update tier',
'delete tier', 'delete tier',
'view tier reward',
'create tier reward',
'update tier reward',
'delete tier reward',
'view voucher', 'view voucher',
'create voucher', 'create voucher',
'update voucher', 'update voucher',

View File

@ -0,0 +1,87 @@
<div>
<flux:modal name="reward-view-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto"
@close="closeModal('reward-view-modal')">
<div class="p-4 space-y-6">
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
<flux:table class="bo">
<flux:table.columns>
<flux:table.column>Nama</flux:table.column>
<flux:table.column>Nilai</flux:table.column>
<flux:table.column>Aksi</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($rewards as $reward)
<flux:table.row>
<flux:table.cell>{{ $reward['name'] }}</flux:table.cell>
<flux:table.cell>{{ $reward['value'] }}</flux:table.cell>
<flux:table.cell>
@can('update tier reward')
<flux:modal.trigger name="reward-form-modal">
<flux:tooltip content="Ubah">
<flux:button variant="primary" color="yellow" icon="pencil-square" size="sm"
wire:click="$dispatch('modal:open:reward', {method: 'update', 'modalTitle': 'Ubah Hadiah', 'tier_id': '{{ $reward['tier_id'] }}', 'id' : '{{ $reward['hash'] }}'})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@endcan
@can('update tier reward')
<flux:modal.trigger name="delete">
<flux:tooltip content="Hapus">
<flux:button variant="danger" icon="trash" size="sm"
wire:click="$dispatch('fn:confirmDelete', {id: '{{ $reward['hash'] }}'})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@endcan
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>
</flux:modal>
<flux:modal name="reward-form-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto"
@close="closeModal('reward-form-modal')">
<div class="p-4 space-y-6">
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
<flux:input name="form.tier_id" type="hidden"></flux:input>
<flux:field>
<flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Voucher Belanja 20k" wire:model.live.debounce.500ms="form.name" autofocus
autocomplete="off" />
<flux:error name="form.name" />
</flux:field>
<flux:field>
<flux:label>Nilai <span class="text-red-500 ms-1">*</span>
</flux:label>
<flux:description>Nilai hanya berupa rupiah, bukan persentase.</flux:description>
<flux:input.group>
<flux:input.group.prefix>Rp</flux:input.group.prefix>
<flux:input placeholder="xxx" x-mask:dynamic="$money($input, ',')"
wire:model.live.debounce.500ms="form.value" autocomplete="off" />
</flux:input.group>
<flux:error name="form.value" />
</flux:field>
<flux:editor label="Cara Mendapatkan" wire:model="form.how_to_get"
placeholder="Jelaskan kepada user cara mendapatkan / claim hadiah nya." auotocomplete="off"
class="**:data-[slot=content]:min-h-[100px]!" />
<div class="flex">
<flux:spacer />
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="{{ $method }}">
Simpan
</flux:button>
</div>
</div>
</flux:modal>
@include('components.confirmation.delete')
</div>

View File

@ -15,7 +15,7 @@
</div> </div>
<div class="mt-6"> <div class="mt-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 auto-rows-auto items-start">
@foreach ($tiers as $tier) @foreach ($tiers as $tier)
<flux:card <flux:card
class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900 text-gray-900 dark:text-white"> class="hover:shadow-lg transition-shadow duration-200 bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
@ -74,12 +74,31 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
</div> </div>
<div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700"> <div class="flex gap-2 pt-2 border-gray-200 dark:border-gray-700">
@can('view tier reward')
<flux:modal.trigger name="reward-view-modal">
<flux:tooltip content="Lihat Hadiah">
<flux:button variant="primary" color="blue" icon="eye" size="sm"
wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': '{{ $tier['name'] }}', 'tier_id': '{{ $tier['id'] }}'})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@endcan
@can('create tier reward')
<flux:modal.trigger name="reward-form-modal">
<flux:tooltip content="Tambah Hadiah">
<flux:button variant="primary" color="primary" icon="plus" size="sm"
wire:click="$dispatch('modal:open:reward', {method: 'create', 'modalTitle': 'Tambah Hadiah', 'tier_id': '{{ $tier['id'] }}'})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@endcan
@can('update tier') @can('update tier')
<flux:modal.trigger name="form-modal"> <flux:modal.trigger name="form-modal">
<flux:tooltip content="Ubah"> <flux:tooltip content="Ubah">
<flux:button variant="primary" color="yellow" icon="pencil-square" size="sm" <flux:button variant="primary" color="yellow" icon="pencil-square" size="sm"
wire:click="$dispatch('modal:open', {method: 'update', 'modalTitle': 'Ubah Tier', 'id': '{{ $tier['hash'] }}'})"> wire:click="$dispatch('modal:open', {method: 'update', 'modalTitle': 'Ubah Tier', 'id': '{{ $tier['hash'] }}'})">
Ubah
</flux:button> </flux:button>
</flux:tooltip> </flux:tooltip>
</flux:modal.trigger> </flux:modal.trigger>
@ -90,7 +109,6 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
<flux:tooltip content="Hapus"> <flux:tooltip content="Hapus">
<flux:button variant="danger" icon="trash" size="sm" <flux:button variant="danger" icon="trash" size="sm"
wire:click="$dispatch('fn:confirmDelete', {id: '{{ $tier['hash'] }}'})"> wire:click="$dispatch('fn:confirmDelete', {id: '{{ $tier['hash'] }}'})">
Hapus
</flux:button> </flux:button>
</flux:tooltip> </flux:tooltip>
</flux:modal.trigger> </flux:modal.trigger>
@ -109,14 +127,14 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
<flux:field> <flux:field>
<flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label> <flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Legenda" wire:model.live.debounce.500ms="form.name" autofocus <flux:input placeholder="Gold" wire:model.live.debounce.500ms="form.name" autofocus
autocomplete="off" /> autocomplete="off" />
<flux:error name="form.name" /> <flux:error name="form.name" />
</flux:field> </flux:field>
<flux:field> <flux:field>
<flux:label>Min. Poin <span class="text-red-500 ms-1">*</span></flux:label> <flux:label>Min. Poin <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="101" wire:model.live.debounce.500ms="form.min_points" autocomplete="off" <flux:input placeholder="100" wire:model.live.debounce.500ms="form.min_points" autocomplete="off"
x-mask:dynamic="$money($input, ',')" /> x-mask:dynamic="$money($input, ',')" />
<flux:error name="form.min_points" /> <flux:error name="form.min_points" />
</flux:field> </flux:field>
@ -137,4 +155,7 @@ class="mt-2 bg-white dark:bg-gray-800 rounded-lg shadow p-4 text-gray-700 dark:t
</div> </div>
</flux:modal> </flux:modal>
@can('create tier reward')
@livewire('studio.loyalty.tier.reward')
@endcan
</flux:main> </flux:main>