Commit 9a0f25a3 authored by Aravind RK's avatar Aravind RK

feat : violation policy alert

parent 68e5aeae
......@@ -262,41 +262,41 @@
}
.logout-btn:hover {
background: var(--danger);
color: #fff;
border-color: var(--danger);
}
background: var(--danger);
color: #fff;
border-color: var(--danger);
}
/* ---- MAIN CONTENT ---- */
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
min-height: 100vh;
min-width: 0;
overflow-x: hidden;
transition: margin-left 0.3s ease;
}
/* ---- MAIN CONTENT ---- */
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
min-height: 100vh;
min-width: 0;
overflow-x: hidden;
transition: margin-left 0.3s ease;
}
/* ---- MOBILE HEADER ---- */
.mobile-header {
display: none;
position: sticky;
top: 0;
z-index: 90;
background: var(--bg-card);
border-bottom: 1px solid var(--border-color);
padding: 12px 16px;
align-items: center;
gap: 12px;
}
/* ---- MOBILE HEADER ---- */
.mobile-header {
display: none;
position: sticky;
top: 0;
z-index: 90;
background: var(--bg-card);
border-bottom: 1px solid var(--border-color);
padding: 12px 16px;
align-items: center;
gap: 12px;
}
.mobile-menu-btn,
.icon-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
.mobile-menu-btn,
.icon-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
border: none;
background: transparent;
......
......@@ -38,9 +38,9 @@
<!-- Navigation -->
<nav class="sidebar-nav">
@for (item of navItems(); track item.route) {
@if (item.route === '/admin/users' || item.route === '/hr/users') {
@if (item.route === '/admin/users' || item.route === '/hr/users' || item.route === '/admin/interviews' || item.route === '/hr/interviews') {
<a class="nav-item" (click)="handleNavClick(item, $event)" href="javascript:void(0)"
[class.active]="router.url.includes('/admin/users') || router.url.includes('/hr/users') || router.url.includes('manage-groups')">
[class.active]="(item.route === '/admin/users' || item.route === '/hr/users') ? (router.url.includes('/admin/users') || router.url.includes('/hr/users') || router.url.includes('manage-groups')) : (router.url.includes('individual-interview') || router.url.includes('group-interview'))">
<span class="material-symbols-rounded nav-icon">{{ item.icon }}</span>
<span class="nav-label">{{ item.label }}</span>
</a>
......
......@@ -41,6 +41,7 @@ export class LayoutComponent {
return [
{ icon: 'dashboard', label: 'Dashboard', route: '/admin/dashboard' },
{ icon: 'group', label: 'Users', route: '/admin/users' },
{ icon: 'work', label: 'Interviews', route: '/admin/interviews' },
{ icon: 'quiz', label: 'Quizzes', route: '/admin/quizzes' },
{ icon: 'add_circle', label: 'Create Quiz', route: '/admin/create-quiz' },
{ icon: 'person', label: 'Profile', route: '/admin/profile' },
......@@ -51,7 +52,7 @@ export class LayoutComponent {
{ icon: 'quiz', label: 'My Quizzes', route: '/hr/quizzes' },
{ icon: 'add_circle', label: 'Create Quiz', route: '/hr/create-quiz' },
{ icon: 'people', label: 'Candidates', route: '/hr/users' },
{ icon: 'work', label: 'Interviews', route: '/hr/individual-interview' },
{ icon: 'work', label: 'Interviews', route: '/hr/interviews' },
{ icon: 'person', label: 'Profile', route: '/hr/profile' },
];
case 'pm':
......@@ -120,6 +121,9 @@ export class LayoutComponent {
if (item.route === '/admin/users' || item.route === '/hr/users') {
event.preventDefault();
this.uiService.showManageUsersPopup.set(true);
} else if (item.route === '/admin/interviews' || item.route === '/hr/interviews') {
event.preventDefault();
this.uiService.showInterviewPopup.set(true);
} else {
this.mobileSidebarOpen = false;
}
......
......@@ -115,9 +115,11 @@
</div>
</div>
} @else if (i === 0 || interview.quizzes[i-1].completed) {
<a [routerLink]="['/candidate/quiz', q.quizId?._id || q.quizId]" class="btn btn-primary" style="padding: 10px 24px; border-radius: 8px; font-weight: 600;">
<button class="btn btn-primary start-assessment-btn"
(click)="openPolicyModal(q.quizId?._id || q.quizId)">
<span class="material-symbols-rounded">play_arrow</span>
Start Assessment
</a>
</button>
}
</div>
</div>
......@@ -181,3 +183,161 @@
</div>
}
</div>
<!-- ══════════════════════════════════════════════════════
Pre-Quiz Violation Policy Modal
══════════════════════════════════════════════════════ -->
@if (showPolicyModal()) {
<div class="policy-overlay" (click)="closePolicyModal()">
<div class="policy-modal animate-scale-in" (click)="$event.stopPropagation()">
<!-- Header -->
<div class="policy-modal-header">
<div class="policy-shield-wrap">
<span class="material-symbols-rounded filled policy-shield-icon">security</span>
</div>
<div class="policy-header-text">
<h2>Assessment Integrity Policy</h2>
<p>Please read all rules carefully before you begin</p>
</div>
<button class="policy-close-btn" (click)="closePolicyModal()">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<!-- Warning Banner -->
<div class="policy-warning-banner">
<span class="material-symbols-rounded filled">warning</span>
<span>This assessment is monitored. Any violations may result in automatic disqualification.</span>
</div>
<!-- Violations List -->
<div class="policy-body">
<h3 class="policy-section-title">
<span class="material-symbols-rounded">gavel</span>
Prohibited Actions During Assessment
</h3>
<div class="policy-rules-grid">
<!-- Rule 1 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap fullscreen">
<span class="material-symbols-rounded filled">fullscreen_exit</span>
</div>
<div class="rule-content">
<h4>Exiting Fullscreen Mode</h4>
<p>The assessment must be taken in fullscreen. Pressing <kbd>Esc</kbd>, using browser controls, or any action that exits fullscreen will be logged as a violation.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 2 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap tabs">
<span class="material-symbols-rounded filled">tab</span>
</div>
<div class="rule-content">
<h4>Switching Browser Tabs</h4>
<p>Navigating to any other browser tab or opening a new tab during the assessment is strictly prohibited and will trigger an immediate violation alert.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 3 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap window">
<span class="material-symbols-rounded filled">desktop_windows</span>
</div>
<div class="rule-content">
<h4>Switching Applications or Windows</h4>
<p>Alt+Tab, clicking outside the browser, opening any other application, or minimizing the browser window will be detected and counted as a violation.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 4 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap search">
<span class="material-symbols-rounded filled">manage_search</span>
</div>
<div class="rule-content">
<h4>Using External Resources</h4>
<p>Searching the internet, consulting notes, textbooks, or communicating with others during the assessment is a violation of academic integrity.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 5 -->
<div class="policy-rule-card">
<div class="rule-icon-wrap copy">
<span class="material-symbols-rounded filled">content_copy</span>
</div>
<div class="rule-content">
<h4>Copy-Pasting or Screen Recording</h4>
<p>Copying question content, recording the screen, or taking screenshots during the assessment is strictly forbidden and may lead to disqualification.</p>
</div>
<span class="violation-chip">Violation</span>
</div>
<!-- Rule 6 — Auto Submit -->
<div class="policy-rule-card danger-card">
<div class="rule-icon-wrap auto-submit">
<span class="material-symbols-rounded filled">block</span>
</div>
<div class="rule-content">
<h4>Automatic Submission Threshold</h4>
<p>After <strong>3 violations</strong>, your assessment will be <strong>automatically submitted</strong> regardless of how many questions you have answered. Your current answers will be saved.</p>
</div>
<span class="violation-chip danger">Auto-Submit</span>
</div>
</div>
<!-- Violation Tracker Visual -->
<div class="policy-tracker">
<div class="tracker-label">Violation Tracker</div>
<div class="tracker-steps">
<div class="tracker-step">
<div class="tracker-dot warn"></div>
<span>1st Violation<br><small>Warning shown</small></span>
</div>
<div class="tracker-connector"></div>
<div class="tracker-step">
<div class="tracker-dot warn"></div>
<span>2nd Violation<br><small>Final warning</small></span>
</div>
<div class="tracker-connector"></div>
<div class="tracker-step">
<div class="tracker-dot danger"></div>
<span>3rd Violation<br><small>Auto-submitted</small></span>
</div>
</div>
</div>
</div>
<!-- Terms & Conditions -->
<div class="policy-footer">
<label class="policy-checkbox-label" [class.checked]="policyAccepted()" (click)="policyAccepted.set(!policyAccepted())">
<div class="policy-checkbox" [class.checked]="policyAccepted()">
@if (policyAccepted()) {
<span class="material-symbols-rounded filled">check</span>
}
</div>
<span>I have read and understood all the assessment policies. I agree to follow the integrity guidelines throughout this assessment.</span>
</label>
<div class="policy-actions">
<button class="btn btn-secondary" (click)="closePolicyModal()">Cancel</button>
<button class="btn btn-primary policy-begin-btn"
[disabled]="!policyAccepted()"
(click)="beginAssessment()">
<span class="material-symbols-rounded">rocket_launch</span>
Begin Assessment
</button>
</div>
</div>
</div>
</div>
}
import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';
import { Router, RouterLink } from '@angular/router';
import { AuthService } from '../../../services/auth.service';
import { QuizService } from '../../../services/quiz.service';
......@@ -16,7 +16,12 @@ export class CandidateDashboardComponent implements OnInit {
loading = signal(true);
uploadingCodingId = signal<string | null>(null);
constructor(public authService: AuthService, private quizService: QuizService) {}
// ── Pre-quiz Policy Modal ─────────────────────
showPolicyModal = signal(false);
policyAccepted = signal(false);
pendingQuizId = signal<string | null>(null);
constructor(public authService: AuthService, private quizService: QuizService, private router: Router) {}
ngOnInit(): void {
this.loadInterviews();
......@@ -32,6 +37,28 @@ export class CandidateDashboardComponent implements OnInit {
});
}
/** Called when candidate clicks "Start Assessment" — shows policy modal first */
openPolicyModal(quizId: string): void {
this.pendingQuizId.set(quizId);
this.policyAccepted.set(false);
this.showPolicyModal.set(true);
}
/** Dismiss modal without proceeding */
closePolicyModal(): void {
this.showPolicyModal.set(false);
this.pendingQuizId.set(null);
this.policyAccepted.set(false);
}
/** Navigate to quiz only after policy is accepted */
beginAssessment(): void {
const id = this.pendingQuizId();
if (!id || !this.policyAccepted()) return;
this.showPolicyModal.set(false);
this.router.navigate(['/candidate/quiz', id]);
}
onCodingZipSelected(event: any, interviewId: string): void {
const file = event.target.files[0];
if (file) {
......
......@@ -12,7 +12,7 @@
.profile-email { color: var(--text-secondary); font-size: 14px; margin: 0 0 2px; }
.profile-joined { color: var(--text-muted); font-size: 12px; margin: 0; }
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 32px; }
.stats-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
.stat-card { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; }
.stat-icon { font-size: 28px; }
.stat-icon.blue { color: var(--accent-primary); }
......@@ -21,6 +21,19 @@
.stat-value { font-size: 28px; font-weight: 700; color: var(--text-primary); }
.stat-label { font-size: 13px; color: var(--text-muted); }
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.section { margin-bottom: 32px; }
.section-title { font-size: 16px; font-weight: 600; color: var(--text-primary); margin: 0 0 16px; }
......
......@@ -43,23 +43,6 @@
</div>
</div>
<div class="stats-row">
<div class="stat-card card card-padding">
<span class="material-symbols-rounded stat-icon blue">assignment</span>
<span class="stat-value">{{ testsTaken }}</span>
<span class="stat-label">Tests Taken</span>
</div>
<div class="stat-card card card-padding">
<span class="material-symbols-rounded stat-icon green">trending_up</span>
<span class="stat-value">{{ avgScore }}%</span>
<span class="stat-label">Average Score</span>
</div>
<div class="stat-card card card-padding">
<span class="material-symbols-rounded stat-icon orange">emoji_events</span>
<span class="stat-value">{{ bestScore }}%</span>
<span class="stat-label">Best Score</span>
</div>
</div>
<!-- Topics of Interest Section -->
<div class="section topics-section card card-padding">
......@@ -119,12 +102,12 @@
</div>
<div class="section">
<h2 class="section-title">Test Results</h2>
<h2 class="section-title">Test History</h2>
@if (submissions().length === 0) {
<div class="empty-state">
<span class="material-symbols-rounded">assignment</span>
<h3>No results yet</h3>
<p>Take a quiz to see results here</p>
<h3>No test history yet</h3>
<p>Take a quiz to see your test history here</p>
</div>
} @else {
<div class="results-list">
......@@ -137,15 +120,9 @@
<h4>{{ sub.quizId?.title }}</h4>
<p>{{ sub.submittedAt | date:'medium' }} · {{ sub.timeTaken ? (sub.timeTaken + 's') : '—' }}</p>
</div>
<div class="result-score-wrap">
<span class="score-circle" [ngClass]="{
'good': sub.percentage >= 70,
'avg': sub.percentage >= 40 && sub.percentage < 70,
'poor': sub.percentage < 40
}">{{ sub.percentage }}%</span>
<span class="score-detail">{{ sub.score }}/{{ sub.totalMarks }}</span>
<div class="result-status-wrap">
<span class="status-badge">Submitted</span>
</div>
<a [routerLink]="['/candidate/results', sub._id]" class="btn btn-outline btn-sm">View</a>
</div>
}
</div>
......
......@@ -294,4 +294,12 @@ export class TakeQuizComponent implements OnInit, OnDestroy {
getMaxViolations(): number {
return this.antiCheat.getMaxViolations();
}
getAnsweredCount(): number {
let count = 0;
for (let i = 0; i < this.questions().length; i++) {
if (this.isAnswered(i)) count++;
}
return count;
}
}
......@@ -43,7 +43,7 @@
.profile-joined { color: rgba(255,255,255,0.3); font-size: 12px; margin: 0; }
/* Stats */
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 36px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 36px; }
.stat-card {
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px; padding: 24px; text-align: center;
......@@ -53,6 +53,19 @@
.stat-value { color: #fff; font-size: 28px; font-weight: 700; }
.stat-label { color: rgba(255,255,255,0.4); font-size: 13px; font-weight: 500; }
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
border: 1px solid rgba(34, 197, 94, 0.2);
}
.section-title { color: #fff; font-size: 18px; font-weight: 600; margin: 0 0 20px; }
.empty-state { text-align: center; padding: 60px 0; }
......
......@@ -33,51 +33,29 @@
</div>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<span class="stat-icon">📝</span>
<span class="stat-value">{{ submissions().length }}</span>
<span class="stat-label">Tests Taken</span>
</div>
<div class="stat-card">
<span class="stat-icon">📊</span>
<span class="stat-value">{{ avgScore }}%</span>
<span class="stat-label">Average Score</span>
</div>
<div class="stat-card">
<span class="stat-icon">🏆</span>
<span class="stat-value">{{ bestScore }}%</span>
<span class="stat-label">Best Score</span>
</div>
</div>
<!-- Test Results -->
<h2 class="section-title">Test Results</h2>
<!-- Test History -->
<h2 class="section-title">Test History</h2>
@if (submissions().length === 0) {
<div class="empty-state">
<span class="empty-icon">📋</span>
<h3>No tests taken yet</h3>
<p>Complete a quiz to see your results here</p>
<h3>No test history yet</h3>
<p>Complete a quiz to see your test history here</p>
</div>
} @else {
<div class="results-list">
@for (sub of submissions(); track sub._id) {
<div class="result-card">
<div class="result-icon-wrap">
<span class="result-icon">{{ sub.percentage >= 70 ? '🏆' : sub.percentage >= 40 ? '📋' : '📝' }}</span>
<span class="result-icon">📝</span>
</div>
<div class="result-info">
<h4>{{ sub.quizId?.title || 'Deleted Quiz' }}</h4>
<p>{{ sub.submittedAt | date:'medium' }} · {{ formatTime(sub.timeTaken) }}</p>
</div>
<div class="result-score">
<div class="score-circle" [class.good]="sub.percentage >= 70" [class.avg]="sub.percentage >= 40 && sub.percentage < 70" [class.poor]="sub.percentage < 40">
<span>{{ sub.percentage }}%</span>
</div>
<span class="score-detail">{{ sub.score }}/{{ sub.totalMarks }}</span>
<div class="result-status-wrap">
<span class="status-badge">Submitted</span>
</div>
<a [routerLink]="['/student/results', sub._id]" class="view-btn">View →</a>
</div>
}
</div>
......
......@@ -236,12 +236,14 @@
html {
height: 100%;
overflow-x: hidden;
overflow: hidden;
scroll-behavior: smooth;
}
body {
min-height: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg-secondary);
color: var(--text-primary);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment