isAdmin() && strtolower($user->role) !== 'hr' && !$user->hasActiveInterviewZone()) { return redirect()->route('dashboard')->with('error', 'Interview Zone is available only during active interview timelines assigned by HR or Admin.'); } $interviews = $user->getAccessibleInterviews(); $interviewers = User::orderBy('name', 'asc')->get(); return view('interview.index', compact('interviews', 'interviewers')); } /** * Store a new Candidate Interview session created by HR. */ public function store(Request $request) { $request->validate([ 'candidate_name' => 'required|string|max:255', 'candidate_email' => 'required|email|max:255', 'candidate_phone' => 'required|string|max:50', 'scheduled_at' => 'nullable|date', 'valid_hours' => 'required|integer|min:1|max:72', 'language' => 'required|string', 'assigned_interviewers' => 'nullable|array', ]); $tempPassword = 'Pass-' . rand(100000, 999999); $cleanPhone = preg_replace('/[^0-9]/', '', $request->candidate_phone); $cleanName = Str::slug($request->candidate_name); $submissionUniqueId = $cleanName . '_' . $cleanPhone . '_' . time(); $scheduledAt = $request->scheduled_at ? \Carbon\Carbon::parse($request->scheduled_at) : now(); $expiresAt = $scheduledAt->copy()->addHours((int)$request->valid_hours); $interview = Interview::create([ 'candidate_name' => $request->candidate_name, 'candidate_email' => strtolower(trim($request->candidate_email)), 'candidate_phone' => $request->candidate_phone, 'temp_password' => $tempPassword, 'scheduled_at' => $scheduledAt, 'expires_at' => $expiresAt, 'language' => $request->language, 'status' => 'scheduled', 'assigned_interviewers' => $request->assigned_interviewers ?? [], 'proctor_logs' => [], 'submission_unique_id' => $submissionUniqueId, 'created_by' => Auth::id(), ]); return redirect()->route('interview.index')->with('success', 'Interview session created for ' . $request->candidate_name . '. Temporary login credentials generated!'); } /** * Show Candidate Login Screen. */ public function showCandidateLogin() { return view('interview.candidate_login'); } /** * Handle Candidate Temporary Credentials Authentication. */ public function loginCandidate(Request $request) { $request->validate([ 'identifier' => 'required|string', // Email or Phone 'temp_password' => 'required|string', ]); $identifier = trim($request->identifier); $interview = Interview::where(function ($q) use ($identifier) { $q->where('candidate_email', strtolower($identifier)) ->orWhere('candidate_phone', $identifier) ->orWhere('submission_unique_id', $identifier); })->where('temp_password', $request->temp_password)->first(); if (!$interview) { return back()->with('error', 'Invalid candidate credentials or temporary password.'); } if ($interview->status === 'blocked' || $interview->blocked_ip === $request->ip()) { return back()->with('error', 'Access Denied: Candidate email and IP address have been blocked by HR/Interviewer.'); } if ($interview->isExpired()) { $interview->update(['status' => 'expired']); return back()->with('error', 'This candidate interview access window has expired. Please contact HR.'); } // Set candidate session session(['candidate_interview_id' => $interview->id]); if ($interview->status === 'scheduled') { $interview->update(['status' => 'in_progress']); } return redirect()->route('interview.room', $interview->submission_unique_id); } /** * Show Candidate Live IDE Proctored Room. */ public function showCandidateRoom($uniqueId) { $interview = Interview::where('submission_unique_id', $uniqueId)->firstOrFail(); if ($interview->status === 'blocked') { return response()->view('interview.expired', compact('interview'), 403); } if ($interview->isExpired()) { $interview->update(['status' => 'expired']); return response()->view('interview.expired', compact('interview'), 403); } return view('interview.candidate_room', compact('interview')); } /** * Proxy Live Code Execution to Piston Multi-Language API. */ public function executeCode(Request $request) { $request->validate([ 'language' => 'required|string', 'code' => 'required|string', ]); $langMap = [ 'python' => ['language' => 'python', 'version' => '*'], 'c' => ['language' => 'c', 'version' => '*'], 'cpp' => ['language' => 'c++', 'version' => '*'], 'java' => ['language' => 'java', 'version' => '*'], 'php' => ['language' => 'php', 'version' => '*'], 'laravel' => ['language' => 'php', 'version' => '*'], 'javascript' => ['language' => 'javascript', 'version' => '*'], ]; $langKey = strtolower($request->language); $targetLang = $langMap[$langKey] ?? ['language' => 'python', 'version' => '*']; try { $response = Http::withHeaders([ 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept' => 'application/json', ])->timeout(10)->post('https://emkc.org/api/v2/piston/execute', [ 'language' => $targetLang['language'], 'version' => $targetLang['version'], 'files' => [ [ 'name' => 'solution', 'content' => $request->code, ] ], ]); if ($response->successful()) { $data = $response->json(); $output = $data['run']['output'] ?? ($data['compile']['output'] ?? 'Code executed successfully. No stdout output.'); return response()->json(['success' => true, 'output' => $output]); } // Retry with explicit fallback version if wildcard returned non-200 $fallbackVersions = [ 'python' => '3.10.0', 'c' => '10.2.0', 'c++' => '10.2.0', 'java' => '15.0.2', 'php' => '8.2.3', 'javascript' => '18.15.0', ]; $retryResponse = Http::withHeaders([ 'User-Agent' => 'SingleLogin-ProctoredIDE/1.0', 'Accept' => 'application/json', ])->timeout(10)->post('https://emkc.org/api/v2/piston/execute', [ 'language' => $targetLang['language'], 'version' => $fallbackVersions[$targetLang['language']] ?? '3.10.0', 'files' => [ [ 'name' => 'solution', 'content' => $request->code, ] ], ]); if ($retryResponse->successful()) { $data = $retryResponse->json(); $output = $data['run']['output'] ?? ($data['compile']['output'] ?? 'Code executed successfully.'); return response()->json(['success' => true, 'output' => $output]); } // Fallback for local PHP execution if PHP/Laravel language requested if (in_array($langKey, ['php', 'laravel'])) { ob_start(); try { $cleanCode = preg_replace('/^\s*<\?php\s*/i', '', $request->code); eval($cleanCode); $localOutput = ob_get_clean(); return response()->json(['success' => true, 'output' => $localOutput ?: '[Local PHP Execution Succeeded]']); } catch (\Throwable $e) { ob_end_clean(); return response()->json(['success' => true, 'output' => 'PHP Evaluation Error: ' . $e->getMessage()]); } } return response()->json([ 'success' => true, 'output' => "=== Code Output ===\n" . "Code submitted successfully for evaluation.\n(Remote execution engine status: " . $response->status() . ")\nSolution saved to proctored record." ]); } catch (\Exception $e) { return response()->json([ 'success' => true, 'output' => "=== Code Output ===\nCode session active.\nSolution saved to proctored record." ]); } } /** * Save Candidate Submitted Code & Execution Results. */ public function submitCode(Request $request, $id) { $interview = Interview::findOrFail($id); $request->validate([ 'code' => 'required|string', 'language' => 'required|string', 'output' => 'nullable|string', ]); $interview->update([ 'submitted_code' => $request->code, 'submitted_language' => $request->language, 'code_output' => $request->output, 'status' => 'completed', ]); return response()->json([ 'success' => true, 'message' => 'Code submitted successfully under unique ID: ' . $interview->submission_unique_id, 'unique_id' => $interview->submission_unique_id ]); } /** * Log Anti-Cheat Proctoring Violations (Tab Switch, Focus Loss, Gaze Anomaly). */ public function logViolation(Request $request, $id) { $interview = Interview::findOrFail($id); $type = $request->input('type'); // tab_switch, focus_lost, paste_event, gaze_anomaly $details = $request->input('details', ''); $logs = $interview->proctor_logs ?? []; $logs[] = [ 'type' => $type, 'details' => $details, 'timestamp' => now()->toIso8601String(), ]; $interview->update(['proctor_logs' => $logs]); return response()->json(['success' => true, 'total_violations' => count($logs)]); } /** * Interviewer / HR sends a Silent Warning to Candidate. */ public function sendWarning(Request $request, $id) { $interview = Interview::findOrFail($id); $request->validate([ 'message' => 'required|string|max:500', ]); $warning = InterviewWarning::create([ 'interview_id' => $interview->id, 'message' => $request->message, 'sent_by' => Auth::id(), ]); return response()->json(['success' => true, 'warning' => $warning]); } /** * Send Real-Time Chat Message from Candidate to Interviewer/HR. */ public function candidateSendMessage(Request $request, $id) { $interview = Interview::findOrFail($id); $request->validate(['message' => 'required|string|max:500']); $warning = InterviewWarning::create([ 'interview_id' => $interview->id, 'message' => $request->message, 'sent_by' => null, // null sent_by indicates candidate sender ]); return response()->json(['success' => true, 'warning' => $warning]); } /** * Live Sync Candidate Code (before submission). */ public function syncCandidateCode(Request $request, $id) { $interview = Interview::findOrFail($id); $interview->update([ 'submitted_code' => $request->input('code'), 'submitted_language' => $request->input('language', $interview->language), ]); return response()->json(['success' => true]); } /** * Save Candidate Live Notepad Content. */ public function saveCandidateNotes(Request $request, $id) { $interview = Interview::findOrFail($id); $interview->update(['candidate_notes' => $request->input('notes')]); return response()->json(['success' => true]); } /** * Save Candidate Live Canvas Drawing Data URL. */ public function saveCandidateDrawing(Request $request, $id) { $interview = Interview::findOrFail($id); $interview->update(['candidate_drawing' => $request->input('drawing')]); return response()->json(['success' => true]); } /** * Regenerate Temporary Candidate Password (by Interviewer/HR). */ public function regeneratePassword($id) { $interview = Interview::findOrFail($id); $newPass = 'Pass-' . rand(100000, 999999); $interview->update(['temp_password' => $newPass]); return response()->json(['success' => true, 'new_password' => $newPass]); } /** * Block Candidate Session, Email & Client IP (by Interviewer/HR). */ public function blockCandidate(Request $request, $id) { $interview = Interview::findOrFail($id); $clientIp = $request->ip(); $interview->update([ 'status' => 'blocked', 'blocked_ip' => $clientIp, ]); // Also block user record if exists User::where('email', strtolower($interview->candidate_email)) ->update(['is_blocked' => true]); return response()->json([ 'success' => true, 'message' => 'Candidate ' . $interview->candidate_name . ' (' . $interview->candidate_email . ') and IP ' . $clientIp . ' have been blocked successfully.' ]); } /** * Serve Storage File (Recordings & Public Assets) directly with Range Support for Video Streaming. */ public function serveStorageFile($path) { $sanitizedPath = ltrim(str_replace('..', '', $path), '/'); $candidatePaths = [ storage_path('app/public/' . $sanitizedPath), storage_path('app/private/public/' . $sanitizedPath), storage_path('app/private/' . $sanitizedPath), storage_path('app/' . $sanitizedPath), public_path('storage/' . $sanitizedPath), public_path($sanitizedPath), ]; // Also check with alternate extensions (.webm / .mp4) if requested file extension differs $baseNoExt = preg_replace('/\.(mp4|webm|m4v|mov)$/i', '', $sanitizedPath); if ($baseNoExt !== $sanitizedPath) { $candidatePaths[] = storage_path('app/public/' . $baseNoExt . '.webm'); $candidatePaths[] = storage_path('app/public/' . $baseNoExt . '.mp4'); $candidatePaths[] = storage_path('app/private/public/' . $baseNoExt . '.webm'); $candidatePaths[] = storage_path('app/private/public/' . $baseNoExt . '.mp4'); } $filePath = null; foreach ($candidatePaths as $cp) { if (file_exists($cp) && is_file($cp)) { $filePath = $cp; break; } } if (!$filePath) { abort(404, 'Storage file not found.'); } // Read first 12 bytes to accurately detect container format (WebM vs MP4 vs Image) $handle = @fopen($filePath, 'rb'); $header = $handle ? fread($handle, 12) : ''; if ($handle) fclose($handle); $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); $isWebmHeader = str_starts_with($header, "\x1A\x45\xDF\xA3"); if ($isWebmHeader || $extension === 'webm') { $mimeType = 'video/webm'; } elseif (str_contains($header, 'ftyp') || in_array($extension, ['mp4', 'm4v'])) { $mimeType = 'video/mp4'; } elseif ($extension === 'png') { $mimeType = 'image/png'; } elseif (in_array($extension, ['jpg', 'jpeg'])) { $mimeType = 'image/jpeg'; } else { $mimeType = @mime_content_type($filePath) ?: 'application/octet-stream'; } return response()->file($filePath, [ 'Content-Type' => $mimeType, 'Accept-Ranges' => 'bytes', 'Cache-Control' => 'public, max-age=86400', ]); } /** * Get normalized recordings list sorted in descending order of creation timestamp (newest first). */ private function getFormattedRecordings($interview) { $rawRecordings = $interview->recordings ?? []; if (!is_array($rawRecordings) || empty($rawRecordings)) { if (!empty($interview->recording_path)) { $rawRecordings = [[ 'url' => $interview->recording_path, 'filename' => basename($interview->recording_path), 'created_at' => $interview->updated_at ? $interview->updated_at->toIso8601String() : now()->toIso8601String(), ]]; } else { $rawRecordings = []; } } $formatted = []; foreach ($rawRecordings as $idx => $rec) { if (is_string($rec)) { $rec = [ 'url' => $rec, 'filename' => basename($rec), 'created_at' => $interview->updated_at ? $interview->updated_at->toIso8601String() : now()->toIso8601String(), ]; } $url = $rec['url'] ?? ''; $urlPath = parse_url($url, PHP_URL_PATH) ?? $url; $cleanPath = ltrim(str_replace(['/storage/', 'storage/'], '', $urlPath), '/'); $streamUrl = url('/media-stream/' . $cleanPath); $rec['original_index'] = $idx; $rec['stream_url'] = $streamUrl; $rec['download_url'] = route('interview.download-recording', $interview->id) . '?index=' . $idx; $formatted[] = $rec; } // Sort descending by created_at timestamp (newest first) usort($formatted, function ($a, $b) { $timeA = isset($a['created_at']) ? strtotime($a['created_at']) : 0; $timeB = isset($b['created_at']) ? strtotime($b['created_at']) : 0; return $timeB <=> $timeA; }); return $formatted; } /** * Live Polling Endpoint for Candidate Warnings & HR Monitoring. /** * Get all currently active interview calls accessible to the logged-in user. */ public function getActiveCalls(Request $request) { $user = Auth::user(); if (!$user) { return response()->json(['active_calls' => [], 'current_user_id' => null]); } $accessible = $user->getAccessibleInterviews(); $activeCalls = []; foreach ($accessible as $interview) { if ($interview->call_status === 'active') { $starterName = 'Panelist'; if ($interview->call_started_by) { $starter = User::find($interview->call_started_by); if ($starter) { $starterName = $starter->name; } } $activeCalls[] = [ 'id' => $interview->id, 'candidate_name' => $interview->candidate_name, 'candidate_email' => $interview->candidate_email, 'submission_unique_id' => $interview->submission_unique_id, 'call_status' => $interview->call_status, 'call_started_by' => $interview->call_started_by, 'starter_name' => $starterName, 'call_started_at' => $interview->call_started_at ? $interview->call_started_at->toIso8601String() : null, ]; } } return response()->json([ 'active_calls' => $activeCalls, 'current_user_id' => $user->id, ]); } /** * Live Polling Endpoint for Candidate Warnings & HR Monitoring. */ public function getPollData($id) { $interview = Interview::with(['warnings.sender', 'callStarter']) ->where('id', $id) ->orWhere('submission_unique_id', $id) ->firstOrFail(); $formattedRecordings = $this->getFormattedRecordings($interview); $starterName = $interview->callStarter ? $interview->callStarter->name : null; return response()->json([ 'status' => $interview->status, 'call_status' => $interview->call_status ?? 'idle', 'call_started_by' => $interview->call_started_by, 'starter_name' => $starterName, 'call_started_at' => $interview->call_started_at ? $interview->call_started_at->toIso8601String() : null, 'enable_tab_switch_screenshot' => (bool) ($interview->enable_tab_switch_screenshot ?? true), 'tab_switch_screenshots' => $interview->tab_switch_screenshots ?? [], 'submitted_code' => $interview->submitted_code, 'submitted_language' => $interview->submitted_language, 'code_output' => $interview->code_output, 'candidate_notes' => $interview->candidate_notes, 'candidate_drawing' => $interview->candidate_drawing, 'temp_password' => $interview->temp_password, 'blocked_ip' => $interview->blocked_ip, 'proctor_logs' => $interview->proctor_logs ?? [], 'recording_path' => $interview->recording_path, 'recordings' => $formattedRecordings, 'warnings' => $interview->warnings, 'is_expired' => $interview->isExpired(), ]); } /** * Update Interview Call Session Status (active, idle, ended). */ public function updateCallStatus(Request $request, $id) { $interview = Interview::where('id', $id) ->orWhere('submission_unique_id', $id) ->firstOrFail(); $status = $request->input('call_status', 'idle'); $isRejoin = $request->boolean('is_rejoin', false); $updateData = ['call_status' => $status]; $isNewCall = false; if ($status === 'active') { if (!$isRejoin && ($interview->call_status !== 'active' || !$interview->call_started_at)) { $updateData['call_started_by'] = Auth::id(); $updateData['call_started_at'] = now(); $isNewCall = true; } } elseif (in_array($status, ['ended', 'idle'])) { $updateData['call_started_by'] = null; $updateData['call_started_at'] = null; } $interview->update($updateData); $starterName = Auth::user() ? Auth::user()->name : 'Panelist'; return response()->json([ 'success' => true, 'call_status' => $status, 'is_new_call' => $isNewCall, 'call_started_by' => $interview->call_started_by, 'starter_name' => $starterName, 'call_started_at' => $interview->call_started_at ? $interview->call_started_at->toIso8601String() : null, ]); } /** * Upload & Save Candidate Tab-Switch Screenshot under public/uploads/candidate_screenshots/{unique_id}/. */ public function uploadTabScreenshot(Request $request, $id) { $interview = Interview::where('id', $id) ->orWhere('submission_unique_id', $id) ->firstOrFail(); if (isset($interview->enable_tab_switch_screenshot) && !$interview->enable_tab_switch_screenshot) { return response()->json([ 'success' => false, 'message' => 'Tab-switch screenshot capture is disabled by admin setting.' ]); } $url = null; $filename = 'screenshot_tab_switch_' . time() . '_' . rand(100, 999) . '.jpg'; $subDir = 'uploads/candidate_screenshots/' . $interview->submission_unique_id; $destinationPath = public_path($subDir); if (!file_exists($destinationPath)) { mkdir($destinationPath, 0777, true); } if ($request->hasFile('screenshot')) { $file = $request->file('screenshot'); $file->move($destinationPath, $filename); $url = asset($subDir . '/' . $filename); } elseif ($request->input('image')) { $base64Image = $request->input('image'); if (str_contains($base64Image, ',')) { $base64Image = explode(',', $base64Image)[1]; } $imageData = base64_decode($base64Image); file_put_contents($destinationPath . '/' . $filename, $imageData); $url = asset($subDir . '/' . $filename); } if ($url) { $relativePath = '/' . $subDir . '/' . $filename; $screenshots = $interview->tab_switch_screenshots ?? []; $screenshotEntry = [ 'url' => $relativePath, 'full_url' => $url, 'filename' => $filename, 'timestamp' => now()->toIso8601String(), 'reason' => 'tab_switch', ]; $screenshots[] = $screenshotEntry; $logs = $interview->proctor_logs ?? []; $logs[] = [ 'type' => 'tab_switch', 'details' => 'Candidate switched active browser tab (Screenshot Captured: ' . $filename . ')', 'screenshot_url' => $relativePath, 'timestamp' => now()->toIso8601String(), ]; $interview->update([ 'tab_switch_screenshots' => $screenshots, 'proctor_logs' => $logs, ]); return response()->json([ 'success' => true, 'url' => $relativePath, 'total_screenshots' => count($screenshots), ]); } return response()->json(['success' => false, 'message' => 'No image payload received']); } /** * Admin Toggle Enable/Disable Candidate Tab Switch Screenshot Capture. */ public function toggleTabScreenshot(Request $request, $id) { $interview = Interview::where('id', $id) ->orWhere('submission_unique_id', $id) ->firstOrFail(); $enable = $request->boolean('enable', true); $interview->update(['enable_tab_switch_screenshot' => $enable]); return response()->json([ 'success' => true, 'enable_tab_switch_screenshot' => $enable, 'message' => $enable ? 'Tab-switch screenshot capture ENABLED.' : 'Tab-switch screenshot capture DISABLED.' ]); } /** * Save Candidate Video Recording under public/uploads/candidate_recordings/{unique_id}/. */ public function uploadRecording(Request $request, $id) { $interview = Interview::where('id', $id) ->orWhere('submission_unique_id', $id) ->firstOrFail(); if ($request->hasFile('video')) { $file = $request->file('video'); $ext = strtolower($file->getClientOriginalExtension() ?: 'webm'); if (!in_array($ext, ['mp4', 'webm', 'm4v', 'mov'])) { $ext = 'webm'; } $subDir = 'uploads/candidate_recordings/' . $interview->submission_unique_id; $destinationPath = public_path($subDir); if (!file_exists($destinationPath)) { mkdir($destinationPath, 0777, true); } $filename = 'recording_' . time() . '.' . $ext; $file->move($destinationPath, $filename); $url = asset($subDir . '/' . $filename); $recordings = $interview->recordings ?? []; if (!is_array($recordings)) { $recordings = $interview->recording_path ? [$interview->recording_path] : []; } $recordings[] = [ 'url' => $url, 'filename' => $filename, 'created_at' => now()->toIso8601String(), ]; $interview->update([ 'recording_path' => $url, 'recordings' => $recordings, ]); return response()->json(['success' => true, 'path' => $url, 'recordings' => $recordings]); } return response()->json(['success' => false, 'message' => 'No video payload received']); } /** * Download Candidate Video Recording as File. */ public function downloadRecording(Request $request, $id) { $interview = Interview::findOrFail($id); $index = (int) $request->input('index', 0); $recordings = $interview->recordings ?? []; $targetUrl = null; if (!empty($recordings) && isset($recordings[$index])) { $rec = $recordings[$index]; $targetUrl = is_array($rec) ? ($rec['url'] ?? null) : $rec; } if (!$targetUrl) { $targetUrl = $interview->recording_path; } if (!$targetUrl) { return back()->with('error', 'No video recording found for this candidate profile.'); } $urlPath = parse_url($targetUrl, PHP_URL_PATH) ?? $targetUrl; $sanitizedPath = ltrim(str_replace(['/storage/', 'storage/'], '', $urlPath), '/'); $candidatePaths = [ storage_path('app/public/' . $sanitizedPath), storage_path('app/private/public/' . $sanitizedPath), storage_path('app/private/' . $sanitizedPath), storage_path('app/' . $sanitizedPath), public_path('storage/' . $sanitizedPath), public_path($sanitizedPath), ]; $baseNoExt = preg_replace('/\.(mp4|webm|m4v|mov)$/i', '', $sanitizedPath); if ($baseNoExt !== $sanitizedPath) { $candidatePaths[] = storage_path('app/public/' . $baseNoExt . '.webm'); $candidatePaths[] = storage_path('app/public/' . $baseNoExt . '.mp4'); $candidatePaths[] = storage_path('app/private/public/' . $baseNoExt . '.webm'); $candidatePaths[] = storage_path('app/private/public/' . $baseNoExt . '.mp4'); } $filePath = null; foreach ($candidatePaths as $cp) { if (file_exists($cp) && is_file($cp)) { $filePath = $cp; break; } } if ($filePath && file_exists($filePath)) { $handle = @fopen($filePath, 'rb'); $header = $handle ? fread($handle, 12) : ''; if ($handle) fclose($handle); $isWebm = str_starts_with($header, "\x1A\x45\xDF\xA3") || str_ends_with($filePath, '.webm'); $ext = $isWebm ? 'webm' : 'mp4'; $mimeType = $isWebm ? 'video/webm' : 'video/mp4'; $downloadFilename = 'candidate_' . Str::slug($interview->candidate_name) . '_recording_' . ($index + 1) . '.' . $ext; return response()->download($filePath, $downloadFilename, [ 'Content-Type' => $mimeType, ]); } return redirect($targetUrl); } /** * Generate Comprehensive Executive Candidate Evaluation & Behavioral Report (PDF View). */ public function generateReport($id) { $interview = Interview::with(['warnings.sender'])->findOrFail($id); $logs = is_array($interview->proctor_logs) ? $interview->proctor_logs : []; $tabSwitches = 0; $focusLosses = 0; $gazeAnomalies = 0; $pasteEvents = 0; $lowerGazeViolations = 0; $questionRepeatViolations = 0; foreach ($logs as $l) { $type = $l['type'] ?? ''; if ($type === 'tab_switch') $tabSwitches++; if ($type === 'focus_lost') $focusLosses++; if (in_array($type, ['gaze_anomaly', 'gaze_fixed_staring'])) $gazeAnomalies++; if ($type === 'paste_event') $pasteEvents++; if ($type === 'gaze_lower_device') $lowerGazeViolations++; if (in_array($type, ['question_repeat_lower', 'question_repetition'])) $questionRepeatViolations++; } $totalViolations = count($logs); // Calculate Integrity Score % if ($totalViolations === 0) { $integrityScore = 100; $integrityLabel = 'Exceptional Integrity'; $integrityColor = '#10b981'; } elseif ($totalViolations <= 2 && $lowerGazeViolations === 0 && $questionRepeatViolations === 0) { $integrityScore = 85; $integrityLabel = 'Low Behavioral Risk'; $integrityColor = '#06b6d4'; } elseif ($totalViolations <= 5) { $integrityScore = 60; $integrityLabel = 'Moderate Integrity Concern'; $integrityColor = '#f59e0b'; } else { $integrityScore = 25; $integrityLabel = 'High Malpractice Risk'; $integrityColor = '#ef4444'; } // Technical Code Score $codeScore = 0; if (!empty($interview->submitted_code)) { $codeScore += 50; if (!empty($interview->code_output) && !str_contains(strtolower($interview->code_output), 'error')) { $codeScore += 40; } else { $codeScore += 20; } if (!empty($interview->candidate_notes) || !empty($interview->candidate_drawing)) { $codeScore += 10; } } $interview->formatted_recordings = $this->getFormattedRecordings($interview); return view('interview.report', compact( 'interview', 'logs', 'tabSwitches', 'focusLosses', 'gazeAnomalies', 'pasteEvents', 'lowerGazeViolations', 'questionRepeatViolations', 'integrityScore', 'integrityLabel', 'integrityColor', 'codeScore' )); } }