seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $this->activateAttendance($pusat->election); $assignment = StaffAssignment::query() ->where('pusat_mengundi_id', $pusat->id) ->where('status', 'active') ->firstOrFail(); $this->actingAs($ppm) ->post(route('ppm.attendance.store', $pusat), [ 'attendance' => [ $assignment->id => [ 'status' => 'present', 'check_in_time' => now()->format('Y-m-d H:i:s'), 'note' => 'Hadir awal.', ], ], ]) ->assertRedirect(route('ppm.attendance.show', $pusat)); $this->assertDatabaseHas('attendances', [ 'staff_assignment_id' => $assignment->id, 'pusat_mengundi_id' => $pusat->id, 'status' => 'present', 'recorded_by_user_id' => $ppm->id, 'note' => 'Hadir awal.', ]); } public function test_ppm_cannot_record_attendance_for_other_pusat(): void { $this->seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $otherPusat = $this->otherPusat(); $this->activateAttendance($otherPusat->election); $position = Position::query()->where('code', 'PAPM')->firstOrFail(); $assignment = StaffAssignment::query()->create([ 'election_id' => $otherPusat->election_id, 'position_id' => $position->id, 'pusat_mengundi_id' => $otherPusat->id, 'status' => 'active', 'assigned_at' => now(), 'source' => 'test', ]); $this->actingAs($ppm) ->post(route('ppm.attendance.store', $otherPusat), [ 'attendance' => [ $assignment->id => ['status' => 'present'], ], ]) ->assertSessionHasErrors('pusat_mengundi_id'); $this->assertDatabaseMissing('attendances', [ 'staff_assignment_id' => $assignment->id, ]); } public function test_attendance_recording_requires_active_attendance_module(): void { $this->seed(DatabaseSeeder::class); $ppm = User::query()->where('email', 'ppm@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $assignment = StaffAssignment::query() ->where('pusat_mengundi_id', $pusat->id) ->where('status', 'active') ->firstOrFail(); $this->actingAs($ppm) ->post(route('ppm.attendance.store', $pusat), [ 'attendance' => [ $assignment->id => ['status' => 'present'], ], ]) ->assertSessionHasErrors('attendance'); } public function test_admin_can_view_attendance_dashboard_and_detail(): void { $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $this->activateAttendance($pusat->election); $this->actingAs($admin) ->get(route('admin.attendance.index')) ->assertOk() ->assertSee($pusat->name); $this->actingAs($admin) ->get(route('admin.attendance.show', $pusat)) ->assertOk() ->assertSee('Ringkasan Mengikut Jawatan'); } public function test_attendance_export_creates_export_log(): void { Storage::fake('local'); $this->seed(DatabaseSeeder::class); $admin = User::query()->where('email', 'admin@prn.local')->firstOrFail(); $pusat = PusatMengundi::query()->where('code', 'PM001')->firstOrFail(); $this->actingAs($admin) ->get(route('admin.attendance.export', $pusat)) ->assertOk(); $exportLog = ExportLog::query()->where('report_type', 'attendance_detail_by_pusat')->firstOrFail(); $this->assertSame($admin->id, $exportLog->generated_by_user_id); $this->assertSame($pusat->election_id, $exportLog->election_id); Storage::disk('local')->assertExists($exportLog->path); } public function test_admin_kewangan_cannot_access_attendance_dashboard(): void { $this->seed(DatabaseSeeder::class); $finance = User::query()->where('email', 'kewangan@prn.local')->firstOrFail(); $this->actingAs($finance) ->get(route('admin.attendance.index')) ->assertForbidden(); } private function activateAttendance(Election $election): void { $election->settings()->update([ 'is_attendance_active' => true, ]); } private function otherPusat(): PusatMengundi { $election = Election::query()->where('code', 'PRN2026')->firstOrFail(); $daerah = DaerahMengundi::factory()->forElection($election)->create([ 'code' => 'D888', 'name' => 'Daerah Attendance Lain', ]); return PusatMengundi::factory()->create([ 'election_id' => $election->id, 'daerah_mengundi_id' => $daerah->id, 'code' => 'PM888', 'name' => 'Pusat Attendance Lain', ]); } }