refactor: UI Layouts and components
Interview module is refactored to use layouts and re-usable components along with domain components. Style remains identitical to previous design.
This commit is contained in:
parent
e6a177028b
commit
9f25fdd648
@ -101,4 +101,44 @@ public function isAssignedInterviewer($userId): bool
|
|||||||
$interviewers = array_map('strval', (array) $this->assigned_interviewers);
|
$interviewers = array_map('strval', (array) $this->assigned_interviewers);
|
||||||
return in_array((string) $userId, $interviewers, true);
|
return in_array((string) $userId, $interviewers, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get computed initials for candidate (e.g. "John Doe" -> "JD").
|
||||||
|
*/
|
||||||
|
public function getCandidateInitialsAttribute(): string
|
||||||
|
{
|
||||||
|
$parts = preg_split('/\s+/', trim($this->candidate_name ?? ''));
|
||||||
|
if (count($parts) >= 2) {
|
||||||
|
return strtoupper(substr($parts[0], 0, 1) . substr(end($parts), 0, 1));
|
||||||
|
}
|
||||||
|
$nameStr = trim($this->candidate_name ?? '');
|
||||||
|
return strtoupper(substr($nameStr, 0, 1) . (strlen($nameStr) > 1 ? substr($nameStr, -1) : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get proctoring alert metrics summary.
|
||||||
|
*/
|
||||||
|
public function getProctorMetricsAttribute(): array
|
||||||
|
{
|
||||||
|
$logs = $this->proctor_logs ?? [];
|
||||||
|
return [
|
||||||
|
'tab_switches' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'tab_switch')),
|
||||||
|
'focus_lost' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'focus_lost')),
|
||||||
|
'gaze_alerts' => count(array_filter($logs, fn($l) => in_array($l['type'] ?? '', ['gaze_anomaly', 'gaze_fixed_staring', 'looking_away']))),
|
||||||
|
'lower_gaze' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'gaze_lower_device')),
|
||||||
|
'question_repeat' => count(array_filter($logs, fn($l) => in_array($l['type'] ?? '', ['question_repeat_lower', 'question_repetition']))),
|
||||||
|
'external_ai' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'external_ai_detected')),
|
||||||
|
'paste_events' => count(array_filter($logs, fn($l) => ($l['type'] ?? '') === 'paste_event')),
|
||||||
|
'total_incidents' => count($logs),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format timeline string: "M d, H:i – H:i".
|
||||||
|
*/
|
||||||
|
public function getFormattedTimelineAttribute(): string
|
||||||
|
{
|
||||||
|
$start = $this->scheduled_at ?? $this->created_at;
|
||||||
|
return $start->format('M d, H:i') . ' – ' . $this->expires_at->format('H:i');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
resources/views/components/alert.blade.php
Normal file
32
resources/views/components/alert.blade.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
@props([
|
||||||
|
'variant' => 'info', // success, danger, warning, info
|
||||||
|
'icon' => null,
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$variants = [
|
||||||
|
'success' => 'bg-emerald-500/15 border-emerald-500/30 text-emerald-300',
|
||||||
|
'danger' => 'bg-red-500/15 border-red-500/30 text-red-300',
|
||||||
|
'warning' => 'bg-amber-500/15 border-amber-500/30 text-amber-300',
|
||||||
|
'info' => 'bg-sky-500/15 border-sky-500/30 text-sky-300',
|
||||||
|
];
|
||||||
|
|
||||||
|
$defaultIcons = [
|
||||||
|
'success' => 'fa-solid fa-circle-check',
|
||||||
|
'danger' => 'fa-solid fa-triangle-exclamation',
|
||||||
|
'warning' => 'fa-solid fa-circle-exclamation',
|
||||||
|
'info' => 'fa-solid fa-circle-info',
|
||||||
|
];
|
||||||
|
|
||||||
|
$selectedIcon = $icon ?? ($defaultIcons[$variant] ?? $defaultIcons['info']);
|
||||||
|
$variantClass = $variants[$variant] ?? $variants['info'];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div {{ $attributes->merge(['class' => "flex items-start gap-2.5 px-4 py-3 border rounded-xl text-xs font-medium {$variantClass}"]) }}>
|
||||||
|
@if($selectedIcon)
|
||||||
|
<i class="{{ $selectedIcon }} text-sm mt-0.5 shrink-0"></i>
|
||||||
|
@endif
|
||||||
|
<div class="flex-1 leading-relaxed">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
29
resources/views/components/input.blade.php
Normal file
29
resources/views/components/input.blade.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
@props([
|
||||||
|
'type' => 'text',
|
||||||
|
'name' => null,
|
||||||
|
'value' => null,
|
||||||
|
'placeholder' => null,
|
||||||
|
'required' => false,
|
||||||
|
'disabled' => false,
|
||||||
|
'size' => 'md', // sm, md, lg
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$sizeClasses = [
|
||||||
|
'sm' => 'px-2.5 py-1.5 text-xs',
|
||||||
|
'md' => 'px-3 py-2 text-xs',
|
||||||
|
'lg' => 'px-3.5 py-2.5 text-sm',
|
||||||
|
];
|
||||||
|
|
||||||
|
$classes = 'w-full bg-white/5 border border-slate-700/60 rounded-lg text-white outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/40 placeholder-slate-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ' . ($sizeClasses[$size] ?? $sizeClasses['md']);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="{{ $type }}"
|
||||||
|
@if($name) name="{{ $name }}" @endif
|
||||||
|
@if($value !== null) value="{{ $value }}" @endif
|
||||||
|
@if($placeholder) placeholder="{{ $placeholder }}" @endif
|
||||||
|
@if($required) required @endif
|
||||||
|
@if($disabled) disabled @endif
|
||||||
|
{{ $attributes->merge(['class' => $classes]) }}
|
||||||
|
/>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<section class="mb-6 p-5 bg-gradient-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::check() && 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::check() && 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>
|
||||||
40
resources/views/components/interview/call-controls.blade.php
Normal file
40
resources/views/components/interview/call-controls.blade.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 1. Mic Toggle (Icon Only) -->
|
||||||
|
<x-button id="btn-toggle-interviewer-mic" onclick="toggleInterviewerMic()" variant="ghost" icon="fa-solid fa-microphone" class="hidden" title="Mic On / Mute"></x-button>
|
||||||
|
|
||||||
|
<!-- 2. Cam Toggle (Icon Only) -->
|
||||||
|
<x-button id="btn-toggle-interviewer-cam" onclick="toggleInterviewerCam()" variant="ghost" icon="fa-solid fa-video" class="hidden" title="Cam On / Off"></x-button>
|
||||||
|
|
||||||
|
<!-- 3. Start / Join / Restart Call -->
|
||||||
|
@if($interview->call_status === 'ended')
|
||||||
|
<x-button id="btn-start-call" disabled variant="primary" icon="fa-solid fa-phone-slash" class="opacity-50 cursor-not-allowed pointer-events-none">
|
||||||
|
Call Ended
|
||||||
|
</x-button>
|
||||||
|
@elseif($interview->call_status === 'active')
|
||||||
|
<x-button id="btn-start-call" onclick="startInterviewerCall()" variant="primary" icon="fa-solid fa-phone">
|
||||||
|
Join Call
|
||||||
|
</x-button>
|
||||||
|
@else
|
||||||
|
<x-button id="btn-start-call" onclick="startInterviewerCall()" variant="primary" icon="fa-solid fa-phone">
|
||||||
|
Start Call
|
||||||
|
</x-button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- 4. Record Call (Silent) -->
|
||||||
|
<x-button id="btn-start-recording" onclick="startCallRecording()" variant="secondary" icon="fa-solid fa-circle-dot" class="hidden">
|
||||||
|
Record Call (Silent)
|
||||||
|
</x-button>
|
||||||
|
<x-button id="btn-stop-recording" onclick="stopCallRecording()" variant="danger" icon="fa-solid fa-square" class="hidden animate-pulse" title="Stop & Save Recording"></x-button>
|
||||||
|
|
||||||
|
<!-- 5. Leave Call -->
|
||||||
|
<x-button id="btn-leave-call" onclick="leaveInterviewerCall()" variant="secondary" icon="fa-solid fa-arrow-right-from-bracket" class="hidden">
|
||||||
|
Leave Call
|
||||||
|
</x-button>
|
||||||
|
|
||||||
|
<!-- 6. End Call for All (Icon Only, Far Right) -->
|
||||||
|
<x-button id="btn-end-all-call" onclick="endCallForAll()" variant="danger" icon="fa-solid fa-phone-slash" class="hidden" title="End Call for All"></x-button>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
<div class="bg-slate-900/80 border border-slate-700/60 rounded-xl mb-4 overflow-hidden backdrop-blur-md">
|
||||||
|
<div class="flex items-center justify-between p-5 gap-4 flex-wrap">
|
||||||
|
<div class="flex items-center gap-3.5">
|
||||||
|
<div class="w-[42px] h-[42px] rounded-full bg-gradient-to-br from-indigo-500 to-sky-600 flex items-center justify-center text-white font-semibold text-[15px] shrink-0 shadow-md shadow-indigo-500/20">
|
||||||
|
{{ $interview->candidate_initials }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-0.5">
|
||||||
|
<h2 id="modal-cand-name" class="text-[15px] font-semibold text-white m-0 leading-none">{{ $interview->candidate_name }}</h2>
|
||||||
|
<span id="candidate-joined-pill" class="pill gray inline-flex items-center gap-1.5 font-semibold text-[11px] px-2.5 py-0.5 rounded-full border border-slate-700 text-slate-400 bg-slate-800/60">
|
||||||
|
<i class="fa-solid fa-circle text-[8px] text-slate-500"></i> Not Joined
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-400 font-mono">{{ $interview->candidate_email }} · {{ $interview->candidate_phone }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5 flex-wrap">
|
||||||
|
@if($interview->isExpired())
|
||||||
|
<x-pill id="timeline-status-pill" variant="danger" icon="fa-solid fa-circle">Expired</x-pill>
|
||||||
|
@elseif($interview->status === 'completed')
|
||||||
|
<x-pill id="timeline-status-pill" variant="success" icon="fa-solid fa-circle">Completed</x-pill>
|
||||||
|
@elseif($interview->call_status === 'active')
|
||||||
|
<x-pill id="timeline-status-pill" variant="info" class="animate-pulse" icon="fa-solid fa-circle">Call in progress</x-pill>
|
||||||
|
@elseif($interview->isActiveTimeline())
|
||||||
|
<x-pill id="timeline-status-pill" variant="info" icon="fa-solid fa-bolt">Timeline live</x-pill>
|
||||||
|
@else
|
||||||
|
<x-pill id="timeline-status-pill" variant="warning" icon="fa-solid fa-clock">Scheduled</x-pill>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="w-px h-[18px] bg-slate-700/60 mx-1"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-xs text-slate-400 cursor-pointer select-none" onclick="toggleSosAutoTrigger()">
|
||||||
|
<span class="toggle relative inline-flex h-4 w-8 shrink-0 cursor-pointer rounded-full border border-slate-700 bg-slate-900 transition-colors duration-200 ease-in-out [&.on]:bg-indigo-500/20 [&.on]:border-indigo-500/50 after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:h-2.5 after:w-2.5 after:rounded-full after:bg-slate-500 after:transition-all duration-150 [&.on]:after:left-[17px] [&.on]:after:bg-indigo-400" id="sosToggle"></span>
|
||||||
|
<span>SOS auto-trigger</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-px h-[18px] bg-slate-700/60 mx-1"></div>
|
||||||
|
|
||||||
|
<x-button onclick="regenerateCandidatePassword()" variant="secondary" icon="fa-solid fa-key">Password</x-button>
|
||||||
|
<x-button onclick="openReportPage()" variant="secondary" icon="fa-solid fa-file-pdf">PDF report</x-button>
|
||||||
|
<x-button onclick="blockCandidateAction()" variant="secondary" icon="fa-solid fa-ban">Block</x-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submission Details Disclosure Accordion -->
|
||||||
|
<details class="group border-t border-slate-700/60">
|
||||||
|
<summary class="flex items-center gap-2 px-5 py-2.5 text-xs text-slate-400 font-medium hover:text-slate-200 transition-colors cursor-pointer list-none select-none">
|
||||||
|
<i class="fa-solid fa-chevron-down text-[10px] transition-transform duration-150 group-open:rotate-180"></i>
|
||||||
|
<span>Show submission details</span>
|
||||||
|
</summary>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2.5 px-5 pb-4">
|
||||||
|
<div class="bg-slate-950/60 border border-slate-800 rounded-lg p-2.5">
|
||||||
|
<div class="text-[10.5px] text-slate-500 uppercase tracking-wider mb-1 font-semibold">Submission ID</div>
|
||||||
|
<div id="modal-cand-uid" class="text-xs font-mono text-slate-300 break-all">{{ $interview->submission_unique_id }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-950/60 border border-slate-800 rounded-lg p-2.5">
|
||||||
|
<div class="text-[10.5px] text-slate-500 uppercase tracking-wider mb-1 font-semibold">Temp Pass</div>
|
||||||
|
<div class="text-xs font-mono text-emerald-400 font-semibold">{{ $interview->temp_password }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-950/60 border border-slate-800 rounded-lg p-2.5">
|
||||||
|
<div class="text-[10.5px] text-slate-500 uppercase tracking-wider mb-1 font-semibold">Language</div>
|
||||||
|
<div class="text-xs font-mono text-slate-300 uppercase">{{ $interview->language }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-950/60 border border-slate-800 rounded-lg p-2.5">
|
||||||
|
<div class="text-[10.5px] text-slate-500 uppercase tracking-wider mb-1 font-semibold">Timeline</div>
|
||||||
|
<div class="text-xs font-mono text-slate-300">{{ $interview->formatted_timeline }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<div id="drawing-modal" class="fixed top-[70px] right-[360px] w-[550px] h-[420px] bg-slate-900 border-2 border-cyan-500 rounded-2xl shadow-2xl hidden flex-col z-50 overflow-hidden">
|
||||||
|
<div class="bg-slate-800 px-4 py-2.5 flex justify-between items-center border-b border-white/10">
|
||||||
|
<div class="font-bold text-xs text-white flex items-center gap-1.5">
|
||||||
|
<span>🎨</span>
|
||||||
|
<span>Candidate Live Drawing Board</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input type="color" id="draw-color" value="#38bdf8" class="w-6 h-6 border-none rounded-full cursor-pointer bg-transparent">
|
||||||
|
<button type="button" onclick="clearCanvasDraw()" class="px-2 py-0.5 text-[11px] font-semibold bg-white/10 hover:bg-white/20 text-white rounded border border-white/10 cursor-pointer transition-colors">Clear</button>
|
||||||
|
<button type="button" onclick="toggleDrawingModal()" class="bg-transparent border-none text-slate-400 hover:text-white text-lg cursor-pointer leading-none">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 bg-[#050811] relative">
|
||||||
|
<canvas id="drawing-canvas" width="550" height="340" class="cursor-crosshair block w-full h-full"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 py-1.5 bg-slate-900 text-[10.5px] text-cyan-400 text-right border-t border-white/5 font-medium">
|
||||||
|
🎨 Draw architecture / logic diagrams (Auto-synced to Interviewer)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
<header class="h-[60px] bg-slate-900 border-b border-white/10 flex items-center justify-between px-5 z-20 shrink-0">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="font-outfit font-bold text-base text-white">
|
||||||
|
Candidate Assessment Room
|
||||||
|
</div>
|
||||||
|
<code class="bg-white/10 px-2.5 py-1 rounded-md text-sky-400 text-xs font-mono">
|
||||||
|
ID: {{ $interview->submission_unique_id }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button type="button" onclick="toggleNotepadModal()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-indigo-500/20 border border-indigo-500/40 text-indigo-300 hover:bg-indigo-500/30 transition-all cursor-pointer">
|
||||||
|
📝 Open Notepad
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" onclick="toggleDrawingModal()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold bg-cyan-500/20 border border-cyan-500/40 text-cyan-300 hover:bg-cyan-500/30 transition-all cursor-pointer">
|
||||||
|
🎨 Open Drawing
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-xs text-slate-400 font-medium">Language:</label>
|
||||||
|
<select id="language-select" class="px-2.5 py-1.5 rounded-lg text-xs font-semibold bg-slate-800 border border-white/10 text-white outline-none focus:border-indigo-500 transition-all cursor-pointer">
|
||||||
|
<option value="python" {{ strtolower($interview->language) === 'python' ? 'selected' : '' }}>Python 3</option>
|
||||||
|
<option value="cpp" {{ strtolower($interview->language) === 'cpp' ? 'selected' : '' }}>C++ (GCC)</option>
|
||||||
|
<option value="c" {{ strtolower($interview->language) === 'c' ? 'selected' : '' }}>C (GCC)</option>
|
||||||
|
<option value="java" {{ strtolower($interview->language) === 'java' ? 'selected' : '' }}>Java 15</option>
|
||||||
|
<option value="php" {{ strtolower($interview->language) === 'php' ? 'selected' : '' }}>PHP 8.2 / Laravel</option>
|
||||||
|
<option value="javascript" {{ strtolower($interview->language) === 'javascript' ? 'selected' : '' }}>JavaScript (Node.js)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" onclick="runCode()" class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold bg-indigo-600 border border-indigo-500 text-white hover:bg-indigo-500 shadow-md shadow-indigo-600/30 transition-all cursor-pointer">
|
||||||
|
▶ Run Code
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" onclick="submitSolution()" class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-xs font-bold bg-emerald-600 border border-emerald-500 text-white hover:bg-emerald-500 shadow-md shadow-emerald-600/30 transition-all cursor-pointer">
|
||||||
|
✓ Submit Final Solution
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" onclick="candidateLogoutAction()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold bg-red-500/20 border border-red-500/40 text-red-300 hover:bg-red-500/30 transition-all cursor-pointer">
|
||||||
|
🚪 Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<div id="candidate-incoming-modal" class="fixed inset-0 w-screen h-screen bg-[#050811]/85 backdrop-blur-md flex items-center justify-center z-[10000] opacity-0 pointer-events-none transition-opacity duration-300 [&.active]:opacity-100 [&.active]:pointer-events-auto">
|
||||||
|
<div class="max-w-[440px] w-[90%] bg-gradient-to-br from-slate-900 via-slate-900 to-indigo-950 border-2 border-emerald-500 rounded-3xl p-8 text-center text-white shadow-2xl shadow-emerald-500/30">
|
||||||
|
<div class="relative w-20 h-20 mx-auto mb-5 flex items-center justify-center">
|
||||||
|
<div class="absolute -inset-3 rounded-full border-2 border-emerald-500/60 animate-ping"></div>
|
||||||
|
<div class="absolute -inset-6 rounded-full border-2 border-indigo-500/40 animate-pulse"></div>
|
||||||
|
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-emerald-500 to-sky-600 flex items-center justify-center text-2xl font-bold text-white shadow-lg shadow-emerald-500/50 z-10">
|
||||||
|
<i class="fa-solid fa-phone"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-[11px] uppercase tracking-widest text-emerald-400 font-extrabold mb-1.5">
|
||||||
|
⚡ INCOMING INTERVIEW CALL
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="font-outfit text-2xl font-extrabold text-white mb-1">
|
||||||
|
Interviewer Panel Calling...
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="text-xs text-indigo-300 mb-6">
|
||||||
|
The interviewer panel has initiated a live 2-way call.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 justify-center">
|
||||||
|
<button type="button" onclick="declineCandidateCall()" class="flex-1 py-3 px-4 bg-red-500/20 border border-red-500 text-red-300 font-bold text-xs rounded-xl hover:bg-red-500/30 transition-all cursor-pointer">
|
||||||
|
🛑 Decline / Busy
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="acceptCandidateCall()" class="flex-[1.4] py-3 px-4 bg-gradient-to-r from-emerald-500 to-emerald-600 text-white font-extrabold text-xs rounded-xl border-none shadow-lg shadow-emerald-500/50 hover:from-emerald-600 hover:to-emerald-700 transition-all cursor-pointer animate-pulse">
|
||||||
|
📞 Accept Call
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
@props([
|
||||||
|
'notes' => '',
|
||||||
|
])
|
||||||
|
|
||||||
|
<div id="notepad-modal" class="fixed top-[70px] right-[360px] w-[420px] h-[350px] bg-slate-900 border-2 border-indigo-500 rounded-2xl shadow-2xl hidden flex-col z-50 overflow-hidden">
|
||||||
|
<div class="bg-slate-800 px-4 py-2.5 flex justify-between items-center border-b border-white/10">
|
||||||
|
<div class="font-bold text-xs text-white flex items-center gap-1.5">
|
||||||
|
<span>📝</span>
|
||||||
|
<span>Candidate Notepad (Synced)</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="toggleNotepadModal()" class="bg-transparent border-none text-slate-400 hover:text-white text-lg cursor-pointer leading-none">×</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="notepad-textarea" oninput="saveNotepadContent()" placeholder="Type rough notes, pseudo-code, or thoughts here..." class="flex-1 w-full p-3.5 bg-slate-950 text-slate-200 border-none outline-none font-mono text-xs resize-none placeholder-slate-600">{{ $notes }}</textarea>
|
||||||
|
<div class="px-3 py-1.5 bg-slate-900 text-[10.5px] text-emerald-400 text-right border-t border-white/5 font-medium">
|
||||||
|
✓ Auto-saved to interviewer portal
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
<aside class="w-[340px] bg-slate-900/90 border-l border-white/10 p-4 flex flex-col gap-4 overflow-y-auto shrink-0 select-none">
|
||||||
|
<!-- Candidate Camera Feed -->
|
||||||
|
<div class="w-full h-[180px] min-h-[180px] bg-black rounded-xl border-2 border-white/15 overflow-hidden relative shrink-0">
|
||||||
|
<video id="webcam" autoplay muted playsinline class="w-full h-full object-cover -scale-x-100"></video>
|
||||||
|
<div id="webcam-avatar-placeholder" class="absolute inset-0 hidden flex-col items-center justify-center bg-slate-900 text-white z-10">
|
||||||
|
<div class="w-14 h-14 rounded-full bg-gradient-to-br from-indigo-500 to-sky-600 flex items-center justify-center text-xl font-bold font-outfit shadow-lg shadow-indigo-500/40">
|
||||||
|
{{ $interview->candidate_initials }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-400 mt-1.5 font-medium">Camera Disabled</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-2 left-2 bg-black/70 px-2 py-0.5 rounded text-[10.5px] text-emerald-400 font-semibold z-10 flex items-center gap-1.5 backdrop-blur-xs">
|
||||||
|
<span>Candidate (You)</span>
|
||||||
|
<span class="opacity-40">|</span>
|
||||||
|
<i id="cand-self-mic-icon" class="fa-solid fa-microphone text-emerald-400 text-[10px]" title="Mic On"></i>
|
||||||
|
<i id="cand-self-cam-icon" class="fa-solid fa-video text-emerald-400 text-[10px]" title="Camera On"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remote Interviewer Panelist Video Grid (Multi-Party WebRTC Mesh) -->
|
||||||
|
<div id="interviewer-mesh-grid" class="flex flex-col gap-2.5 w-full">
|
||||||
|
<div id="interviewer-placeholder-box" class="w-full h-[180px] min-h-[180px] bg-black rounded-xl border-2 border-indigo-500/40 overflow-hidden relative shrink-0">
|
||||||
|
<video id="interviewer-video-default" autoplay playsinline class="w-full h-full object-cover bg-slate-950 hidden -scale-x-100"></video>
|
||||||
|
<div id="interviewer-video-placeholder" class="absolute inset-0 flex flex-col items-center justify-center bg-slate-900/95 text-slate-400 text-xs text-center p-2.5 z-10">
|
||||||
|
<div id="interviewer-avatar-circle" class="w-14 h-14 rounded-full bg-gradient-to-br from-sky-400 to-indigo-500 flex items-center justify-center text-xl font-bold font-outfit text-white mb-1.5 shadow-lg shadow-sky-500/30">
|
||||||
|
IV
|
||||||
|
</div>
|
||||||
|
<div id="call-status-sub" class="text-xs text-slate-200 font-semibold">Interviewer Panel</div>
|
||||||
|
<div class="text-[10.5px] text-sky-400 mt-0.5 font-medium">Waiting for interviewer panelist to join call...</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-2 left-2 bg-indigo-600/90 px-2 py-0.5 rounded text-[10.5px] text-white font-semibold z-10 backdrop-blur-xs">
|
||||||
|
🎙️ Interviewer / Panelist
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Proctor Canvas for Hidden AI Analysis -->
|
||||||
|
<canvas id="proctor-canvas" class="hidden" width="320" height="240"></canvas>
|
||||||
|
|
||||||
|
<!-- Candidate Profile Info -->
|
||||||
|
<div class="bg-white/[0.03] border border-white/10 rounded-xl p-3.5 text-xs leading-relaxed">
|
||||||
|
<div class="font-bold text-white mb-1.5">Candidate Profile</div>
|
||||||
|
<div class="text-slate-400">Name: <strong class="text-white">{{ $interview->candidate_name }}</strong></div>
|
||||||
|
<div class="text-slate-400">Email: <span class="text-white font-mono text-[11.5px]">{{ $interview->candidate_email }}</span></div>
|
||||||
|
<div class="text-slate-400 mt-1.5">Access Expires: <span class="text-red-400 font-semibold">{{ $interview->expires_at->format('H:i:s T') }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Real-Time Call Controls (Mic, Camera, Screen Sharing) -->
|
||||||
|
<div class="bg-indigo-500/10 border border-indigo-500/30 rounded-xl p-3.5">
|
||||||
|
<div class="text-xs font-bold text-white mb-2.5 flex items-center justify-between">
|
||||||
|
<span>📞 Real-Time Interview Call</span>
|
||||||
|
<span id="call-status-badge" class="text-[10px] font-bold bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded-full">READY</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="candidate-join-active-call-btn" onclick="acceptCandidateCall()" class="hidden w-full p-2.5 mb-2.5 text-xs font-bold bg-gradient-to-r from-emerald-500 to-emerald-600 text-white rounded-lg border-none shadow-lg shadow-emerald-500/30 cursor-pointer animate-pulse">
|
||||||
|
🟢 📞 Call in Progress • Join Call Now
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mb-2.5">
|
||||||
|
<button type="button" id="toggle-mic-btn" onclick="toggleMic()" class="flex-1 py-2 px-2 text-xs font-semibold rounded-lg bg-emerald-500/20 border border-emerald-500 text-emerald-300 hover:bg-emerald-500/30 transition-all cursor-pointer">
|
||||||
|
🎤 Mic On
|
||||||
|
</button>
|
||||||
|
<button type="button" id="toggle-cam-btn" onclick="toggleCam()" class="flex-1 py-2 px-2 text-xs font-semibold rounded-lg bg-emerald-500/20 border border-emerald-500 text-emerald-300 hover:bg-emerald-500/30 transition-all cursor-pointer">
|
||||||
|
📷 Cam On
|
||||||
|
</button>
|
||||||
|
<button type="button" id="leave-call-btn" onclick="candidateLeaveCall()" class="py-2 px-2.5 text-xs font-semibold rounded-lg bg-red-500/20 border border-red-500 text-red-300 hover:bg-red-500/30 transition-all cursor-pointer" title="Leave 2-Way Call">
|
||||||
|
🚪 Leave Call
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" id="share-screen-btn" onclick="startScreenShare()" class="w-full py-2.5 px-3 text-xs font-bold bg-gradient-to-r from-indigo-500 to-sky-600 hover:from-indigo-600 hover:to-sky-700 text-white rounded-lg border-none shadow-md shadow-indigo-500/30 transition-all cursor-pointer">
|
||||||
|
🖥️ Share Screen with Interviewer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2-Way Real-Time Chat Container with Interviewer/Panelist -->
|
||||||
|
<div class="bg-slate-900/80 border border-white/10 rounded-xl p-3">
|
||||||
|
<div class="text-xs font-bold text-white mb-2 flex items-center gap-1.5">
|
||||||
|
<span>💬 Live Chat with Interviewer</span>
|
||||||
|
</div>
|
||||||
|
<div id="candidate-chat-history" class="bg-[#050811] border border-white/10 rounded-lg p-2 h-[130px] overflow-y-auto text-xs mb-2 flex flex-col gap-1">
|
||||||
|
<div class="text-slate-500 italic text-center pt-10 text-[11px]">No messages yet. Send a message below.</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
<input type="text" id="candidate-chat-input" placeholder="Type message to panelist..." onkeydown="if(event.key==='Enter') sendCandidateMessage()" class="flex-1 text-xs bg-slate-950 text-white border border-white/10 rounded-lg px-2.5 py-1.5 outline-none focus:border-indigo-500 placeholder-slate-500">
|
||||||
|
<button type="button" onclick="sendCandidateMessage()" class="px-3 py-1.5 text-xs font-semibold bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg border-none cursor-pointer transition-colors">
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<div class="h-[220px] bg-[#050811] border-t border-white/10 flex flex-col shrink-0">
|
||||||
|
<div class="h-9 bg-[#090e1a] border-b border-white/10 flex items-center justify-between px-4 text-xs font-semibold text-slate-400">
|
||||||
|
<span>EXECUTION OUTPUT TERMINAL (STDOUT / STDERR)</span>
|
||||||
|
<span id="run-status" class="text-slate-400">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div id="terminal-output" class="flex-1 p-3.5 font-['Fira_Code',monospace] text-xs text-emerald-400 overflow-y-auto whitespace-pre-wrap leading-relaxed">// Press 'Run Code' to execute candidate solution live...</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<x-modal id="incoming-call-modal" maxWidth="md">
|
||||||
|
<div class="p-4 sm:p-6 text-center">
|
||||||
|
<div class="relative w-20 h-20 mx-auto mb-5 flex items-center justify-center">
|
||||||
|
<div class="absolute -inset-3 rounded-full border-2 border-emerald-500/60 animate-ping"></div>
|
||||||
|
<div class="absolute -inset-6 rounded-full border-2 border-indigo-500/40 animate-pulse"></div>
|
||||||
|
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-emerald-500 to-sky-600 flex items-center justify-center text-2xl font-bold text-white shadow-lg shadow-emerald-500/50 z-10">
|
||||||
|
<i class="fa-solid fa-phone"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-[11px] uppercase tracking-widest text-emerald-400 font-bold mb-1.5">
|
||||||
|
⚡ Incoming Assessment Call
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 id="incoming-cand-name" class="font-outfit text-2xl font-extrabold text-white mb-1">
|
||||||
|
Candidate Name
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div id="incoming-starter-info" class="text-xs text-sky-400 mb-4">
|
||||||
|
Initiated by Panelist
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white/5 border border-white/10 rounded-xl p-3 text-xs text-slate-300 mb-6 leading-relaxed">
|
||||||
|
Another interviewer has started calling the candidate. Click <strong>Accept Call</strong> to join live, or <strong>Decline</strong> if busy.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 justify-center">
|
||||||
|
<x-button id="incoming-decline-btn" onclick="declineIncomingCall()" variant="danger" size="md" icon="fa-solid fa-xmark" class="flex-1">
|
||||||
|
Decline / Miss
|
||||||
|
</x-button>
|
||||||
|
<x-button id="incoming-accept-btn" onclick="acceptIncomingCall()" variant="success" size="md" icon="fa-solid fa-phone" class="flex-[1.3] font-extrabold animate-pulse">
|
||||||
|
Accept Call
|
||||||
|
</x-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-modal>
|
||||||
35
resources/views/components/interview/navbar.blade.php
Normal file
35
resources/views/components/interview/navbar.blade.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@props([
|
||||||
|
'activeNav' => 'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<a href="{{ route('interview.index') }}" 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 no-underline">
|
||||||
|
<i class="fa-solid fa-bolt text-lg"></i>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h1 class="font-outfit text-lg font-bold text-white m-0 leading-tight">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::check() && 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>
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
@props([
|
||||||
|
'metrics' => [],
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$tabSwitches = $metrics['tab_switches'] ?? 0;
|
||||||
|
$focusLost = $metrics['focus_lost'] ?? 0;
|
||||||
|
$gazeAlerts = $metrics['gaze_alerts'] ?? 0;
|
||||||
|
$lowerGaze = $metrics['lower_gaze'] ?? 0;
|
||||||
|
$questionRepeat = $metrics['question_repeat'] ?? 0;
|
||||||
|
$externalAi = $metrics['external_ai'] ?? 0;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div {{ $attributes->merge(['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>
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
@if(!empty($interview->candidate_notes) || !empty($interview->interviewer_notes) || !empty($interview->candidate_drawing) || (is_array($interview->formatted_recordings) && count($interview->formatted_recordings) > 0))
|
||||||
|
<div class="font-outfit text-base font-bold text-slate-900 mb-3 flex items-center gap-2">
|
||||||
|
📝 Workspace Artifacts, Evaluation Notes & Call Session Recordings
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4 mb-7">
|
||||||
|
@if(!empty($interview->candidate_notes))
|
||||||
|
<div class="text-xs font-bold text-slate-600 mb-1.5 uppercase tracking-wide">
|
||||||
|
CANDIDATE NOTEPAD NOTES (UNIQUE ID SYNCED):
|
||||||
|
</div>
|
||||||
|
<pre class="font-mono text-xs bg-white text-slate-800 p-3 rounded-lg border border-slate-200 mb-3.5 whitespace-pre-wrap">{{ $interview->candidate_notes }}</pre>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(!empty($interview->interviewer_notes))
|
||||||
|
<div class="text-xs font-bold text-emerald-700 mb-1.5 uppercase tracking-wide">
|
||||||
|
INTERVIEWER EVALUATION NOTES (STORED BY UNIQUE ID):
|
||||||
|
</div>
|
||||||
|
<pre class="font-sans text-xs bg-emerald-50 text-emerald-900 p-3 rounded-lg border border-emerald-200 mb-3.5 whitespace-pre-wrap">{{ $interview->interviewer_notes }}</pre>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(!empty($interview->candidate_drawing))
|
||||||
|
<div class="text-xs font-bold text-slate-600 mb-1.5 uppercase tracking-wide">
|
||||||
|
CANDIDATE DRAWING DIAGRAM:
|
||||||
|
</div>
|
||||||
|
<img src="{{ $interview->candidate_drawing }}" alt="Candidate Drawing" class="max-w-full max-h-[250px] rounded-lg border border-slate-200 mb-3.5" />
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(is_array($interview->formatted_recordings) && count($interview->formatted_recordings) > 0)
|
||||||
|
<div class="text-xs font-bold text-indigo-700 mb-2 flex justify-between items-center uppercase tracking-wide">
|
||||||
|
<span>📹 STORED CALL SESSION RECORDINGS ({{ count($interview->formatted_recordings) }} RECORDINGS):</span>
|
||||||
|
<span class="text-[11px] text-slate-400 font-medium">📜 Scroll to view all</span>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-[480px] overflow-y-auto space-y-3 p-2.5 bg-slate-100/60 rounded-lg border border-slate-200 mb-2">
|
||||||
|
@foreach($interview->formatted_recordings as $displayIdx => $rec)
|
||||||
|
<div class="bg-white border border-slate-200 rounded-lg p-2.5">
|
||||||
|
<div class="flex justify-between items-center mb-1.5">
|
||||||
|
<span class="text-xs font-bold text-slate-900">Recording #{{ count($interview->formatted_recordings) - $displayIdx }} ({{ $rec['created_at'] }})</span>
|
||||||
|
<a href="{{ $rec['download_url'] }}" download class="text-[11px] text-indigo-600 font-bold hover:underline bg-indigo-50 px-2 py-1 rounded">
|
||||||
|
📥 Download MP4 Video
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<video controls preload="metadata" class="w-full max-h-[220px] rounded bg-black">
|
||||||
|
<source src="{{ $rec['stream_url'] }}" type="{{ $rec['mime_type'] ?? 'video/webm' }}">
|
||||||
|
<source src="{{ $rec['stream_url'] }}">
|
||||||
|
Your browser does not support video playback.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
@props([
|
||||||
|
'logs' => [],
|
||||||
|
])
|
||||||
|
|
||||||
|
@if(is_array($logs) && count($logs) > 0)
|
||||||
|
<div class="overflow-x-auto w-full mb-7 border border-slate-200 rounded-xl">
|
||||||
|
<table class="w-full text-left text-xs border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-slate-100 border-b border-slate-200 text-slate-600 font-bold uppercase text-[10.5px]">
|
||||||
|
<th class="py-2.5 px-3.5">Timestamp</th>
|
||||||
|
<th class="py-2.5 px-3.5">Violation Category</th>
|
||||||
|
<th class="py-2.5 px-3.5">Audit Details</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-200 bg-white">
|
||||||
|
@foreach($logs as $log)
|
||||||
|
<tr>
|
||||||
|
<td class="py-2.5 px-3.5 text-slate-500 font-mono text-[11px] whitespace-nowrap">
|
||||||
|
{{ isset($log['timestamp']) ? \Carbon\Carbon::parse($log['timestamp'])->format('H:i:s T') : '--:--:--' }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3.5 font-bold text-slate-800 uppercase">
|
||||||
|
{{ str_replace('_', ' ', $log['type'] ?? 'Violation') }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2.5 px-3.5 text-slate-600">
|
||||||
|
{{ $log['details'] ?? 'Incident flagged by proctoring engine.' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="bg-emerald-500/10 border border-emerald-500/25 text-emerald-700 px-4 py-3 rounded-xl text-xs font-semibold mb-7 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-circle-check text-emerald-600 text-sm"></i>
|
||||||
|
<span>Zero proctoring violations or malpractice detected during the session.</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
@props([
|
||||||
|
'code' => null,
|
||||||
|
'output' => null,
|
||||||
|
'language' => 'code',
|
||||||
|
])
|
||||||
|
|
||||||
|
<!-- Code Solution Block -->
|
||||||
|
<div class="font-outfit text-base font-bold text-slate-900 mb-3 flex items-center gap-2">
|
||||||
|
💻 Submitted Code Solution ({{ strtoupper($language) }})
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-950 text-sky-400 rounded-xl p-4 font-mono text-xs whitespace-pre-wrap mb-5 max-h-[300px] overflow-y-auto leading-relaxed border border-slate-800">
|
||||||
|
{{ $code ?? '// No code solution submitted by candidate.' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Code Output Block -->
|
||||||
|
<div class="font-outfit text-base font-bold text-slate-900 mb-3 flex items-center gap-2">
|
||||||
|
🖥️ Engine Execution Stdout Output
|
||||||
|
</div>
|
||||||
|
<div class="bg-[#050811] text-emerald-400 rounded-xl p-3.5 font-mono text-xs whitespace-pre-wrap mb-7 max-h-[120px] overflow-y-auto leading-relaxed border border-slate-900">
|
||||||
|
{{ $output ?? 'No execution output recorded.' }}
|
||||||
|
</div>
|
||||||
39
resources/views/components/interview/report/header.blade.php
Normal file
39
resources/views/components/interview/report/header.blade.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
<!-- Top Bar with Download PDF Button -->
|
||||||
|
<div class="flex justify-between items-center mb-5 pb-3 border-b border-slate-200 print:hidden">
|
||||||
|
<div class="text-sm font-bold text-slate-900 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-file-lines text-indigo-600"></i>
|
||||||
|
<span>Executive Assessment Performance Report</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="window.print()" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-xs py-2.5 px-4 rounded-xl cursor-pointer flex items-center gap-2 shadow-md shadow-indigo-600/30 transition-all">
|
||||||
|
<i class="fa-solid fa-download"></i>
|
||||||
|
<span>Download PDF Report</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex justify-between items-start border-b-2 border-indigo-600 pb-5 mb-6">
|
||||||
|
<div>
|
||||||
|
<div class="font-outfit text-2xl font-extrabold text-slate-900 flex items-center gap-2">
|
||||||
|
<i class="fa-solid fa-bolt text-indigo-600"></i> SingleLogin Enterprise Assessment
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-500 mt-1">
|
||||||
|
Candidate Technical Competency & Behavioral Audit Report
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<span class="inline-block bg-indigo-500/10 text-indigo-600 text-[11px] font-bold px-3 py-1 rounded-full uppercase">
|
||||||
|
Executive Summary
|
||||||
|
</span>
|
||||||
|
<div class="text-xs text-slate-500 mt-1.5 font-mono">
|
||||||
|
Report ID: RPT-{{ $interview->submission_unique_id }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-500">
|
||||||
|
Date: {{ now()->format('M d, Y - H:i:s T') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
@props([
|
||||||
|
'metrics' => [],
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$tabSwitches = $metrics['tab_switches'] ?? 0;
|
||||||
|
$focusLosses = $metrics['focus_lost'] ?? 0;
|
||||||
|
$gazeAnomalies = $metrics['gaze_alerts'] ?? 0;
|
||||||
|
$pasteEvents = $metrics['paste_events'] ?? 0;
|
||||||
|
$lowerGaze = $metrics['lower_gaze'] ?? 0;
|
||||||
|
$questionRepeat = $metrics['question_repeat'] ?? 0;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="font-outfit text-base font-bold text-slate-900 mb-3 flex items-center gap-2">
|
||||||
|
🔍 Behavioral Audit & Proctoring Incident Breakdown
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2.5 mb-5">
|
||||||
|
<div class="bg-slate-50 p-2.5 rounded-xl border border-slate-200 text-center">
|
||||||
|
<div class="text-lg font-extrabold text-red-500">{{ $tabSwitches }}</div>
|
||||||
|
<div class="text-[10px] text-slate-500 font-bold uppercase mt-0.5">Tab Switches</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 p-2.5 rounded-xl border border-slate-200 text-center">
|
||||||
|
<div class="text-lg font-extrabold text-amber-500">{{ $focusLosses }}</div>
|
||||||
|
<div class="text-[10px] text-slate-500 font-bold uppercase mt-0.5">Focus Lost</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 p-2.5 rounded-xl border border-slate-200 text-center">
|
||||||
|
<div class="text-lg font-extrabold text-sky-500">{{ $gazeAnomalies }}</div>
|
||||||
|
<div class="text-[10px] text-slate-500 font-bold uppercase mt-0.5">Gaze Anomalies</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 p-2.5 rounded-xl border border-slate-200 text-center">
|
||||||
|
<div class="text-lg font-extrabold text-indigo-500">{{ $pasteEvents }}</div>
|
||||||
|
<div class="text-[10px] text-slate-500 font-bold uppercase mt-0.5">Code Pastes</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 p-2.5 rounded-xl border border-slate-200 text-center">
|
||||||
|
<div class="text-lg font-extrabold text-red-500">{{ $lowerGaze }}</div>
|
||||||
|
<div class="text-[10px] text-slate-500 font-bold uppercase mt-0.5">📱 Lower Gaze</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 p-2.5 rounded-xl border border-slate-200 text-center">
|
||||||
|
<div class="text-lg font-extrabold text-rose-500">{{ $questionRepeat }}</div>
|
||||||
|
<div class="text-[10px] text-slate-500 font-bold uppercase mt-0.5">🗣️ Question Repeat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$statusColor = match($interview->status) {
|
||||||
|
'completed' => 'text-emerald-600',
|
||||||
|
'blocked' => 'text-red-600',
|
||||||
|
default => 'text-indigo-600',
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 bg-slate-50 border border-slate-200 rounded-xl p-4 mb-7">
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] uppercase font-bold text-slate-500 mb-1">Candidate Name</label>
|
||||||
|
<span class="font-bold text-sm text-slate-900">{{ $interview->candidate_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] uppercase font-bold text-slate-500 mb-1">Email Address</label>
|
||||||
|
<span class="font-semibold text-xs text-slate-700 font-mono">{{ $interview->candidate_email }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] uppercase font-bold text-slate-500 mb-1">Language & Tech</label>
|
||||||
|
<span class="font-bold text-sm text-slate-900 uppercase font-mono">{{ $interview->language }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-[11px] uppercase font-bold text-slate-500 mb-1">Assessment Status</label>
|
||||||
|
<span class="font-extrabold text-sm uppercase {{ $statusColor }}">
|
||||||
|
{{ $interview->status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
@props([
|
||||||
|
'integrityScore' => 100,
|
||||||
|
'integrityLabel' => 'Optimal Candidate Trust',
|
||||||
|
'integrityColor' => '#10b981',
|
||||||
|
'incidentCount' => 0,
|
||||||
|
'codeScore' => 0,
|
||||||
|
'hasSubmittedCode' => false,
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$codeLabel = $codeScore >= 80 ? 'Strong Technical Match' : ($codeScore >= 50 ? 'Moderate Competency' : 'Needs Further Review');
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-7">
|
||||||
|
<!-- Behavioral Integrity Score -->
|
||||||
|
<div class="bg-slate-50 border border-slate-200 border-l-4 rounded-xl p-5 relative" style="border-left-color: {{ $integrityColor }};">
|
||||||
|
<div class="text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
🛡️ Behavioral Integrity Rating
|
||||||
|
</div>
|
||||||
|
<div class="font-outfit text-4xl font-extrabold mt-1.5" style="color: {{ $integrityColor }};">
|
||||||
|
{{ $integrityScore }}%
|
||||||
|
</div>
|
||||||
|
<div class="text-xs font-bold mt-1" style="color: {{ $integrityColor }};">
|
||||||
|
{{ $integrityLabel }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-500 mt-1.5 font-medium">
|
||||||
|
Recorded Violations: <strong class="text-slate-700">{{ $incidentCount }} incident(s)</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Technical Score -->
|
||||||
|
<div class="bg-slate-50 border border-slate-200 border-l-4 border-l-indigo-600 rounded-xl p-5 relative">
|
||||||
|
<div class="text-xs font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
💻 Technical Code Competency
|
||||||
|
</div>
|
||||||
|
<div class="font-outfit text-4xl font-extrabold text-indigo-600 mt-1.5">
|
||||||
|
{{ $codeScore }} / 100
|
||||||
|
</div>
|
||||||
|
<div class="text-xs font-bold text-indigo-600 mt-1">
|
||||||
|
{{ $codeLabel }}
|
||||||
|
</div>
|
||||||
|
<div class="text-[11px] text-slate-500 mt-1.5 font-medium">
|
||||||
|
Solution Code: <strong class="text-slate-700">{{ $hasSubmittedCode ? 'Submitted' : 'Pending' }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
@props([
|
||||||
|
'recommended' => true,
|
||||||
|
])
|
||||||
|
|
||||||
|
<div class="border-t-2 border-slate-200 pt-5 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<div class="text-[11px] font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
HR & Lead Panel Approval
|
||||||
|
</div>
|
||||||
|
<div class="font-bold text-sm text-slate-900 mt-1">
|
||||||
|
SingleLogin Enterprise HR Board
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-[11px] font-bold text-slate-500 uppercase tracking-wider">
|
||||||
|
Final Recommendation
|
||||||
|
</div>
|
||||||
|
<div class="font-outfit font-extrabold text-base mt-1 {{ $recommended ? 'text-emerald-600' : 'text-red-600' }}">
|
||||||
|
{{ $recommended ? 'RECOMMENDED FOR HIRE' : 'REQUIRES FURTHER REVIEW / FLAGGED' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
@props([
|
||||||
|
'interviewers' => [],
|
||||||
|
])
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<x-label required>Candidate Name</x-label>
|
||||||
|
<x-input type="text" name="candidate_name" required placeholder="e.g. John Doe" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<x-label required>Candidate Email</x-label>
|
||||||
|
<x-input type="email" name="candidate_email" required placeholder="john@gmail.com" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3.5">
|
||||||
|
<div>
|
||||||
|
<x-label required>Phone Number</x-label>
|
||||||
|
<x-input type="text" name="candidate_phone" required placeholder="9876543210" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<x-label>Start Time (Timeline)</x-label>
|
||||||
|
<x-input type="datetime-local" name="scheduled_at" value="{{ now()->format('Y-m-d\TH:i') }}" class="bg-slate-900" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<x-label required>Access Duration</x-label>
|
||||||
|
<x-select name="valid_hours" required>
|
||||||
|
<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>
|
||||||
|
</x-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3.5">
|
||||||
|
<x-label required>Coding Language</x-label>
|
||||||
|
<x-select name="language" required>
|
||||||
|
<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>
|
||||||
|
</x-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex justify-between items-center mb-1.5">
|
||||||
|
<x-label>Assign Internal Interviewers / Panelists</x-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 transition-colors">
|
||||||
|
<input type="checkbox" name="assigned_interviewers[]" value="{{ $usr->id }}" class="accent-indigo-500 rounded">
|
||||||
|
{{ $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>
|
||||||
89
resources/views/components/interview/session-row.blade.php
Normal file
89
resources/views/components/interview/session-row.blade.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
@props([
|
||||||
|
'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$metrics = $interview->proctor_metrics;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<tr class="hover:bg-white/[0.02] transition-colors">
|
||||||
|
<!-- Candidate Info & Temp Pass -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<div>
|
||||||
|
<strong class="text-white text-sm block">{{ $interview->candidate_name }}</strong>
|
||||||
|
<div class="text-xs text-slate-400 mt-0.5">{{ $interview->candidate_email }} • {{ $interview->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>{{ $interview->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>
|
||||||
|
|
||||||
|
<!-- Unique Submission ID -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<code class="bg-white/5 px-2 py-1 rounded text-sky-400 text-xs font-mono">
|
||||||
|
{{ $interview->submission_unique_id }}
|
||||||
|
</code>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status & Validity -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
@if($interview->isExpired())
|
||||||
|
<x-pill variant="danger" size="sm">EXPIRED</x-pill>
|
||||||
|
@elseif($interview->status === 'completed')
|
||||||
|
<x-pill variant="success" size="sm">COMPLETED</x-pill>
|
||||||
|
@elseif($interview->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($interview->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: {{ $interview->formatted_timeline }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Language -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<x-pill variant="ghost" size="sm" class="uppercase font-bold">
|
||||||
|
{{ $interview->language }}
|
||||||
|
</x-pill>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Proctoring Alerts -->
|
||||||
|
<td class="py-3.5 px-4">
|
||||||
|
<x-interview.proctor-badges :metrics="$metrics" />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Review & Actions -->
|
||||||
|
<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', $interview->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($interview->call_status === 'active')
|
||||||
|
<a href="{{ route('interview.show', $interview->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', $interview->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>
|
||||||
13
resources/views/components/interview/sos-modal.blade.php
Normal file
13
resources/views/components/interview/sos-modal.blade.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<x-modal id="sos-modal" title="Candidate Potential Cheating Alert" maxWidth="lg" onClose="stopSosAlarm">
|
||||||
|
<div id="sos-violation-list" class="bg-red-500/10 border border-red-500/30 rounded-lg p-3.5 text-xs text-red-400 mb-4 max-h-[180px] overflow-y-auto font-medium">
|
||||||
|
Violation details loading...
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<x-button onclick="stopSosAlarm()" variant="secondary" size="md" icon="fa-solid fa-check">
|
||||||
|
Acknowledge Alert
|
||||||
|
</x-button>
|
||||||
|
<x-button onclick="disableSosAndStopAlarm()" variant="danger" size="md" icon="fa-solid fa-bell-slash">
|
||||||
|
Acknowledge & Disable SOS
|
||||||
|
</x-button>
|
||||||
|
</div>
|
||||||
|
</x-modal>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<div id="tab-content-code" class="block">
|
||||||
|
<div class="text-[11.5px] text-slate-400 mb-2 font-medium">Current code solution — visible even if candidate does not share screen</div>
|
||||||
|
<pre id="modal-code-display" class="bg-slate-950/70 border border-slate-800 rounded-lg p-3.5 font-mono text-[13px] text-emerald-300 min-h-[220px] max-h-[300px] overflow-y-auto whitespace-pre-wrap"></pre>
|
||||||
|
|
||||||
|
<div class="text-[11.5px] text-slate-400 mt-4 mb-2 font-medium">Execution stdout / stderr</div>
|
||||||
|
<pre id="modal-output-display" class="bg-slate-950/70 border border-slate-800 rounded-lg p-3 font-mono text-xs text-slate-400 max-h-[100px] overflow-y-auto whitespace-pre-wrap"></pre>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<div id="tab-content-drawing" class="hidden">
|
||||||
|
<div class="text-[11.5px] text-slate-400 mb-2 font-medium">Candidate live drawing / architecture diagram</div>
|
||||||
|
<div class="bg-slate-950/70 border border-slate-800 rounded-lg p-3 flex justify-center items-center min-h-[250px]">
|
||||||
|
<img id="modal-drawing-img" src="" alt="Candidate Drawing Board" class="max-w-full max-h-[320px] hidden rounded-lg border border-dashed border-slate-700" />
|
||||||
|
<div id="no-drawing-placeholder" class="text-slate-500 text-xs">No drawing data submitted by candidate yet.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<div id="tab-content-notes" class="hidden">
|
||||||
|
<div class="text-[11.5px] text-slate-400 mb-2 font-medium">Candidate live notepad notes (auto-synced)</div>
|
||||||
|
<pre id="modal-notes-display" class="bg-slate-950/70 border border-slate-800 rounded-lg p-3.5 font-mono text-xs text-slate-200 min-h-[250px] max-h-[350px] overflow-y-auto whitespace-pre-wrap"></pre>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<div id="tab-content-recordings" class="hidden">
|
||||||
|
<div class="text-[11.5px] text-slate-400 mb-2 font-medium">Saved candidate video sessions (sorted by newest first)</div>
|
||||||
|
<div id="modal-recordings-container" class="bg-slate-950/70 border border-slate-800 rounded-lg p-3 min-h-[250px] max-h-[420px] overflow-y-auto">
|
||||||
|
<div class="text-slate-500 text-xs text-center pt-10">No video recordings saved for this candidate yet.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<div id="tab-content-screenshots" class="hidden">
|
||||||
|
<div class="bg-slate-950/70 border border-slate-800 rounded-lg p-8 flex flex-col items-center justify-center min-h-[250px] text-center">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-400 mb-3 text-base">
|
||||||
|
<i class="fa-solid fa-clock"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-semibold text-white mb-1">Coming Soon</div>
|
||||||
|
<div class="text-xs text-slate-400 max-w-xs leading-relaxed">Tab switch screenshot capturing and inspection is currently under maintenance.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
10
resources/views/components/label.blade.php
Normal file
10
resources/views/components/label.blade.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
@props([
|
||||||
|
'required' => false,
|
||||||
|
])
|
||||||
|
|
||||||
|
<label {{ $attributes->merge(['class' => 'block text-xs font-medium text-slate-400 mb-1']) }}>
|
||||||
|
{{ $slot }}
|
||||||
|
@if($required)
|
||||||
|
<span class="text-red-500 font-bold ml-0.5">*</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
37
resources/views/components/layouts/candidate.blade.php
Normal file
37
resources/views/components/layouts/candidate.blade.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
@props([
|
||||||
|
'title' => 'Live Assessment Room',
|
||||||
|
])
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>{{ $title }} | 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=Fira+Code:wght@400;500;600&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.2/css/all.min.css">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
|
||||||
|
<!-- WebRTC, PeerJS, Metered SDK, SweetAlert2 & html2canvas -->
|
||||||
|
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
||||||
|
<script src="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Monaco Code Editor Loader -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
|
||||||
|
|
||||||
|
@stack('styles')
|
||||||
|
@stack('head-scripts')
|
||||||
|
</head>
|
||||||
|
<body {{ $attributes->merge(['class' => "bg-[#080c14] text-white h-screen flex flex-col font-['Instrument_Sans',sans-serif] overflow-hidden antialiased select-none"]) }}>
|
||||||
|
|
||||||
|
{{ $slot }}
|
||||||
|
|
||||||
|
@stack('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
resources/views/components/layouts/guest.blade.php
Normal file
32
resources/views/components/layouts/guest.blade.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
@props([
|
||||||
|
'title' => 'Candidate Portal',
|
||||||
|
])
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>{{ $title }} | 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.2/css/all.min.css">
|
||||||
|
|
||||||
|
@stack('styles')
|
||||||
|
@stack('head-scripts')
|
||||||
|
</head>
|
||||||
|
<body {{ $attributes->merge(['class' => "bg-[#0b0f19] text-white min-h-screen flex items-center justify-center p-4 relative overflow-hidden font-['Instrument_Sans',sans-serif] antialiased"]) }}>
|
||||||
|
|
||||||
|
<!-- Decorative background glow -->
|
||||||
|
<div class="absolute -top-[15%] -left-[15%] w-[60%] h-[60%] rounded-full bg-radial from-indigo-500/20 to-transparent pointer-events-none z-0"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 w-full flex items-center justify-center">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@stack('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
57
resources/views/components/layouts/interview.blade.php
Normal file
57
resources/views/components/layouts/interview.blade.php
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
@props([
|
||||||
|
'title' => 'Candidate Live Assessment Portal',
|
||||||
|
'activeNav' => 'interview',
|
||||||
|
])
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<title>{{ $title }} | 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&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
@stack('styles')
|
||||||
|
@stack('head-scripts')
|
||||||
|
</head>
|
||||||
|
<body {{ $attributes->merge(['class' => "bg-[#0a0e17] text-white min-h-screen flex flex-col font-['Instrument_Sans',sans-serif] antialiased"]) }}>
|
||||||
|
|
||||||
|
<x-interview.navbar :activeNav="$activeNav" />
|
||||||
|
|
||||||
|
<main class="w-full mx-auto my-6 px-7 flex-1">
|
||||||
|
@if(session('success'))
|
||||||
|
<x-alert variant="success" class="mb-6">
|
||||||
|
{{ session('success') }}
|
||||||
|
</x-alert>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(session('error'))
|
||||||
|
<x-alert variant="danger" class="mb-6">
|
||||||
|
{{ session('error') }}
|
||||||
|
</x-alert>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
{{ $slot }}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Global shared Incoming Call Modal -->
|
||||||
|
<x-interview.incoming-call-modal />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.MAX_INTERVIEWERS = {{ config('interview.max_interviewer', 4) }};
|
||||||
|
window.currentUserId = {{ Auth::id() ?? 'null' }};
|
||||||
|
window.currentUserName = "{{ Auth::check() ? addslashes(Auth::user()->name) : '' }}";
|
||||||
|
window.currentUserRole = "{{ Auth::check() && Auth::user()->isAdmin() ? 'admin' : 'interviewer' }}";
|
||||||
|
window.csrfToken = "{{ csrf_token() }}";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@stack('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
resources/views/components/layouts/report.blade.php
Normal file
26
resources/views/components/layouts/report.blade.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
@props([
|
||||||
|
'title' => 'Executive Evaluation Report',
|
||||||
|
])
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ $title }} | 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;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
||||||
|
|
||||||
|
@stack('styles')
|
||||||
|
@stack('head-scripts')
|
||||||
|
</head>
|
||||||
|
<body {{ $attributes->merge(['class' => "bg-slate-100 text-slate-900 leading-normal p-5 md:p-10 font-['Instrument_Sans',sans-serif] antialiased print:bg-white print:p-0 print:text-black"]) }}>
|
||||||
|
|
||||||
|
{{ $slot }}
|
||||||
|
|
||||||
|
@stack('scripts')
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
resources/views/components/select.blade.php
Normal file
25
resources/views/components/select.blade.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@props([
|
||||||
|
'name' => null,
|
||||||
|
'required' => false,
|
||||||
|
'disabled' => false,
|
||||||
|
'size' => 'md',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$sizeClasses = [
|
||||||
|
'sm' => 'px-2.5 py-1.5 text-xs',
|
||||||
|
'md' => 'px-3 py-2 text-xs',
|
||||||
|
'lg' => 'px-3.5 py-2.5 text-sm',
|
||||||
|
];
|
||||||
|
|
||||||
|
$classes = 'w-full bg-slate-900 border border-slate-700/60 rounded-lg text-white outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/40 transition-colors disabled:opacity-50 disabled:cursor-not-allowed ' . ($sizeClasses[$size] ?? $sizeClasses['md']);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<select
|
||||||
|
@if($name) name="{{ $name }}" @endif
|
||||||
|
@if($required) required @endif
|
||||||
|
@if($disabled) disabled @endif
|
||||||
|
{{ $attributes->merge(['class' => $classes]) }}
|
||||||
|
>
|
||||||
|
{{ $slot }}
|
||||||
|
</select>
|
||||||
@ -1,103 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<x-layouts.guest title="Candidate Assessment Login">
|
||||||
<html lang="en">
|
<div class="bg-slate-900/80 border border-white/20 rounded-3xl p-8 sm:p-10 w-full max-w-md backdrop-blur-2xl shadow-2xl relative z-10">
|
||||||
<head>
|
<div class="text-center mb-6">
|
||||||
<meta charset="UTF-8">
|
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-indigo-500 to-sky-600 flex items-center justify-center text-2xl mx-auto mb-3 shadow-lg shadow-indigo-500/40">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Candidate Assessment Login | 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">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-color: #0b0f19;
|
|
||||||
--card-bg: rgba(15, 23, 42, 0.7);
|
|
||||||
--card-border: rgba(255, 255, 255, 0.22);
|
|
||||||
--accent-primary: #6366f1;
|
|
||||||
--accent-secondary: #0078d4;
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
body {
|
|
||||||
font-family: 'Instrument Sans', sans-serif;
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: white;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -15%;
|
|
||||||
left: -15%;
|
|
||||||
width: 60%;
|
|
||||||
height: 60%;
|
|
||||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.2) 0%, rgba(0,0,0,0) 70%);
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
.login-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
border-radius: 24px;
|
|
||||||
padding: 36px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 440px;
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
box-shadow: 0 25px 50px rgba(0,0,0,0.5);
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.form-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 14px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
color: white;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
outline: none;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.form-input:focus {
|
|
||||||
border-color: var(--accent-primary);
|
|
||||||
box-shadow: 0 0 12px rgba(99, 102, 241, 0.35);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="login-card">
|
|
||||||
<div style="text-align: center; margin-bottom: 24px;">
|
|
||||||
<div style="width: 50px; height: 50px; border-radius: 14px; background: linear-gradient(135deg, #6366f1 0%, #0078d4 100%); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; margin: 0 auto 12px auto;">
|
|
||||||
⚡
|
⚡
|
||||||
</div>
|
</div>
|
||||||
<h2 style="font-family: 'Outfit', sans-serif; font-size: 1.5rem; font-weight: 700; color: white;">Candidate Assessment Portal</h2>
|
<h2 class="font-outfit text-2xl font-bold text-white leading-tight">Candidate Assessment Portal</h2>
|
||||||
<div style="font-size: 0.8rem; color: #94a3b8; margin-top: 4px;">Enter temporary login credentials provided by HR</div>
|
<div class="text-xs text-slate-400 mt-1">Enter temporary login credentials provided by HR</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(session('error'))
|
@if(session('error'))
|
||||||
<div style="margin-bottom: 16px; background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #fca5a5; padding: 10px 14px; border-radius: 10px; font-size: 0.8rem;">
|
<x-alert variant="danger" class="mb-5">
|
||||||
{{ session('error') }}
|
{{ session('error') }}
|
||||||
</div>
|
</x-alert>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<form action="{{ route('interview.candidate.login.submit') }}" method="POST">
|
<form action="{{ route('interview.candidate.login.submit') }}" method="POST">
|
||||||
@csrf
|
@csrf
|
||||||
<label style="display:block; font-size:0.8rem; color:#cbd5e1; margin-bottom:6px; font-weight:500;">Email or Phone Number</label>
|
<div class="mb-4">
|
||||||
<input type="text" name="identifier" required placeholder="alex@company.com or 9876543210" class="form-input">
|
<x-label required>Email or Phone Number</x-label>
|
||||||
|
<x-input type="text" name="identifier" required placeholder="alex@company.com or 9876543210" size="lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<label style="display:block; font-size:0.8rem; color:#cbd5e1; margin-bottom:6px; font-weight:500;">Temporary Password</label>
|
<div class="mb-5">
|
||||||
<input type="password" name="temp_password" required placeholder="e.g. Pass-829103" class="form-input">
|
<x-label required>Temporary Password</x-label>
|
||||||
|
<x-input type="password" name="temp_password" required placeholder="e.g. Pass-829103" size="lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" style="width: 100%; padding: 12px; border-radius: 10px; background: linear-gradient(135deg, #6366f1 0%, #0078d4 100%); color: white; border: none; font-weight: 700; font-size: 0.95rem; cursor: pointer; margin-top: 10px;">
|
<x-button type="submit" variant="primary" size="lg" class="w-full justify-center py-3 bg-gradient-to-r from-indigo-500 to-sky-600 hover:from-indigo-600 hover:to-sky-700 text-white font-bold border-none shadow-lg shadow-indigo-500/30">
|
||||||
Enter Proctored Assessment Room
|
Enter Proctored Assessment Room
|
||||||
</button>
|
</x-button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style="text-align: center; margin-top: 20px;">
|
<div class="text-center mt-6">
|
||||||
<a href="{{ route('login') }}" style="color: #94a3b8; font-size: 0.8rem; text-decoration: none;">← Back to Corporate SSO Login</a>
|
<a href="{{ route('login') }}" class="text-slate-400 hover:text-slate-200 text-xs no-underline transition-colors">
|
||||||
|
← Back to Corporate SSO Login
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</x-layouts.guest>
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,430 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
<x-layouts.candidate
|
||||||
<html lang="en">
|
:title="'Live Assessment Room - ' . $interview->candidate_name"
|
||||||
<head>
|
data-interview-id="{{ $interview->id }}"
|
||||||
<meta charset="UTF-8">
|
data-candidate-name="{{ addslashes($interview->candidate_name) }}"
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
data-candidate-email="{{ $interview->candidate_email }}"
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
data-submission-id="{{ $interview->submission_unique_id }}"
|
||||||
<title>Live Assessment Room - {{ $interview->candidate_name }} | SingleLogin</title>
|
data-language="{{ strtolower($interview->language) }}"
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
data-call-status="{{ $interview->call_status ?? 'idle' }}"
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
data-login-url="{{ route('interview.candidate.login') }}"
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Instrument+Sans:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
|
||||||
|
|
||||||
<!-- PeerJS, Metered SDK, SweetAlert2 & html2canvas -->
|
|
||||||
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
|
||||||
<script src="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Monaco Code Editor Loader (loaded after UMD libraries to prevent define.amd conflicts) -->
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-color: #080c14;
|
|
||||||
--card-bg: #0f172a;
|
|
||||||
--card-border: rgba(255, 255, 255, 0.15);
|
|
||||||
--accent-primary: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modern Custom Scrollbars */
|
|
||||||
html, body, *, ::-webkit-scrollbar {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.4) rgba(15, 23, 42, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: rgba(15, 23, 42, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.4);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Instrument Sans', sans-serif;
|
|
||||||
background-color: var(--bg-color);
|
|
||||||
color: white;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ide-header {
|
|
||||||
height: 60px;
|
|
||||||
background: #0d1322;
|
|
||||||
border-bottom: 1px solid var(--card-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ide-layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 340px;
|
|
||||||
height: calc(100vh - 60px);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-right: 1px solid var(--card-border);
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#monaco-editor-container {
|
|
||||||
flex: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-section {
|
|
||||||
height: 220px;
|
|
||||||
background: #050811;
|
|
||||||
border-top: 1px solid var(--card-border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-header {
|
|
||||||
height: 36px;
|
|
||||||
background: #090e1a;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
#terminal-output {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-family: 'Fira Code', monospace;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #34d399;
|
|
||||||
overflow-y: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proctor-sidebar {
|
|
||||||
background: #0d1322;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.webcam-box {
|
|
||||||
width: 100%;
|
|
||||||
height: 180px;
|
|
||||||
min-height: 180px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px solid var(--card-border);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#webcam {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ide {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
color: white;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ide-primary {
|
|
||||||
background: #10b981;
|
|
||||||
border-color: #10b981;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ide-run {
|
|
||||||
background: #6366f1;
|
|
||||||
border-color: #6366f1;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Warning Popup Overlay */
|
|
||||||
#warning-banner {
|
|
||||||
position: fixed;
|
|
||||||
top: 70px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: rgba(239, 68, 68, 0.95);
|
|
||||||
color: white;
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
box-shadow: 0 10px 30px rgba(239, 68, 68, 0.5);
|
|
||||||
z-index: 9999;
|
|
||||||
display: none;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body data-interview-id="{{ $interview->id }}" data-candidate-name="{{ addslashes($interview->candidate_name) }}" data-candidate-email="{{ $interview->candidate_email }}" data-submission-id="{{ $interview->submission_unique_id }}" data-language="{{ strtolower($interview->language) }}" data-call-status="{{ $interview->call_status ?? 'idle' }}" data-login-url="{{ route('interview.candidate.login') }}">
|
|
||||||
|
|
||||||
<script id="candidate-submitted-code" type="application/json">@json($interview->submitted_code)</script>
|
|
||||||
<script id="candidate-drawing-data" type="application/json">@json($interview->candidate_drawing)</script>
|
|
||||||
|
|
||||||
<!-- IDE Header Bar -->
|
<!-- IDE Header Bar -->
|
||||||
<header class="ide-header">
|
<x-interview.candidate.ide-header :interview="$interview" />
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
|
||||||
<div style="font-weight: 700; font-family: 'Outfit', sans-serif; font-size: 1.05rem; color: white;">
|
|
||||||
Candidate Assessment Room
|
|
||||||
</div>
|
|
||||||
<code style="background: rgba(255,255,255,0.06); padding: 4px 10px; border-radius: 6px; color: #38bdf8; font-size: 0.75rem;">
|
|
||||||
ID: {{ $interview->submission_unique_id }}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 14px;">
|
<!-- Candidate Notepad Modal (Synced) -->
|
||||||
<button onclick="toggleNotepadModal()" class="btn-ide" style="background: rgba(99,102,241,0.2); border-color: #6366f1;">
|
<x-interview.candidate.notepad-modal :notes="$interview->candidate_notes" />
|
||||||
📝 Open Notepad
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="toggleDrawingModal()" class="btn-ide" style="background: rgba(6,182,212,0.2); border-color: #06b6d4;">
|
|
||||||
🎨 Open Drawing
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<label style="font-size: 0.75rem; color: #94a3b8; font-weight: 500;">Language:</label>
|
|
||||||
<select id="language-select" class="btn-ide" style="background: #151b2c;">
|
|
||||||
<option value="python" {{ strtolower($interview->language) === 'python' ? 'selected' : '' }}>Python 3</option>
|
|
||||||
<option value="cpp" {{ strtolower($interview->language) === 'cpp' ? 'selected' : '' }}>C++ (GCC)</option>
|
|
||||||
<option value="c" {{ strtolower($interview->language) === 'c' ? 'selected' : '' }}>C (GCC)</option>
|
|
||||||
<option value="java" {{ strtolower($interview->language) === 'java' ? 'selected' : '' }}>Java 15</option>
|
|
||||||
<option value="php" {{ strtolower($interview->language) === 'php' ? 'selected' : '' }}>PHP 8.2 / Laravel</option>
|
|
||||||
<option value="javascript" {{ strtolower($interview->language) === 'javascript' ? 'selected' : '' }}>JavaScript (Node.js)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="runCode()" class="btn-ide btn-ide-run">
|
|
||||||
▶ Run Code
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="submitSolution()" class="btn-ide btn-ide-primary">
|
|
||||||
✓ Submit Final Solution
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button onclick="candidateLogoutAction()" class="btn-ide" style="background: rgba(239,68,68,0.2); border-color: #ef4444; color: #fca5a5; font-weight: 700;">
|
|
||||||
🚪 Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Candidate Notepad Modal -->
|
|
||||||
<div id="notepad-modal" style="position: fixed; top: 70px; right: 360px; width: 420px; height: 350px; background: #0f172a; border: 1.5px solid #6366f1; border-radius: 16px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); display: none; flex-direction: column; z-index: 1000; overflow: hidden;">
|
|
||||||
<div style="background: #1e293b; padding: 10px 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
||||||
<div style="font-weight: 700; font-size: 0.85rem; color: white;">📝 Candidate Notepad (Synced)</div>
|
|
||||||
<button onclick="toggleNotepadModal()" style="background: none; border: none; color: #94a3b8; font-size: 1.2rem; cursor: pointer;">×</button>
|
|
||||||
</div>
|
|
||||||
<textarea id="notepad-textarea" oninput="saveNotepadContent()" placeholder="Type rough notes, pseudo-code, or thoughts here..." style="flex: 1; width: 100%; padding: 14px; background: #0b0f19; color: #e2e8f0; border: none; outline: none; font-family: 'Fira Code', monospace; font-size: 0.85rem; resize: none;">{{ $interview->candidate_notes }}</textarea>
|
|
||||||
<div style="padding: 6px 12px; background: #0d1322; font-size: 0.65rem; color: #34d399; text-align: right;">
|
|
||||||
✓ Auto-saved to interviewer portal
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Candidate Canvas Drawing Modal -->
|
<!-- Candidate Canvas Drawing Modal -->
|
||||||
<div id="drawing-modal" style="position: fixed; top: 70px; right: 360px; width: 550px; height: 420px; background: #0f172a; border: 1.5px solid #06b6d4; border-radius: 16px; box-shadow: 0 20px 40px rgba(0,0,0,0.6); display: none; flex-direction: column; z-index: 1000; overflow: hidden;">
|
<x-interview.candidate.drawing-modal />
|
||||||
<div style="background: #1e293b; padding: 10px 16px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
|
||||||
<div style="font-weight: 700; font-size: 0.85rem; color: white;">🎨 Candidate Live Drawing Board</div>
|
|
||||||
<div style="display: flex; gap: 8px; align-items: center;">
|
|
||||||
<input type="color" id="draw-color" value="#38bdf8" style="width: 26px; height: 26px; border: none; border-radius: 50%; cursor: pointer;">
|
|
||||||
<button onclick="clearCanvasDraw()" class="btn-ide" style="padding: 2px 8px; font-size: 0.7rem;">Clear</button>
|
|
||||||
<button onclick="toggleDrawingModal()" style="background: none; border: none; color: #94a3b8; font-size: 1.2rem; cursor: pointer;">×</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="flex: 1; background: #050811; position: relative;">
|
|
||||||
<canvas id="drawing-canvas" width="550" height="340" style="cursor: crosshair; display: block; width: 100%; height: 100%;"></canvas>
|
|
||||||
</div>
|
|
||||||
<div style="padding: 6px 12px; background: #0d1322; font-size: 0.65rem; color: #38bdf8; text-align: right;">
|
|
||||||
🎨 Draw architecture / logic diagrams (Auto-synced to Interviewer)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- IDE Room Layout -->
|
<!-- IDE Main Grid Layout -->
|
||||||
<div class="ide-layout">
|
<div class="grid grid-cols-[1fr_340px] h-[calc(100vh-60px)] w-full overflow-hidden">
|
||||||
<!-- Main Editor & Terminal Area -->
|
<!-- Main Editor & Terminal Area -->
|
||||||
<div class="editor-section">
|
<div class="flex flex-col border-r border-white/10 h-full overflow-hidden">
|
||||||
<div id="monaco-editor-container"></div>
|
<div id="monaco-editor-container" class="flex-1 w-full h-full min-h-0 bg-[#080c14]"></div>
|
||||||
|
|
||||||
<div class="terminal-section">
|
<x-interview.candidate.terminal />
|
||||||
<div class="terminal-header">
|
|
||||||
<span>EXECUTION OUTPUT TERMINAL (STDOUT / STDERR)</span>
|
|
||||||
<span id="run-status" style="color: #94a3b8;">Ready</span>
|
|
||||||
</div>
|
|
||||||
<div id="terminal-output">// Press 'Run Code' to execute candidate solution live...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@php
|
<!-- Sidebar: Real-Time 2-Way Interview Call & Proctor Controls -->
|
||||||
$words = preg_split('/\s+/', trim($interview->candidate_name));
|
<x-interview.candidate.proctor-sidebar :interview="$interview" />
|
||||||
if (count($words) >= 2) {
|
|
||||||
$candInitials = strtoupper(substr($words[0], 0, 1) . substr(end($words), 0, 1));
|
|
||||||
} else {
|
|
||||||
$nameStr = trim($interview->candidate_name);
|
|
||||||
$candInitials = strtoupper(substr($nameStr, 0, 1) . (strlen($nameStr) > 1 ? substr($nameStr, -1) : ''));
|
|
||||||
}
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<!-- Sidebar: Real-Time 2-Way Interview Call & Screen Share -->
|
|
||||||
<div class="proctor-sidebar">
|
|
||||||
<!-- Candidate Camera Feed -->
|
|
||||||
<div class="webcam-box" style="position: relative;">
|
|
||||||
<video id="webcam" autoplay muted playsinline style="width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1);"></video>
|
|
||||||
<div id="webcam-avatar-placeholder" style="position: absolute; inset: 0; display: none; flex-direction: column; align-items: center; justify-content: center; background: #0f172a; color: white; z-index: 5;">
|
|
||||||
<div style="width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #6366f1 0%, #0078d4 100%); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; font-weight: 800; font-family: 'Outfit', sans-serif; box-shadow: 0 0 15px rgba(99,102,241,0.4);">
|
|
||||||
{{ $candInitials }}
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.7rem; color: #94a3b8; margin-top: 6px;">Camera Disabled</div>
|
|
||||||
</div>
|
|
||||||
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.6); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: #34d399; font-weight: 600; z-index: 10; display: flex; align-items: center; gap: 6px;">
|
|
||||||
<span>Candidate (You)</span>
|
|
||||||
<span style="opacity: 0.4;">|</span>
|
|
||||||
<i id="cand-self-mic-icon" class="fa-solid fa-microphone" style="color: #34d399; font-size: 10px;" title="Mic On"></i>
|
|
||||||
<i id="cand-self-cam-icon" class="fa-solid fa-video" style="color: #34d399; font-size: 10px;" title="Camera On"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Remote Interviewer Panelist Video Grid (Multi-Party WebRTC Mesh) -->
|
<!-- Candidate Incoming Call Ringing Overlay -->
|
||||||
<div id="interviewer-mesh-grid" style="display: flex; flex-direction: column; gap: 10px; width: 100%;">
|
<x-interview.candidate.incoming-overlay />
|
||||||
<div id="interviewer-placeholder-box" class="webcam-box" style="position: relative; border-color: #6366f1;">
|
|
||||||
<video id="interviewer-video-default" autoplay playsinline style="width: 100%; height: 100%; object-fit: cover; background: #050811; display: none; transform: scaleX(-1);"></video>
|
|
||||||
<div id="interviewer-video-placeholder" style="position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(15,23,42,0.95); color: #94a3b8; font-size: 0.75rem; text-align: center; padding: 10px; z-index: 5;">
|
|
||||||
<div id="interviewer-avatar-circle" style="width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #38bdf8 0%, #6366f1 100%); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; font-weight: 800; font-family: 'Outfit', sans-serif; color: white; margin-bottom: 6px; box-shadow: 0 0 15px rgba(56,189,248,0.4);">
|
|
||||||
IV
|
|
||||||
</div>
|
|
||||||
<div id="call-status-sub" style="font-size: 0.7rem; color: #e2e8f0; font-weight: 600;">Interviewer Panel</div>
|
|
||||||
<div style="font-size: 0.65rem; color: #38bdf8; margin-top: 2px;">Waiting for interviewer panelist to join call...</div>
|
|
||||||
</div>
|
|
||||||
<div style="position: absolute; bottom: 8px; left: 8px; background: rgba(99,102,241,0.8); padding: 2px 8px; border-radius: 4px; font-size: 0.65rem; color: white; font-weight: 600; z-index: 10;">
|
|
||||||
🎙️ Interviewer / Panelist
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
<canvas id="proctor-canvas" style="display: none;" width="320" height="240"></canvas>
|
<script id="candidate-submitted-code" type="application/json">@json($interview->submitted_code)</script>
|
||||||
|
<script id="candidate-drawing-data" type="application/json">@json($interview->candidate_drawing)</script>
|
||||||
<!-- Candidate Profile Info -->
|
@endpush
|
||||||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--card-border); border-radius: 12px; padding: 14px; font-size: 0.8rem;">
|
</x-layouts.candidate>
|
||||||
<div style="font-weight: 700; color: white; margin-bottom: 6px;">Candidate Profile</div>
|
|
||||||
<div style="color: #94a3b8;">Name: <strong style="color: white;">{{ $interview->candidate_name }}</strong></div>
|
|
||||||
<div style="color: #94a3b8;">Email: <span style="color: white;">{{ $interview->candidate_email }}</span></div>
|
|
||||||
<div style="color: #94a3b8; margin-top: 6px;">Access Expires: <span style="color: #fca5a5;">{{ $interview->expires_at->format('H:i:s T') }}</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Real-Time Call Controls (Mic, Camera, Screen Sharing) -->
|
|
||||||
<div style="background: rgba(99,102,241,0.1); border: 1px solid rgba(99,102,241,0.3); border-radius: 14px; padding: 14px;">
|
|
||||||
<div style="font-size: 0.8rem; font-weight: 700; color: white; margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between;">
|
|
||||||
<span>📞 Real-Time Interview Call</span>
|
|
||||||
<span id="call-status-badge" style="font-size: 0.65rem; background: rgba(16,185,129,0.2); color: #34d399; padding: 2px 8px; border-radius: 999px;">READY</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="candidate-join-active-call-btn" onclick="acceptCandidateCall()" class="btn-ide" style="display: none; width: 100%; padding: 10px; margin-bottom: 10px; font-size: 0.8rem; font-weight: 700; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; box-shadow: 0 0 15px rgba(16,185,129,0.4); cursor: pointer; animation: acceptGlow 1.5s infinite alternate;">
|
|
||||||
🟢 📞 Call in Progress • Join Call Now
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px; margin-bottom: 10px;">
|
|
||||||
<button id="toggle-mic-btn" onclick="toggleMic()" class="btn-ide" style="flex: 1; padding: 8px; font-size: 0.75rem; background: rgba(16,185,129,0.2); border-color: #10b981;">
|
|
||||||
🎤 Mic On
|
|
||||||
</button>
|
|
||||||
<button id="toggle-cam-btn" onclick="toggleCam()" class="btn-ide" style="flex: 1; padding: 8px; font-size: 0.75rem; background: rgba(16,185,129,0.2); border-color: #10b981;">
|
|
||||||
📷 Cam On
|
|
||||||
</button>
|
|
||||||
<button id="leave-call-btn" onclick="candidateLeaveCall()" class="btn-ide" style="padding: 8px; font-size: 0.75rem; background: rgba(239,68,68,0.2); border-color: #ef4444; color: #fca5a5;" title="Leave 2-Way Call">
|
|
||||||
🚪 Leave Call
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="share-screen-btn" onclick="startScreenShare()" class="btn-ide btn-ide-run" style="width: 100%; padding: 10px; font-size: 0.8rem; background: linear-gradient(135deg, #6366f1 0%, #0078d4 100%);">
|
|
||||||
🖥️ Share Screen with Interviewer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 2-Way Real-Time Chat Container with Interviewer/Panelist -->
|
|
||||||
<div style="background: rgba(15,23,42,0.8); border: 1px solid var(--card-border); border-radius: 14px; padding: 12px; margin-top: 10px;">
|
|
||||||
<div style="font-size: 0.8rem; font-weight: 700; color: white; margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
|
|
||||||
💬 Live Chat with Interviewer
|
|
||||||
</div>
|
|
||||||
<div id="candidate-chat-history" style="background: #050811; border: 1px solid var(--card-border); border-radius: 10px; padding: 8px; height: 130px; overflow-y: auto; font-size: 0.72rem; margin-bottom: 8px;">
|
|
||||||
<div style="color: #94a3b8; font-style: italic; text-align: center; padding-top: 40px;">No messages yet. Send a message below.</div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 6px;">
|
|
||||||
<input type="text" id="candidate-chat-input" placeholder="Type message to panelist..." onkeydown="if(event.key==='Enter') sendCandidateMessage()" class="form-input" style="font-size: 0.75rem; background: #0b0f19; color: #ffffff !important; border: 1px solid var(--card-border); padding: 6px 10px; flex: 1;">
|
|
||||||
<button onclick="sendCandidateMessage()" class="btn-ide" style="padding: 6px 10px; font-size: 0.75rem; background: #6366f1; border: none; font-weight: 600;">
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CANDIDATE INCOMING CALL MODAL / RINGING OVERLAY -->
|
|
||||||
<div id="candidate-incoming-modal" style="display: flex; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(5, 8, 17, 0.85); backdrop-filter: blur(16px); align-items: center; justify-content: center; z-index: 10000; opacity: 0; pointer-events: none; transition: opacity 0.3s ease;">
|
|
||||||
|
|
||||||
<div style="max-width: 440px; width: 90%; background: linear-gradient(145deg, #0f172a 0%, #1e1b4b 100%); border: 2px solid #10b981; border-radius: 24px; padding: 32px; text-align: center; color: white; box-shadow: 0 0 50px rgba(16, 185, 129, 0.6);">
|
|
||||||
<div style="position: relative; width: 90px; height: 90px; margin: 0 auto 20px auto; display: flex; align-items: center; justify-content: center;">
|
|
||||||
<div style="position: absolute; inset: -15px; border-radius: 50%; border: 2px solid rgba(16, 185, 129, 0.6); animation: ringPulse 1.4s infinite ease-out;"></div>
|
|
||||||
<div style="position: absolute; inset: -30px; border-radius: 50%; border: 2px solid rgba(99, 102, 241, 0.4); animation: ringPulse 1.4s infinite ease-out 0.4s;"></div>
|
|
||||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: linear-gradient(135deg, #10b981 0%, #0078d4 100%); display: flex; align-items: center; justify-content: center; font-size: 2.4rem; font-weight: 800; color: white; box-shadow: 0 10px 25px rgba(16, 185, 129, 0.5); z-index: 5;">
|
|
||||||
📞
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1.5px; color: #34d399; font-weight: 800; margin-bottom: 6px;">
|
|
||||||
⚡ INCOMING INTERVIEW CALL
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.4rem; font-weight: 800; color: white; margin-bottom: 4px;">
|
|
||||||
Interviewer Panel Calling...
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div style="font-size: 0.8rem; color: #a5b4fc; margin-bottom: 20px;">
|
|
||||||
The interviewer panel has initiated a live 2-way call.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 12px; margin-top: 10px;">
|
|
||||||
<button onclick="declineCandidateCall()" style="flex: 1; padding: 14px; background: rgba(239, 68, 68, 0.2); border: 1.5px solid #ef4444; color: #fca5a5; font-weight: 700; font-size: 0.9rem; border-radius: 14px; cursor: pointer;">
|
|
||||||
🛑 Decline / Busy
|
|
||||||
</button>
|
|
||||||
<button onclick="acceptCandidateCall()" style="flex: 1.4; padding: 14px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); border: none; color: white; font-weight: 800; font-size: 0.95rem; border-radius: 14px; box-shadow: 0 0 25px rgba(16, 185, 129, 0.6); cursor: pointer; animation: acceptGlow 1.5s infinite alternate;">
|
|
||||||
📞 Accept Call
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,44 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<x-layouts.guest title="Assessment Access Expired">
|
||||||
<html lang="en">
|
<div class="bg-slate-900/80 border border-red-500/30 rounded-3xl p-8 sm:p-10 text-center max-w-md w-full backdrop-blur-2xl shadow-2xl relative z-10">
|
||||||
<head>
|
<div class="text-5xl mb-4">⌛</div>
|
||||||
<meta charset="UTF-8">
|
<h2 class="font-outfit text-2xl font-bold text-red-400 mb-2">Assessment Access Expired</h2>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<p class="text-xs text-slate-400 mb-5 leading-relaxed">
|
||||||
<title>Assessment Access Expired | SingleLogin</title>
|
The temporary access window for <strong class="text-white">{{ $interview->candidate_name }}</strong> expired on {{ $interview->expires_at->format('Y-m-d H:i T') }}.
|
||||||
@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">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Instrument Sans', sans-serif;
|
|
||||||
background-color: #0b0f19;
|
|
||||||
color: white;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
background: rgba(15, 23, 42, 0.7);
|
|
||||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 36px;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 450px;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="card">
|
|
||||||
<div style="font-size: 3rem; margin-bottom: 16px;">⌛</div>
|
|
||||||
<h2 style="font-family: 'Outfit', sans-serif; font-size: 1.4rem; color: #ef4444; margin-bottom: 8px;">Assessment Access Expired</h2>
|
|
||||||
<p style="font-size: 0.85rem; color: #94a3b8; margin-bottom: 20px;">
|
|
||||||
The temporary access window for <strong>{{ $interview->candidate_name }}</strong> expired on {{ $interview->expires_at->format('Y-m-d H:i T') }}.
|
|
||||||
</p>
|
</p>
|
||||||
<div style="font-size: 0.8rem; color: #cbd5e1; background: rgba(255,255,255,0.05); padding: 10px; border-radius: 8px;">
|
<div class="text-xs text-slate-300 bg-white/5 border border-white/10 p-3 rounded-xl font-mono">
|
||||||
Unique Submission ID: <code style="color: #38bdf8;">{{ $interview->submission_unique_id }}</code>
|
Unique Submission ID: <code class="text-sky-400 font-bold">{{ $interview->submission_unique_id }}</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</x-layouts.guest>
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,131 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<x-layouts.interview title="Candidate Live Assessment Portal">
|
||||||
<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 -->
|
<!-- 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">
|
<x-interview.admin-settings-card />
|
||||||
<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 -->
|
<!-- Page Header Row -->
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
@ -140,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Assessment Table Card -->
|
<!-- Assessment Table Card -->
|
||||||
<div class="bg-slate-900/80 border border-slate-700/60 rounded-2xl p-6 shadow-2xl backdrop-blur-md">
|
<x-card class="p-6">
|
||||||
<div class="overflow-x-auto w-full">
|
<div class="overflow-x-auto w-full">
|
||||||
<table class="w-full text-left text-xs border-collapse">
|
<table class="w-full text-left text-xs border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
@ -155,111 +30,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-white/5">
|
<tbody class="divide-y divide-white/5">
|
||||||
@forelse($interviews as $inv)
|
@forelse($interviews as $inv)
|
||||||
@php
|
<x-interview.session-row :interview="$inv" />
|
||||||
$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
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="text-center text-slate-400 py-8">
|
<td colspan="6" class="text-center text-slate-400 py-8">
|
||||||
@ -270,126 +41,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</x-card>
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Modal: Schedule Candidate Interview -->
|
<!-- Modal: Schedule Candidate Interview -->
|
||||||
<x-modal id="create-interview-modal" title="Schedule Candidate Assessment" maxWidth="lg">
|
<x-interview.schedule-modal :interviewers="$interviewers" />
|
||||||
<form action="{{ route('interview.store') }}" method="POST">
|
</x-layouts.interview>
|
||||||
@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>
|
|
||||||
|
|
||||||
<!-- Incoming Call Ring Modal -->
|
|
||||||
<x-modal id="incoming-call-modal" maxWidth="md">
|
|
||||||
<div class="p-6 text-center">
|
|
||||||
<div class="relative w-20 h-20 mx-auto mb-5 flex items-center justify-center">
|
|
||||||
<div class="absolute -inset-3 rounded-full border-2 border-emerald-500/60 animate-ping"></div>
|
|
||||||
<div class="absolute -inset-6 rounded-full border-2 border-indigo-500/40 animate-pulse"></div>
|
|
||||||
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-emerald-500 to-sky-600 flex items-center justify-center text-2xl font-bold text-white shadow-lg shadow-emerald-500/50 z-10">
|
|
||||||
<i class="fa-solid fa-phone"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-[11px] uppercase tracking-widest text-emerald-400 font-bold mb-1.5">
|
|
||||||
⚡ Incoming Assessment Call
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 id="incoming-cand-name" class="font-outfit text-2xl font-extrabold text-white mb-1">
|
|
||||||
Candidate Name
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div id="incoming-starter-info" class="text-xs text-[#4b82f7] mb-4">
|
|
||||||
Initiated by Panelist
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white/5 border border-white/10 rounded-xl p-3 text-xs text-slate-300 mb-6 leading-relaxed">
|
|
||||||
Another interviewer has started calling the candidate. Click <strong>Accept Call</strong> to join live, or <strong>Decline</strong> if busy.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 justify-center">
|
|
||||||
<x-button id="incoming-decline-btn" onclick="declineIncomingCall()" variant="danger" size="md" icon="fa-solid fa-xmark" class="flex-1">
|
|
||||||
Decline / Miss
|
|
||||||
</x-button>
|
|
||||||
<x-button id="incoming-accept-btn" onclick="acceptIncomingCall()" variant="success" size="md" icon="fa-solid fa-phone" class="flex-[1.3] font-extrabold animate-pulse">
|
|
||||||
Accept Call
|
|
||||||
</x-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
|
|||||||
@ -1,451 +1,53 @@
|
|||||||
<!DOCTYPE html>
|
<x-layouts.report :title="'Executive Evaluation Report - ' . $interview->candidate_name">
|
||||||
<html lang="en">
|
<!-- Floating Quick Print Button -->
|
||||||
<head>
|
<button type="button" onclick="window.print()" class="fixed bottom-7 right-7 bg-indigo-600 hover:bg-indigo-700 text-white font-bold text-sm py-3.5 px-7 rounded-full shadow-2xl shadow-indigo-600/50 flex items-center gap-2 z-50 cursor-pointer print:hidden transition-all">
|
||||||
<meta charset="UTF-8">
|
<i class="fa-solid fa-download"></i>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<span>Download / Print Executive PDF Report</span>
|
||||||
<title>Executive Evaluation Report - {{ $interview->candidate_name }} | SingleLogin</title>
|
|
||||||
<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;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary: #0f172a;
|
|
||||||
--accent: #6366f1;
|
|
||||||
--text-dark: #0f172a;
|
|
||||||
--text-muted: #64748b;
|
|
||||||
--card-bg: #f8fafc;
|
|
||||||
--border: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Instrument Sans', sans-serif;
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
color: var(--text-dark);
|
|
||||||
line-height: 1.5;
|
|
||||||
padding: 40px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 20px 40px rgba(0,0,0,0.08);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 40px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
border-bottom: 2px solid var(--accent);
|
|
||||||
padding-bottom: 20px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-title {
|
|
||||||
font-family: 'Outfit', sans-serif;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--primary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-badge {
|
|
||||||
background: rgba(99,102,241,0.1);
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-item label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-item span {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.score-val {
|
|
||||||
font-family: 'Outfit', sans-serif;
|
|
||||||
font-size: 2.4rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-heading {
|
|
||||||
font-family: 'Outfit', sans-serif;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--primary);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table th {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: left;
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table td {
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #edf2f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-box {
|
|
||||||
background: #0f172a;
|
|
||||||
color: #38bdf8;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 18px;
|
|
||||||
font-family: 'Fira Code', monospace;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.output-box {
|
|
||||||
background: #050811;
|
|
||||||
color: #34d399;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 14px;
|
|
||||||
font-family: 'Fira Code', monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-btn {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 30px;
|
|
||||||
right: 30px;
|
|
||||||
background: var(--accent);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 14px 28px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
box-shadow: 0 10px 25px rgba(99,102,241,0.4);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
body {
|
|
||||||
background: white;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.report-container {
|
|
||||||
box-shadow: none;
|
|
||||||
border: none;
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.print-btn, .top-download-pdf-btn {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<button onclick="window.print()" class="print-btn">
|
|
||||||
📥 Download / Print Executive PDF Report
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="report-container">
|
<div class="max-w-[900px] mx-auto bg-white rounded-2xl shadow-xl border border-slate-200 p-6 sm:p-10 relative print:shadow-none print:border-none print:p-0 print:max-w-full">
|
||||||
<!-- Top Bar with Download PDF Button -->
|
<!-- Report Header -->
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid var(--border);">
|
<x-interview.report.header :interview="$interview" />
|
||||||
<div style="font-size: 0.85rem; font-weight: 700; color: var(--primary); display: flex; align-items: center; gap: 6px;">
|
|
||||||
<span>📄 Executive Assessment Performance Report</span>
|
|
||||||
</div>
|
|
||||||
<button onclick="window.print()" class="top-download-pdf-btn" style="background: #6366f1; color: white; border: none; font-weight: 700; font-size: 0.85rem; padding: 10px 20px; border-radius: 10px; cursor: pointer; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 14px rgba(99,102,241,0.3);">
|
|
||||||
📥 Download PDF Report
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Candidate Meta Grid -->
|
||||||
<div class="header-bar">
|
<x-interview.report.meta-grid :interview="$interview" />
|
||||||
<div>
|
|
||||||
<div class="brand-title">
|
|
||||||
⚡ SingleLogin Enterprise Assessment
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 4px;">
|
|
||||||
Candidate Technical Competency & Behavioral Audit Report
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<span class="report-badge">Executive Summary</span>
|
|
||||||
<div style="font-size: 0.7rem; color: var(--text-muted); margin-top: 6px;">
|
|
||||||
Report ID: RPT-{{ $interview->submission_unique_id }}
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.7rem; color: var(--text-muted);">
|
|
||||||
Date: {{ now()->format('M d, Y - H:i:s T') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Meta Grid -->
|
|
||||||
<div class="meta-grid">
|
|
||||||
<div class="meta-item">
|
|
||||||
<label>Candidate Name</label>
|
|
||||||
<span>{{ $interview->candidate_name }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-item">
|
|
||||||
<label>Email Address</label>
|
|
||||||
<span style="font-size: 0.8rem;">{{ $interview->candidate_email }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-item">
|
|
||||||
<label>Language & Tech</label>
|
|
||||||
<span>{{ strtoupper($interview->language) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="meta-item">
|
|
||||||
<label>Assessment Status</label>
|
|
||||||
<span style="color: {{ $interview->status === 'completed' ? '#10b981' : ($interview->status === 'blocked' ? '#ef4444' : '#6366f1') }};">
|
|
||||||
{{ strtoupper($interview->status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scores Summary Grid -->
|
<!-- Scores Summary Grid -->
|
||||||
<div class="score-grid">
|
<x-interview.report.score-card
|
||||||
<!-- Behavioral Integrity Score -->
|
:integrityScore="$integrityScore"
|
||||||
<div class="score-card" style="border-left: 5px solid {{ $integrityColor }};">
|
:integrityLabel="$integrityLabel"
|
||||||
<div style="font-size: 0.75rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;">
|
:integrityColor="$integrityColor"
|
||||||
🛡️ Behavioral Integrity Rating
|
:incidentCount="is_array($logs) ? count($logs) : 0"
|
||||||
</div>
|
:codeScore="$codeScore"
|
||||||
<div class="score-val" style="color: {{ $integrityColor }};">
|
:hasSubmittedCode="!empty($interview->submitted_code)"
|
||||||
{{ $integrityScore }}%
|
/>
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.8rem; font-weight: 700; color: {{ $integrityColor }}; margin-top: 4px;">
|
|
||||||
{{ $integrityLabel }}
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.7rem; color: var(--text-muted); margin-top: 6px;">
|
|
||||||
Recorded Violations: <strong>{{ count($logs) }} incident(s)</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Technical Score -->
|
|
||||||
<div class="score-card" style="border-left: 5px solid #6366f1;">
|
|
||||||
<div style="font-size: 0.75rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;">
|
|
||||||
💻 Technical Code Competency
|
|
||||||
</div>
|
|
||||||
<div class="score-val" style="color: #6366f1;">
|
|
||||||
{{ $codeScore }} / 100
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.8rem; font-weight: 700; color: #6366f1; margin-top: 4px;">
|
|
||||||
{{ $codeScore >= 80 ? 'Strong Technical Match' : ($codeScore >= 50 ? 'Moderate Competency' : 'Needs Further Review') }}
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.7rem; color: var(--text-muted); margin-top: 6px;">
|
|
||||||
Solution Code: <strong>{{ !empty($interview->submitted_code) ? 'Submitted' : 'Pending' }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Proctoring Violation Metrics & Breakdown -->
|
<!-- Proctoring Violation Metrics & Breakdown -->
|
||||||
<div class="section-heading">
|
<x-interview.report.incident-grid
|
||||||
🔍 Behavioral Audit & Proctoring Incident Breakdown
|
:metrics="[
|
||||||
</div>
|
'tab_switches' => $tabSwitches,
|
||||||
<div style="display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-bottom: 20px;">
|
'focus_lost' => $focusLosses,
|
||||||
<div style="background: var(--card-bg); padding: 10px; border-radius: 10px; border: 1px solid var(--border); text-align: center;">
|
'gaze_alerts' => $gazeAnomalies,
|
||||||
<div style="font-size: 1.1rem; font-weight: 800; color: #ef4444;">{{ $tabSwitches }}</div>
|
'paste_events' => $pasteEvents,
|
||||||
<div style="font-size: 0.6rem; color: var(--text-muted); font-weight: 700;">Tab Switches</div>
|
'lower_gaze' => $lowerGazeViolations,
|
||||||
</div>
|
'question_repeat' => $questionRepeatViolations,
|
||||||
<div style="background: var(--card-bg); padding: 10px; border-radius: 10px; border: 1px solid var(--border); text-align: center;">
|
]"
|
||||||
<div style="font-size: 1.1rem; font-weight: 800; color: #f59e0b;">{{ $focusLosses }}</div>
|
/>
|
||||||
<div style="font-size: 0.6rem; color: var(--text-muted); font-weight: 700;">Focus Lost</div>
|
|
||||||
</div>
|
|
||||||
<div style="background: var(--card-bg); padding: 10px; border-radius: 10px; border: 1px solid var(--border); text-align: center;">
|
|
||||||
<div style="font-size: 1.1rem; font-weight: 800; color: #38bdf8;">{{ $gazeAnomalies }}</div>
|
|
||||||
<div style="font-size: 0.6rem; color: var(--text-muted); font-weight: 700;">Gaze Anomalies</div>
|
|
||||||
</div>
|
|
||||||
<div style="background: var(--card-bg); padding: 10px; border-radius: 10px; border: 1px solid var(--border); text-align: center;">
|
|
||||||
<div style="font-size: 1.1rem; font-weight: 800; color: #818cf8;">{{ $pasteEvents }}</div>
|
|
||||||
<div style="font-size: 0.6rem; color: var(--text-muted); font-weight: 700;">Code Pastes</div>
|
|
||||||
</div>
|
|
||||||
<div style="background: var(--card-bg); padding: 10px; border-radius: 10px; border: 1px solid var(--border); text-align: center;">
|
|
||||||
<div style="font-size: 1.1rem; font-weight: 800; color: #ef4444;">{{ $lowerGazeViolations }}</div>
|
|
||||||
<div style="font-size: 0.6rem; color: var(--text-muted); font-weight: 700;">📱 Lower Gaze</div>
|
|
||||||
</div>
|
|
||||||
<div style="background: var(--card-bg); padding: 10px; border-radius: 10px; border: 1px solid var(--border); text-align: center;">
|
|
||||||
<div style="font-size: 1.1rem; font-weight: 800; color: #f43f5e;">{{ $questionRepeatViolations }}</div>
|
|
||||||
<div style="font-size: 0.6rem; color: var(--text-muted); font-weight: 700;">🗣️ Question Repeat</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(is_array($logs) && count($logs) > 0)
|
<!-- Audit Log Table -->
|
||||||
<table class="audit-table">
|
<x-interview.report.audit-table :logs="$logs" />
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Timestamp</th>
|
|
||||||
<th>Violation Category</th>
|
|
||||||
<th>Audit Details</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@foreach($logs as $log)
|
|
||||||
<tr>
|
|
||||||
<td style="color: var(--text-muted);">{{ \Carbon\Carbon::parse($log['timestamp'])->format('H:i:s T') }}</td>
|
|
||||||
<td><strong>{{ strtoupper($log['type']) }}</strong></td>
|
|
||||||
<td>{{ $log['details'] ?? 'Incident flagged by proctoring engine.' }}</td>
|
|
||||||
</tr>
|
|
||||||
@endforeach
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
@else
|
|
||||||
<div style="background: rgba(16,185,129,0.1); color: #10b981; padding: 12px; border-radius: 10px; font-size: 0.8rem; font-weight: 600; margin-bottom: 28px;">
|
|
||||||
✅ Zero proctoring violations or malpractice detected during the session.
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<!-- Code Solution Block -->
|
<!-- Code Solution & Output Section -->
|
||||||
<div class="section-heading">
|
<x-interview.report.code-section
|
||||||
💻 Submitted Code Solution ({{ strtoupper($interview->submitted_language ?? $interview->language) }})
|
:code="$interview->submitted_code"
|
||||||
</div>
|
:output="$interview->code_output"
|
||||||
<div class="code-box">
|
:language="$interview->submitted_language ?? $interview->language"
|
||||||
{{ $interview->submitted_code ?? '// No code solution submitted by candidate.' }}
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Code Output Block -->
|
<!-- Candidate Workspace Artifacts & Recordings -->
|
||||||
<div class="section-heading">
|
<x-interview.report.artifacts-section :interview="$interview" />
|
||||||
🖥️ Engine Execution Stdout Output
|
|
||||||
</div>
|
|
||||||
<div class="output-box">
|
|
||||||
{{ $interview->code_output ?? 'No execution output recorded.' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Candidate Notes, Interviewer Evaluation Notes, Drawing & Video Recordings Section -->
|
|
||||||
@if(!empty($interview->candidate_notes) || !empty($interview->interviewer_notes) || !empty($interview->candidate_drawing) || (is_array($interview->formatted_recordings) && count($interview->formatted_recordings) > 0))
|
|
||||||
<div class="section-heading">
|
|
||||||
📝 Workspace Artifacts, Evaluation Notes & Call Session Recordings
|
|
||||||
</div>
|
|
||||||
<div style="background: var(--card-bg); border: 1px solid var(--border); border-radius: 12px; padding: 16px; margin-bottom: 28px;">
|
|
||||||
@if(!empty($interview->candidate_notes))
|
|
||||||
<div style="font-size: 0.75rem; font-weight: 700; color: var(--text-muted); margin-bottom: 6px;">CANDIDATE NOTEPAD NOTES (UNIQUE ID SYNCED):</div>
|
|
||||||
<pre style="font-family: monospace; font-size: 0.8rem; background: white; padding: 10px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 12px;">{{ $interview->candidate_notes }}</pre>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if(!empty($interview->interviewer_notes))
|
|
||||||
<div style="font-size: 0.75rem; font-weight: 700; color: #10b981; margin-bottom: 6px;">INTERVIEWER EVALUATION NOTES (STORED BY UNIQUE ID):</div>
|
|
||||||
<pre style="font-family: sans-serif; font-size: 0.8rem; background: #f0fdf4; color: #166534; padding: 10px; border-radius: 8px; border: 1px solid #bbf7d0; margin-bottom: 12px;">{{ $interview->interviewer_notes }}</pre>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if(!empty($interview->candidate_drawing))
|
|
||||||
<div style="font-size: 0.75rem; font-weight: 700; color: var(--text-muted); margin-bottom: 6px;">CANDIDATE DRAWING DIAGRAM:</div>
|
|
||||||
<img src="{{ $interview->candidate_drawing }}" alt="Candidate Drawing" style="max-width: 100%; max-height: 250px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 12px;" />
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if(is_array($interview->formatted_recordings) && count($interview->formatted_recordings) > 0)
|
|
||||||
<div style="font-size: 0.75rem; font-weight: 700; color: #6366f1; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<span>📹 STORED CALL SESSION RECORDINGS ({{ count($interview->formatted_recordings) }} RECORDINGS - NEWEST FIRST):</span>
|
|
||||||
<span style="font-size: 0.65rem; color: #94a3b8;">📜 Scroll to view all</span>
|
|
||||||
</div>
|
|
||||||
<div style="max-height: 480px; overflow-y: auto; padding-right: 4px; border: 1px solid var(--border); border-radius: 8px; padding: 10px; background: #fafafa; margin-bottom: 12px;">
|
|
||||||
@foreach($interview->formatted_recordings as $displayIdx => $rec)
|
|
||||||
<div style="margin-bottom: 12px; background: white; border: 1px solid var(--border); border-radius: 8px; padding: 10px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
|
||||||
<span style="font-size: 0.75rem; font-weight: 700; color: var(--primary);">Recording #{{ count($interview->formatted_recordings) - $displayIdx }} ({{ $rec['created_at'] }})</span>
|
|
||||||
<a href="{{ $rec['download_url'] }}" download style="font-size: 0.7rem; color: #6366f1; font-weight: 700; text-decoration: none; padding: 3px 8px; background: rgba(99,102,241,0.1); border-radius: 4px;">📥 Download MP4 Video</a>
|
|
||||||
</div>
|
|
||||||
<video controls preload="metadata" style="width: 100%; max-height: 220px; border-radius: 6px; background: #000; outline: none;">
|
|
||||||
<source src="{{ $rec['stream_url'] }}" type="{{ $rec['mime_type'] ?? 'video/webm' }}">
|
|
||||||
<source src="{{ $rec['stream_url'] }}">
|
|
||||||
Your browser does not support video playback.
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<!-- Panel Sign-Off Block -->
|
<!-- Panel Sign-Off Block -->
|
||||||
<div style="border-top: 2px solid var(--border); padding-top: 20px; display: flex; justify-content: space-between; align-items: center;">
|
<x-interview.report.signoff :recommended="$integrityScore >= 80 && $codeScore >= 50" />
|
||||||
<div>
|
|
||||||
<div style="font-size: 0.7rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;">
|
|
||||||
HR & Lead Panel Approval
|
|
||||||
</div>
|
</div>
|
||||||
<div style="font-weight: 700; font-size: 0.9rem; color: var(--primary); margin-top: 4px;">
|
</x-layouts.report>
|
||||||
SingleLogin Enterprise HR Board
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<div style="font-size: 0.7rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase;">
|
|
||||||
Final Recommendation
|
|
||||||
</div>
|
|
||||||
<div style="font-weight: 800; font-size: 1rem; color: {{ $integrityScore >= 80 && $codeScore >= 50 ? '#10b981' : '#ef4444' }};">
|
|
||||||
{{ $integrityScore >= 80 && $codeScore >= 50 ? 'RECOMMENDED FOR HIRE' : 'REQUIRES FURTHER REVIEW / FLAGGED' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@ -1,223 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<x-layouts.interview
|
||||||
<html lang="en">
|
:title="$interview->candidate_name . ' - Assessment Review'"
|
||||||
<head>
|
data-interview-id="{{ $interview->id }}"
|
||||||
<meta charset="UTF-8">
|
data-submission-id="{{ $interview->submission_unique_id }}"
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
>
|
||||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
@push('head-scripts')
|
||||||
<title>{{ $interview->candidate_name }} - Assessment Review | 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=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
|
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
||||||
<script src="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
|
<script src="//cdn.metered.ca/sdk/video/1.4.6/sdk.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
@endpush
|
||||||
|
|
||||||
<style>
|
<!-- Candidate Session Summary Card -->
|
||||||
:root {
|
<x-interview.candidate-header :interview="$interview" />
|
||||||
--bg: #0a0e17;
|
|
||||||
--panel: #121826;
|
|
||||||
--panel-2: #0d1220;
|
|
||||||
--border: #232b3d;
|
|
||||||
--border-soft: #1b2233;
|
|
||||||
--text: #e8ebf3;
|
|
||||||
--text-dim: #9199ad;
|
|
||||||
--text-faint: #5b637a;
|
|
||||||
--accent: #4b82f7;
|
|
||||||
--accent-soft: rgba(75,130,247,0.12);
|
|
||||||
--accent-border: rgba(75,130,247,0.35);
|
|
||||||
--success: #21c274;
|
|
||||||
--danger: #ef4a5f;
|
|
||||||
--danger-soft: rgba(239,74,95,0.1);
|
|
||||||
--warning: #f0a939;
|
|
||||||
--warning-soft: rgba(240,169,57,0.12);
|
|
||||||
--radius: 12px;
|
|
||||||
--mono: 'JetBrains Mono', monospace;
|
|
||||||
--sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
<!-- Video & Screen Call Panel -->
|
||||||
background: radial-gradient(1200px 600px at 15% -10%, rgba(75,130,247,0.06), transparent 60%), var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
font-family: var(--sans);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle switch */
|
|
||||||
.toggle {
|
|
||||||
width: 32px; height: 18px; border-radius: 20px; background: var(--panel-2);
|
|
||||||
border: 1px solid var(--border); position: relative; cursor: pointer; flex-shrink: 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.toggle::after {
|
|
||||||
content: ''; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px;
|
|
||||||
border-radius: 50%; background: var(--text-faint); transition: left .15s ease;
|
|
||||||
}
|
|
||||||
.toggle.on { background: var(--accent-soft); border-color: var(--accent-border); }
|
|
||||||
.toggle.on::after { left: 16px; background: var(--accent); }
|
|
||||||
|
|
||||||
/* Accordion disclosure */
|
|
||||||
details.meta summary {
|
|
||||||
list-style: none; cursor: pointer; user-select: none;
|
|
||||||
}
|
|
||||||
details.meta summary::-webkit-details-marker { display: none; }
|
|
||||||
details.meta summary i.chev { transition: transform .15s ease; }
|
|
||||||
details.meta[open] summary i.chev { transform: rotate(180deg); }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="min-h-screen flex flex-col antialiased" data-interview-id="{{ $interview->id }}" data-submission-id="{{ $interview->submission_unique_id }}">
|
|
||||||
|
|
||||||
<div class="w-full mx-auto px-7 py-5 pb-16 flex-1">
|
|
||||||
|
|
||||||
<!-- Top bar -->
|
|
||||||
<div class="flex items-center justify-between pb-4 mb-4 border-b border-[#1b2233]">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<a href="{{ route('interview.index') }}" class="w-9 h-9 rounded-xl bg-gradient-to-br from-[#5b8bff] to-[#3b63e0] flex items-center justify-center text-sm text-white shadow-[0_4px_14px_rgba(75,130,247,0.35)] no-underline">
|
|
||||||
<i class="fa-solid fa-bolt"></i>
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-base font-semibold text-white m-0 tracking-tight">Candidate Assessment Review</h1>
|
|
||||||
<p class="text-xs text-[#5b637a] m-0 mt-0.5">Live proctored session & review room</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1.5">
|
|
||||||
<a href="{{ route('interview.index') }}">
|
|
||||||
<x-button variant="ghost" icon="fa-solid fa-arrow-left">Back to assessments</x-button>
|
|
||||||
</a>
|
|
||||||
@if(Auth::user()->isAdmin())
|
|
||||||
<a href="{{ route('admin.dashboard') }}">
|
|
||||||
<x-button variant="ghost" icon="fa-solid fa-sliders">Control panel</x-button>
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
<form action="{{ route('logout') }}" method="POST" id="logout-form" class="inline">
|
|
||||||
@csrf
|
|
||||||
<x-button type="button" onclick="interviewerLogoutAction()" variant="ghost" icon="fa-solid fa-arrow-right-from-bracket">Logout</x-button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(session('success'))
|
|
||||||
<div class="mb-4 bg-[#21c274]/15 border border-[#21c274]/35 text-[#21c274] px-4 py-2.5 rounded-lg text-xs font-medium">
|
|
||||||
{{ session('success') }}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<!-- Session Panel -->
|
|
||||||
<div class="bg-[#121826] border border-[#1b2233] rounded-[12px] mb-4 overflow-hidden">
|
|
||||||
<div class="flex items-center justify-between p-5 gap-4 flex-wrap">
|
|
||||||
<div class="flex items-center gap-3.5">
|
|
||||||
@php
|
|
||||||
$parts = preg_split('/\s+/', trim($interview->candidate_name));
|
|
||||||
$candInitials = count($parts) >= 2 ? strtoupper(substr($parts[0],0,1).substr(end($parts),0,1)) : strtoupper(substr($interview->candidate_name,0,2));
|
|
||||||
@endphp
|
|
||||||
<div class="w-[42px] h-[42px] rounded-full bg-gradient-to-br from-[#5b8bff] to-[#3b63e0] flex items-center justify-center text-white font-semibold text-[15px] shrink-0">
|
|
||||||
{{ $candInitials }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-2 mb-0.5">
|
|
||||||
<h2 id="modal-cand-name" class="text-[15px] font-semibold text-white m-0 leading-none">{{ $interview->candidate_name }}</h2>
|
|
||||||
<span id="candidate-joined-pill" class="pill gray inline-flex items-center gap-1.5 font-semibold text-[11px] px-2.5 py-0.5 rounded-full border border-slate-700 text-slate-400 bg-slate-800/60">
|
|
||||||
<i class="fa-solid fa-circle text-[8px] text-slate-500"></i> Not Joined
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-[#5b637a] font-mono">{{ $interview->candidate_email }} · {{ $interview->candidate_phone }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1.5 flex-wrap">
|
|
||||||
@if($interview->isExpired())
|
|
||||||
<x-pill id="timeline-status-pill" variant="red" icon="fa-solid fa-circle">Expired</x-pill>
|
|
||||||
@elseif($interview->status === 'completed')
|
|
||||||
<x-pill id="timeline-status-pill" variant="success" icon="fa-solid fa-circle">Completed</x-pill>
|
|
||||||
@elseif($interview->call_status === 'active')
|
|
||||||
<x-pill id="timeline-status-pill" variant="blue" class="live" icon="fa-solid fa-circle">Call in progress</x-pill>
|
|
||||||
@elseif($interview->isActiveTimeline())
|
|
||||||
<x-pill id="timeline-status-pill" variant="blue" class="live" icon="fa-solid fa-circle">Timeline live</x-pill>
|
|
||||||
@else
|
|
||||||
<x-pill id="timeline-status-pill" variant="secondary" icon="fa-solid fa-clock">Scheduled</x-pill>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="w-px h-[18px] bg-[#232b3d] mx-1"></div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 text-xs text-[#9199ad] cursor-pointer" onclick="toggleSosAutoTrigger()">
|
|
||||||
<span class="toggle" id="sosToggle"></span> SOS auto-trigger
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-px h-[18px] bg-[#232b3d] mx-1"></div>
|
|
||||||
|
|
||||||
<x-button onclick="regenerateCandidatePassword()" variant="secondary" icon="fa-solid fa-key">Password</x-button>
|
|
||||||
<x-button onclick="openReportPage()" variant="secondary" icon="fa-solid fa-file-pdf">PDF report</x-button>
|
|
||||||
<x-button onclick="blockCandidateAction()" variant="secondary" icon="fa-solid fa-ban">Block</x-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Submission Details Disclosure Accordion -->
|
|
||||||
<details class="meta border-t border-[#1b2233]">
|
|
||||||
<summary class="flex items-center gap-2 px-5 py-2.5 text-xs text-[#5b637a] font-medium hover:text-[#9199ad] transition-colors">
|
|
||||||
<i class="fa-solid fa-chevron-down chev text-[10px]"></i> Show submission details
|
|
||||||
</summary>
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2.5 px-5 pb-4">
|
|
||||||
<div class="bg-[#0d1220] border border-[#1b2233] rounded-lg p-2.5">
|
|
||||||
<div class="text-[10.5px] text-[#5b637a] uppercase tracking-wider mb-1">Submission ID</div>
|
|
||||||
<div id="modal-cand-uid" class="text-xs font-mono text-[#9199ad] break-all">{{ $interview->submission_unique_id }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-[#0d1220] border border-[#1b2233] rounded-lg p-2.5">
|
|
||||||
<div class="text-[10.5px] text-[#5b637a] uppercase tracking-wider mb-1">Temp Pass</div>
|
|
||||||
<div class="text-xs font-mono text-[#9199ad]">{{ $interview->temp_password }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-[#0d1220] border border-[#1b2233] rounded-lg p-2.5">
|
|
||||||
<div class="text-[10.5px] text-[#5b637a] uppercase tracking-wider mb-1">Language</div>
|
|
||||||
<div class="text-xs font-mono text-[#9199ad] uppercase">{{ $interview->language }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-[#0d1220] border border-[#1b2233] rounded-lg p-2.5">
|
|
||||||
<div class="text-[10.5px] text-[#5b637a] uppercase tracking-wider mb-1">Timeline</div>
|
|
||||||
<div class="text-xs font-mono text-[#9199ad]">{{ ($interview->scheduled_at ?? $interview->created_at)->format('M d, H:i') }} – {{ $interview->expires_at->format('H:i') }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Call Panel -->
|
|
||||||
<x-card>
|
<x-card>
|
||||||
<x-slot:headerExtra>
|
<x-slot:headerExtra>
|
||||||
<div class="flex items-center gap-2">
|
<x-interview.call-controls :interview="$interview" />
|
||||||
<!-- 1. Mic Toggle (Icon Only) -->
|
|
||||||
<x-button id="btn-toggle-interviewer-mic" onclick="toggleInterviewerMic()" variant="ghost" icon="fa-solid fa-microphone" style="display: none;" title="Mic On / Mute"></x-button>
|
|
||||||
|
|
||||||
<!-- 2. Cam Toggle (Icon Only) -->
|
|
||||||
<x-button id="btn-toggle-interviewer-cam" onclick="toggleInterviewerCam()" variant="ghost" icon="fa-solid fa-video" style="display: none;" title="Cam On / Off"></x-button>
|
|
||||||
|
|
||||||
<!-- 3. Start / Join / Restart Call -->
|
|
||||||
@if($interview->call_status === 'ended')
|
|
||||||
<x-button id="btn-start-call" disabled variant="primary" icon="fa-solid fa-phone-slash" class="opacity-50 cursor-not-allowed pointer-events-none">
|
|
||||||
Call Ended
|
|
||||||
</x-button>
|
|
||||||
@elseif($interview->call_status === 'active')
|
|
||||||
<x-button id="btn-start-call" onclick="startInterviewerCall()" variant="primary" icon="fa-solid fa-phone">
|
|
||||||
Join Call
|
|
||||||
</x-button>
|
|
||||||
@else
|
|
||||||
<x-button id="btn-start-call" onclick="startInterviewerCall()" variant="primary" icon="fa-solid fa-phone">
|
|
||||||
Start Call
|
|
||||||
</x-button>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<!-- 4. Record Call (Silent) -->
|
|
||||||
<x-button id="btn-start-recording" onclick="startCallRecording()" variant="secondary" icon="fa-solid fa-circle-dot" style="display: none;">
|
|
||||||
Record Call (Silent)
|
|
||||||
</x-button>
|
|
||||||
<x-button id="btn-stop-recording" onclick="stopCallRecording()" variant="danger" icon="fa-solid fa-square" style="display: none;" class="animate-pulse" title="Stop & Save Recording"></x-button>
|
|
||||||
|
|
||||||
<!-- 5. Leave Call -->
|
|
||||||
<x-button id="btn-leave-call" onclick="leaveInterviewerCall()" variant="secondary" icon="fa-solid fa-arrow-right-from-bracket" style="display: none;">
|
|
||||||
Leave Call
|
|
||||||
</x-button>
|
|
||||||
|
|
||||||
<!-- 6. End Call for All (Icon Only, Far Right) -->
|
|
||||||
<x-button id="btn-end-all-call" onclick="endCallForAll()" variant="danger" icon="fa-solid fa-phone-slash" style="display: none;" title="End Call for All"></x-button>
|
|
||||||
</div>
|
|
||||||
</x-slot:headerExtra>
|
</x-slot:headerExtra>
|
||||||
|
|
||||||
<!-- Video Grid -->
|
<!-- Video Grid -->
|
||||||
@ -237,7 +34,7 @@
|
|||||||
avatarCircleId="modal-cand-avatar-circle"
|
avatarCircleId="modal-cand-avatar-circle"
|
||||||
avatarNameId="cand-avatar-name"
|
avatarNameId="cand-avatar-name"
|
||||||
avatarTitle="{{ $interview->candidate_name }}"
|
avatarTitle="{{ $interview->candidate_name }}"
|
||||||
initials="{{ $candInitials }}">
|
initials="{{ $interview->candidate_initials }}">
|
||||||
<span id="cand-status-text">Waiting for candidate…</span>
|
<span id="cand-status-text">Waiting for candidate…</span>
|
||||||
</x-video-tile>
|
</x-video-tile>
|
||||||
|
|
||||||
@ -262,8 +59,8 @@
|
|||||||
|
|
||||||
<!-- Panelist Self -->
|
<!-- Panelist Self -->
|
||||||
@php
|
@php
|
||||||
$usrWords = preg_split('/\s+/', trim(Auth::user()->name));
|
$usrWords = preg_split('/\s+/', trim(Auth::user()->name ?? 'Interviewer'));
|
||||||
$usrInitials = count($usrWords) >= 2 ? strtoupper(substr($usrWords[0],0,1).substr(end($usrWords),0,1)) : strtoupper(substr(trim(Auth::user()->name),0,2));
|
$usrInitials = count($usrWords) >= 2 ? strtoupper(substr($usrWords[0],0,1).substr(end($usrWords),0,1)) : strtoupper(substr(trim(Auth::user()->name ?? 'IV'),0,2));
|
||||||
@endphp
|
@endphp
|
||||||
<x-video-tile
|
<x-video-tile
|
||||||
id="self-video-wrapper"
|
id="self-video-wrapper"
|
||||||
@ -272,7 +69,7 @@
|
|||||||
badgeText="Panelist self"
|
badgeText="Panelist self"
|
||||||
micStatusId="self-mic-status"
|
micStatusId="self-mic-status"
|
||||||
camStatusId="self-cam-status"
|
camStatusId="self-cam-status"
|
||||||
avatarTitle="{{ Auth::user()->name }}"
|
avatarTitle="{{ Auth::user()->name ?? 'Interviewer' }}"
|
||||||
initials="{{ $usrInitials }}">
|
initials="{{ $usrInitials }}">
|
||||||
Panelist (self)
|
Panelist (self)
|
||||||
</x-video-tile>
|
</x-video-tile>
|
||||||
@ -282,7 +79,7 @@
|
|||||||
<div id="panelist-mesh-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3.5 px-5 pb-5"></div>
|
<div id="panelist-mesh-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3.5 px-5 pb-5"></div>
|
||||||
</x-card>
|
</x-card>
|
||||||
|
|
||||||
<!-- Inspector Panel -->
|
<!-- Inspector Panel (Tabs & Audit Log / Live Chat) -->
|
||||||
<x-card>
|
<x-card>
|
||||||
<div class="inline-flex gap-1 p-1 bg-[#0d1220] border border-[#1b2233] rounded-[10px] mx-5 mt-4">
|
<div class="inline-flex gap-1 p-1 bg-[#0d1220] border border-[#1b2233] rounded-[10px] mx-5 mt-4">
|
||||||
<x-tab-button id="tab-btn-code" tabKey="code" active="true" icon="fa-solid fa-code" onclick="switchIdeTab('code')">Live IDE code</x-tab-button>
|
<x-tab-button id="tab-btn-code" tabKey="code" active="true" icon="fa-solid fa-code" onclick="switchIdeTab('code')">Live IDE code</x-tab-button>
|
||||||
@ -293,65 +90,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-4 p-5">
|
<div class="grid grid-cols-1 lg:grid-cols-5 gap-4 p-5">
|
||||||
<!-- Left 3 Cols: Tab Content -->
|
<!-- Left 3 Cols: Tab Content Panes -->
|
||||||
<div class="lg:col-span-3">
|
<div class="lg:col-span-3">
|
||||||
<!-- Tab 1: Live IDE Code -->
|
<x-interview.tabs.code-tab />
|
||||||
<div id="tab-content-code" class="block">
|
<x-interview.tabs.notes-tab />
|
||||||
<div class="text-[11.5px] text-[#5b637a] mb-2 font-medium">Current code solution — visible even if candidate does not share screen</div>
|
<x-interview.tabs.drawing-tab />
|
||||||
<pre id="modal-code-display" class="bg-[#0d1220] border border-[#1b2233] rounded-[9px] p-3.5 font-mono text-[13px] text-[#6ee7c9] min-h-[220px] max-h-[300px] overflow-y-auto whitespace-pre-wrap"></pre>
|
<x-interview.tabs.recordings-tab />
|
||||||
|
<x-interview.tabs.screenshots-tab />
|
||||||
<div class="text-[11.5px] text-[#5b637a] mt-4 mb-2 font-medium">Execution stdout / stderr</div>
|
|
||||||
<pre id="modal-output-display" class="bg-[#0d1220] border border-[#1b2233] rounded-[9px] p-3 font-mono text-xs text-[#5b637a] max-h-[100px] overflow-y-auto whitespace-pre-wrap"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab 2: Candidate Notepad -->
|
|
||||||
<div id="tab-content-notes" class="hidden">
|
|
||||||
<div class="text-[11.5px] text-[#5b637a] mb-2 font-medium">Candidate live notepad notes (auto-synced)</div>
|
|
||||||
<pre id="modal-notes-display" class="bg-[#0d1220] border border-[#1b2233] rounded-[9px] p-3.5 font-mono text-xs text-slate-200 min-h-[250px] max-h-[350px] overflow-y-auto whitespace-pre-wrap"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab 3: Candidate Canvas Drawing -->
|
|
||||||
<div id="tab-content-drawing" class="hidden">
|
|
||||||
<div class="text-[11.5px] text-[#5b637a] mb-2 font-medium">Candidate live drawing / architecture diagram</div>
|
|
||||||
<div class="bg-[#0d1220] border border-[#1b2233] rounded-[9px] p-3 flex justify-center items-center min-h-[250px]">
|
|
||||||
<img id="modal-drawing-img" src="" alt="Candidate Drawing Board" class="max-w-full max-h-[320px] hidden rounded-lg border border-dashed border-[#232b3d]" />
|
|
||||||
<div id="no-drawing-placeholder" class="text-[#5b637a] text-xs">No drawing data submitted by candidate yet.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab 4: Saved Video Recordings -->
|
|
||||||
<div id="tab-content-recordings" class="hidden">
|
|
||||||
<div class="text-[11.5px] text-[#5b637a] mb-2 font-medium">Saved candidate video sessions (sorted by newest first)</div>
|
|
||||||
<div id="modal-recordings-container" class="bg-[#0d1220] border border-[#1b2233] rounded-[9px] p-3 min-h-[250px] max-h-[420px] overflow-y-auto">
|
|
||||||
<div class="text-[#5b637a] text-xs text-center pt-10">No video recordings saved for this candidate yet.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab 5: Tab-Switch Screenshots -->
|
|
||||||
<div id="tab-content-screenshots" class="hidden">
|
|
||||||
<div class="bg-[#0d1220] border border-[#1b2233] rounded-[9px] p-8 flex flex-col items-center justify-center min-h-[250px] text-center">
|
|
||||||
<div class="w-10 h-10 rounded-full bg-amber-500/10 border border-amber-500/20 flex items-center justify-center text-amber-400 mb-3 text-base">
|
|
||||||
<i class="fa-solid fa-clock"></i>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm font-semibold text-white mb-1">Coming Soon</div>
|
|
||||||
<div class="text-xs text-[#5b637a] max-w-xs">Tab switch screenshot capturing and inspection is currently under maintenance.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{--
|
|
||||||
<div class="text-[11.5px] text-[#5b637a] mb-2 font-medium flex justify-between items-center">
|
|
||||||
<span>Candidate tab-switch captured screenshots</span>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-xs text-[#9199ad]">Admin Screenshot Capture:</span>
|
|
||||||
<x-button id="modal-toggle-screenshot-btn" onclick="toggleTabScreenshotCapture()" variant="success" size="sm" icon="fa-solid fa-camera">
|
|
||||||
ENABLED
|
|
||||||
</x-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="modal-screenshots-container" class="bg-[#0d1220] border border-[#1b2233] rounded-[9px] p-3 min-h-[250px] max-h-[420px] overflow-y-auto">
|
|
||||||
<div class="text-[#5b637a] text-xs text-center pt-10">No tab-switch screenshots captured for this candidate yet.</div>
|
|
||||||
</div>
|
|
||||||
--}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right 2 Cols: Audit Log & Chat -->
|
<!-- Right 2 Cols: Audit Log & Chat -->
|
||||||
@ -376,67 +121,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</x-card>
|
</x-card>
|
||||||
|
|
||||||
</div>
|
<!-- SOS Cheating Alert Modal -->
|
||||||
|
<x-interview.sos-modal />
|
||||||
|
|
||||||
<!-- SOS CHEATING ALERT MODAL -->
|
@push('scripts')
|
||||||
<x-modal id="sos-modal" title="candidate might cheating" maxWidth="lg" onClose="stopSosAlarm">
|
|
||||||
<div id="sos-violation-list" class="bg-[rgba(239,74,95,0.1)] border border-[rgba(239,74,95,0.32)] rounded-lg p-3.5 text-xs text-[#ef4a5f] mb-4 max-h-[180px] overflow-y-auto">
|
|
||||||
Violation details loading...
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<x-button onclick="stopSosAlarm()" variant="secondary" size="md" icon="fa-solid fa-check">
|
|
||||||
Acknowledge Alert
|
|
||||||
</x-button>
|
|
||||||
<x-button onclick="disableSosAndStopAlarm()" variant="danger" size="md" icon="fa-solid fa-bell-slash">
|
|
||||||
Acknowledge & Disable SOS
|
|
||||||
</x-button>
|
|
||||||
</div>
|
|
||||||
</x-modal>
|
|
||||||
|
|
||||||
<!-- INCOMING CALL RING MODAL -->
|
|
||||||
<x-modal id="incoming-call-modal" maxWidth="md">
|
|
||||||
<div class="text-center p-2">
|
|
||||||
<div class="relative w-[90px] h-[90px] mx-auto mb-5 flex items-center justify-center">
|
|
||||||
<div class="absolute -inset-4 rounded-full border-2 border-[#21c274]/60 animate-ping"></div>
|
|
||||||
<div class="w-20 h-20 rounded-full bg-gradient-to-br from-[#5b8bff] to-[#3b63e0] flex items-center justify-center text-3xl font-extrabold text-white shadow-xl shadow-[#4b82f7]/50 z-10">
|
|
||||||
<i class="fa-solid fa-phone"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-xs uppercase tracking-wider text-[#21c274] font-extrabold mb-1">
|
|
||||||
⚡ INCOMING INTERVIEW CALL
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 id="incoming-cand-name" class="font-outfit text-2xl font-extrabold text-white mb-1">
|
|
||||||
{{ $interview->candidate_name }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div id="incoming-starter-info" class="text-xs text-[#4b82f7] mb-4">
|
|
||||||
Initiated by Panelist
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white/5 border border-white/10 rounded-xl p-3 text-xs text-slate-300 mb-6 leading-relaxed">
|
|
||||||
Another interviewer has started calling the candidate. Click <strong>Accept Call</strong> to join live, or <strong>Decline</strong> if busy.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 justify-center">
|
|
||||||
<x-button id="incoming-decline-btn" onclick="declineIncomingCall()" variant="danger" size="md" icon="fa-solid fa-xmark" class="flex-1">
|
|
||||||
Decline / Miss
|
|
||||||
</x-button>
|
|
||||||
<x-button id="incoming-accept-btn" onclick="acceptIncomingCall()" variant="success" size="md" icon="fa-solid fa-phone" class="flex-[1.3] font-extrabold animate-pulse">
|
|
||||||
Accept Call
|
|
||||||
</x-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</x-modal>
|
|
||||||
|
|
||||||
<!-- Page Initialization Script -->
|
|
||||||
<script>
|
<script>
|
||||||
window.currentUserId = {{ Auth::id() }};
|
|
||||||
window.currentUserName = "{{ addslashes(Auth::user()->name) }}";
|
|
||||||
window.currentUserRole = "{{ Auth::user()->is_admin ? 'admin' : 'interviewer' }}";
|
|
||||||
window.csrfToken = "{{ csrf_token() }}";
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
openReviewModal(
|
openReviewModal(
|
||||||
{{ $interview->id }},
|
{{ $interview->id }},
|
||||||
@ -446,5 +135,5 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
@endpush
|
||||||
</html>
|
</x-layouts.interview>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user