diff --git a/app/Livewire/Datatable/CustomersTable.php b/app/Livewire/Datatable/CustomersTable.php
new file mode 100644
index 0000000..556b301
--- /dev/null
+++ b/app/Livewire/Datatable/CustomersTable.php
@@ -0,0 +1,60 @@
+searchable(),
+
+ Column::make('Nomor Telepon', 'phone_number')->searchable(),
+
+ Column::make('Jenis Kelamin', 'gender')
+ ->format(fn ($value) => Blade::render(
+ ''
+ .$value->label()
+ .''
+ ))
+ ->html(),
+
+ Column::make('Aksi')
+ ->label(function ($row) {
+ $actions = '';
+
+ $actions .= view('components.datatables.edit-modal', [
+ 'id' => $row->id,
+ 'method' => 'update',
+ 'modalTitle' => 'Ubah Customer',
+ ])->render();
+
+ $actions .= view('components.datatables.delete', [
+ 'id' => $row->id,
+ 'deleteRoute' => route('studio.loyalty.customer.delete', $row->id),
+ ])->render();
+
+ return $actions;
+ })
+ ->html(),
+ ];
+ }
+
+ public function builder(): Builder
+ {
+ return Customer::select('id', 'name', 'phone_number', 'gender');
+ }
+}
diff --git a/app/Livewire/Forms/CustomerForm.php b/app/Livewire/Forms/CustomerForm.php
new file mode 100644
index 0000000..d6174c9
--- /dev/null
+++ b/app/Livewire/Forms/CustomerForm.php
@@ -0,0 +1,69 @@
+ ['required', 'string', 'max:50'],
+ 'phone_number' => ['nullable', 'string', new PhoneNumber],
+ 'gender' => ['required', Rule::in(Gender::cases())],
+ ];
+ }
+
+ public function validationAttributes(): array
+ {
+ return [
+ 'name' => 'nama',
+ 'phone_number' => 'nomor telepon',
+ 'gender' => 'jenis kelamin',
+ ];
+ }
+
+ public function setCustomer(Customer $customer)
+ {
+ $this->customer = $customer;
+
+ $this->name = $customer->name;
+ $this->phone_number = $customer->phone_number;
+ $this->gender = $customer->gender->value;
+ }
+
+ public function store()
+ {
+ $this->validate();
+
+ Customer::create([
+ 'name' => $this->name,
+ 'phone_number' => $this->phone_number,
+ 'gender' => $this->gender,
+ ]);
+ }
+
+ public function update()
+ {
+ $this->validate();
+
+ $this->customer->update([
+ 'name' => $this->name,
+ 'phone_number' => $this->phone_number,
+ 'gender' => $this->gender,
+ ]);
+ }
+}
diff --git a/app/Livewire/Studio/Loyalty/Customer.php b/app/Livewire/Studio/Loyalty/Customer.php
new file mode 100644
index 0000000..a851f59
--- /dev/null
+++ b/app/Livewire/Studio/Loyalty/Customer.php
@@ -0,0 +1,93 @@
+resetValidation();
+ $this->resetErrorBag();
+
+ $this->method = $method;
+ $this->modalTitle = $modalTitle;
+
+ if ($id) {
+ $this->form->setCustomer(CustomerModel::findOrFail($id));
+ }
+ }
+
+ public function create()
+ {
+ $this->form->store();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Customer berhasil ditambahkan.',
+ variant: 'success',
+ duration: 3000
+ );
+
+ Flux::modals()->close();
+ }
+
+ public function update()
+ {
+ $this->form->update();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Customer berhasil diperbarui.',
+ variant: 'success',
+ duration: 3000
+ );
+
+ Flux::modals()->close();
+ }
+
+ public function delete(CustomerModel $customer)
+ {
+ $customer->delete();
+
+ $this->dispatch('refreshDatatable');
+
+ Flux::toast(
+ heading: 'Berhasil',
+ text: 'Customer berhasil dihapus.',
+ variant: 'success',
+ );
+
+ Flux::modals()->close();
+ }
+
+ public function render()
+ {
+ return view('livewire.studio.loyalty.customers', [
+ 'pageTitle' => 'Customer',
+ ]);
+ }
+}
diff --git a/app/Models/Customer.php b/app/Models/Customer.php
new file mode 100644
index 0000000..d2637fe
--- /dev/null
+++ b/app/Models/Customer.php
@@ -0,0 +1,28 @@
+ Gender::class,
+ ];
+ }
+
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index a8d8ffb..f4c9187 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -30,4 +30,9 @@ public function employee(): HasOne
{
return $this->hasOne(Employee::class);
}
+
+ public function customer(): HasOne
+ {
+ return $this->hasOne(Customer::class);
+ }
}
diff --git a/database/factories/CustomerFactory.php b/database/factories/CustomerFactory.php
new file mode 100644
index 0000000..2d1f1ce
--- /dev/null
+++ b/database/factories/CustomerFactory.php
@@ -0,0 +1,22 @@
+ $this->faker->name(),
+ 'phone_number' => fake()->randomElement([
+ fake()->numerify('0812 #### ###'),
+ fake()->numerify('0812 #### ####'),
+ fake()->numerify('0812 #### #####'),
+ ]),
+ 'gender' => $this->faker->randomElement(Gender::cases()),
+ ];
+ }
+}
diff --git a/database/migrations/2025_09_30_031349_create_customers_table.php b/database/migrations/2025_09_30_031349_create_customers_table.php
new file mode 100644
index 0000000..587e007
--- /dev/null
+++ b/database/migrations/2025_09_30_031349_create_customers_table.php
@@ -0,0 +1,33 @@
+id();
+ $table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
+ $table->string('name', 50);
+ $table->string('phone_number', 20)->nullable();
+ $table->enum('gender', [Gender::values()])->comment(Gender::comment());
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('customers');
+ }
+};
diff --git a/database/seeders/CustomerSeeder.php b/database/seeders/CustomerSeeder.php
new file mode 100644
index 0000000..517dec8
--- /dev/null
+++ b/database/seeders/CustomerSeeder.php
@@ -0,0 +1,35 @@
+ 'Yoga Pangestu',
+ 'phone_number' => '082121495806',
+ 'gender' => Gender::MALE,
+ ],
+ [
+ 'name' => 'Budi',
+ 'phone_number' => '082121673845',
+ 'gender' => Gender::MALE,
+ ],
+ [
+ 'name' => 'Jaka',
+ 'phone_number' => '082127382347',
+ 'gender' => Gender::MALE,
+ ],
+ ];
+
+ foreach ($customers as $customer) {
+ Customer::create($customer);
+ }
+ }
+}
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index 4451cb8..5bc56b3 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -15,6 +15,7 @@ public function run(): void
VoucherSeeder::class,
CategorySeeder::class,
BrandSeeder::class,
+ CustomerSeeder::class,
]);
}
}
diff --git a/resources/views/components/partials/studio/_sidebar.blade.php b/resources/views/components/partials/studio/_sidebar.blade.php
index 34bdd77..17c0606 100644
--- a/resources/views/components/partials/studio/_sidebar.blade.php
+++ b/resources/views/components/partials/studio/_sidebar.blade.php
@@ -31,6 +31,10 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z
Voucher
+
+ Customer
+
diff --git a/resources/views/livewire/studio/loyalty/customers.blade.php b/resources/views/livewire/studio/loyalty/customers.blade.php
new file mode 100644
index 0000000..0d2e2e8
--- /dev/null
+++ b/resources/views/livewire/studio/loyalty/customers.blade.php
@@ -0,0 +1,48 @@
+
+
+
+ {{ $pageTitle }}
+
+
+
+ Tambah
+
+
+
+
+
+
+
+
+
+ @include('components.confirmation.delete')
+
+
+
{{ $modalTitle }}
+
+
+
+
+
+
+ @foreach (\App\Enums\Gender::cases() as $gender)
+
+ {{ $gender->label() }}
+
+ @endforeach
+
+
+
+
+
+ Simpan
+
+
+
+
+
+
diff --git a/resources/views/livewire/studio/loyalty/voucher/form.blade.php b/resources/views/livewire/studio/loyalty/voucher/form.blade.php
index 165b681..3da2fcb 100644
--- a/resources/views/livewire/studio/loyalty/voucher/form.blade.php
+++ b/resources/views/livewire/studio/loyalty/voucher/form.blade.php
@@ -43,8 +43,8 @@
wire:model.live.debounce.500ms="form.quota"
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
-
diff --git a/routes/web.php b/routes/web.php
index 89cf3a0..88f59d4 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -5,6 +5,7 @@
use App\Livewire\Studio\Catalog\Brand as BrandComponent;
use App\Livewire\Studio\Catalog\Category as CategoryComponent;
use App\Livewire\Studio\Dashboard\Overview;
+use App\Livewire\Studio\Loyalty\Customer as CustomerComponent;
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;
@@ -62,6 +63,13 @@
Route::delete('memberships/{membership}/delete', MembershipComponent::class)->name('delete');
});
+ Route::prefix('loyalty')
+ ->as('studio.loyalty.customer.')
+ ->group(function () {
+ Route::get('customers', CustomerComponent::class)->name('index');
+ Route::delete('customers/{customer}/delete', CustomerComponent::class)->name('delete');
+ });
+
Route::prefix('loyalty')
->as('studio.loyalty.voucher.')
->group(function () {
diff --git a/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php b/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php
new file mode 100644
index 0000000..eb79ef5
--- /dev/null
+++ b/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php
@@ -0,0 +1,69 @@
+assertViewIs('livewire.studio.loyalty.customers')
+ ->assertViewHas('pageTitle', 'Customer');
+});
+
+it('displays all customers', function () {
+ CustomerModel::factory()->count(10)->create();
+
+ $customer = CustomerModel::first();
+
+ Livewire::test(Customer::class)->assertSee($customer->name);
+});
+
+it('can create a customer', function () {
+ $data = CustomerModel::factory()->raw();
+
+ Livewire::test(Customer::class)
+ ->set('form.name', $data['name'])
+ ->set('form.phone_number', $data['phone_number'])
+ ->set('form.gender', $data['gender'])
+ ->call('create')
+ ->assertHasNoErrors()
+ ->assertDispatched('refreshDatatable');
+
+ expect(CustomerModel::count())->toBe(1);
+ expect(CustomerModel::first()->name)->toBe($data['name']);
+});
+
+it('can update a customer', function () {
+ $customer = CustomerModel::factory()->create();
+
+ $data = CustomerModel::factory()->raw();
+
+ Livewire::test(Customer::class)
+ ->call('openModal', 'update', 'Edit Customer', $customer->id)
+ ->set('form.name', $data['name'])
+ ->set('form.phone_number', $data['phone_number'])
+ ->set('form.gender', $data['gender'])
+ ->call('update')
+ ->assertHasNoErrors()
+ ->assertDispatched('refreshDatatable');
+
+ $customer->refresh();
+
+ expect($customer->name)->toBe($data['name']);
+ expect($customer->phone_number)->toBe($data['phone_number']);
+ expect($customer->gender)->toBe($data['gender']);
+});
+
+it('can delete an customer', function () {
+ $customer = CustomerModel::factory()->create();
+
+ Livewire::test(Customer::class)
+ ->call('delete', $customer)
+ ->assertHasNoErrors()
+ ->assertDispatched('refreshDatatable');
+
+ expect(CustomerModel::count())->toBe(0);
+});