refactor(user): memisahkan menjadi tangung jawab " kecil

- menghapus map dan toArray()
- menyesuaikan view dengan perubahan tersebut
- menyesuaikan testing
- kasih return type
This commit is contained in:
Yoga Pangestu 2025-12-09 15:52:38 +07:00
parent c41d1b1fb2
commit a99d7863b9
9 changed files with 423 additions and 596 deletions

View File

@ -37,13 +37,20 @@ class UserForm extends Form
public string $gender = '';
public string $status = '1';
public int $status = UserStatus::ACTIVE->value;
/** @var array<int> */
public array $outlet_ids = [];
/** @var array<int> */
public array $role_ids = [];
public function rules(): array
{
return array_merge($this->getBasicRules(), $this->getRelationshipRules());
}
private function getBasicRules(): array
{
return [
'full_name' => ['required', 'string', 'max:100'],
@ -56,6 +63,12 @@ public function rules(): array
'address' => ['nullable', 'string'],
'gender' => ['required', Rule::in(Gender::cases())],
'status' => ['required', Rule::in(UserStatus::cases())],
];
}
private function getRelationshipRules(): array
{
return [
'outlet_ids' => ['required', 'array', 'min:1'],
'outlet_ids.*' => Rule::exists('outlets', 'id'),
'role_ids' => ['required', 'array', 'min:1'],
@ -81,11 +94,19 @@ public function validationAttributes(): array
];
}
public function setUser(User $user)
public function setUser(User $user): void
{
$user->load(['employee', 'outlets']);
$user->load(['employee', 'outlets', 'roles']);
$this->user = $user;
$this->fillBasicAttributes($user);
$this->outlet_ids = $user->outlets->pluck('id')->toArray();
$this->role_ids = $user->roles->pluck('id')->toArray();
}
private function fillBasicAttributes(User $user): void
{
$employee = $user->employee;
$this->full_name = $employee->full_name;
@ -98,88 +119,112 @@ public function setUser(User $user)
$this->address = $employee->address;
$this->gender = $employee->gender->value;
$this->status = $user->status->value;
$this->outlet_ids = $user->outlets->pluck('id')->toArray();
$this->role_ids = $user->roles->pluck('id')->toArray();
}
public function store()
public function store(): void
{
$this->validate();
$data = $this->all();
if ($data['status'] != UserStatus::ACTIVE) {
$data['resign_date'] = now();
} else {
$data['resign_date'] = null;
}
$data = $this->prepareDataForSave();
DB::transaction(function () use ($data) {
$user = User::create([
...$data,
'password' => Hash::make(config('myconfig.password_default')),
'referral_code' => generateReferralCode($data['username'], User::max('id') + 1),
'status' => $data['status'],
]);
Employee::create([
'user_id' => $user->id,
'full_name' => $data['full_name'],
'code' => Employee::generateEmployeeCode(),
'phone_number' => $data['phone_number'],
'base_salary' => replaceCurrency($data['base_salary']),
'birthdate' => $data['birthdate'],
'hire_date' => $data['hire_date'],
'resign_date' => $data['resign_date'],
'address' => $data['address'],
'gender' => $data['gender'],
]);
ReferralCode::create([
'user_id' => $user->id,
'code' => generateReferralCode($data['username'], $user->id),
]);
$user->outlets()->attach($this->outlet_ids);
$user->assignRole(array_map('intval', $this->role_ids));
$user = $this->createUser($data);
$this->createEmployee($user, $data);
$this->createReferralCode($user, $data);
$this->assignOutletsAndRoles($user);
});
}
public function update()
private function prepareDataForSave(): array
{
$data = $this->all();
$data['resign_date'] = $data['status'] != UserStatus::ACTIVE->value
? now()
: null;
return $data;
}
private function createUser(array $data): User
{
return User::create([
'username' => $data['username'],
'email' => $data['email'],
'password' => Hash::make(config('myconfig.password_default')),
'status' => $data['status'],
]);
}
private function createEmployee(User $user, array $data): void
{
Employee::create([
'user_id' => $user->id,
'full_name' => $data['full_name'],
'code' => Employee::generateEmployeeCode(),
'phone_number' => $data['phone_number'],
'base_salary' => replaceCurrency($data['base_salary']),
'birthdate' => $data['birthdate'],
'hire_date' => $data['hire_date'],
'resign_date' => $data['resign_date'],
'address' => $data['address'],
'gender' => $data['gender'],
]);
}
private function createReferralCode(User $user, array $data): void
{
ReferralCode::create([
'user_id' => $user->id,
'code' => generateReferralCode($data['username'], $user->id),
]);
}
private function assignOutletsAndRoles(User $user): void
{
$user->outlets()->attach($this->outlet_ids);
$user->assignRole(array_map('intval', $this->role_ids));
}
public function update(): void
{
$this->validate();
$data = $this->all();
if ($data['status'] == UserStatus::INACTIVE->value) {
$data['resign_date'] = now();
} else {
$data['resign_date'] = null;
}
$data = $this->prepareDataForSave();
DB::transaction(function () use ($data) {
$this->user->update([
...$data,
'status' => $data['status'],
]);
$this->user->employee->update([
'user_id' => $this->user->id,
'full_name' => $data['full_name'],
'phone_number' => $data['phone_number'],
'base_salary' => replaceCurrency($data['base_salary']),
'birthdate' => $data['birthdate'],
'hire_date' => $data['hire_date'],
'resign_date' => $data['resign_date'],
'address' => $data['address'],
'gender' => $data['gender'],
'resign_date' => $data['resign_date'],
]);
$this->user->outlets()->sync($this->outlet_ids);
$this->user->syncRoles(array_map('intval', $this->role_ids));
$this->updateUser($data);
$this->updateEmployee($data);
$this->syncOutletsAndRoles();
});
}
private function updateUser(array $data): void
{
$this->user->update([
'username' => $data['username'],
'email' => $data['email'],
'status' => $data['status'],
]);
}
private function updateEmployee(array $data): void
{
$this->user->employee->update([
'full_name' => $data['full_name'],
'phone_number' => $data['phone_number'],
'base_salary' => replaceCurrency($data['base_salary']),
'birthdate' => $data['birthdate'],
'hire_date' => $data['hire_date'],
'resign_date' => $data['resign_date'],
'address' => $data['address'],
'gender' => $data['gender'],
]);
}
private function syncOutletsAndRoles(): void
{
$this->user->outlets()->sync($this->outlet_ids);
$this->user->syncRoles(array_map('intval', $this->role_ids));
}
}

View File

