get('status', ''); if ($status === 'pending') { $query->whereHas('dataWajah', fn($q) => $q->where('is_verified', StatusVerifikasiWajah::PENDING)); } elseif ($status === 'approved') { $query->whereHas('dataWajah', fn($q) => $q->where('is_verified', StatusVerifikasiWajah::APPROVED)); } elseif ($status === 'rejected') { $query->whereHas('dataWajah', fn($q) => $q->where('is_verified', StatusVerifikasiWajah::REJECTED)); } elseif ($status === 'unregistered') { $query->whereDoesntHave('dataWajah'); } else { $query->whereHas('dataWajah'); } if ($request->search) { $search = $request->search; $query->where(function($q) use ($search) { $q->where('nama_lengkap', 'like', "%{$search}%") ->orWhere('nik', 'like', "%{$search}%"); }); } $users = $query->get(); $users->each(function ($user) { $datasetPath = "face_datasets/{$user->id}"; $frameList = []; if (Storage::disk('local')->exists($datasetPath)) { $files = Storage::disk('local')->files($datasetPath); foreach ($files as $file) { if (preg_match('/\/frame_\d+\.jpg$/', $file)) { $frameList[] = $file; } } sort($frameList); } $user->face_frames = $frameList; $user->face_frame_count = count($frameList); }); $stats = [ 'pending' => DataWajah::where('is_verified', StatusVerifikasiWajah::PENDING)->count(), 'approved' => DataWajah::where('is_verified', StatusVerifikasiWajah::APPROVED)->count(), 'rejected' => DataWajah::where('is_verified', StatusVerifikasiWajah::REJECTED)->count(), 'unregistered' => User::whereDoesntHave('dataWajah')->count(), ]; return view('face-approval.index', compact('users', 'stats', 'status')); } public function approve($id) { $user = User::findOrFail($id); if (!$user->dataWajah) { return redirect()->back()->with('error', 'Data wajah belum tersedia.'); } $datasetPath = Storage::disk('local')->path("face_datasets/{$user->id}"); if (!file_exists($datasetPath) || count(glob("$datasetPath/frame_*.jpg")) < 50) { return redirect()->back()->with('error', 'Dataset wajah belum cukup (minimal 50 frame). Pastikan proses extract frames sudah selesai.'); } $user->dataWajah->update(['is_verified' => StatusVerifikasiWajah::APPROVED]); app(NotifikasiService::class)->kirim( $user->id, 'face_disetujui', 'Wajah Terverifikasi', 'Data wajah Anda telah diverifikasi. Sekarang Anda bisa melakukan presensi.' ); broadcast(new FaceEnrollmentUpdated($user->id, 'approved', 'Data wajah diverifikasi.')); return redirect()->back()->with('success', 'Wajah berhasil diverifikasi.'); } public function reject($id) { $user = User::findOrFail($id); $wasApproved = $user->dataWajah && $user->dataWajah->is_verified == StatusVerifikasiWajah::APPROVED; if ($user->dataWajah) { $user->dataWajah->delete(); } $user->update(['is_face_registered' => 0]); $this->cleanupUserFaceData($user->id); app(NotifikasiService::class)->kirim( $user->id, 'face_ditolak', 'Registrasi Wajah Ditolak', 'Data wajah Anda ditolak. Silakan lakukan registrasi ulang melalui aplikasi.' ); broadcast(new FaceEnrollmentUpdated($user->id, 'rejected', 'Data wajah ditolak.')); return redirect()->back()->with('success', 'Wajah ditolak. Karyawan diminta melakukan registrasi ulang.'); } public function reset($id) { $user = User::findOrFail($id); $wasApproved = $user->dataWajah && $user->dataWajah->is_verified == StatusVerifikasiWajah::APPROVED; if ($user->dataWajah) { $user->dataWajah->delete(); } $user->update(['is_face_registered' => 0]); $this->cleanupUserFaceData($user->id); app(NotifikasiService::class)->kirim( $user->id, 'face_reset', 'Data Wajah Direset', 'Data wajah Anda telah direset oleh HRD. Silakan lakukan registrasi ulang melalui aplikasi.' ); broadcast(new FaceEnrollmentUpdated($user->id, 'reset', 'Data wajah direset.')); return redirect()->back()->with('success', 'Data wajah berhasil direset. Karyawan harus melakukan registrasi ulang.'); } public function showFrame($userId, $frameIndex) { $rawFilePath = "face_datasets/{$userId}/raw_frame_{$frameIndex}.jpg"; $filePath = "face_datasets/{$userId}/frame_{$frameIndex}.jpg"; if (Storage::disk('local')->exists($rawFilePath)) { return response()->file(Storage::disk('local')->path($rawFilePath)); } elseif (Storage::disk('local')->exists($filePath)) { return response()->file(Storage::disk('local')->path($filePath)); } abort(404); } public function showPhoto($userId, $pose) { $extensions = ['jpg', 'jpeg', 'png']; foreach ($extensions as $ext) { $filePath = "face_datasets/{$userId}/user_{$userId}_{$pose}.{$ext}"; if (Storage::disk('local')->exists($filePath)) { return response()->file(Storage::disk('local')->path($filePath)); } } abort(404); } private function cleanupUserFaceData($userId) { $paths = [ "face_datasets/{$userId}", "face_videos/{$userId}", ]; foreach ($paths as $path) { if (Storage::disk('local')->exists($path)) { Storage::disk('local')->deleteDirectory($path); } } $modelFiles = [ "face_models/user_{$userId}_svm.pkl", "face_models/user_{$userId}_scaler.pkl", "face_models/user_{$userId}_centroid.pkl", "face_models/user_{$userId}.yml", ]; foreach ($modelFiles as $file) { if (Storage::disk('local')->exists($file)) { Storage::disk('local')->delete($file); } } } public function reextractAll() { dispatch(new ReextractAllFrames()); return redirect()->back()->with('success', 'Job re-extract frame dari video berhasil ditambahkan ke antrian. Mohon tunggu beberapa saat untuk hasilnya.'); } public function migrateEmbeddings() { dispatch(new MigrateExistingEmbeddings()); return redirect()->back()->with('success', 'Proses migrasi embedding sedang berjalan di background. Anda akan menerima notifikasi setelah selesai.'); } }