get(route('admin.hr.attendances.index')); $response->assertRedirect(route('login')); }); test('authenticated users can visit the attendance index page', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.hr.attendances.index')); $response->assertOk(); }); /* |-------------------------------------------------------------------------- | INDEX PAGE |-------------------------------------------------------------------------- */ test('index page displays attendances', function () { $user = User::factory()->create(); $this->actingAs($user); $employeeUser = User::factory()->create(); $employeeUser->employee()->create([ 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => now()->toDateString(), 'check_in_at' => now()->setHour(8)->setMinute(0), 'check_out_at' => now()->setHour(17)->setMinute(0), ]); $response = $this->get(route('admin.hr.attendances.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/hr/attendance/index') ->has('attendances.data', 1) ->where('attendances.total', 1) ->where('attendances.current_page', 1) ->where('attendances.per_page', 25) ); }); test('index page works with zero attendances', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.hr.attendances.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/hr/attendance/index') ->has('attendances.data', 0) ->where('attendances.total', 0) ->where('attendances.current_page', 1) ->where('attendances.per_page', 25) ); }); test('index page passes current year and month', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.hr.attendances.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('currentYear', now()->year) ->where('currentMonth', now()->month) ); }); test('index page can filter by year and month', function () { $user = User::factory()->create(); $this->actingAs($user); $employeeUser = User::factory()->create(); $employeeUser->employee()->create([ 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => '2024-03-15', 'check_in_at' => '2024-03-15 08:00:00', 'check_out_at' => '2024-03-15 17:00:00', ]); $response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 3])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('currentYear', 2024) ->where('currentMonth', 3) ->has('attendances.data', 1) ->where('attendances.total', 1) ->where('attendances.current_page', 1) ->where('attendances.per_page', 25) ); }); test('index page excludes attendances from other months', function () { $user = User::factory()->create(); $this->actingAs($user); $employeeUser = User::factory()->create(); $employeeUser->employee()->create([ 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => '2024-03-15', 'check_in_at' => '2024-03-15 08:00:00', ]); $response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 4])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('attendances.data', 0) ->where('attendances.total', 0) ->where('attendances.current_page', 1) ->where('attendances.per_page', 25) ); }); test('index page passes hrSettings props', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.hr.attendances.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('hrSettings.scheduled_check_in_time') ->has('hrSettings.scheduled_check_out_time') ); }); test('index page passes monthStats', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.hr.attendances.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('monthStats.working_days') ->has('monthStats.present') ->has('monthStats.absent') ->has('monthStats.leave') ); }); test('index page passes todayAttendance as null when no attendance today', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.hr.attendances.index')); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('todayAttendance', null) ); }); /* |-------------------------------------------------------------------------- | STORE / CHECK-IN |-------------------------------------------------------------------------- */ test('guest cannot check in', function () { $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(route('login')); }); test('authenticated user without employee cannot check in', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(); }); test('employee can check in successfully', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(); $this->assertDatabaseHas('attendances', [ 'employee_id' => $employee->id, 'attendance_date' => now()->toDateString(), ]); }); test('employee cannot check in twice on the same day', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); Attendance::factory()->create([ 'employee_id' => $employee->id, 'attendance_date' => now()->toDateString(), 'check_in_at' => now(), ]); $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(); $this->assertDatabaseCount('attendances', 1); }); test('check in requires photo', function () { $user = User::factory()->create(); $this->actingAs($user); Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $response = $this->post(route('admin.hr.attendances.store'), [ 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(); $this->assertDatabaseCount('attendances', 0); }); test('check in requires latitude', function () { $user = User::factory()->create(); $this->actingAs($user); Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'longitude' => 106.8456, ]); $response->assertRedirect(); $this->assertDatabaseCount('attendances', 0); }); test('check in requires longitude', function () { $user = User::factory()->create(); $this->actingAs($user); Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, ]); $response->assertRedirect(); $this->assertDatabaseCount('attendances', 0); }); test('check in latitude must be numeric', function () { $user = User::factory()->create(); $this->actingAs($user); Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => 'not-a-number', 'longitude' => 106.8456, ]); $response->assertRedirect(); $this->assertDatabaseCount('attendances', 0); }); test('check in longitude must be numeric', function () { $user = User::factory()->create(); $this->actingAs($user); Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $response = $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 'not-a-number', ]); $response->assertRedirect(); $this->assertDatabaseCount('attendances', 0); }); /* |-------------------------------------------------------------------------- | UPDATE / CHECK-OUT |-------------------------------------------------------------------------- */ test('guest cannot check out', function () { $attendance = Attendance::factory()->create([ 'check_in_at' => now()->subHours(8), 'check_out_at' => null, ]); $response = $this->put(route('admin.hr.attendances.update', $attendance), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(route('login')); }); test('employee can check out successfully', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $attendance = Attendance::factory()->create([ 'employee_id' => $employee->id, 'attendance_date' => now()->toDateString(), 'check_in_at' => now()->subHours(8), 'check_out_at' => null, ]); $response = $this->put(route('admin.hr.attendances.update', $attendance), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(); $attendance->refresh(); $this->assertNotNull($attendance->check_out_at); $this->assertEquals(-6.2088, $attendance->check_out_latitude); $this->assertEquals(106.8456, $attendance->check_out_longitude); }); test('check out requires photo', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $attendance = Attendance::factory()->create([ 'employee_id' => $employee->id, 'attendance_date' => now()->toDateString(), 'check_in_at' => now()->subHours(8), 'check_out_at' => null, ]); $response = $this->put(route('admin.hr.attendances.update', $attendance), [ 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertRedirect(); $attendance->refresh(); $this->assertNull($attendance->check_out_at); }); test('check out requires latitude', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $attendance = Attendance::factory()->create([ 'employee_id' => $employee->id, 'attendance_date' => now()->toDateString(), 'check_in_at' => now()->subHours(8), 'check_out_at' => null, ]); $response = $this->put(route('admin.hr.attendances.update', $attendance), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'longitude' => 106.8456, ]); $response->assertRedirect(); $attendance->refresh(); $this->assertNull($attendance->check_out_at); }); test('check out requires longitude', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $attendance = Attendance::factory()->create([ 'employee_id' => $employee->id, 'attendance_date' => now()->toDateString(), 'check_in_at' => now()->subHours(8), 'check_out_at' => null, ]); $response = $this->put(route('admin.hr.attendances.update', $attendance), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, ]); $response->assertRedirect(); $attendance->refresh(); $this->assertNull($attendance->check_out_at); }); test('updating non-existent attendance returns 404', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->put(route('admin.hr.attendances.update', 999999), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $response->assertStatus(404); }); /* |-------------------------------------------------------------------------- | BY-DATE |-------------------------------------------------------------------------- */ test('guest cannot access by-date endpoint', function () { $response = $this->get(route('admin.hr.attendances.by-date')); $response->assertRedirect(route('login')); }); test('by-date returns empty when no attendance for date', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->getJson(route('admin.hr.attendances.by-date', ['date' => '2024-01-01'])); $response->assertOk(); }); test('by-date defaults to today when no date parameter', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->getJson(route('admin.hr.attendances.by-date')); $response->assertOk(); }); /* |-------------------------------------------------------------------------- | MONTH STATS |-------------------------------------------------------------------------- */ test('month stats calculates correct working days for january 2024', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('monthStats.working_days', 23) ); }); test('month stats present count matches attendance records', function () { $user = User::factory()->create(); $this->actingAs($user); $employeeUser = User::factory()->create(); $employeeUser->employee()->create([ 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => '2024-01-15', 'check_in_at' => '2024-01-15 08:00:00', ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => '2024-01-16', 'check_in_at' => '2024-01-16 08:00:00', ]); $response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('monthStats.present', 2) ); }); /* |-------------------------------------------------------------------------- | MULTIPLE ATTENDANCES |-------------------------------------------------------------------------- */ test('multiple employees can have attendances on the same day', function () { $user = User::factory()->create(); $this->actingAs($user); $emp1User = User::factory()->create(); $emp1User->employee()->create([ 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $emp2User = User::factory()->create(); $emp2User->employee()->create([ 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); Attendance::factory()->create([ 'employee_id' => $emp1User->employee->id, 'attendance_date' => '2024-01-15', 'check_in_at' => '2024-01-15 08:00:00', ]); Attendance::factory()->create([ 'employee_id' => $emp2User->employee->id, 'attendance_date' => '2024-01-15', 'check_in_at' => '2024-01-15 08:30:00', ]); $response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('attendances.data', 2) ->where('attendances.total', 2) ->where('attendances.current_page', 1) ->where('attendances.per_page', 25) ); }); test('employee can have multiple attendances across different days', function () { $user = User::factory()->create(); $this->actingAs($user); $employeeUser = User::factory()->create(); $employeeUser->employee()->create([ 'join_date' => '2024-01-01', 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => '2024-01-15', 'check_in_at' => '2024-01-15 08:00:00', ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => '2024-01-16', 'check_in_at' => '2024-01-16 08:00:00', ]); Attendance::factory()->create([ 'employee_id' => $employeeUser->employee->id, 'attendance_date' => '2024-01-17', 'check_in_at' => '2024-01-17 08:00:00', ]); $response = $this->get(route('admin.hr.attendances.index', ['year' => 2024, 'month' => 1])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->has('attendances.data', 3) ->where('attendances.total', 3) ->where('attendances.current_page', 1) ->where('attendances.per_page', 25) ); }); /* |-------------------------------------------------------------------------- | DATA INTEGRITY |-------------------------------------------------------------------------- */ test('attendance stores correct check-in coordinates', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $attendance = Attendance::where('employee_id', $employee->id)->first(); $this->assertEquals(-6.2088, $attendance->check_in_latitude); $this->assertEquals(106.8456, $attendance->check_in_longitude); }); test('attendance has correct attendance_date', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $this->assertDatabaseHas('attendances', [ 'employee_id' => $employee->id, 'attendance_date' => now()->toDateString(), ]); }); test('check-in sets check_out_at to null', function () { $user = User::factory()->create(); $this->actingAs($user); $employee = Employee::factory()->create([ 'user_id' => $user->id, 'join_date' => now()->subMonth()->toDateString(), 'employment_status' => 'full_time', 'base_salary' => 5000000, ]); $this->post(route('admin.hr.attendances.store'), [ 'photo' => 'data:image/jpeg;base64,'.base64_encode('fake-image'), 'latitude' => -6.2088, 'longitude' => 106.8456, ]); $attendance = Attendance::where('employee_id', $employee->id)->first(); $this->assertNull($attendance->check_out_at); });