diff --git a/app/Enums/VoucherType.php b/app/Enums/VoucherType.php
new file mode 100644
index 0000000..8fffc41
--- /dev/null
+++ b/app/Enums/VoucherType.php
@@ -0,0 +1,38 @@
+ 'Nominal',
+ self::PERCENTAGE => 'Persentase',
+ };
+ }
+
+ public function color()
+ {
+ return match ($this) {
+ self::FIXED => 'blue',
+ self::PERCENTAGE => 'pink',
+ };
+ }
+
+ public function icon()
+ {
+ return match ($this) {
+ self::FIXED => 'currency-dollar',
+ self::PERCENTAGE => 'percent-badge',
+ };
+ }
+}
diff --git a/app/Livewire/Datatable/MembershipsTable.php b/app/Livewire/Datatable/MembershipsTable.php
index b1d7b26..31e9fa6 100644
--- a/app/Livewire/Datatable/MembershipsTable.php
+++ b/app/Livewire/Datatable/MembershipsTable.php
@@ -21,7 +21,7 @@ public function columns(): array
return [
Column::make('Nama', 'name')->searchable(),
- Column::make('Min. Pembelian', 'min_purchase')
+ Column::make('Min. Belanja', 'min_purchase')
->format(fn ($value) => currency($value, 'Rp'))
->searchable()
->sortable(),
diff --git a/app/Livewire/Datatable/VouchersTable.php b/app/Livewire/Datatable/VouchersTable.php
new file mode 100644
index 0000000..643d132
--- /dev/null
+++ b/app/Livewire/Datatable/VouchersTable.php
@@ -0,0 +1,110 @@
+label(function ($row) {
+ return <<
+
{$row->name}
+ {$row->code}
+
+ HTML;
+ })
+ ->searchable(function ($query, $searchTerm) {
+ $query->where('name', 'like', "%{$searchTerm}%")
+ ->orWhere('code', 'like', "%{$searchTerm}%");
+ })
+ ->html(),
+
+ Column::make('Min. Belanja', 'min_purchase')
+ ->format(fn ($value) => currency($value, 'Rp'))
+ ->searchable()
+ ->sortable(),
+
+ Column::make('Jumlah Diskon', 'discount_amount')
+ ->label(fn ($row, $column) => formatDiscount($row->discount_amount, $row->type))
+ ->searchable()
+ ->sortable(),
+
+ Column::make('Maks. Diskon')
+ ->label(fn ($row, $column) => currency($row->max_discount, 'Rp'))
+ ->searchable()
+ ->sortable(),
+
+ Column::make('Kuota', 'quota')->searchable()->sortable(),
+
+ Column::make('Batas Per Pembali', 'limit_per_user')->searchable()->sortable(),
+
+ Column::make('Tanggal')
+ ->label(function ($row) {
+ $startDate = formatDate($row->start_date);
+ $startAgo = timeAgo($row->start_date);
+
+ $endDate = formatDate($row->end_date);
+ $endAgo = timeAgo($row->end_date);
+
+ return <<
+
+ Tgl Dimulai:
+ {$startDate}
+ ({$startAgo})
+
+
+ Tgl Berakhir:
+ {$endDate}
+ ({$endAgo})
+
+
+ HTML;
+ })
+ ->html(),
+
+ Column::make('Akitf?', 'is_active')
+ ->format(fn ($value) => Blade::render(''.$value->label().''))
+ ->html(),
+
+ Column::make('Aksi')
+ ->label(function ($row) {
+ $actions = '';
+
+ $actions .= view('components.datatables.edit', [
+ 'id' => $row->id,
+ 'editRoute' => route('studio.loyalty.voucher.edit', $row->id),
+ ])->render();
+
+ $actions .= view('components.datatables.delete', [
+ 'id' => $row->id,
+ 'deleteRoute' => route('studio.loyalty.voucher.delete', $row->id),
+ ])->render();
+
+ return $actions;
+ })
+ ->html(),
+ ];
+ }
+
+ public function builder(): Builder
+ {
+ return Voucher::select('id', 'name', 'code', 'min_purchase', 'discount_amount', 'max_discount', 'quota', 'limit_per_user', 'start_date', 'end_date', 'is_active', 'type');
+ }
+}
diff --git a/app/Livewire/Forms/MembershipForm.php b/app/Livewire/Forms/MembershipForm.php
index 7957642..db3c16c 100644
--- a/app/Livewire/Forms/MembershipForm.php
+++ b/app/Livewire/Forms/MembershipForm.php
@@ -31,7 +31,7 @@ public function validationAttributes(): array
{
return [
'name' => 'nama',
- 'min_purchase' => 'min. pembelian',
+ 'min_purchase' => 'min. belanja',
'discount_percentage' => 'persentase diskon',
'terms_conditions' => 'syarat dan ketentuan',
];
diff --git a/app/Livewire/Forms/VoucherForm.php b/app/Livewire/Forms/VoucherForm.php
new file mode 100644
index 0000000..9e3c989
--- /dev/null
+++ b/app/Livewire/Forms/VoucherForm.php
@@ -0,0 +1,147 @@
+ ['required', 'string', 'max:50'],
+ 'code' => [
+ 'required',
+ 'string',
+ 'max:20',
+ 'alpha_num',
+ Rule::unique('vouchers', 'code')->ignore($this->voucher),
+ ],
+ 'type' => ['required', Rule::in(VoucherType::values())],
+ 'discount_amount' => [
+ 'required',
+ Rule::when(
+ fn () => $this->type == VoucherType::PERCENTAGE->value,
+ ['integer', 'between:0,100'],
+ ['numeric'],
+ ),
+ ],
+ 'min_purchase' => ['nullable', 'numeric'],
+ 'max_discount' => [
+ Rule::when(
+ fn () => $this->type == VoucherType::PERCENTAGE->value,
+ ['required', 'numeric', 'between:0,100'],
+ ['nullable'],
+ ),
+ ],
+ 'quota' => ['nullable', 'numeric'],
+ 'limit_per_user' => ['nullable', 'numeric'],
+ 'start_date' => ['required', 'date'],
+ 'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
+ 'terms_conditions' => ['nullable', 'string'],
+ 'is_active' => ['required', Rule::in(IsActive::values())],
+ ];
+ }
+
+ public function validationAttributes(): array
+ {
+ return [
+ 'name' => 'nama',
+ 'code' => 'kode',
+ 'min_purchase' => 'min. belanja',
+ 'max_discount' => 'maks. diskon',
+ 'quota' => 'kuota',
+ 'limit_per_user' => 'limit per user',
+ 'terms_conditions' => 'syarat dan ketentuan',
+ 'type' => 'Tipe',
+ 'discount_amount' => 'Jumlah Diskon',
+ 'start_date' => 'tanggal mulai',
+ 'end_date' => 'tanggal selesai',
+ 'is_active' => 'aktif?',
+ ];
+ }
+
+ public function setVoucher(Voucher $voucher)
+ {
+ $this->voucher = $voucher;
+
+ $this->name = $this->voucher->name;
+ $this->code = $this->voucher->code;
+ $this->type = $this->voucher->type->value;
+ $this->discount_amount = replaceCurrency($this->voucher->discount_amount);
+ $this->min_purchase = replaceCurrency($this->voucher->min_purchase);
+ $this->max_discount = replaceCurrency($this->voucher->max_discount);
+ $this->quota = $this->voucher->quota;
+ $this->limit_per_user = $this->voucher->limit_per_user;
+ $this->terms_conditions = $this->voucher->terms_conditions;
+ $this->start_date = $this->voucher->start_date;
+ $this->end_date = $this->voucher->end_date;
+ $this->is_active = $this->voucher->is_active->value;
+ }
+
+ public function store()
+ {
+ $this->validate();
+
+ $data = $this->prepareSavedData();
+
+ Voucher::create($data);
+ }
+
+ public function update()
+ {
+ $this->validate();
+
+ $data = $this->prepareSavedData();
+
+ $this->voucher->update($data);
+ }
+
+ private function prepareSavedData()
+ {
+ return [
+ 'name' => $this->name,
+ 'code' => $this->code,
+ 'type' => $this->type,
+ 'discount_amount' => replaceCurrency($this->discount_amount),
+ 'min_purchase' => replaceCurrency($this->min_purchase),
+ 'max_discount' => $this->type == VoucherType::PERCENTAGE->value ? replaceCurrency($this->max_discount) : null,
+ 'quota' => $this->quota,
+ 'limit_per_user' => $this->limit_per_user,
+ 'terms_conditions' => $this->terms_conditions,
+ 'start_date' => $this->start_date,
+ 'end_date' => $this->end_date,
+ 'is_active' => $this->is_active,
+ ];
+ }
+}
diff --git a/app/Livewire/Studio/Loyalty/Voucher/Create.php b/app/Livewire/Studio/Loyalty/Voucher/Create.php
new file mode 100644
index 0000000..1693f45
--- /dev/null
+++ b/app/Livewire/Studio/Loyalty/Voucher/Create.php
@@ -0,0 +1,40 @@
+form->store();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Voucher berhasil ditambahkan.',
+ variant: 'success',
+ duration: 3000
+ );
+
+ $this->redirectRoute('studio.loyalty.voucher.index', navigate: true);
+ }
+
+ public function render()
+ {
+ return view('livewire.studio.loyalty.voucher.form', [
+ 'pageTitle' => 'Tambah Voucher',
+ ]);
+ }
+}
diff --git a/app/Livewire/Studio/Loyalty/Voucher/Edit.php b/app/Livewire/Studio/Loyalty/Voucher/Edit.php
new file mode 100644
index 0000000..f47c82e
--- /dev/null
+++ b/app/Livewire/Studio/Loyalty/Voucher/Edit.php
@@ -0,0 +1,46 @@
+form->setVoucher($voucher);
+ }
+
+ public function save()
+ {
+ $this->form->update();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Voucher berhasil diperbarui.',
+ variant: 'success',
+ duration: 3000
+ );
+
+ $this->redirectRoute('studio.loyalty.voucher.index', navigate: true);
+ }
+
+ public function render()
+ {
+ return view('livewire.studio.loyalty.voucher.form', [
+ 'pageTitle' => 'Ubah Voucher',
+ ]);
+ }
+}
diff --git a/app/Livewire/Studio/Loyalty/Voucher/Index.php b/app/Livewire/Studio/Loyalty/Voucher/Index.php
new file mode 100644
index 0000000..a16d084
--- /dev/null
+++ b/app/Livewire/Studio/Loyalty/Voucher/Index.php
@@ -0,0 +1,37 @@
+delete();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Voucher berhasil dihapus.',
+ variant: 'success',
+ );
+
+ Flux::modals()->close();
+ }
+
+ public function render()
+ {
+ return view('livewire.studio.loyalty.voucher.index', [
+ 'pageTitle' => 'Voucher',
+ ]);
+ }
+}
diff --git a/app/Models/Voucher.php b/app/Models/Voucher.php
new file mode 100644
index 0000000..b3a1573
--- /dev/null
+++ b/app/Models/Voucher.php
@@ -0,0 +1,29 @@
+ VoucherType::class,
+ 'discount_amount' => 'int',
+ 'min_purchase' => 'int',
+ 'max_discount' => 'int',
+ 'quota' => 'int',
+ 'limit_per_user' => 'int',
+ 'is_active' => IsActive::class,
+ ];
+ }
+}
diff --git a/database/migrations/2025_09_25_125559_create_vouchers_table.php b/database/migrations/2025_09_25_125559_create_vouchers_table.php
new file mode 100644
index 0000000..29cc6cc
--- /dev/null
+++ b/database/migrations/2025_09_25_125559_create_vouchers_table.php
@@ -0,0 +1,42 @@
+id();
+ $table->string('name', 50);
+ $table->string('code', 20)->unique();
+ $table->text('terms_conditions')->nullable();
+ $table->enum('type', [VoucherType::values()])->default(VoucherType::PERCENTAGE)->comment(VoucherType::comment());
+ $table->unsignedInteger('discount_amount');
+ $table->unsignedInteger('min_purchase')->nullable();
+ $table->unsignedInteger('max_discount')->nullable();
+ $table->unsignedInteger('quota')->nullable();
+ $table->unsignedInteger('limit_per_user')->nullable();
+ $table->date('start_date');
+ $table->date('end_date')->nullable();
+ $table->enum('is_active', [IsActive::values()])->default(IsActive::ACTIVE)->comment(IsActive::comment());
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('vouchers');
+ }
+};
diff --git a/resources/views/components/partials/studio/_sidebar.blade.php b/resources/views/components/partials/studio/_sidebar.blade.php
index c8b89be..203c904 100644
--- a/resources/views/components/partials/studio/_sidebar.blade.php
+++ b/resources/views/components/partials/studio/_sidebar.blade.php
@@ -11,8 +11,7 @@ class="px-2 hidden dark:flex" />
Ringkasan
-
Master
-
+
Master
Outlet
@@ -25,12 +24,15 @@ class="px-2 hidden dark:flex" />
-
Loyalty
-
+
Loyalty
Membership
+
+ Voucher
+
diff --git a/resources/views/livewire/studio/loyalty/voucher/form.blade.php b/resources/views/livewire/studio/loyalty/voucher/form.blade.php
new file mode 100644
index 0000000..03a59f3
--- /dev/null
+++ b/resources/views/livewire/studio/loyalty/voucher/form.blade.php
@@ -0,0 +1,138 @@
+
+
+
+ {{ $pageTitle }}
+
+
+
+ Kembali
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Min. Belanja
+
+ Rp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @foreach (\App\Enums\VoucherType::cases() as $item)
+
+ {{ $item->label() }}
+
+ @endforeach
+
+
+
+
+ @if ($form->type == \App\Enums\VoucherType::FIXED->value)
+ Rp
+
+
+ @else
+
+
+ %
+ @endif
+
+
+
+
+
+ @if ($form->type == \App\Enums\VoucherType::PERCENTAGE->value)
+
+ Maks. Diskon
+
+ Rp
+
+
+
+
+
+
+ @endif
+
+
+
+
+ @foreach (\App\Enums\IsActive::cases() as $item)
+
+ {{ $item->label() }}
+
+ @endforeach
+
+
+
+
+
+
+
+ Simpan
+
+
+
+
+
diff --git a/resources/views/livewire/studio/loyalty/voucher/index.blade.php b/resources/views/livewire/studio/loyalty/voucher/index.blade.php
new file mode 100644
index 0000000..340055a
--- /dev/null
+++ b/resources/views/livewire/studio/loyalty/voucher/index.blade.php
@@ -0,0 +1,19 @@
+
+
+
+ {{ $pageTitle }}
+
+
+
+ Tambah
+
+
+
+
+
+
+
+
+ @include('components.confirmation.delete')
+
diff --git a/routes/web.php b/routes/web.php
index 0dd863f..66fe4a2 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -4,6 +4,9 @@
use App\Livewire\Auth\Logout;
use App\Livewire\Studio\Dashboard\Overview;
use App\Livewire\Studio\Loyalty\Membership as MembershipComponent;
+use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate;
+use App\Livewire\Studio\Loyalty\Voucher\Edit as VoucherEdit;
+use App\Livewire\Studio\Loyalty\Voucher\Index as VoucherIndex;
use App\Livewire\Studio\Master\Outlet\Create as OutletCreate;
use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit;
use App\Livewire\Studio\Master\Outlet\Index as OutletIndex;
@@ -56,4 +59,13 @@
Route::get('memberships', MembershipComponent::class)->name('index');
Route::delete('memberships/{membership}/delete', MembershipComponent::class)->name('delete');
});
+
+ Route::prefix('loyalty')
+ ->as('studio.loyalty.voucher.')
+ ->group(function () {
+ Route::get('vouchers', VoucherIndex::class)->name('index');
+ Route::get('vouchers/create', VoucherCreate::class)->name('create');
+ Route::get('vouchers/{voucher}/edit', VoucherEdit::class)->name('edit');
+ Route::get('vouchers/{voucher}/delete', VoucherCreate::class)->name('delete');
+ });
});