This commit is contained in:
subhajit 2026-07-31 15:24:24 +05:30
parent c67ee1fceb
commit 367771772f
3 changed files with 189 additions and 157 deletions

View File

@ -577,12 +577,15 @@
.badge { .badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
gap: 4px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 9999px; border-radius: 9999px;
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
white-space: nowrap;
} }
.badge-admin { .badge-admin {
@ -910,76 +913,7 @@
</div> </div>
@endif @endif
<!-- Global Admin Settings & Proctoring Control Panel -->
<section class="admin-card" style="margin-bottom: 35px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%); border: 1.5px solid rgba(99, 102, 241, 0.35); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); border-radius: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 12px;">
<div>
<h3 style="font-family: 'Outfit', sans-serif; font-size: 1.3rem; font-weight: 800; color: #ffffff; margin: 0; display: flex; align-items: center; gap: 8px;">
⚙️ Global Admin Proctoring & System Settings
</h3>
<div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">Master control panel for candidate assessment rules, system screen capturing, and module toggles</div>
</div>
<span class="badge" style="background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); font-weight: 700; padding: 6px 12px; font-size: 0.75rem;">
🛡️ ADMIN CONTROL PANEL
</span>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top: 16px;">
<!-- 1. Candidate System Screenshot Capture Master Toggle -->
<div style="padding: 16px; background: rgba(16, 185, 129, 0.08); border: 1.5px solid rgba(16, 185, 129, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
📸 Candidate System Screenshot Capture
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }}
</span>
<label style="display: inline-flex; align-items: center; gap: 6px; 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' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'Stop Screenshot Capture' : 'Enable Screenshot Capture' }}
</span>
</label>
</div>
</form>
</div>
<!-- 2. Onboarding & Offboarding Module Master Toggle -->
<div style="padding: 16px; background: rgba(99, 102, 241, 0.08); border: 1.5px solid rgba(99, 102, 241, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
🚀 Candidate Provisioning & Onboarding Module
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }}
</span>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }}
</span>
</label>
</div>
</form>
</div>
</div>
</section>
<!-- Users Table Card --> <!-- Users Table Card -->
<section class="users-card" style="margin-bottom: 50px;"> <section class="users-card" style="margin-bottom: 50px;">
@ -1241,41 +1175,7 @@
</select> </select>
<button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; font-size:0.75rem; border-radius:8px; padding:8px 12px; border:none; cursor:pointer; font-weight:600; white-space: nowrap;">Save Default</button> <button type="submit" class="btn" style="background-color: var(--accent-primary); color: white; font-size:0.75rem; border-radius:8px; padding:8px 12px; border:none; cursor:pointer; font-weight:600; white-space: nowrap;">Save Default</button>
</div> </div>
</div> </div>
</form>
<!-- Onboarding & Offboarding Module Master Toggle -->
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST" style="margin-bottom: 12px; padding: 12px 14px; background: rgba(99, 102, 241, 0.08); border: 1px solid rgba(99, 102, 241, 0.25); border-radius: 12px;">
@csrf
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 0.8rem; color: #ffffff; font-weight: 600;">🚀 Onboarding & Offboarding Module</div>
<div style="font-size: 0.7rem; color: var(--text-muted);">Enable/disable candidate provisioning & 1-click revocation</div>
</div>
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 18px; height: 18px;">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'ENABLED' : 'DISABLED' }}
</span>
</label>
</div>
</form>
<!-- Candidate System Screenshot Master Toggle (Admin Control) -->
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST" style="margin-bottom: 20px; padding: 12px 14px; background: rgba(16, 185, 129, 0.08); border: 1px solid rgba(16, 185, 129, 0.25); border-radius: 12px;">
@csrf
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 0.8rem; color: #ffffff; font-weight: 600;">📸 Candidate System Screenshot Capture</div>
<div style="font-size: 0.7rem; color: var(--text-muted);">Master Admin control to stop/start candidate system screen capturing (call start & tab switch)</div>
</div>
<label style="display: inline-flex; align-items: center; gap: 6px; 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' : '' }} style="accent-color: #10b981; width: 18px; height: 18px;">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'ENABLED' : 'STOPPED / DISABLED' }}
</span>
</label>
</div>
</form> </form>
<!-- Registered Roles List --> <!-- Registered Roles List -->

