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); +});