update
This commit is contained in:
parent
c67ee1fceb
commit
367771772f
@ -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 -->
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user