View File

@ -458,13 +458,20 @@
} }
}); });
// Real-Time Code Sync to Interviewer (Before Submission) // Real-Time Instant BroadcastChannel + Backend Code Sync
const liveSyncChannel = new BroadcastChannel('live_sync_' + '{{ $interview->id }}');
let codeSyncTimeout = null; let codeSyncTimeout = null;
editor.onDidChangeModelContent(function () { editor.onDidChangeModelContent(function () {
const code = editor.getValue();
const lang = document.getElementById('language-select').value;
// Broadcast instant local event (0ms latency)
if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) {
liveSyncChannel.postMessage({ type: 'code_sync', code: code, language: lang });
}
clearTimeout(codeSyncTimeout); clearTimeout(codeSyncTimeout);
codeSyncTimeout = setTimeout(function () { codeSyncTimeout = setTimeout(function () {
const code = editor.getValue();
const lang = document.getElementById('language-select').value;
fetch(`{{ route("interview.candidate.sync-code", $interview->id) }}`, { fetch(`{{ route("interview.candidate.sync-code", $interview->id) }}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -473,7 +480,7 @@
}, },
body: JSON.stringify({ code: code, language: lang }) body: JSON.stringify({ code: code, language: lang })
}); });
}, 600); }, 200);
}); });
}); });
@ -1176,6 +1183,10 @@ function showCandidateJoinOption() {
if (joinBtn) joinBtn.style.display = 'none'; if (joinBtn) joinBtn.style.display = 'none';
if (!localMediaStream) {
await startCandidateCameraStream();
}
isCandidateCallConnected = true; isCandidateCallConnected = true;
const badge = document.getElementById('call-status-badge'); const badge = document.getElementById('call-status-badge');
@ -1380,6 +1391,12 @@ function initCandidatePeer() {
peerInstance.on('call', call => { peerInstance.on('call', call => {
activeCallInstance = call; activeCallInstance = call;
latestCallStatus = 'active';
if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) {
triggerCandidateRing();
}
const sendStream = localMediaStream || new MediaStream(); const sendStream = localMediaStream || new MediaStream();
call.answer(sendStream); call.answer(sendStream);
@ -1388,15 +1405,17 @@ function initCandidatePeer() {
const placeholder = document.getElementById('interviewer-video-placeholder'); const placeholder = document.getElementById('interviewer-video-placeholder');
if (remoteVideo) { if (remoteVideo) {
remoteVideo.srcObject = remoteStream; remoteVideo.srcObject = remoteStream;
remoteVideo.play().catch(e => console.log(e)); remoteVideo.play().catch(e => console.log('Candidate remote video play error:', e));
} }
if (placeholder) placeholder.style.display = 'none'; if (placeholder) placeholder.style.display = 'none';
const badge = document.getElementById('call-status-badge'); if (isCandidateCallConnected) {
if (badge) { const badge = document.getElementById('call-status-badge');
badge.innerText = '🟢 CALL CONNECTED (LIVE)'; if (badge) {
badge.style.background = 'rgba(16,185,129,0.3)'; badge.innerText = '🟢 CALL CONNECTED (LIVE)';
badge.style.color = '#34d399'; badge.style.background = 'rgba(16,185,129,0.3)';
badge.style.color = '#34d399';
}
} }
}); });
@ -1830,9 +1849,13 @@ function toggleNotepadModal() {
} }
function saveNotepadContent() { function saveNotepadContent() {
const notes = document.getElementById('notepad-textarea').value;
if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) {
liveSyncChannel.postMessage({ type: 'notes_sync', notes: notes });
}
clearTimeout(notesTimeout); clearTimeout(notesTimeout);
notesTimeout = setTimeout(() => { notesTimeout = setTimeout(() => {
const notes = document.getElementById('notepad-textarea').value;
fetch(`{{ route('interview.candidate.notes', $interview->id) }}`, { fetch(`{{ route('interview.candidate.notes', $interview->id) }}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -1841,13 +1864,14 @@ function saveNotepadContent() {
}, },
body: JSON.stringify({ notes: notes }) body: JSON.stringify({ notes: notes })
}); });
}, 300); }, 150);
} }
// HTML5 Canvas Drawing Logic // HTML5 Canvas Drawing Logic
let isDrawing = false; let isDrawing = false;
let canvas, ctx; let canvas, ctx;
let drawingSaveTimeout = null; let drawingSaveTimeout = null;
let lastDrawingSaveTime = 0;
function toggleDrawingModal() { function toggleDrawingModal() {
const modal = document.getElementById('drawing-modal'); const modal = document.getElementById('drawing-modal');
@ -1889,29 +1913,38 @@ function draw(e) {
ctx.strokeStyle = document.getElementById('draw-color').value; ctx.strokeStyle = document.getElementById('draw-color').value;
ctx.lineTo(e.offsetX, e.offsetY); ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke(); ctx.stroke();
saveCanvasDrawing(); saveCanvasDrawing(false);
} }
function stopDraw() { function stopDraw() {
if (isDrawing) { if (isDrawing) {
isDrawing = false; isDrawing = false;
ctx.closePath(); ctx.closePath();
saveCanvasDrawing(); saveCanvasDrawing(true);
} }
} }
function clearCanvasDraw() { function clearCanvasDraw() {
if (ctx && canvas) { if (ctx && canvas) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
saveCanvasDrawing(); saveCanvasDrawing(true);
} }
} }
function saveCanvasDrawing() { function saveCanvasDrawing(force = false) {
clearTimeout(drawingSaveTimeout); if (!canvas) return;
drawingSaveTimeout = setTimeout(() => { const now = Date.now();
if (!canvas) return; const drawingData = canvas.toDataURL();
const drawingData = canvas.toDataURL();
// Broadcast instant local event (0ms latency)
if (typeof liveSyncChannel !== 'undefined' && liveSyncChannel) {
liveSyncChannel.postMessage({ type: 'drawing_sync', drawing: drawingData });
}
// Throttled HTTP POST save to backend (fires every 250ms while drawing & on mouseup)
if (force || (now - lastDrawingSaveTime >= 250)) {
clearTimeout(drawingSaveTimeout);
lastDrawingSaveTime = now;
fetch(`{{ route('interview.candidate.drawing', $interview->id) }}`, { fetch(`{{ route('interview.candidate.drawing', $interview->id) }}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -1920,7 +1953,12 @@ function saveCanvasDrawing() {
}, },
body: JSON.stringify({ drawing: drawingData }) body: JSON.stringify({ drawing: drawingData })
}); });
}, 300); } else {
clearTimeout(drawingSaveTimeout);
drawingSaveTimeout = setTimeout(() => {
saveCanvasDrawing(true);
}, 250);
}
} }
setInterval(() => { setInterval(() => {
@ -1944,18 +1982,13 @@ function saveCanvasDrawing() {
}, 1200); }, 1200);
} }
if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) { if (!isCandidateCallConnected && !candidateDeclinedOrLeftCall) {
const elapsed = data.call_started_at ? (Date.now() - new Date(data.call_started_at).getTime()) : 999999; triggerCandidateRing(null, data.call_started_at);
if (elapsed <= 12000 && lastRungCallStartedAt !== data.call_started_at) {
lastRungCallStartedAt = data.call_started_at;
triggerCandidateRing(null, data.call_started_at);
} else if (elapsed > 12000) {
showCandidateJoinOption();
}
} }
} else if (data.call_status === 'ended' || data.call_status === 'idle') { } else if (data.call_status === 'ended' || data.call_status === 'idle') {
latestCallStatus = data.call_status; latestCallStatus = data.call_status;
hasCapturedCallStartScreenshot = false; hasCapturedCallStartScreenshot = false;
lastRungCallStartedAt = null; lastRungCallStartedAt = null;
candidateDeclinedOrLeftCall = false;
candRingtone.stop(); candRingtone.stop();
if (candRingTimer) clearTimeout(candRingTimer); if (candRingTimer) clearTimeout(candRingTimer);
isCandidateCallConnected = false; isCandidateCallConnected = false;
@ -1978,7 +2011,7 @@ function saveCanvasDrawing() {
} }
}) })
.catch(() => {}); .catch(() => {});
}, 2200); }, 1000);
</script> </script>
</body> </body>
</html> </html>

View File

@ -128,10 +128,14 @@
.badge { .badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
padding: 4px 8px; justify-content: center;
gap: 5px;
padding: 4px 10px;
border-radius: 9999px; border-radius: 9999px;
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 600; font-weight: 600;
white-space: nowrap;
line-height: 1.2;
} }
.admin-modal { .admin-modal {
@ -215,32 +219,80 @@
{{ session('success') }} {{ session('success') }}
</div> </div>
@endif @endif
<!-- Admin Proctoring & System Screenshot Control Banner --> <!-- Global Admin Settings & Proctoring Control Panel -->
<div style="margin-bottom: 20px; padding: 14px 20px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.9) 100%); border: 1.5px solid rgba(16, 185, 129, 0.35); border-radius: 14px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);"> <section class="admin-card" style="margin-bottom: 25px; padding: 20px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.95) 100%); border: 1.5px solid rgba(99, 102, 241, 0.35); box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5); border-radius: 16px;">
<div style="display: flex; align-items: center; gap: 12px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 12px;">
<div style="font-size: 1.5rem;">📸</div>
<div> <div>
<div style="font-size: 0.9rem; font-weight: 700; color: white;">Global Candidate System Screenshot Control</div> <h3 style="font-family: 'Outfit', sans-serif; font-size: 1.25rem; font-weight: 800; color: #ffffff; margin: 0; display: flex; align-items: center; gap: 8px;">
<div style="font-size: 0.75rem; color: var(--text-muted);"> ⚙️ Global Admin Proctoring & System Settings
Captures candidate desktop screen on call start & tab switches. </h3>
Admin can stop/start screen capturing globally anytime. <div style="font-size: 0.75rem; color: var(--text-muted); margin-top: 4px;">Master control panel for candidate assessment rules, system screen capturing, and module toggles</div>
</div>
</div> </div>
</div> <span class="badge" style="background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.4); font-weight: 700; padding: 6px 12px; font-size: 0.75rem;">
<div style="display: flex; align-items: center; gap: 12px;"> 🛡️ ADMIN CONTROL PANEL
<span id="global-screenshot-status-text" style="font-size: 0.8rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Screen Capturing Active)' : '🔴 STOPPED / DISABLED' }}
</span> </span>
@if(Auth::user() && Auth::user()->isAdmin())
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST" style="margin: 0;">
@csrf
<button type="submit" class="btn-nav" style="padding: 6px 14px; font-size: 0.75rem; font-weight: 700; background: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)' }}; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#fca5a5' : '#34d399' }}; border: 1px solid {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#ef4444' : '#10b981' }}; border-radius: 8px; cursor: pointer;">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🛑 Stop Screenshot Capture' : '🟢 Start Screenshot Capture' }}
</button>
</form>
@endif
</div> </div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top: 16px;">
<!-- 1. Candidate System Screenshot Capture Master Toggle -->
<div style="padding: 16px; background: rgba(16, 185, 129, 0.08); border: 1.5px solid rgba(16, 185, 129, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.screenshot-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
📸 Candidate System Screenshot Capture
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Captures candidate system desktop screen automatically on call start & tab switches. Admin can stop taking screenshots anytime across all candidates.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span id="global-screenshot-status-text" style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('enable_candidate_screenshots', '1') === '1' ? '🟢 ENABLED (Active)' : '🔴 STOPPED / DISABLED' }}
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<label style="display: inline-flex; align-items: center; gap: 6px; 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' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \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 style="padding: 16px; background: rgba(99, 102, 241, 0.08); border: 1.5px solid rgba(99, 102, 241, 0.3); border-radius: 12px;">
<form action="{{ route('admin.settings.onboarding-toggle') }}" method="POST">
@csrf
<div style="margin-bottom: 12px;">
<div style="font-size: 0.95rem; color: #ffffff; font-weight: 700; display: flex; align-items: center; gap: 6px;">
🚀 Candidate Provisioning & Onboarding Module
</div>
<div style="font-size: 0.73rem; color: var(--text-muted); margin-top: 4px; line-height: 1.4;">
Master toggle to enable/disable automated candidate provisioning, onboarding checklists, and 1-click revocation.
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<span style="font-size: 0.75rem; font-weight: 700; color: {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '#34d399' : '#fca5a5' }};">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? '🟢 ENABLED' : '🔴 DISABLED' }}
</span>
@if(Auth::user() && Auth::user()->isAdmin())
<label style="display: inline-flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" name="onboarding_enabled" value="1" onchange="this.form.submit()" {{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'checked' : '' }} style="accent-color: #10b981; width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 0.8rem; font-weight: 800; color: white; background: rgba(255,255,255,0.12); padding: 5px 12px; border-radius: 6px;">
{{ \App\Models\Setting::get('onboarding_enabled', '1') === '1' ? 'Disable Onboarding' : 'Enable Onboarding' }}
</span>
</label>
@endif
</div>
</form>
</div>
</div>
</section>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<div> <div>
@ -1180,6 +1232,48 @@ function declineIncomingCall(silent = false) {
} }
} }
let liveSyncChannel = null;
function subscribeLiveSync(interviewId) {
if (liveSyncChannel) {
try { liveSyncChannel.close(); } catch(e) {}
}
liveSyncChannel = new BroadcastChannel('live_sync_' + interviewId);
liveSyncChannel.onmessage = function(event) {
const data = event.data;
if (!data) return;
if (data.type === 'code_sync') {
if (data.language) {
const langEl = document.getElementById('modal-code-lang');
if (langEl) langEl.innerText = data.language.toUpperCase();
}
if (data.code !== undefined) {
const codeEl = document.getElementById('modal-code-display');
if (codeEl) codeEl.innerText = data.code || '// Candidate has not typed any code yet.';
}
} else if (data.type === 'notes_sync') {
if (data.notes !== undefined) {
const notesEl = document.getElementById('modal-notes-display');
if (notesEl) notesEl.innerText = data.notes || 'No candidate notes written yet.';
}
} else if (data.type === 'drawing_sync') {
const drawingImg = document.getElementById('modal-drawing-img');
const noDrawing = document.getElementById('no-drawing-placeholder');
if (data.drawing) {
if (drawingImg) {
drawingImg.src = data.drawing;
drawingImg.style.display = 'block';
}
if (noDrawing) noDrawing.style.display = 'none';
} else {
if (drawingImg) drawingImg.style.display = 'none';
if (noDrawing) noDrawing.style.display = 'block';
}
}
};
}
function openReviewModal(id, name, uid, autoJoinCall = false) { function openReviewModal(id, name, uid, autoJoinCall = false) {
currentInterviewId = id; currentInterviewId = id;
acknowledgedViolationCount = 0; acknowledgedViolationCount = 0;
@ -1199,9 +1293,10 @@ function openReviewModal(id, name, uid, autoJoinCall = false) {
initInterviewerPeer(); initInterviewerPeer();
switchIdeTab('code'); switchIdeTab('code');
subscribeLiveSync(id);
pollReviewData(); pollReviewData();
clearInterval(sosPollInterval); clearInterval(sosPollInterval);
sosPollInterval = setInterval(pollReviewData, 1200); sosPollInterval = setInterval(pollReviewData, 500);
if (autoJoinCall) { if (autoJoinCall) {
setTimeout(() => { setTimeout(() => {
@ -2135,6 +2230,10 @@ function endInterviewerCall() {
function closeReviewModal() { function closeReviewModal() {
clearInterval(sosPollInterval); clearInterval(sosPollInterval);
if (liveSyncChannel) {
try { liveSyncChannel.close(); } catch(e) {}
liveSyncChannel = null;
}
leaveInterviewerCall(true); leaveInterviewerCall(true);
const modal = document.getElementById('review-modal'); const modal = document.getElementById('review-modal');
if (modal) modal.classList.remove('active'); if (modal) modal.classList.remove('active');