feat(customer): membuat module customer
-membuat model, migrasi seeder dan faker -membuat relasi di user -membuat test untuk memastikan jalannya aplikasi
This commit is contained in:
parent
ef05482000
commit
ac44f25001
60
app/Livewire/Datatable/CustomersTable.php
Normal file
60
app/Livewire/Datatable/CustomersTable.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Traits\Datatable\WithAppendColumn;
|
||||
use App\Traits\Datatable\WithConfiguration;
|
||||
use App\Traits\Datatable\WithPrependColumn;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
|
||||
class CustomersTable extends DataTableComponent
|
||||
{
|
||||
use WithAppendColumn, WithConfiguration, WithPrependColumn;
|
||||
|
||||
protected $model = Customer::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Nama', 'name')->searchable(),
|
||||
|
||||
Column::make('Nomor Telepon', 'phone_number')->searchable(),
|
||||
|
||||
Column::make('Jenis Kelamin', 'gender')
|
||||
->format(fn ($value) => Blade::render(
|
||||
'<flux:badge color="'.$value->color().'">'
|
||||
.$value->label()
|
||||
.'</flux:badge>'
|
||||
))
|
||||
->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');
|
||||
}
|
||||
}
|
||||
69
app/Livewire/Forms/CustomerForm.php
Normal file
69
app/Livewire/Forms/CustomerForm.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Models\Customer;
|
||||
use App\Rules\PhoneNumber;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class CustomerForm extends Form
|
||||
{
|
||||
public ?Customer $customer = null;
|
||||
|
||||
public string $name = '';
|
||||
|
||||
public ?string $phone_number = null;
|
||||
|
||||
public string $gender = '';
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
93
app/Livewire/Studio/Loyalty/Customer.php
Normal file
93
app/Livewire/Studio/Loyalty/Customer.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Loyalty;
|
||||
|
||||
use App\Livewire\Forms\CustomerForm;
|
||||
use App\Models\Customer as CustomerModel;
|
||||
use App\Traits\WithCloseModal;
|
||||
use App\Traits\WithConfirmation;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Customer')]
|
||||
class Customer extends Component
|
||||
{
|
||||
use WithCloseModal, WithConfirmation, WithUpdatedData;
|
||||
|
||||
public CustomerForm $form;
|
||||
|
||||
public string $method = 'create';
|
||||
|
||||
public string $modalTitle = '';
|
||||
|
||||
#[On('modal:open')]
|
||||
public function openModal(string $method, string $modalTitle, ?string $id = null)
|
||||
{
|
||||
$this->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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
28
app/Models/Customer.php
Normal file
28
app/Models/Customer.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Customer extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'gender' => Gender::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -30,4 +30,9 @@ public function employee(): HasOne
|
||||
{
|
||||
return $this->hasOne(Employee::class);
|
||||
}
|
||||
|
||||
public function customer(): HasOne
|
||||
{
|
||||
return $this->hasOne(Customer::class);
|
||||
}
|
||||
}
|
||||
|
||||
22
database/factories/CustomerFactory.php
Normal file
22
database/factories/CustomerFactory.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class CustomerFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->name(),
|
||||
'phone_number' => fake()->randomElement([
|
||||
fake()->numerify('0812 #### ###'),
|
||||
fake()->numerify('0812 #### ####'),
|
||||
fake()->numerify('0812 #### #####'),
|
||||
]),
|
||||
'gender' => $this->faker->randomElement(Gender::cases()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Gender;
|
||||
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('customers', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
35
database/seeders/CustomerSeeder.php
Normal file
35
database/seeders/CustomerSeeder.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CustomerSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$customers = [
|
||||
[
|
||||
'name' => '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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ public function run(): void
|
||||
VoucherSeeder::class,
|
||||
CategorySeeder::class,
|
||||
BrandSeeder::class,
|
||||
CustomerSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,10 @@ class="bg-zinc-50 dark:bg-zinc-900 border-r rtl:border-r-0 rtl:border-l border-z
|
||||
<flux:navlist.item icon="ticket" href="{{ route('studio.loyalty.voucher.index') }}"
|
||||
:current="request()->routeIs('studio.loyalty.voucher.*')" wire:navigate.hover>Voucher
|
||||
</flux:navlist.item>
|
||||
|
||||
<flux:navlist.item icon="user-plus" href="{{ route('studio.loyalty.customer.index') }}"
|
||||
:current="request()->routeIs('studio.loyalty.customer.*')" wire:navigate.hover>Customer
|
||||
</flux:navlist.item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
48
resources/views/livewire/studio/loyalty/customers.blade.php
Normal file
48
resources/views/livewire/studio/loyalty/customers.blade.php
Normal file
@ -0,0 +1,48 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div>
|
||||
<flux:modal.trigger name="form-modal">
|
||||
<flux:button variant="primary" class="text-sm"
|
||||
wire:click="$dispatch('modal:open', {method: 'create', 'modalTitle': 'Tambah Customer'})">Tambah
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.customers-table />
|
||||
</div>
|
||||
|
||||
@include('components.confirmation.delete')
|
||||
<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:input label="Nama" placeholder="Masukkan nama customer" wire:model.live.debounce.500ms="form.name"
|
||||
autofocus autocomplete="off" clearable />
|
||||
|
||||
<flux:input label="Nomor Telepon" placeholder="Masukkan nomor telepon" mask="9999 9999 99999"
|
||||
wire:model.live.debounce.500ms="form.phone_number" autocomplete="off" clearable />
|
||||
|
||||
<flux:radio.group wire:model.live="form.gender" variant="buttons" class="w-full *:flex-1"
|
||||
label="Jenis Kelamin">
|
||||
@foreach (\App\Enums\Gender::cases() as $gender)
|
||||
<flux:radio value="{{ $gender->value }}" icon="{{ $gender->icon() }}">
|
||||
{{ $gender->label() }}
|
||||
</flux:radio>
|
||||
@endforeach
|
||||
</flux:radio.group>
|
||||
|
||||
<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>
|
||||
@ -43,8 +43,8 @@
|
||||
wire:model.live.debounce.500ms="form.quota"
|
||||
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
|
||||
|
||||
<flux:input label="Batas Per Pembeli"
|
||||
placeholder="Masukkan batas per pembeli"
|
||||
<flux:input label="Batas Per Customer"
|
||||
placeholder="Masukkan batas per customer"
|
||||
wire:model.live.debounce.500ms="form.limit_per_user"
|
||||
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
@ -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 () {
|
||||
|
||||
69
tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php
Normal file
69
tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Loyalty\Customer;
|
||||
use App\Models\Customer as CustomerModel;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('renders successfully', function () {
|
||||
Livewire::test(Customer::class)
|
||||
->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);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user