358 lines
24 KiB
PHP
358 lines
24 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Candidate Live Assessment Portal | SingleLogin</title>
|
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
</head>
|
|
<body class="bg-[#0b0f19] text-white min-h-screen flex flex-col font-['Instrument_Sans',sans-serif]">
|
|
|
|
<!-- Header Navbar -->
|
|
<header class="h-[70px] bg-slate-900/80 backdrop-blur-md border-b border-slate-700/60 flex items-center justify-between px-6 sticky top-0 z-50">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-indigo-500 to-sky-600 flex items-center justify-center font-bold text-white shadow-md shadow-indigo-500/30">
|
|
<i class="fa-solid fa-bolt text-lg"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="font-outfit text-lg font-bold text-white m-0">Candidate Live Assessment Portal</h1>
|
|
<div class="text-xs text-slate-400">Proctored IDE & Code Submissions</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
@if(Auth::user()->isAdmin())
|
|
<a href="{{ route('admin.dashboard') }}">
|
|
<x-button variant="ghost" size="sm" icon="fa-solid fa-sliders" class="text-indigo-400 border-indigo-500/40">Control Panel</x-button>
|
|
</a>
|
|
@endif
|
|
<a href="{{ route('onboarding.index') }}">
|
|
<x-button variant="secondary" size="sm" icon="fa-solid fa-rocket">Onboarding Hub</x-button>
|
|
</a>
|
|
<a href="{{ route('dashboard') }}">
|
|
<x-button variant="secondary" size="sm" icon="fa-solid fa-user">User Portal</x-button>
|
|
</a>
|
|
<form action="{{ route('logout') }}" method="POST" id="logout-form" class="inline">
|
|
@csrf
|
|
<x-button type="button" onclick="interviewerLogoutAction()" variant="danger" size="sm" icon="fa-solid fa-right-from-bracket" class="bg-red-500/20 text-red-300 border-red-500/40 font-semibold">
|
|
Logout
|
|
</x-button>
|
|
</form>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="w-full mx-auto my-6 px-7 flex-1">
|
|
@if(session('success'))
|
|
<div class="mb-6 bg-emerald-500/15 border border-emerald-500/30 text-emerald-300 px-5 py-3 rounded-xl text-sm">
|
|
{{ session('success') }}
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Global Admin Settings & Proctoring Control Panel -->
|
|
<section class="mb-6 p-5 bg-linear-to-r from-slate-900/95 to-slate-800/95 border border-indigo-500/35 rounded-2xl shadow-xl">
|
|
<div class="flex justify-between items-center pb-3 mb-4 border-b border-white/10">
|
|
<div>
|
|
<h3 class="font-outfit text-xl font-bold text-white m-0 flex items-center gap-2">
|
|
<i class="fa-solid fa-gear text-indigo-400"></i> Global Admin Proctoring & System Settings
|
|
</h3>
|
|
<div class="text-xs text-slate-400 mt-1">Master control panel for candidate assessment rules, system screen capturing, and module toggles</div>
|
|
</div>
|
|
<x-pill variant="primary" size="md" icon="fa-solid fa-shield-halved">
|
|
ADMIN CONTROL PANEL
|
|
</x-pill>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<!-- 1. Candidate System Screenshot Capture Master Toggle -->
|
|
<div class="p-4 bg-emerald-500/10 border border-emerald-500/30 rounded-xl">
|
|
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST">
|
|
@csrf
|
|
<div class="mb-3">
|
|
<div class="text-sm text-white font-bold flex items-center gap-2">
|
|
<i class="fa-solid fa-camera text-emerald-400"></i> Candidate System Screenshot Capture
|
|
</div>
|
|
<div class="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates.
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between items-center pt-2.5 border-t border-white/10">
|
|
<span id="global-screenshot-status-text" class="text-xs font-bold {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'text-emerald-400' : 'text-red-400' }}">
|
|
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }}
|
|
</span>
|
|
@if(Auth::user() && Auth::user()->isAdmin())
|
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" name="enable_candidate_screenshots" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'checked' : '' }} class="accent-emerald-500 w-5 h-5 cursor-pointer">
|
|
<span class="text-xs font-bold text-white bg-white/10 px-3 py-1 rounded-md hover:bg-white/20 transition-colors">
|
|
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'Stop Screenshot Capture' : 'Enable Screenshot Capture' }}
|
|
</span>
|
|
</label>
|
|
@endif
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 2. Onboarding & Offboarding Module Master Toggle -->
|
|
<div class="p-4 bg-indigo-500/10 border border-indigo-500/30 rounded-xl">
|
|
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST">
|
|
@csrf
|
|
<div class="mb-3">
|
|
<div class="text-sm text-white font-bold flex items-center gap-2">
|
|
<i class="fa-solid fa-rocket text-indigo-400"></i> Candidate Provisioning & Onboarding Module
|
|
</div>
|
|
<div class="text-xs text-slate-400 mt-1 leading-relaxed">
|
|
Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation.
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-between items-center pt-2.5 border-t border-white/10">
|
|
<span class="text-xs font-bold {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'text-emerald-400' : 'text-red-400' }}">
|
|
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }}
|
|
</span>
|
|
@if(Auth::user() && Auth::user()->isAdmin())
|
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} class="accent-emerald-500 w-5 h-5 cursor-pointer">
|
|
<span class="text-xs font-bold text-white bg-white/10 px-3 py-1 rounded-md hover:bg-white/20 transition-colors">
|
|
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }}
|
|
</span>
|
|
</label>
|
|
@endif
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Page Header Row -->
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h2 class="font-outfit text-2xl font-bold text-white m-0">Live Interview Assessments</h2>
|
|
<div class="text-xs text-slate-400 mt-0.5">Temporary Candidate Access, Real-Time Proctored IDE, and Unique Code Records</div>
|
|
</div>
|
|
|
|
<x-button onclick="document.getElementById('create-interview-modal').classList.add('active')" variant="primary" size="md" icon="fa-solid fa-plus">
|
|
Schedule Candidate Interview
|
|
</x-button>
|
|
</div>
|
|
|
|
<!-- Assessment Table Card -->
|
|
<div class="bg-slate-900/80 border border-slate-700/60 rounded-2xl p-6 shadow-2xl backdrop-blur-md">
|
|
<div class="overflow-x-auto w-full">
|
|
<table class="w-full text-left text-xs border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-slate-700/60 text-slate-400 uppercase text-[0.7rem] tracking-wider">
|
|
<th class="py-3 px-4">Candidate & Temp Pass</th>
|
|
<th class="py-3 px-4">Unique Submission ID</th>
|
|
<th class="py-3 px-4">Status & Validity</th>
|
|
<th class="py-3 px-4">Language</th>
|
|
<th class="py-3 px-4">Proctoring Alerts</th>
|
|
<th class="py-3 px-4 text-right">Review & Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-white/5">
|
|
@forelse($interviews as $inv)
|
|
@php
|
|
$logs = $inv->proctor_logs ?? [];
|
|
$tabSwitches = count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'tab_switch'));
|
|
$focusLost = count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'focus_lost'));
|
|
$gazeAlerts = count(array_filter($logs, fn($l) => in_array($l['type'] ?? '', ['gaze_anomaly', 'gaze_fixed_staring'])));
|
|
$lowerGaze = count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'gaze_lower_device'));
|
|
$questionRepeat = count(array_filter($logs, fn($l) => in_array($l['type'] ?? '', ['question_repeat_lower', 'question_repetition'])));
|
|
$externalAi = count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'external_ai_detected'));
|
|
@endphp
|
|
<tr class="hover:bg-white/[0.02] transition-colors">
|
|
<td class="py-3.5 px-4">
|
|
<div>
|
|
<strong class="text-white text-sm block">{{ $inv->candidate_name }}</strong>
|
|
<div class="text-xs text-slate-400 mt-0.5">{{ $inv->candidate_email }} • {{ $inv->candidate_phone }}</div>
|
|
<div class="mt-1.5 flex flex-col gap-1">
|
|
<div class="text-[0.7rem] text-emerald-300 bg-emerald-500/10 px-2 py-0.5 rounded inline-block w-fit">
|
|
<i class="fa-solid fa-key text-[0.65rem] mr-1"></i> Temp Pass: <strong>{{ $inv->temp_password }}</strong>
|
|
</div>
|
|
<div class="text-[0.7rem] text-sky-400">
|
|
<i class="fa-solid fa-globe text-[0.65rem] mr-1"></i> Login URL: <a href="{{ route('interview.candidate.login') }}" target="_blank" class="text-sky-400 underline">{{ route('interview.candidate.login') }}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3.5 px-4">
|
|
<code class="bg-white/5 px-2 py-1 rounded text-sky-400 text-xs font-mono">
|
|
{{ $inv->submission_unique_id }}
|
|
</code>
|
|
</td>
|
|
<td class="py-3.5 px-4">
|
|
@if($inv->isExpired())
|
|
<x-pill variant="danger" size="sm">EXPIRED</x-pill>
|
|
@elseif($inv->status === 'completed')
|
|
<x-pill variant="success" size="sm">COMPLETED</x-pill>
|
|
@elseif($inv->call_status === 'active')
|
|
<x-pill variant="success" size="sm" icon="fa-solid fa-phone" class="animate-pulse font-extrabold">CALL IN PROGRESS</x-pill>
|
|
@elseif($inv->isActiveTimeline())
|
|
<x-pill variant="info" size="sm" icon="fa-solid fa-bolt">TIMELINE LIVE</x-pill>
|
|
@else
|
|
<x-pill variant="warning" size="sm">SCHEDULED</x-pill>
|
|
@endif
|
|
<div class="text-[0.7rem] text-slate-400 mt-1">
|
|
Timeline: {{ ($inv->scheduled_at ?? $inv->created_at)->format('M d, H:i') }} – {{ $inv->expires_at->format('H:i') }}
|
|
</div>
|
|
</td>
|
|
<td class="py-3.5 px-4">
|
|
<x-pill variant="ghost" size="sm" class="uppercase font-bold">
|
|
{{ $inv->language }}
|
|
</x-pill>
|
|
</td>
|
|
<td class="py-3.5 px-4">
|
|
<div class="flex flex-col gap-0.5 text-xs">
|
|
<span class="{{ $tabSwitches > 0 ? 'text-red-400 font-bold' : 'text-slate-400' }}">
|
|
<i class="fa-solid fa-circle text-[0.5rem] mr-1 text-red-500"></i> Tab Switches: {{ $tabSwitches }}
|
|
</span>
|
|
<span class="{{ $focusLost > 0 ? 'text-amber-400 font-semibold' : 'text-slate-400' }}">
|
|
<i class="fa-solid fa-circle text-[0.5rem] mr-1 text-amber-500"></i> Focus Loss / Overlay: {{ $focusLost }}
|
|
</span>
|
|
<span class="{{ $gazeAlerts > 0 ? 'text-rose-400 font-semibold' : 'text-slate-400' }}">
|
|
<i class="fa-solid fa-eye mr-1"></i> Eye Gaze Anomaly: {{ $gazeAlerts }}
|
|
</span>
|
|
@if($lowerGaze > 0)
|
|
<span class="text-red-400 font-bold">
|
|
<i class="fa-solid fa-mobile-screen mr-1"></i> Lower Gaze (>10s): {{ $lowerGaze }}
|
|
</span>
|
|
@endif
|
|
@if($questionRepeat > 0)
|
|
<span class="text-rose-400 font-bold">
|
|
<i class="fa-solid fa-comments mr-1"></i> Question Repetition: {{ $questionRepeat }}
|
|
</span>
|
|
@endif
|
|
@if($externalAi > 0)
|
|
<span class="text-red-400 font-extrabold bg-red-500/20 px-1.5 py-0.5 rounded">
|
|
<i class="fa-solid fa-robot mr-1"></i> Parakeet / External AI: {{ $externalAi }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<!-- Review & Actions: EXACTLY 2 BUTTONS USING FONT-AWESOME ICONS -->
|
|
<td class="py-3.5 px-4 text-right whitespace-nowrap">
|
|
<div class="inline-flex items-center gap-2 justify-end">
|
|
<!-- Button 1: Report Page -->
|
|
<a href="{{ route('interview.report', $inv->id) }}" target="_blank">
|
|
<x-button variant="success" size="sm" icon="fa-solid fa-file-pdf" class="bg-emerald-500/15 border-emerald-500/30 text-emerald-300 hover:bg-emerald-500/25">
|
|
PDF Report
|
|
</x-button>
|
|
</a>
|
|
|
|
<!-- Button 2: Join Call -->
|
|
@if($inv->call_status === 'active')
|
|
<a href="{{ route('interview.show', $inv->id) }}?join_call=1">
|
|
<x-button variant="success" size="sm" icon="fa-solid fa-phone-volume" class="animate-pulse font-extrabold shadow-md shadow-emerald-500/50">
|
|
Join Active Call
|
|
</x-button>
|
|
</a>
|
|
@else
|
|
<a href="{{ route('interview.show', $inv->id) }}">
|
|
<x-button variant="primary" size="sm" icon="fa-solid fa-video" class="bg-indigo-500/15 border-indigo-500/30 text-indigo-300 hover:bg-indigo-500/25">
|
|
Join Call
|
|
</x-button>
|
|
</a>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="6" class="text-center text-slate-400 py-8">
|
|
No candidate interview sessions found. Click "+ Schedule Candidate Interview" above to create one.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Modal: Schedule Candidate Interview -->
|
|
<x-modal id="create-interview-modal" title="Schedule Candidate Assessment" maxWidth="lg">
|
|
<form action="{{ route('interview.store') }}" method="POST">
|
|
@csrf
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3.5 mb-3.5">
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-400 mb-1">Candidate Name <span class="text-red-500">*</span></label>
|
|
<input type="text" name="candidate_name" required placeholder="e.g. John Doe" class="w-full px-3 py-2 bg-white/5 border border-slate-700/60 rounded-lg text-white text-xs outline-none focus:border-indigo-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-400 mb-1">Candidate Email <span class="text-red-500">*</span></label>
|
|
<input type="email" name="candidate_email" required placeholder="john@gmail.com" class="w-full px-3 py-2 bg-white/5 border border-slate-700/60 rounded-lg text-white text-xs outline-none focus:border-indigo-500">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3.5">
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-400 mb-1">Phone Number <span class="text-red-500">*</span></label>
|
|
<input type="text" name="candidate_phone" required placeholder="9876543210" class="w-full px-3 py-2 bg-white/5 border border-slate-700/60 rounded-lg text-white text-xs outline-none focus:border-indigo-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-400 mb-1">Start Time (Timeline)</label>
|
|
<input type="datetime-local" name="scheduled_at" value="{{ now()->format('Y-m-d\TH:i') }}" class="w-full px-3 py-2 bg-slate-900 border border-slate-700/60 rounded-lg text-white text-xs outline-none focus:border-indigo-500">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-slate-400 mb-1">Access Duration <span class="text-red-500">*</span></label>
|
|
<select name="valid_hours" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700/60 rounded-lg text-white text-xs outline-none focus:border-indigo-500">
|
|
<option value="1">1 Hour</option>
|
|
<option value="2" selected>2 Hours</option>
|
|
<option value="4">4 Hours</option>
|
|
<option value="12">12 Hours</option>
|
|
<option value="24">24 Hours</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3.5">
|
|
<label class="block text-xs font-medium text-slate-400 mb-1">Coding Language <span class="text-red-500">*</span></label>
|
|
<select name="language" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700/60 rounded-lg text-white text-xs outline-none focus:border-indigo-500">
|
|
<option value="python">Python 3</option>
|
|
<option value="cpp">C++ (GCC)</option>
|
|
<option value="c">C (GCC)</option>
|
|
<option value="java">Java 15</option>
|
|
<option value="php">PHP 8.2 / Laravel</option>
|
|
<option value="javascript">JavaScript (Node.js)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<div class="flex justify-between items-center mb-1.5">
|
|
<label class="text-xs font-medium text-slate-400">Assign Internal Interviewers / Panelists</label>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[120px] overflow-y-auto bg-black/30 p-2.5 rounded-lg border border-slate-700/60">
|
|
@foreach($interviewers as $usr)
|
|
<label class="flex items-center gap-2 text-xs text-white cursor-pointer hover:text-indigo-300">
|
|
<input type="checkbox" name="assigned_interviewers[]" value="{{ $usr->id }}" class="accent-indigo-500">
|
|
{{ $usr->name }} ({{ $usr->email }})
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end gap-2.5 mt-5">
|
|
<x-button type="button" onclick="document.getElementById('create-interview-modal').classList.remove('active')" variant="secondary">
|
|
Cancel
|
|
</x-button>
|
|
<x-button type="submit" variant="primary" icon="fa-solid fa-check">
|
|
Create Session & Password
|
|
</x-button>
|
|
</div>
|
|
</form>
|
|
</x-modal>
|
|
|
|
<script>
|
|
window.MAX_INTERVIEWERS = {{ config('interview.max_interviewer', 4) }};
|
|
window.currentUserId = {{ Auth::id() }};
|
|
window.currentUserName = "{{ addslashes(Auth::user()->name) }}";
|
|
window.currentUserRole = "{{ Auth::user()->is_admin ? 'admin' : 'interviewer' }}";
|
|
window.csrfToken = "{{ csrf_token() }}";
|
|
</script>
|
|
</body>
|
|
</html>
|