feat: Implement loyalty reward management with dedicated routes, model, UI, and permissions.
This commit is contained in:
parent
b132f86ed7
commit
e07a2386cf
65
app/Livewire/Datatable/Loyalty/RewardsTable.php
Normal file
65
app/Livewire/Datatable/Loyalty/RewardsTable.php
Normal file
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable\Loyalty;
|
||||
|
||||
use App\Models\Reward;
|
||||
use App\Traits\Datatable\WithAppendColumn;
|
||||
use App\Traits\Datatable\WithConfiguration;
|
||||
use App\Traits\Datatable\WithPrependColumn;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
|
||||
class RewardsTable extends DataTableComponent
|
||||
{
|
||||
use WithAppendColumn, WithConfiguration, WithPrependColumn;
|
||||
|
||||
protected $model = Reward::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Nama', 'name')
|
||||
->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');
|
||||
}
|
||||
}
|
||||
91
app/Livewire/Forms/Studio/Loyalty/RewardForm.php
Normal file
91
app/Livewire/Forms/Studio/Loyalty/RewardForm.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms\Studio\Loyalty;
|
||||
|
||||
use App\Models\Reward;
|
||||
use App\Rules\UnsignedInteger;
|
||||
use App\Traits\Media\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Form;
|
||||
|
||||
class RewardForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
|
||||
public ?Reward $reward = null;
|
||||
|
||||
public string $name = '';
|
||||
|
||||
public string $points = '';
|
||||
|
||||
public ?string $stock = null;
|
||||
|
||||
public string $how_to_get = '';
|
||||
|
||||
public array $thumbnail = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['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');
|
||||
});
|
||||
}
|
||||
}
|
||||
87
app/Livewire/Studio/Loyalty/Reward.php
Normal file
87
app/Livewire/Studio/Loyalty/Reward.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Loyalty;
|
||||
|
||||
use App\Livewire\Forms\Studio\Loyalty\RewardForm;
|
||||
use App\Models\Reward as RewardModel;
|
||||
use App\Traits\Authorization\WithAuthorization;
|
||||
use App\Traits\Components\WithCloseModal;
|
||||
use App\Traits\Components\WithConfirmation;
|
||||
use App\Traits\Components\WithToast;
|
||||
use App\Traits\Notification\WithSubscribeNotification;
|
||||
use App\Traits\Utilities\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Hadiah')]
|
||||
class Reward extends Component
|
||||
{
|
||||
use WithAuthorization, WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdatedData;
|
||||
|
||||
public RewardForm $form;
|
||||
|
||||
public string $method = 'create';
|
||||
|
||||
public string $modalTitle = '';
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
$this->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
25
app/Models/Reward.php
Normal file
25
app/Models/Reward.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Veelasky\LaravelHashId\Eloquent\HashableId;
|
||||
|
||||
class Reward extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, HashableId, InteractsWithMedia, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'points' => 'int',
|
||||
'stock' => 'int',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
<?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('rewards', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -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',
|
||||
|
||||
90
resources/views/livewire/studio/loyalty/rewards.blade.php
Normal file
90
resources/views/livewire/studio/loyalty/rewards.blade.php
Normal file
@ -0,0 +1,90 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@can('create reward')
|
||||
<flux:modal.trigger name="form-modal">
|
||||
<flux:button variant="primary" class="text-sm"
|
||||
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Hadiah'})">Tambah
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.loyalty.rewards-table />
|
||||
</div>
|
||||
|
||||
@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',
|
||||
])
|
||||
<flux:modal name="form-modal" class="w-[95%] max-w-sm md:max-w-xl mx-auto" @close="closeModal('form-modal')">
|
||||
<div class="p-4 space-y-6">
|
||||
<flux:heading size="lg">{{ $modalTitle }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Nama <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="Blue Emotion 10ml" wire:model="form.name" autofocus autocomplete="off" />
|
||||
<flux:error name="form.name" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Poin <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="300" x-mask:dynamic="$money($input, ',')" wire:model="form.points"
|
||||
autocomplete="off" />
|
||||
<flux:error name="form.points" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Stok </flux:label>
|
||||
<flux:description>Kosongkan jika tidak ada batas maksimum.</flux:description>
|
||||
<flux:input placeholder="300" x-mask:dynamic="$money($input, ',')" wire:model="form.stock"
|
||||
autocomplete="off" />
|
||||
<flux:error name="form.stock" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Cara Mendapatkan <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:editor wire:model="form.how_to_get" placeholder="..." autocomplete="off"
|
||||
class="**:data-[slot=content]:min-h-[100px]!" />
|
||||
<flux:error name="form.how_to_get" />
|
||||
</flux:field>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium">Thumbnail</h3>
|
||||
<div class="dropzone-wrapper">
|
||||
<livewire:dropzone wire:model="form.thumbnail" :rules="['image', 'mimes:png,jpeg', 'max:10420']" :max-files="1" :key="'thumbnail'"
|
||||
:files="$form->thumbnail ?? []" />
|
||||
@error('form.thumbnail')
|
||||
<div role="alert" aria-live="polite" aria-atomic="true"
|
||||
class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
||||
<svg class="shrink-0 [:where(&)]:size-5 inline" data-flux-icon=""
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</flux:main>
|
||||
@ -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 () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user