@ -9,6 +9,7 @@
use App\Traits\WithRoleSelector;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
use Spatie\Permission\Models\Role;
@ -24,14 +25,14 @@ class Create extends Component
public array $roles = [];
public function mount()
public function mount(): void
{
$this->outlets = Outlet::pluck('name', 'id')->toArray();
$this->roles = Role::where('name', '!=', 'Developer')->orderBy('id')->pluck('name', 'id')->toArray();
}
public function save()
public function save(): void
{
$this->canOrAbort('create user');
@ -42,7 +43,7 @@ public function save()
$this->redirectRoute('studio.master.user.index');
}
public function render()
public function render(): View
{
return view('livewire.studio.master.user.form', [
'pageTitle' => 'Tambah Pegawai',

View File

@ -10,6 +10,7 @@
use App\Traits\WithRoleSelector;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
use Spatie\Permission\Models\Role;
@ -17,7 +18,7 @@
#[Title('Ubah Pegawai')]
class Edit extends Component
{
use WithAuthorization, WithOutletSelector, WithRoleSelector, WithToast,WithUpdatedData;
use WithAuthorization, WithOutletSelector, WithRoleSelector, WithToast, WithUpdatedData;
public UserForm $form;
@ -25,7 +26,7 @@ class Edit extends Component
public array $roles = [];
public function mount(User $user)
public function mount(User $user): void
{
$this->form->setUser($user);
@ -34,7 +35,7 @@ public function mount(User $user)
$this->roles = Role::where('name', '!=', 'Developer')->orderBy('id')->pluck('name', 'id')->toArray();
}
public function save()
public function save(): void
{
$this->canOrAbort('update user');
@ -45,7 +46,7 @@ public function save()
$this->redirectRoute('studio.master.user.index');
}
public function render()
public function render(): View
{
return view('livewire.studio.master.user.form', [
'pageTitle' => 'Ubah Pegawai',

View File

@ -8,7 +8,9 @@
use App\Traits\WithConfirmation;
use App\Traits\WithToast;
use Flux\Flux;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Hash;
use Illuminate\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
@ -17,18 +19,19 @@ class Index extends Component
{
use WithConfirmation, WithSubscribeNotification, WithToast;
public array $employees = [];
/** @var Collection<int, \App\Models\Employee> */
public Collection $employees;
public string $search = '';
public array $status = [];
public function mount()
public function mount(): void
{
$this->loadEmployees();
}
protected function loadEmployees()
protected function loadEmployees(): void
{
$this->employees = Employee::with(['user', 'user.referralCode', 'user.outlets'])
->when(! empty($this->search), function ($query) {
@ -40,52 +43,22 @@ protected function loadEmployees()
->when(! empty($this->status), fn ($query) => $query->whereHas('user', fn ($q) => $q->whereIn('status', $this->status)))
->withoutDeveloper()
->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),
'hire_date' => formatDate($employee->hire_date),
'hire_ago' => timeAgo($employee->hire_date),
'resign_date' => formatDate($employee->resign_date),
'resign_ago' => timeAgo($employee->resign_date),
'user' => [
'id' => $employee->user?->id,
'hash' => $employee->user?->hash,
'email' => $employee->user?->email,
'username' => $employee->user?->username,
'status' => [
'gradient' => $employee->user?->status->gradient(),
],
],
'referral_code' => $employee->user?->referralCode?->code,
'outlets' => $employee->user?->outlets?->pluck('name')->toArray() ?? [],
'roles' => $employee->user?->roles?->pluck('name')->toArray() ?? [],
])
->toArray();
->get();
}
public function updatedSearch(string $value)
public function updatedSearch(string $value): void
{
$this->search = $value;
$this->loadEmployees();
}
public function updatedStatus()
public function updatedStatus(): void
{
$this->loadEmployees();
}
public function delete(User $user)
public function delete(User $user): void
{
$user->delete();
@ -96,7 +69,7 @@ public function delete(User $user)
Flux::modals()->close();
}
public function resetPassword(User $user)
public function resetPassword(User $user): void
{
$user->update(['password' => Hash::make(config('myconfig.password_default'))]);
@ -105,7 +78,7 @@ public function resetPassword(User $user)
Flux::modals()->close();
}
public function render()
public function render(): View
{
return view('livewire.studio.master.user.index', [
'pageTitle' => 'Pegawai',

View File

@ -27,7 +27,7 @@ class="text-sm">
<flux:separator class="mb-6" />
@if (!empty($outlets))
@if ($outlets->isNotEmpty())
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-start">
@foreach ($outlets as $outlet)
<flux:card

View File

@ -35,17 +35,17 @@ class="text-sm">
<flux:separator class="mb-6" />
@if (!empty($employees))
@if ($employees->isNotEmpty())
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 items-start">
@foreach ($employees as $employee)
<flux:card
class="relative space-y-4 transition-transform duration-300 ease-out hover:-translate-y-1 hover:shadow-lg">
<div
class="flex flex-col items-center text-center mb-4 p-4 rounded-t-lg {{ $employee['user']['status']['gradient'] }}">
<flux:avatar circle size="xl" name="{{ $employee['full_name'] }}" class="mb-3" />
<flux:heading size="lg" class="mb-1 text-white">{{ $employee['full_name'] }}
class="flex flex-col items-center text-center mb-4 p-4 rounded-t-lg {{ $employee->user?->status?->gradient() }}">
<flux:avatar circle size="xl" name="{{ $employee->full_name }}" class="mb-3" />
<flux:heading size="lg" class="mb-1 text-white">{{ $employee->full_name }}
</flux:heading>
<div class="text-sm text-white/80 mb-2">{{ $employee['code'] }}</div>
<div class="text-sm text-white/80 mb-2">{{ $employee->code }}</div>
</div>
<flux:separator></flux:separator>
@ -59,8 +59,9 @@ class="flex flex-col items-center text-center mb-4 p-4 rounded-t-lg {{ $employee
<flux:icon.envelope
class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<div class="text-gray-700 dark:text-gray-300">
<flux:text>{{ $employee['user']['email'] }}</flux:text>
<flux:text class="text-xs">{{ $employee['user']['username'] }}</flux:text>
<flux:text>{{ $employee->user?->email }}</flux:text>
<flux:text class="text-xs">{{ $employee->user?->username }}
</flux:text>
</div>
</div>
</div>
@ -70,7 +71,7 @@ class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
</flux:heading>
<div class="flex items-center gap-2 text-sm">
<flux:icon.phone class="w-4 h-4 text-gray-500 dark:text-white" />
<flux:text>{{ $employee['phone_number'] }}</flux:text>
<flux:text>{{ $employee->phone_number }}</flux:text>
</div>
</div>
@ -82,7 +83,7 @@ class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<div class="flex items-start gap-2 text-sm">
<flux:icon.map-pin
class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<flux:text>{{ $employee['address'] }}</flux:text>
<flux:text>{{ $employee->address }}</flux:text>
</div>
</div>
@endif
@ -92,15 +93,15 @@ class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<flux:heading class="uppercase font-semibold text-gray-500 dark:text-gray-400 ">
Jenis
Kelamin</flux:heading>
<flux:badge color="{{ $employee['gender']['color'] }}" size="sm">
{{ $employee['gender']['label'] }}
<flux:badge color="{{ $employee->gender->color() }}" size="sm">
{{ $employee->gender->label() }}
</flux:badge>
</div>
<div>
<flux:heading class="uppercase font-semibold text-gray-500 dark:text-gray-400 ">Gaji
</flux:heading>
<flux:text>{{ $employee['base_salary'] }}</flux:text>
<flux:text>{{ currency($employee->base_salary, 'Rp') }}</flux:text>
</div>
</div>
@ -108,7 +109,7 @@ class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<div>
<flux:heading class="uppercase font-semibold text-gray-500 dark:text-gray-400 ">Kode
Referral</flux:heading>
<flux:badge color="zinc" size="sm">{{ $employee['referral_code'] }}
<flux:badge color="zinc" size="sm">{{ $employee->refferal_code }}
</flux:badge>
</div>
@endif
@ -117,9 +118,9 @@ class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<flux:heading class="uppercase font-semibold text-gray-500 dark:text-gray-400 ">Outlet
</flux:heading>
<div class="flex flex-wrap gap-2">
@foreach ($employee['outlets'] as $outlet)
@foreach ($employee->user?->outlets as $outlet)
<span class="text-xs text-gray-400 border border-gray-400 rounded px-2 py-1">
{{ $outlet }}
{{ $outlet->name }}
</span>
@endforeach
</div>
@ -129,9 +130,9 @@ class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<flux:heading class="uppercase font-semibold text-gray-500 dark:text-gray-400 ">Peran
</flux:heading>
<div class="flex flex-wrap gap-2">
@foreach ($employee['roles'] as $role)
@foreach ($employee->user?->roles as $role)
<span class="text-xs text-gray-400 border border-gray-400 rounded px-2 py-1">
{{ $role }}
{{ $role->name }}
</span>
@endforeach
</div>
@ -146,23 +147,24 @@ class="w-4 h-4 text-gray-500 dark:text-white flex-shrink-0 mt-0.5" />
<div>
<span class="font-medium text-gray-900 dark:text-white">Tgl Lahir:</span>
<span
class="text-gray-700 dark:text-gray-300">{{ $employee['birthdate'] }}</span>
<span class="text-gray-500">({{ $employee['birth_age'] }})</span>
class="text-gray-700 dark:text-gray-300">{{ formatDate($employee->birthdate) }}</span>
<span class="text-gray-500">({{ getAge($employee->birthdate) }})</span>
</div>
<div>
<span class="font-medium text-gray-900 dark:text-white">Tgl
Bergabung:</span>
<span
class="text-gray-700 dark:text-gray-300">{{ $employee['hire_date'] }}</span>
<span class="text-gray-500">({{ $employee['hire_ago'] }})</span>
class="text-gray-700 dark:text-gray-300">{{ formatDate($employee->hire_date) }}</span>
<span class="text-gray-500">({{ timeAgo($employee->hire_date) }})</span>
</div>
@if ($employee['resign_date'])
@if ($employee->resign_date)
<div>
<span class="font-medium text-gray-900 dark:text-white">Tgl
Resign:</span>
<span
class="text-gray-700 dark:text-gray-300">{{ $employee['resign_date'] }}</span>
<span class="text-gray-500">({{ $employee['resign_ago'] }})</span>
class="text-gray-700 dark:text-gray-300">{{ formatDate($employee->resign_date) }}</span>
<span
class="text-gray-500">({{ timeAgo($employee->resign_date) }})</span>
</div>
@endif
</div>
@ -170,15 +172,15 @@ class="text-gray-700 dark:text-gray-300">{{ $employee['resign_date'] }}</span>
</div>
</div>
@canany(['update user', 'delete user'])
@if (auth()->id() !== $employee['user']['id'])
@canany(['reset password user', 'update user', 'delete user'])
@if (auth()->id() !== $employee->user_id)
<flux:separator></flux:separator>
<div class="flex gap-2 pt-2">
@can('reset password user')
<flux:modal.trigger name="reset-password">
<flux:modal.trigger name="reset-password-confirmation">
<flux:tooltip content="Reset Kata Sandi">
<flux:button variant="primary" color="blue" icon="lock-open" size="sm"
wire:click="$dispatch('fn:confirmAction', { id: '{{ $employee['user']['hash'] }}', title: 'Reset Kata Sandi', message: 'Anda yakin ingin mereset kata sandi pegawai ini menjadi {{ config('myconfig.password_default') }}?' ,confirmButtonText: 'Ya, Reset' })">
wire:click="$dispatch('fn:confirmAction', { id: '{{ $employee->user?->hash }}' })">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@ -186,8 +188,7 @@ class="text-gray-700 dark:text-gray-300">{{ $employee['resign_date'] }}</span>
@can('update user')
<flux:tooltip content="Ubah">
<flux:button
href="{{ route('studio.master.user.edit', $employee['user']['hash']) }}"
<flux:button href="{{ route('studio.master.user.edit', $employee->user?->hash) }}"
wire:navigate.hover variant="primary" color="yellow" icon="pencil-square"
size="sm"></flux:button>
</flux:tooltip>
@ -197,7 +198,7 @@ class="text-gray-700 dark:text-gray-300">{{ $employee['resign_date'] }}</span>
<flux:modal.trigger name="delete-confirmation">
<flux:tooltip content="Hapus">
<flux:button variant="danger" icon="trash" size="sm"
wire:click="$dispatch('fn:confirmAction', {id: '{{ $employee['user']['hash'] }}'})">
wire:click="$dispatch('fn:confirmAction', {id: '{{ $employee->user?->hash }}'})">
</flux:button>
</flux:tooltip>
</flux:modal.trigger>
@ -210,24 +211,30 @@ class="text-gray-700 dark:text-gray-300">{{ $employee['resign_date'] }}</span>
</div>
@else
@include('components.animations.lottie.not-found')
@endif
</div>
@include('components.modals.confirmation', [
'modalName' => 'confirmation-modal',
'modalName' => 'reset-password-confirmation',
'modalTitle' => 'Reset Kata Sandi',
'modalMessage' =>
'Anda yakin ingin mereset kata sandi pegawai ini menjadi ' . config('myconfig.password_default') . '?',
'buttonVariant' => 'primary',
'buttonColor' => 'reset',
'buttonText' => 'Ya, Hapus',
'target' => 'resetPassword',
])
@include('components.modals.confirmation', [
'modalName' => 'delete-confirmation',
'modalTitle' => 'Apakah Anda yakin?',
'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.',
'buttonVariant' => 'primary',
'buttonColor' => 'danger',
'buttonText' => 'Ya, Hapus',
])
@include('components.modals.confirmation', [
'modalName' => 'reset-password',
'confirmButtonColor' => 'danger',
'target' => 'resetPassword',
])
</flux:main>
@assets
<script src="https://unpkg.com/@lottiefiles/dotlottie-wc@0.8.5/dist/dotlottie-wc.js" type="module"></script>
@endassets

View File

@ -10,6 +10,7 @@
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
@ -17,18 +18,13 @@
uses(RefreshDatabase::class);
beforeEach(function () {
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->map(fn ($role) => Role::create(['name' => $role]));
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
$this->user->roles()->attach($roles->pluck('id'));
});
function mountCreateComponent(User $user)
function mountCreateComponent(User $user): Testable
{
return Livewire::actingAs($user)->test(Create::class);
}
@ -39,56 +35,53 @@ function mountCreateComponent(User $user)
->assertViewHas('pageTitle', 'Tambah Pegawai');
});
it('mounts outlets and roles correctly', function () {
$outlet1 = Outlet::factory()->create(['name' => 'Outlet A']);
$outlet2 = Outlet::factory()->create(['name' => 'Outlet B']);
it('loads outlets and roles on mount', function () {
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$component = mountCreateComponent($this->user);
expect($component->outlets)->toBeArray();
expect($component->outlets)->toHaveKeys([$outlet1->id, $outlet2->id]);
expect($component->outlets[$outlet1->id])->toBe('Outlet A');
expect($component->outlets[$outlet2->id])->toBe('Outlet B');
expect($component->outlets)->toHaveKey($outlet->id);
expect($component->roles)->toBeArray();
expect($component->roles)->not->toHaveKey(Role::where('name', 'Developer')->first()->id);
expect($component->roles)->toHaveKey($role->id);
});
it('excludes developer role from roles list', function () {
Role::create(['name' => 'Developer']);
$adminRole = Role::create(['name' => 'Admin']);
$component = mountCreateComponent($this->user);
$developerRole = Role::where('name', 'Developer')->first();
expect($component->roles)->not->toHaveKey($developerRole->id);
expect($component->roles)->not->toHaveKey(Role::where('name', 'Developer')->first()->id);
expect($component->roles)->toHaveKey($adminRole->id);
});
it('prevents create when user is unauthorized', function () {
Gate::define('create user', fn () => false);
Gate::define('create user', fn() => false);
mountCreateComponent($this->user)
->call('save')
->assertForbidden();
});
it('saves user successfully when authorized', function () {
it('creates user successfully when authorized', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$defaultPassword = config('myconfig.password_default');
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'John Doe')
->set('form.username', 'johndoe')
->set('form.email', 'john@example.com')
->set('form.phone_number', '0812 3456 7890')
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.address', 'Jl. Test No. 123')
->set('form.base_salary', '5000000')
->set('form.birthdate', now()->subYears(25)->format('Y-m-d'))
->set('form.hire_date', now()->subMonths(6)->format('Y-m-d'))
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
@ -99,23 +92,20 @@ function mountCreateComponent(User $user)
$createdUser = User::where('email', 'john@example.com')->first();
expect($createdUser)->not->toBeNull();
expect($createdUser->username)->toBe('johndoe');
expect($createdUser->status)->toBe(UserStatus::ACTIVE);
expect(Hash::check($defaultPassword, $createdUser->password))->toBeTrue();
expect($createdUser)->not->toBeNull()
->and($createdUser->username)->toBe('johndoe')
->and($createdUser->status)->toBe(UserStatus::ACTIVE)
->and(Hash::check($defaultPassword, $createdUser->password))->toBeTrue();
$this->assertDatabaseHas('employees', [
'user_id' => $createdUser->id,
'full_name' => 'John Doe',
'phone_number' => '0812 3456 7890',
'gender' => Gender::MALE,
]);
expect($createdUser->outlets->pluck('id')->toArray())->toContain($outlet->id);
expect($createdUser->roles->pluck('id')->toArray())->toContain($role->id);
expect($createdUser->referralCode)->not->toBeNull();
expect($createdUser->referralCode->code)->toBeString();
expect($createdUser->outlets)->toHaveCount(1)
->and($createdUser->roles)->toHaveCount(1)
->and($createdUser->referralCode)->not->toBeNull();
});
it('creates user with inactive status and sets resign date', function () {
@ -123,7 +113,7 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -157,7 +147,7 @@ function mountCreateComponent(User $user)
$outlet1 = Outlet::factory()->create();
$outlet2 = Outlet::factory()->create();
$outlet3 = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -188,8 +178,8 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$adminRole = Role::where('name', 'Admin')->first();
$leaderRole = Role::where('name', 'Leader')->first();
$adminRole = Role::create(['name' => 'Admin']);
$leaderRole = Role::create(['name' => 'Leader']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -220,8 +210,7 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
$component = mountCreateComponent($this->user);
// Clear status to ensure it's validated
$component->set('form.status', '');
// Don't set any required fields to ensure they're validated
$component->call('save')
->assertHasErrors([
'form.full_name',
@ -232,7 +221,6 @@ function mountCreateComponent(User $user)
'form.birthdate',
'form.hire_date',
'form.gender',
'form.status',
'form.outlet_ids',
'form.role_ids',
]);
@ -326,7 +314,7 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.email', str_repeat('a', 90).'@example.com')
->set('form.email', str_repeat('a', 90) . '@example.com')
->call('save')
->assertHasErrors(['form.email']);
});
@ -433,7 +421,7 @@ function mountCreateComponent(User $user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->set('form.status', '')
->set('form.status', 99) // Invalid enum value
->call('save')
->assertHasErrors(['form.status']);
});
@ -541,7 +529,7 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -573,7 +561,7 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -604,7 +592,7 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -635,7 +623,7 @@ function mountCreateComponent(User $user)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');

View File

@ -9,6 +9,7 @@
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
@ -16,44 +17,31 @@
uses(RefreshDatabase::class);
beforeEach(function () {
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->map(fn ($role) => Role::create(['name' => $role]));
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
$this->user->roles()->attach($roles->pluck('id'));
});
function createEditTestUser(array $userAttributes = [], array $employeeAttributes = [])
function createEditUser(array $attributes = []): User
{
$factory = User::factory();
// Ensure status is set if not provided
if (! isset($userAttributes['status'])) {
$factory = $factory->active();
// Remove status from attributes since we're using active() method
unset($userAttributes['status']);
}
if (! empty($userAttributes)) {
$factory = $factory->state($userAttributes);
}
return $factory
->has(Employee::factory()->state($employeeAttributes))
return User::factory()
->active()
->has(Employee::factory())
->state(fn() => array_merge([
'status' => fake()->randomElement(UserStatus::cases()),
], $attributes))
->create();
}
function mountEditComponent(User $user, User $employeeUser)
function mountEditComponent(User $user, User $employeeUser): Testable
{
return Livewire::actingAs($user)->test(Edit::class, ['user' => $employeeUser]);
}
it('renders page successfully', function () {
$employeeUser = createEditTestUser();
$employeeUser = createEditUser();
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Admin']);
$employeeUser->assignRole('Admin');
mountEditComponent($this->user, $employeeUser)
@ -61,55 +49,49 @@ function mountEditComponent(User $user, User $employeeUser)
->assertViewHas('pageTitle', 'Ubah Pegawai');
});
it('mounts outlets and roles correctly', function () {
$employeeUser = createEditTestUser();
$employeeUser->assignRole('Admin');
$outlet1 = Outlet::factory()->create(['name' => 'Outlet A']);
$outlet2 = Outlet::factory()->create(['name' => 'Outlet B']);
it('loads outlets and roles on mount', function () {
$employeeUser = createEditUser();
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$component = mountEditComponent($this->user, $employeeUser);
expect($component->outlets)->toBeArray();
expect($component->outlets)->toHaveKeys([$outlet1->id, $outlet2->id]);
expect($component->outlets[$outlet1->id])->toBe('Outlet A');
expect($component->outlets[$outlet2->id])->toBe('Outlet B');
expect($component->outlets)->toHaveKey($outlet->id);
expect($component->roles)->toBeArray();
expect($component->roles)->not->toHaveKey(Role::where('name', 'Developer')->first()->id);
expect($component->roles)->toHaveKey($role->id);
});
it('loads existing user data into form', function () {
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::create(['name' => 'Admin']);
$employeeUser = createEditTestUser(
[
'username' => 'existinguser',
'email' => 'existing@example.com',
'status' => UserStatus::ACTIVE,
],
[
'full_name' => 'Existing User',
'phone_number' => '0812 3456 7890',
'base_salary' => 5000000,
'birthdate' => '1995-01-15',
'hire_date' => '2020-01-01',
'address' => 'Existing Address',
'gender' => Gender::MALE,
]
);
$employeeUser = createEditUser([
'username' => 'existinguser',
'email' => 'existing@example.com',
'status' => UserStatus::ACTIVE,
]);
$employeeUser->employee->update([
'full_name' => 'Existing User',
'phone_number' => '081234567890',
'base_salary' => 5000000,
'birthdate' => '1995-01-15',
'hire_date' => '2020-01-01',
'address' => 'Existing Address',
'gender' => Gender::MALE,
]);
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet->id);
$component = mountEditComponent($this->user, $employeeUser);
expect($component->form->full_name)->toBe('Existing User');
expect($component->form->username)->toBe('existinguser');
expect($component->form->email)->toBe('existing@example.com');
expect($component->form->phone_number)->toBe('0812 3456 7890');
expect($component->form->base_salary)->toBe('5000000');
expect($component->form->full_name)->toBe('Existing User')
->and($component->form->username)->toBe('existinguser')
->and($component->form->email)->toBe('existing@example.com')
->and($component->form->phone_number)->toBe('081234567890')
->and($component->form->base_salary)->toBe('5000000');
expect($component->form->birthdate)->toBe('1995-01-15');
expect($component->form->hire_date)->toBe('2020-01-01');
expect($component->form->address)->toBe('Existing Address');
@ -120,8 +102,12 @@ function mountEditComponent(User $user, User $employeeUser)
});
it('excludes developer role from roles list', function () {
$employeeUser = createEditTestUser();
$employeeUser->assignRole('Admin');
Role::firstOrCreate(['name' => 'Admin']);
Role::firstOrCreate(['name' => 'Developer']);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$component = mountEditComponent($this->user, $employeeUser);
@ -130,10 +116,13 @@ function mountEditComponent(User $user, User $employeeUser)
});
it('prevents update when user is unauthorized', function () {
$employeeUser = createEditTestUser();
$employeeUser->assignRole('Admin');
Role::firstOrCreate(['name' => 'Admin']);
Gate::define('update user', fn () => false);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
Gate::define('update user', fn() => false);
mountEditComponent($this->user, $employeeUser)
->call('save')
@ -145,9 +134,9 @@ function mountEditComponent(User $user, User $employeeUser)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditTestUser();
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet->id);
@ -193,9 +182,9 @@ function mountEditComponent(User $user, User $employeeUser)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditTestUser(['status' => UserStatus::ACTIVE]);
$employeeUser = createEditUser(['status' => UserStatus::ACTIVE]);
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet->id);
@ -229,9 +218,9 @@ function mountEditComponent(User $user, User $employeeUser)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditTestUser([
$employeeUser = createEditUser([
'status' => UserStatus::INACTIVE,
]);
@ -274,9 +263,9 @@ function mountEditComponent(User $user, User $employeeUser)
$outlet1 = Outlet::factory()->create();
$outlet2 = Outlet::factory()->create();
$outlet3 = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditTestUser();
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet1->id);
@ -310,10 +299,10 @@ function mountEditComponent(User $user, User $employeeUser)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$adminRole = Role::where('name', 'Admin')->first();
$leaderRole = Role::where('name', 'Leader')->first();
$adminRole = Role::firstOrCreate(['name' => 'Admin']);
$leaderRole = Role::firstOrCreate(['name' => 'Leader']);
$employeeUser = createEditTestUser();
$employeeUser = createEditUser();
$employeeUser->assignRole($adminRole->id);
$employeeUser->outlets()->attach($outlet->id);
@ -349,9 +338,9 @@ function mountEditComponent(User $user, User $employeeUser)
$outlet1 = Outlet::factory()->create();
$outlet2 = Outlet::factory()->create();
$outlet3 = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditTestUser();
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach([$outlet1->id, $outlet2->id]);
@ -388,11 +377,11 @@ function mountEditComponent(User $user, User $employeeUser)
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$adminRole = Role::where('name', 'Admin')->first();
$leaderRole = Role::where('name', 'Leader')->first();
$ownerRole = Role::where('name', 'Owner')->first();
$adminRole = Role::firstOrCreate(['name' => 'Admin']);
$leaderRole = Role::firstOrCreate(['name' => 'Leader']);
$ownerRole = Role::firstOrCreate(['name' => 'Owner']);
$employeeUser = createEditTestUser();
$employeeUser = createEditUser();
$employeeUser->assignRole([$adminRole->id, $leaderRole->id]);
$employeeUser->outlets()->attach($outlet->id);
@ -428,8 +417,9 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$employeeUser = createEditTestUser();
$employeeUser->assignRole('Admin');
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$component = mountEditComponent($this->user, $employeeUser);
@ -442,7 +432,6 @@ function mountEditComponent(User $user, User $employeeUser)
->set('form.birthdate', '')
->set('form.hire_date', '')
->set('form.gender', '')
->set('form.status', '')
->set('form.outlet_ids', [])
->set('form.role_ids', [])
->call('save')
@ -455,7 +444,6 @@ function mountEditComponent(User $user, User $employeeUser)
'form.birthdate',
'form.hire_date',
'form.gender',
'form.status',
'form.outlet_ids',
'form.role_ids',
]);
@ -465,11 +453,11 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$employeeUser = createEditTestUser(['username' => 'existinguser']);
$employeeUser->assignRole('Admin');
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser(['username' => 'existinguser']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -495,14 +483,15 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$existingUser = createEditTestUser(['username' => 'existinguser']);
$existingUser->assignRole('Admin');
$role = Role::firstOrCreate(['name' => 'Admin']);
$existingUser = createEditUser(['username' => 'existinguser']);
$existingUser->assignRole($role->id);
$employeeUser = createEditTestUser(['username' => 'otheruser']);
$employeeUser->assignRole('Admin');
$employeeUser = createEditUser(['username' => 'otheruser']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -527,11 +516,11 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$employeeUser = createEditTestUser(['email' => 'existing@example.com']);
$employeeUser->assignRole('Admin');
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser(['email' => 'existing@example.com']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -557,14 +546,15 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$existingUser = createEditTestUser(['email' => 'existing@example.com']);
$existingUser->assignRole('Admin');
$role = Role::firstOrCreate(['name' => 'Admin']);
$existingUser = createEditUser(['email' => 'existing@example.com']);
$existingUser->assignRole($role->id);
$employeeUser = createEditTestUser(['email' => 'other@example.com']);
$employeeUser->assignRole('Admin');
$employeeUser = createEditUser(['email' => 'other@example.com']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -589,11 +579,12 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$employeeUser = createEditTestUser();
$employeeUser->assignRole('Admin');
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(17)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -618,11 +609,12 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$employeeUser = createEditTestUser();
$employeeUser->assignRole('Admin');
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
@ -652,12 +644,12 @@ function mountEditComponent(User $user, User $employeeUser)
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$employeeUser = createEditTestUser();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->employee->update(['address' => 'Old Address']);
$employeeUser->assignRole('Admin');
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::where('name', 'Admin')->first();
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');

View File

@ -5,44 +5,38 @@
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$roles = collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->map(fn ($role) => Role::create(['name' => $role]));
// Create roles for testing
collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->each(fn($role) => \Spatie\Permission\Models\Role::create(['name' => $role]));
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
$this->user->roles()->attach($roles->pluck('id'));
$this->user->assignRole('Admin'); // Give admin role to test user
});
function createIndexTestUser(array $userAttributes = [], array $employeeAttributes = [])
function createUser(array $attributes = []): User
{
$factory = User::factory();
if (! isset($userAttributes['status'])) {
$factory = $factory->active();
unset($userAttributes['status']);
}
if (! empty($userAttributes)) {
$factory = $factory->state($userAttributes);
}
return $factory
->has(Employee::factory()->state($employeeAttributes))
return User::factory()
->active()
->has(Employee::factory())
->state(fn() => array_merge([
'status' => fake()->randomElement(UserStatus::cases()),
], $attributes))
->create();
}
function mountIndexComponent(User $user)
function mountIndexComponent(User $user): Testable
{
return Livewire::actingAs($user)->test(Index::class);
}
@ -53,353 +47,179 @@ function mountIndexComponent(User $user)
->assertViewHas('pageTitle', 'Pegawai');
});
it('mounts employees correctly', function () {
$employee = createIndexTestUser();
$employee->assignRole('Admin');
mountIndexComponent($this->user)
->assertViewHas('employees', function ($employees) use ($employee) {
return collect($employees)->contains(
fn ($e) => $e['full_name'] === $employee->employee->full_name &&
$e['code'] === $employee->employee->code
);
});
});
it('loads employee data with all required fields', function () {
$outlet = Outlet::factory()->create();
$user = createIndexTestUser();
$user->assignRole('Admin');
$user->outlets()->attach($outlet->id);
$user->referralCode()->create(['code' => 'REF123']);
it('loads employees as Eloquent collection', function () {
$user = createUser();
$component = mountIndexComponent($this->user);
$employeeData = collect($component->get('employees'))->first(
fn ($e) => $e['full_name'] === $user->employee->full_name
);
expect($employeeData)->toHaveKeys([
'full_name',
'code',
'phone_number',
'address',
'base_salary',
'gender',
'birthdate',
'birth_age',
'hire_date',
'hire_ago',
'resign_date',
'resign_ago',
'user',
'referral_code',
'outlets',
'roles',
]);
expect(
$component->get('employees')->pluck('id')
)->toContain($user->id);
});
expect($employeeData['user'])->toHaveKeys(['id', 'hash', 'email', 'username', 'status']);
expect($employeeData['gender'])->toHaveKeys(['label', 'color']);
expect($employeeData['user']['status'])->toHaveKeys(['gradient']);
it('loads employees with relationships', function () {
$user = createUser();
$user->assignRole('Admin'); // Assign role so user appears in list
$outlet = Outlet::factory()->create();
$user->outlets()->attach($outlet->id);
$component = mountIndexComponent($this->user);
$loadedEmployee = $component->get('employees')->first(fn($emp) => $emp->user_id === $user->id);
expect($loadedEmployee)->not->toBeNull()
->and($loadedEmployee->relationLoaded('user'))->toBeTrue()
->and($loadedEmployee->user->relationLoaded('outlets'))->toBeTrue()
->and($loadedEmployee->user->outlets)->toHaveCount(1);
});
it('excludes developer users from listing', function () {
$developer = createIndexTestUser();
$developer = createUser();
$developer->assignRole('Developer');
$admin = createIndexTestUser();
$admin = createUser();
$admin->assignRole('Admin');
$component = mountIndexComponent($this->user);
$employeeNames = collect($component->get('employees'))->pluck('full_name')->toArray();
$employeeNames = $component->get('employees')->pluck('full_name')->toArray();
expect($employeeNames)->not->toContain($developer->employee->full_name);
expect($employeeNames)->toContain($admin->employee->full_name);
});
it('displays all employees', function () {
$employees = [];
$initialCount = Employee::count();
for ($i = 0; $i < 5; $i++) {
$user = createIndexTestUser();
$user->assignRole('Admin');
$employees[] = $user;
createUser();
}
$component = mountIndexComponent($this->user);
$employeeCount = count($component->get('employees'));
expect($employeeCount)->toBeGreaterThanOrEqual(5);
expect($component->get('employees'))->toHaveCount($initialCount + 5);
expect($component->get('employees')->first())->toBeInstanceOf(Employee::class);
});
it('filters employees by search query - full name', function () {
$user1 = createIndexTestUser([], ['full_name' => 'John Doe']);
$user1->assignRole('Admin');
$user2 = createIndexTestUser([], ['full_name' => 'Jane Smith']);
$user2->assignRole('Admin');
it('filters employees by search query', function () {
$user1 = createUser(['username' => 'john_doe']);
$user2 = createUser(['username' => 'jane_smith']);
$component = mountIndexComponent($this->user);
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2);
expect(count($component->get('employees')))->toBeGreaterThan(2); // 2 new users + logged in user
$component->set('search', 'John')
->assertSet('search', 'John');
// Test filtering by username
$filteredComponent = mountIndexComponent($this->user)->set('search', 'john_doe');
$employees = $component->get('employees');
expect(count($employees))->toBeGreaterThanOrEqual(1);
expect($employees[0]['full_name'])->toBe('John Doe');
expect($filteredComponent->get('employees'))->toHaveCount(1);
expect($filteredComponent->get('employees')->first()->user->username)->toBe('john_doe');
});
it('filters employees by search query - employee code', function () {
$user1 = createIndexTestUser([], ['code' => 'EMP20240001']);
$user1->assignRole('Admin');
it('filters employees by status', function () {
$activeUser = createUser(['status' => UserStatus::ACTIVE]);
$inactiveUser = createUser(['status' => UserStatus::INACTIVE]);
$component = mountIndexComponent($this->user);
$component->set('search', 'EMP20240001');
expect(count($component->get('employees')))->toBeGreaterThan(2); // 2 new users + logged in user
$employees = $component->get('employees');
expect(collect($employees)->pluck('code'))->toContain('EMP20240001');
});
$filteredComponent = mountIndexComponent($this->user)->set('status', [UserStatus::ACTIVE->value]);
it('filters employees by search query - email', function () {
$user1 = createIndexTestUser(['email' => 'john@example.com']);
$user1->assignRole('Admin');
$component = mountIndexComponent($this->user);
$component->set('search', 'john@example.com');
$employees = $component->get('employees');
expect(collect($employees)->pluck('user.email'))->toContain('john@example.com');
});
it('filters employees by search query - username', function () {
$user1 = createIndexTestUser(['username' => 'johndoe']);
$user1->assignRole('Admin');
$component = mountIndexComponent($this->user);
$component->set('search', 'johndoe');
$employees = $component->get('employees');
expect(collect($employees)->pluck('user.username'))->toContain('johndoe');
});
it('filters employees by status - active', function () {
$activeUser = createIndexTestUser(['status' => UserStatus::ACTIVE]);
$activeUser->assignRole('Admin');
$inactiveUser = createIndexTestUser(['status' => UserStatus::INACTIVE]);
$inactiveUser->assignRole('Admin');
$component = mountIndexComponent($this->user);
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2);
$component->set('status', [UserStatus::ACTIVE->value]);
$employees = $component->get('employees');
$activeEmployees = collect($employees)->filter(
fn ($e) => $e['user']['status']['gradient'] === (UserStatus::ACTIVE->gradient())
);
expect(count($activeEmployees))->toBeGreaterThanOrEqual(1);
});
it('filters employees by status - inactive', function () {
$activeUser = createIndexTestUser(['status' => UserStatus::ACTIVE]);
$activeUser->assignRole('Admin');
$inactiveUser = createIndexTestUser(['status' => UserStatus::INACTIVE]);
$inactiveUser->assignRole('Admin');
$component = mountIndexComponent($this->user);
$component->set('status', [UserStatus::INACTIVE->value]);
$employees = $component->get('employees');
$inactiveEmployees = collect($employees)->filter(
fn ($e) => $e['user']['status']['gradient'] === (UserStatus::INACTIVE->gradient())
);
expect(count($inactiveEmployees))->toBeGreaterThanOrEqual(1);
expect(count($filteredComponent->get('employees')))->toBeGreaterThanOrEqual(1); // At least the active user
expect($filteredComponent->get('employees')->contains(fn($emp) => $emp->user->status->value === UserStatus::ACTIVE->value))->toBeTrue();
});
it('filters employees by multiple statuses', function () {
$activeUser = createIndexTestUser(['status' => UserStatus::ACTIVE]);
$activeUser->assignRole('Admin');
$inactiveUser = createIndexTestUser(['status' => UserStatus::INACTIVE]);
$inactiveUser->assignRole('Admin');
$activeUser = createUser(['status' => UserStatus::ACTIVE]);
$inactiveUser = createUser(['status' => UserStatus::INACTIVE]);
$component = mountIndexComponent($this->user);
$component->set('status', [
expect(count($component->get('employees')))->toBeGreaterThan(2); // 2 new users + logged in user
$filteredComponent = mountIndexComponent($this->user)->set('status', [
UserStatus::ACTIVE->value,
UserStatus::INACTIVE->value,
]);
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2);
});
it('filters employees by both search and status', function () {
$activeUser = createIndexTestUser(
['status' => UserStatus::ACTIVE],
['full_name' => 'Active User']
);
$activeUser->assignRole('Admin');
$inactiveUser = createIndexTestUser(
['status' => UserStatus::INACTIVE],
['full_name' => 'Inactive User']
);
$inactiveUser->assignRole('Admin');
$component = mountIndexComponent($this->user)
->set('search', 'Active')
->set('status', [UserStatus::ACTIVE->value]);
$employees = $component->get('employees');
expect(count($employees))->toBeGreaterThanOrEqual(1);
expect($employees[0]['full_name'])->toBe('Active User');
expect(count($filteredComponent->get('employees')))->toBeGreaterThan(2); // All users with these statuses
});
it('loads employees ordered by latest', function () {
$user1 = createIndexTestUser([], ['full_name' => 'First Employee']);
$user1->assignRole('Admin');
$user1 = createUser();
sleep(1);
$user2 = createIndexTestUser([], ['full_name' => 'Second Employee']);
$user2->assignRole('Admin');
$user2 = createUser();
$component = mountIndexComponent($this->user);
$employees = $component->get('employees');
$firstEmployee = collect($employees)->first(fn ($e) => $e['full_name'] === 'Second Employee');
$secondEmployee = collect($employees)->first(fn ($e) => $e['full_name'] === 'First Employee');
expect($firstEmployee)->not->toBeNull();
expect($secondEmployee)->not->toBeNull();
// The second employee should appear before the first
$firstIndex = array_search('Second Employee', array_column($employees, 'full_name'));
$secondIndex = array_search('First Employee', array_column($employees, 'full_name'));
expect($firstIndex)->toBeLessThan($secondIndex);
expect($component->get('employees')->first()->id)->toBe($user2->id);
expect($component->get('employees')->last()->id)->toBe($user1->id);
});
it('loads employee with outlets', function () {
$outlet1 = Outlet::factory()->create(['name' => 'Outlet A']);
$outlet2 = Outlet::factory()->create(['name' => 'Outlet B']);
$user = createIndexTestUser();
$user->assignRole('Admin');
$user->outlets()->attach([$outlet1->id, $outlet2->id]);
it('loads employees with all required relationships', function () {
$user = createUser();
$user->assignRole('Admin'); // Ensure user appears in list
$outlet = Outlet::factory()->create();
$user->outlets()->attach($outlet->id);
$component = mountIndexComponent($this->user);
$loadedEmployee = $component->get('employees')->first(fn($emp) => $emp->user_id === $user->id);
$employeeData = collect($component->get('employees'))->first(
fn ($e) => $e['full_name'] === $user->employee->full_name
);
expect($employeeData['outlets'])->toContain('Outlet A', 'Outlet B');
});
it('loads employee with roles', function () {
$user = createIndexTestUser();
$user->assignRole(['Admin', 'Leader']);
$component = mountIndexComponent($this->user);
$employeeData = collect($component->get('employees'))->first(
fn ($e) => $e['full_name'] === $user->employee->full_name
);
expect($employeeData['roles'])->toContain('Admin', 'Leader');
});
it('loads employee with referral code', function () {
$user = createIndexTestUser();
$user->assignRole('Admin');
$user->referralCode()->create(['code' => 'REF12345']);
$component = mountIndexComponent($this->user);
$employeeData = collect($component->get('employees'))->first(
fn ($e) => $e['full_name'] === $user->employee->full_name
);
expect($employeeData['referral_code'])->toBe('REF12345');
expect($loadedEmployee)->not->toBeNull()
->and($loadedEmployee->user->outlets)->toHaveCount(1)
->and($loadedEmployee->user->roles)->toHaveCount(1) // Admin role assigned
->and($loadedEmployee->full_name)->toBe($user->employee->full_name);
});
it('deletes an employee successfully', function () {
$employeeUser = createIndexTestUser();
$employeeUser->assignRole('Admin');
$user = createUser();
mountIndexComponent($this->user)
->call('delete', $employeeUser)
->call('delete', $user)
->assertHasNoErrors();
$this->assertSoftDeleted('users', ['id' => $employeeUser->id]);
$this->assertSoftDeleted('employees', ['user_id' => $employeeUser->id]);
$this->assertSoftDeleted('users', ['id' => $user->id]);
$this->assertSoftDeleted('employees', ['user_id' => $user->id]);
});
it('reloads employees after deletion', function () {
$employeeUser = createIndexTestUser();
$employeeUser->assignRole('Admin');
it('handles empty search results gracefully', function () {
createUser(['username' => 'existing_user']);
$component = mountIndexComponent($this->user);
$initialCount = count($component->get('employees'));
$component = mountIndexComponent($this->user)->set('search', 'NonExistentUser');
$component->call('delete', $employeeUser);
expect(count($component->get('employees')))->toBeLessThan($initialCount);
expect($component->get('employees'))->toHaveCount(0);
});
it('resets password successfully', function () {
$defaultPassword = config('myconfig.password_default');
$employeeUser = createIndexTestUser();
$employeeUser->assignRole('Admin');
$employeeUser->update(['password' => Hash::make('oldpassword')]);
it('maintains search state across component lifecycle', function () {
$user1 = createUser(['username' => 'test_user']);
$user2 = createUser(['username' => 'another_user']);
$oldPasswordHash = $employeeUser->password;
$component = mountIndexComponent($this->user)->set('search', 'test');
mountIndexComponent($this->user)
->call('resetPassword', $employeeUser)
->assertHasNoErrors();
$employeeUser->refresh();
expect($employeeUser->password)->not->toBe($oldPasswordHash);
expect(Hash::check($defaultPassword, $employeeUser->password))->toBeTrue();
expect($component->get('employees'))->toHaveCount(1);
expect($component->get('search'))->toBe('test');
});
it('does not display delete and edit buttons for current authenticated user', function () {
$component = mountIndexComponent($this->user);
it('handles status filter with empty array', function () {
createUser(['status' => UserStatus::ACTIVE]);
createUser(['status' => UserStatus::INACTIVE]);
$component
->assertDontSee('Delete')
->assertDontSee('Edit');
$component = mountIndexComponent($this->user)->set('status', []);
expect(count($component->get('employees')))->toBeGreaterThan(2); // 2 new users + logged in user
});
it('updates search with debounce', function () {
$user1 = createIndexTestUser([], ['full_name' => 'John Doe']);
$user1->assignRole('Admin');
it('loads employees efficiently with eager loading', function () {
$users = collect();
for ($i = 0; $i < 10; $i++) {
$users->push(createUser());
}
$component = mountIndexComponent($this->user);
$component->set('search', 'John');
expect($component->get('search'))->toBe('John');
});
it('updates status filter correctly', function () {
$component = mountIndexComponent($this->user);
$component->set('status', ['active']);
expect($component->get('status'))->toBeArray();
// Verify relationships are loaded
$loadedEmployees = $component->get('employees');
foreach ($loadedEmployees as $loadedEmployee) {
expect($loadedEmployee->relationLoaded('user'))->toBeTrue();
expect($loadedEmployee->user->relationLoaded('outlets'))->toBeTrue();
}
});