diff --git a/app/Enums/UserStatus.php b/app/Enums/UserStatus.php index 01986bd..e9546fb 100644 --- a/app/Enums/UserStatus.php +++ b/app/Enums/UserStatus.php @@ -30,4 +30,13 @@ public function color() self::SUSPENDED => 'red', }; } + + public function gradient(): string + { + return match ($this) { + self::ACTIVE => 'bg-gradient-to-b from-emerald-800 to-emerald-400', + self::INACTIVE => 'bg-gradient-to-b from-yellow-800 to-yellow-400', + self::SUSPENDED => 'bg-gradient-to-b from-red-800 to-red-400', + }; + } } diff --git a/app/Livewire/Datatable/Studio/Master/UsersTable.php b/app/Livewire/Datatable/Studio/Master/UsersTable.php deleted file mode 100644 index 444c8ea..0000000 --- a/app/Livewire/Datatable/Studio/Master/UsersTable.php +++ /dev/null @@ -1,192 +0,0 @@ -label( - fn ($row) => << -
{$row->full_name}
- {$row->code} - - HTML - ) - ->searchable( - fn ($query, $searchTerm) => $query->where('full_name', 'like', "%{$searchTerm}%") - ->orWhere('code', 'like', "%{$searchTerm}%") - ) - ->html(), - - Column::make('Kredensial') - ->label( - fn ($row) => << -
{$row?->user?->email}
- {$row?->user?->username} - - HTML - ) - ->searchable( - fn ($query, $searchTerm) => $query->whereHas( - 'user', - fn ($q) => $q->where('email', 'like', "%{$searchTerm}%") - ->orWhere('username', 'like', "%{$searchTerm}%") - ) - ) - ->html(), - - Column::make('Nomor Telepon', 'phone_number')->searchable(), - - Column::make('Jenis Kelamin', 'gender') - ->format(fn ($value) => Blade::render( - '' - .$value->label() - .'' - )) - ->html(), - - Column::make('Kode Referral', 'user.referralCode.code') - ->format( - fn ($value) => $value - ? Blade::render(''.$value.'') - : '' - ) - ->html(), - - Column::make('Gaji Pokok', 'base_salary') - ->searchable() - ->sortable() - ->format(fn ($value) => currency($value, 'Rp')), - - Column::make('Alamat', 'address')->searchable(), - - ArrayColumn::make('Outlet') - ->data(fn ($value, $row) => $row->user->outlets->pluck('name')->toArray()) - ->outputFormat(fn ($index, $value) => Blade::render(''.$value.'')) - ->flexRow(['class' => 'gap-2 flex-wrap']), - - Column::make('Tanggal') - ->label(function ($row) { - $birthdate = formatDate($row->birthdate); - $birthAgo = getAge($row->birthdate); - - $hireDate = formatDate($row?->user?->hire_date); - $openedAgo = timeAgo($row?->user?->hire_date); - - $resigndate = formatDate($row?->user?->resign_date); - $resignAgo = timeAgo($row?->user?->resign_date); - - return << -
- Tgl Lahir: - {$birthdate} - ({$birthAgo}) -
-
- Tgl Bergabung: - {$hireDate} - ({$openedAgo}) -
-
- Tgl Resign: - {$resigndate} - ({$resignAgo}) -
- - HTML; - }) - ->html(), - - Column::make('Status', 'user.status') - ->format( - fn ($value) => Blade::render(''.UserStatus::tryFrom($value)->label().'') - ) - ->html(), - - Column::make('Aksi') - ->label(function ($row) { - $actions = ''; - - if (auth()->id() !== $row?->user?->id) { - if (auth()->user()->can('update user')) { - $actions .= view('components.datatables.edit', [ - 'id' => $row->hash, - 'editRoute' => route('studio.master.user.edit', $row->hash), - ])->render(); - } - - if (auth()->user()->can('delete user')) { - $actions .= view('components.datatables.delete', [ - 'id' => $row->hash, - 'deleteRoute' => route('studio.master.user.delete', $row->hash), - ])->render(); - } - } - - return $actions; - }) - ->html(), - ]; - } - - public function builder(): Builder - { - return Employee::query() - ->select( - 'employees.id', - 'employees.user_id', - 'employees.code', - 'employees.full_name', - 'employees.phone_number', - 'employees.gender', - 'employees.birthdate', - 'employees.base_salary', - 'employees.address' - ) - ->with([ - 'user', - 'user.referralCode', - 'user.outlets', - ]); - } - - public function filters(): array - { - return [ - MultiSelectFilter::make('Status') - ->options( - collect(UserStatus::cases()) - ->mapWithKeys(fn ($case) => [$case->value => $case->label()]) - ->toArray() - ) - ->filter(function (Builder $builder, array $values) { - $builder->whereHas( - 'user', - fn ($q) => $q->whereIn('status', $values) - ); - }), - ]; - } -} diff --git a/app/Livewire/Studio/Master/User/Create.php b/app/Livewire/Studio/Master/User/Create.php index fae94ec..dbd3130 100644 --- a/app/Livewire/Studio/Master/User/Create.php +++ b/app/Livewire/Studio/Master/User/Create.php @@ -31,8 +31,6 @@ public function save() $this->form->store(); - $this->dispatch('refreshDatatable'); - $this->toast('Pegawai berhasil ditambahkan.'); $this->redirectRoute('studio.master.user.index', navigate: true); diff --git a/app/Livewire/Studio/Master/User/Edit.php b/app/Livewire/Studio/Master/User/Edit.php index 744107c..6e40c03 100644 --- a/app/Livewire/Studio/Master/User/Edit.php +++ b/app/Livewire/Studio/Master/User/Edit.php @@ -33,8 +33,6 @@ public function save() $this->form->update(); - $this->dispatch('refreshDatatable'); - $this->toast('Pegawai berhasil diperbarui.'); $this->redirectRoute('studio.master.user.index', navigate: true); diff --git a/app/Livewire/Studio/Master/User/Index.php b/app/Livewire/Studio/Master/User/Index.php index 2ea6c2f..b2c63a6 100644 --- a/app/Livewire/Studio/Master/User/Index.php +++ b/app/Livewire/Studio/Master/User/Index.php @@ -2,6 +2,8 @@ namespace App\Livewire\Studio\Master\User; +use App\Enums\UserStatus; +use App\Models\Employee; use App\Models\User; use App\Traits\WithConfirmation; use App\Traits\WithToast; @@ -14,12 +16,69 @@ class Index extends Component { use WithConfirmation, WithToast; + public array $employees = []; + + public array $status = []; + + public function mount() + { + $this->loadEmployees(); + + $this->status = UserStatus::cases(); + } + + protected function loadEmployees() + { + $this->employees = Employee::with(['user', 'user.referralCode', 'user.outlets']) + ->when(! empty($this->status), fn ($query) => $query->whereHas('user', fn ($q) => $q->whereIn('status', $this->status))) + ->latest() + ->get() + ->map(fn ($employee) => [ + 'full_name' => $employee->full_name, + 'code' => $employee->code, + 'phone_number' => $employee->phone_number, + 'address' => $employee->address, + 'base_salary' => currency($employee->base_salary, 'Rp'), + 'gender' => [ + 'label' => $employee->gender->label(), + 'color' => $employee->gender->color(), + ], + 'birthdate' => formatDate($employee->birthdate), + 'birth_age' => getAge($employee->birthdate), + 'user' => [ + 'id' => $employee->user?->id, + 'hash' => $employee->user?->hash, + 'email' => $employee->user?->email, + 'username' => $employee->user?->username, + 'status' => [ + 'gradient' => $employee->user?->status->gradient(), + ], + 'hire_date' => formatDate($employee->user?->hire_date), + 'hire_ago' => timeAgo($employee->user?->hire_date), + 'resign_date' => formatDate($employee->user?->resign_date), + 'resign_ago' => timeAgo($employee->user?->resign_date), + ], + 'referral_code' => $employee->user?->referralCode?->code, + 'outlets' => $employee->user?->outlets?->pluck('name')->toArray() ?? [], + ]) + ->toArray(); + } + + public function updatedStatus() + { + if (empty($this->status)) { + $this->employees = []; + + return; + } + + $this->loadEmployees(); + } + public function delete(User $user) { $user->delete(); - $this->dispatch('refreshDatatable'); - $this->toast('Pegawai berhasil dihapus.'); Flux::modals()->close(); diff --git a/database/factories/ReferralCodeFactory.php b/database/factories/ReferralCodeFactory.php new file mode 100644 index 0000000..e1e1160 --- /dev/null +++ b/database/factories/ReferralCodeFactory.php @@ -0,0 +1,18 @@ + + */ +class ReferralCodeFactory extends Factory +{ + public function definition(): array + { + return [ + 'code' => $this->faker->unique()->randomNumber(6), + ]; + } +} diff --git a/resources/views/livewire/studio/master/user/index.blade.php b/resources/views/livewire/studio/master/user/index.blade.php index d97f91f..e8a8227 100644 --- a/resources/views/livewire/studio/master/user/index.blade.php +++ b/resources/views/livewire/studio/master/user/index.blade.php @@ -3,18 +3,185 @@
{{ $pageTitle }}
- @if (auth()->user()->can('create user')) + @can('create user')
Tambah
- @endif + @endcan
- +
+ +
+ Status: + @foreach (\App\Enums\UserStatus::cases() as $status) +
+ + {{ $status->label() }} + +
+ @endforeach +
+
+ + + + @foreach (\App\Enums\UserStatus::cases() as $status) + + @endforeach + + +
+ +
+ @foreach ($employees as $employee) + +
+ + {{ $employee['full_name'] }} + +
{{ $employee['code'] }}
+
+ + + +
+
+ Kredensial + +
+ +
+ {{ $employee['user']['email'] }} + {{ $employee['user']['username'] }} +
+
+
+ +
+ Telepon + +
+ + {{ $employee['phone_number'] }} +
+
+ +
+ Alamat + +
+ + {{ $employee['address'] }} +
+
+ +
+
+ Jenis + Kelamin + + {{ $employee['gender']['label'] }} + +
+ +
+ Gaji + + {{ $employee['base_salary'] }} +
+
+ + @if ($employee['referral_code']) +
+ Kode + Referral + {{ $employee['referral_code'] }} +
+ @endif + + @if (count($employee['outlets']) > 0) +
+ Outlet + +
+ @foreach ($employee['outlets'] as $outlet) + + {{ $outlet }} + + @endforeach +
+
+ @endif + +
+ + Informasi + Tanggal +
+
+
+ Tgl Lahir: + {{ $employee['birthdate'] }} + ({{ $employee['birth_age'] }}) +
+
+ Tgl + Bergabung: + {{ $employee['user']['hire_date'] }} + ({{ $employee['user']['hire_ago'] }}) +
+ @if ($employee['user']['resign_date']) +
+ Tgl + Resign: + {{ $employee['user']['resign_date'] }} + ({{ $employee['user']['resign_ago'] }}) +
+ @endif +
+
+
+
+ + @canany(['update user', 'delete user']) + @if (auth()->id() !== $employee['user']['id']) + +
+ @can('update user') + + + + @endcan + + @can('delete user') + + + + + + + @endcan +
+ @endif + @endcanany +
+ @endforeach +
@include('components.confirmation.delete') diff --git a/tests/Feature/Livewire/Studio/Master/User/CreateTest.php b/tests/Feature/Livewire/Studio/Master/User/CreateTest.php index e76dcdc..acee068 100644 --- a/tests/Feature/Livewire/Studio/Master/User/CreateTest.php +++ b/tests/Feature/Livewire/Studio/Master/User/CreateTest.php @@ -1,28 +1,74 @@ user = User::factory()->create(); +}); + +it('renders create user page', function () { + Livewire::actingAs($this->user) + ->test(Create::class) ->assertViewIs('livewire.studio.master.user.form') ->assertViewHas('pageTitle', 'Tambah Pegawai'); }); -it('can create an user', function () { - $user = User::factory()->raw(); +it('mounts outlets correctly', function () { + $outlets = Outlet::factory()->count(3)->create(); - $employee = Employee::factory()->raw(); + $component = Livewire::actingAs($this->user) + ->test(Create::class); - $data = array_merge($user, $employee); + expect($component->outlets)->toHaveCount(3); + foreach ($outlets as $outlet) { + expect($component->outlets)->toHaveKey($outlet->id); + expect($component->outlets[$outlet->id])->toBe($outlet->name); + } +}); - Livewire::test(Create::class) +it('prevents creation when unauthorized', function () { + Gate::define('create user', fn () => false); + + Livewire::actingAs($this->user) + ->test(Create::class) + ->call('save') + ->assertForbidden(); +}); + +it('saves outlet successfully when authorized', function () { + $permission = Permission::create(['name' => 'create user']); + $this->user->givePermissionTo($permission); + + $outlets = Outlet::factory()->count(2)->create(); + + $userRaw = User::factory()->make()->toArray(); + $employeeRaw = Employee::factory()->make()->toArray(); + + $data = array_merge($userRaw, [ + 'full_name' => $employeeRaw['full_name'], + 'phone_number' => $employeeRaw['phone_number'], + 'base_salary' => $employeeRaw['base_salary'], + 'birthdate' => $employeeRaw['birthdate'], + 'hire_date' => $employeeRaw['hire_date'], + 'address' => $employeeRaw['address'], + 'gender' => $employeeRaw['gender'], + 'status' => UserStatus::ACTIVE->value, + 'outlet_ids' => $outlets->pluck('id')->toArray(), + ]); + + Livewire::actingAs($this->user) + ->test(Create::class) ->set('form.full_name', $data['full_name']) ->set('form.username', $data['username']) ->set('form.email', $data['email']) @@ -31,15 +77,26 @@ ->set('form.birthdate', $data['birthdate']) ->set('form.hire_date', $data['hire_date']) ->set('form.address', $data['address']) - ->set('form.gender', $data['gender']->value) - ->set('form.status', $data['status']->value) + ->set('form.gender', $data['gender']) + ->set('form.status', $data['status']) + ->set('form.outlet_ids', $data['outlet_ids']) ->call('save') ->assertHasNoErrors() - ->assertDispatched('refreshDatatable') ->assertRedirect(Index::class); + $user = User::where('email', $data['email'])->first(); + $this->assertDatabaseHas('users', [ - 'email' => $data['email'], + 'id' => $user->id, 'username' => $data['username'], + 'email' => $data['email'], ]); + + $this->assertDatabaseHas('employees', [ + 'user_id' => $user->id, + 'full_name' => $data['full_name'], + 'phone_number' => $data['phone_number'], + ]); + + expect($user->outlets->pluck('id')->toArray())->toMatchArray($data['outlet_ids']); }); diff --git a/tests/Feature/Livewire/Studio/Master/User/EditTest.php b/tests/Feature/Livewire/Studio/Master/User/EditTest.php index f85d93f..8e9bab9 100644 --- a/tests/Feature/Livewire/Studio/Master/User/EditTest.php +++ b/tests/Feature/Livewire/Studio/Master/User/EditTest.php @@ -1,53 +1,114 @@ has(Employee::factory()) - ->create(); +beforeEach(function () { + $this->admin = User::factory()->create(); +}); - Livewire::test(Edit::class, ['user' => $user]) +function mountEditUser(User $admin, User $user) +{ + return Livewire::actingAs($admin)->test(Edit::class, ['user' => $user]); +} + +it('renders page successfully', function () { + $userToEdit = User::factory()->has(Employee::factory())->create(); + + mountEditUser($this->admin, $userToEdit) ->assertViewIs('livewire.studio.master.user.form') ->assertViewHas('pageTitle', 'Ubah Pegawai'); }); -it('can update an user', function () { - $existingUser = User::factory() - ->has(Employee::factory()) - ->create(); +it('mounts form with existing user data', function () { + $userToEdit = User::factory()->has(Employee::factory())->create(); - $user = User::factory()->raw(); - $employee = Employee::factory()->raw(); + $component = mountEditUser($this->admin, $userToEdit); - $data = array_merge($user, $employee); + expect($component->form->username)->toBe($userToEdit->username); + expect($component->form->email)->toBe($userToEdit->email); + expect($component->form->full_name)->toBe($userToEdit->employee->full_name); + expect($component->form->phone_number)->toBe($userToEdit->employee->phone_number); + expect($component->form->outlet_ids)->toBe($userToEdit->outlets->pluck('id')->toArray()); +}); - Livewire::test(Edit::class, ['user' => $existingUser]) - ->set('form.full_name', $data['full_name']) +it('prevents update when user is unauthorized', function () { + $userToEdit = User::factory()->has(Employee::factory())->create(); + Gate::define('update user', fn () => false); + + mountEditUser($this->admin, $userToEdit) + ->call('save') + ->assertForbidden(); +}); + +it('updates user successfully when authorized', function () { + $permission = Permission::create(['name' => 'update user']); + $this->admin->givePermissionTo($permission); + + $userToEdit = User::factory()->has(Employee::factory())->create(); + $outlets = Outlet::factory()->count(3)->create(); + + $userRaw = User::factory()->raw(); + $employeeRaw = Employee::factory()->raw(); + + $data = array_merge($userRaw, $employeeRaw, [ + 'outlet_ids' => $outlets->pluck('id')->toArray(), + 'status' => UserStatus::ACTIVE->value, + 'gender' => $employeeRaw['gender'] ?? Gender::MALE->value, + ]); + + $data = array_merge($data, [ + 'full_name' => $data['full_name'], + 'phone_number' => $data['phone_number'], + 'outlet_ids' => $outlets->pluck('id')->toArray(), + 'status' => UserStatus::ACTIVE->value, + 'gender' => Gender::MALE->value, + 'birthdate' => $data['birthdate'], + 'hire_date' => $data['hire_date'], + 'address' => $data['address'], + 'base_salary' => $data['base_salary'], + ]); + + mountEditUser($this->admin, $userToEdit) ->set('form.username', $data['username']) ->set('form.email', $data['email']) + ->set('form.full_name', $data['full_name']) ->set('form.phone_number', $data['phone_number']) - ->set('form.base_salary', $data['base_salary']) + ->set('form.address', $data['address']) ->set('form.birthdate', $data['birthdate']) ->set('form.hire_date', $data['hire_date']) - ->set('form.address', $data['address']) - ->set('form.gender', $data['gender']->value) - ->set('form.status', $data['status']->value) + ->set('form.base_salary', $data['base_salary']) + ->set('form.gender', $data['gender']) + ->set('form.status', $data['status']) + ->set('form.outlet_ids', $data['outlet_ids']) ->call('save') ->assertHasNoErrors() - ->assertDispatched('refreshDatatable') ->assertRedirect(Index::class); + $userToEdit->refresh(); + $this->assertDatabaseHas('users', [ - 'id' => $existingUser->id, - 'email' => $data['email'], + 'id' => $userToEdit->id, 'username' => $data['username'], + 'email' => $data['email'], ]); + + $this->assertDatabaseHas('employees', [ + 'user_id' => $userToEdit->id, + 'full_name' => $data['full_name'], + 'phone_number' => $data['phone_number'], + ]); + + expect($userToEdit->outlets->pluck('id')->toArray())->toBe($data['outlet_ids']); }); diff --git a/tests/Feature/Livewire/Studio/Master/User/IndexTest.php b/tests/Feature/Livewire/Studio/Master/User/IndexTest.php index 1b4cea0..4c4a297 100644 --- a/tests/Feature/Livewire/Studio/Master/User/IndexTest.php +++ b/tests/Feature/Livewire/Studio/Master/User/IndexTest.php @@ -1,39 +1,68 @@ user = User::factory()->create(); +}); + +it('renders user index page successfully', function () { + Livewire::actingAs($this->user) + ->test(Index::class) ->assertViewIs('livewire.studio.master.user.index') ->assertViewHas('pageTitle', 'Pegawai'); }); -it('displays all users', function () { - User::factory() - ->count(10) +it('mounts employees correctly', function () { + $userWithEmployee = User::factory() + ->has(Employee::factory()) + ->has(ReferralCode::factory()) + ->create(); + + $employee = $userWithEmployee->employee; + + Livewire::actingAs($this->user) + ->test(Index::class) + ->assertSee($employee->full_name) + ->assertSet('employees.0.full_name', $employee->full_name) + ->assertSet('employees.0.user.id', $userWithEmployee->id) + ->assertSet('employees.0.referral_code', $userWithEmployee?->referralCode?->code); +}); + +it('filters employees by status', function () { + $activeUser = User::factory()->create(['status' => UserStatus::ACTIVE->value]); + $inactiveUser = User::factory()->create(['status' => UserStatus::INACTIVE->value]); + + $activeUser->employee()->save(Employee::factory()->make()); + $inactiveUser->employee()->save(Employee::factory()->make()); + + $component = Livewire::actingAs($this->user) + ->test(Index::class); + + $component->set('status', [UserStatus::ACTIVE->value]); + + $employees = $component->employees; + expect(count($employees))->toBe(1); + expect($employees[0]['user']['id'])->toBe($activeUser->id); +}); + +it('deletes a user successfully', function () { + $userToDelete = User::factory() ->has(Employee::factory()) ->create(); - $user = User::first(); + Livewire::actingAs($this->user) + ->test(Index::class) + ->call('delete', $userToDelete) + ->assertHasNoErrors(); - Livewire::test(Index::class)->assertSee($user->email); -}); - -it('can delete an user', function () { - $user = User::factory() - ->has(Employee::factory()) - ->create(); - - Livewire::test(Index::class) - ->call('delete', $user) - ->assertHasNoErrors() - ->assertDispatched('refreshDatatable'); - - expect(User::count())->toBe(0); + $this->assertSoftDeleted('users', ['id' => $userToDelete->id]); });