Commit c5f72512 authored by Aravind RK's avatar Aravind RK

Added signature of respective evaluators into the evaluation sheet

parent cccb180b
...@@ -48,8 +48,8 @@ const userSchema = new mongoose.Schema({ ...@@ -48,8 +48,8 @@ const userSchema = new mongoose.Schema({
}, },
level: { level: {
type: String, type: String,
enum: ['Fresher', 'Intern', 'Pre final year', 'Final year'], enum: ['Fresher', 'Intern', 'Pre final year', 'Final year', 'beginner', 'intermediate', 'advanced', 'expert'],
default: 'Fresher' default: 'beginner'
}, },
topicsOfInterest: [{ topicsOfInterest: [{
topic: { type: String, required: true }, topic: { type: String, required: true },
......
...@@ -227,7 +227,7 @@ ...@@ -227,7 +227,7 @@
.populate('createdBy', 'name') .populate('createdBy', 'name')
.populate('decidedBy', 'name') .populate('decidedBy', 'name')
.populate('quizzes.quizId', 'title category difficulty timer totalQuestions') .populate('quizzes.quizId', 'title category difficulty timer totalQuestions')
.populate('evaluations.evaluatorId', 'name email role') .populate('evaluations.evaluatorId', 'name email role signature')
.sort({ createdAt: -1 }); .sort({ createdAt: -1 });
res.json({ user, interviews }); res.json({ user, interviews });
......
...@@ -139,10 +139,10 @@ router.post('/signature', protect, upload.single('signature'), async (req, res) ...@@ -139,10 +139,10 @@ router.post('/signature', protect, upload.single('signature'), async (req, res)
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
} }
user.signature = `/uploads/${req.file.filename}`; const newSignaturePath = `/uploads/${req.file.filename}`;
await user.save(); await User.updateOne({ _id: user._id }, { signature: newSignaturePath });
res.json({ message: 'Signature uploaded successfully', signature: user.signature }); res.json({ message: 'Signature uploaded successfully', signature: newSignaturePath });
} catch (error) { } catch (error) {
res.status(500).json({ message: 'Server error', error: error.message }); res.status(500).json({ message: 'Server error', error: error.message });
} }
...@@ -174,13 +174,15 @@ router.put('/profile', protect, upload.none(), async (req, res) => { ...@@ -174,13 +174,15 @@ router.put('/profile', protect, upload.none(), async (req, res) => {
if (!user) return res.status(404).json({ message: 'User not found' }); if (!user) return res.status(404).json({ message: 'User not found' });
if (name) user.name = name; const updateData = {};
if (email) user.email = email; if (name) updateData.name = name;
if (phoneNumber !== undefined) user.phoneNumber = phoneNumber; if (email) updateData.email = email;
if (phoneNumber !== undefined) updateData.phoneNumber = phoneNumber;
await user.save(); await User.updateOne({ _id: user._id }, { $set: updateData });
const updatedUser = await User.findById(req.user._id).select('-password');
res.json({ message: 'Profile updated', user }); res.json({ message: 'Profile updated', user: updatedUser });
} catch (error) { } catch (error) {
res.status(500).json({ message: 'Server error', error: error.message }); res.status(500).json({ message: 'Server error', error: error.message });
} }
......
...@@ -227,6 +227,34 @@ ...@@ -227,6 +227,34 @@
} }
}); });
// @route GET /api/hr/users/:userId/interviews
// @desc Get all interviews assigned to a specific user
// @access HR
router.get('/users/:userId/interviews', async (req, res) => {
try {
const { userId } = req.params;
const Interview = require('../models/Interview');
const user = await User.findById(userId).select('-password');
if (!user) return res.status(404).json({ message: 'User not found' });
const interviews = await Interview.find({ candidateId: userId })
.populate('interviewerId', 'name email role')
.populate('assignedInterviewers', 'name email')
.populate('assignedHRs', 'name email')
.populate('assignedPMs', 'name email')
.populate('createdBy', 'name')
.populate('decidedBy', 'name')
.populate('quizzes.quizId', 'title category difficulty timer totalQuestions')
.populate('evaluations.evaluatorId', 'name email role signature')
.sort({ createdAt: -1 });
res.json({ user, interviews });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
// ============ QUIZ MANAGEMENT ============ // ============ QUIZ MANAGEMENT ============
// @route POST /api/hr/quiz/create // @route POST /api/hr/quiz/create
......
...@@ -6,6 +6,7 @@ const { protect, authorize } = require('../middleware/auth'); ...@@ -6,6 +6,7 @@ const { protect, authorize } = require('../middleware/auth');
const upload = require('../middleware/upload'); const upload = require('../middleware/upload');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const Submission = require('../models/Submission');
const router = express.Router(); const router = express.Router();
// All interview routes require authentication // All interview routes require authentication
...@@ -124,6 +125,8 @@ router.get('/', authorize('admin', 'hr', 'pm', 'interviewer'), async (req, res) ...@@ -124,6 +125,8 @@ router.get('/', authorize('admin', 'hr', 'pm', 'interviewer'), async (req, res)
router.get('/stats', authorize('admin', 'hr', 'pm', 'interviewer'), async (req, res) => { router.get('/stats', authorize('admin', 'hr', 'pm', 'interviewer'), async (req, res) => {
try { try {
let filter = {}; let filter = {};
let submissionFilter = {};
if (!['admin', 'hr'].includes(req.user.role)) { if (!['admin', 'hr'].includes(req.user.role)) {
filter.$or = [ filter.$or = [
{ interviewerId: req.user._id }, { interviewerId: req.user._id },
...@@ -132,6 +135,11 @@ router.get('/stats', authorize('admin', 'hr', 'pm', 'interviewer'), async (req, ...@@ -132,6 +135,11 @@ router.get('/stats', authorize('admin', 'hr', 'pm', 'interviewer'), async (req,
{ assignedPMs: req.user._id }, { assignedPMs: req.user._id },
{ createdBy: req.user._id } { createdBy: req.user._id }
]; ];
// Get IDs of candidates this user is assigned to
const assignedInterviews = await Interview.find(filter).select('candidateId');
const candidateIds = [...new Set(assignedInterviews.map(iv => iv.candidateId))];
submissionFilter = { studentId: { $in: candidateIds } };
} }
const total = await Interview.countDocuments(filter); const total = await Interview.countDocuments(filter);
...@@ -140,7 +148,14 @@ router.get('/stats', authorize('admin', 'hr', 'pm', 'interviewer'), async (req, ...@@ -140,7 +148,14 @@ router.get('/stats', authorize('admin', 'hr', 'pm', 'interviewer'), async (req,
const accepted = await Interview.countDocuments({ ...filter, finalDecision: 'accepted' }); const accepted = await Interview.countDocuments({ ...filter, finalDecision: 'accepted' });
const rejected = await Interview.countDocuments({ ...filter, finalDecision: 'rejected' }); const rejected = await Interview.countDocuments({ ...filter, finalDecision: 'rejected' });
res.json({ total, pending, completed, accepted, rejected }); // Fetch recent submissions
const recentSubmissions = await Submission.find(submissionFilter)
.populate('studentId', 'name email')
.populate('quizId', 'title')
.sort({ submittedAt: -1 })
.limit(5);
res.json({ total, pending, completed, accepted, rejected, recentSubmissions });
} catch (error) { } catch (error) {
res.status(500).json({ message: 'Server error', error: error.message }); res.status(500).json({ message: 'Server error', error: error.message });
} }
......
...@@ -110,24 +110,63 @@ ...@@ -110,24 +110,63 @@
/* Quick Actions */ /* Quick Actions */
.actions-grid { .actions-grid {
display: grid; display: grid;
grid-template-columns: repeat(4,1fr); grid-template-columns: repeat(2, 1fr);
gap: 16px; gap: 20px;
} }
.action-card { .action-card {
display: flex; display: flex;
align-items: flex-start; align-items: center;
gap: 16px; gap: 20px;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
height: 100%; height: 100%;
padding: 28px !important;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.action-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, rgba(102,126,234,0.05) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s;
}
.action-card:hover::before {
opacity: 1;
}
.action-card:hover {
border-color: var(--accent-primary);
box-shadow: 0 12px 30px rgba(102,126,234,0.12);
transform: translateY(-4px);
} }
.action-icon { .action-icon {
font-size: 28px; width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(102,126,234,0.1);
color: var(--accent-primary); color: var(--accent-primary);
border-radius: 14px;
font-size: 32px;
flex-shrink: 0; flex-shrink: 0;
margin-top: 2px; transition: all 0.3s;
}
.action-card:hover .action-icon {
background: var(--accent-primary);
color: #fff;
transform: scale(1.1) rotate(-5deg);
} }
.action-info { .action-info {
......
...@@ -67,5 +67,45 @@ ...@@ -67,5 +67,45 @@
</a> </a>
</div> </div>
</div> </div>
<!-- Recent Submissions -->
@if (recentSubmissions().length > 0) {
<div class="section" style="margin-top: 32px;">
<h2 class="section-title">Recent Submissions</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Candidate</th>
<th>Quiz</th>
<th>Score</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@for (sub of recentSubmissions(); track sub._id) {
<tr>
<td>
<div class="user-cell">
<span class="user-cell-name">{{ sub.studentId?.name }}</span>
<span class="user-cell-email">{{ sub.studentId?.email }}</span>
</div>
</td>
<td>{{ sub.quizId?.title }}</td>
<td>
<span class="badge" [ngClass]="{
'badge-success': sub.percentage >= 70,
'badge-warning': sub.percentage >= 40 && sub.percentage < 70,
'badge-danger': sub.percentage < 40
}">{{ sub.percentage }}%</span>
</td>
<td class="text-muted">{{ sub.submittedAt | date:'short' }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
} }
</div> </div>
\ No newline at end of file
...@@ -12,6 +12,7 @@ import { QuizService } from '../../../services/quiz.service'; ...@@ -12,6 +12,7 @@ import { QuizService } from '../../../services/quiz.service';
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
stats = signal<any>({ total: 0, pending: 0, completed: 0 }); stats = signal<any>({ total: 0, pending: 0, completed: 0 });
recentSubmissions = signal<any[]>([]);
loading = signal(true); loading = signal(true);
constructor(private quizService: QuizService) {} constructor(private quizService: QuizService) {}
...@@ -20,6 +21,7 @@ export class DashboardComponent implements OnInit { ...@@ -20,6 +21,7 @@ export class DashboardComponent implements OnInit {
this.quizService.getInterviewStats().subscribe({ this.quizService.getInterviewStats().subscribe({
next: (res) => { next: (res) => {
this.stats.set(res); this.stats.set(res);
this.recentSubmissions.set(res.recentSubmissions || []);
this.loading.set(false); this.loading.set(false);
}, },
error: () => this.loading.set(false) error: () => this.loading.set(false)
......
...@@ -252,9 +252,6 @@ ...@@ -252,9 +252,6 @@
<div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)"> <div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)">
<span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span> <span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span>
<span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span> <span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span>
@if (needsEvaluation(m)) {
<span class="eval-pending-badge" title="Your evaluation is pending" style="margin-left: 4px;">⚠️ Evaluate</span>
}
</div> </div>
@if (m.finalDecision !== 'pending') { @if (m.finalDecision !== 'pending') {
......
...@@ -110,24 +110,63 @@ ...@@ -110,24 +110,63 @@
/* Quick Actions */ /* Quick Actions */
.actions-grid { .actions-grid {
display: grid; display: grid;
grid-template-columns: repeat(4,1fr); grid-template-columns: repeat(2, 1fr);
gap: 16px; gap: 20px;
} }
.action-card { .action-card {
display: flex; display: flex;
align-items: flex-start; align-items: center;
gap: 16px; gap: 20px;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
height: 100%; height: 100%;
padding: 28px !important;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.action-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, rgba(102,126,234,0.05) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.3s;
}
.action-card:hover::before {
opacity: 1;
}
.action-card:hover {
border-color: var(--accent-primary);
box-shadow: 0 12px 30px rgba(102,126,234,0.12);
transform: translateY(-4px);
} }
.action-icon { .action-icon {
font-size: 28px; width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(102,126,234,0.1);
color: var(--accent-primary); color: var(--accent-primary);
border-radius: 14px;
font-size: 32px;
flex-shrink: 0; flex-shrink: 0;
margin-top: 2px; transition: all 0.3s;
}
.action-card:hover .action-icon {
background: var(--accent-primary);
color: #fff;
transform: scale(1.1) rotate(-5deg);
} }
.action-info { .action-info {
......
...@@ -67,5 +67,45 @@ ...@@ -67,5 +67,45 @@
</a> </a>
</div> </div>
</div> </div>
<!-- Recent Submissions -->
@if (recentSubmissions().length > 0) {
<div class="section" style="margin-top: 32px;">
<h2 class="section-title">Recent Submissions</h2>
<div class="table-container">
<table>
<thead>
<tr>
<th>Candidate</th>
<th>Quiz</th>
<th>Score</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@for (sub of recentSubmissions(); track sub._id) {
<tr>
<td>
<div class="user-cell">
<span class="user-cell-name">{{ sub.studentId?.name }}</span>
<span class="user-cell-email">{{ sub.studentId?.email }}</span>
</div>
</td>
<td>{{ sub.quizId?.title }}</td>
<td>
<span class="badge" [ngClass]="{
'badge-success': sub.percentage >= 70,
'badge-warning': sub.percentage >= 40 && sub.percentage < 70,
'badge-danger': sub.percentage < 40
}">{{ sub.percentage }}%</span>
</td>
<td class="text-muted">{{ sub.submittedAt | date:'short' }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
} }
</div> </div>
\ No newline at end of file
...@@ -12,6 +12,7 @@ import { QuizService } from '../../../services/quiz.service'; ...@@ -12,6 +12,7 @@ import { QuizService } from '../../../services/quiz.service';
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
stats = signal<any>({ total: 0, pending: 0, completed: 0 }); stats = signal<any>({ total: 0, pending: 0, completed: 0 });
recentSubmissions = signal<any[]>([]);
loading = signal(true); loading = signal(true);
constructor(private quizService: QuizService) {} constructor(private quizService: QuizService) {}
...@@ -20,6 +21,7 @@ export class DashboardComponent implements OnInit { ...@@ -20,6 +21,7 @@ export class DashboardComponent implements OnInit {
this.quizService.getInterviewStats().subscribe({ this.quizService.getInterviewStats().subscribe({
next: (res) => { next: (res) => {
this.stats.set(res); this.stats.set(res);
this.recentSubmissions.set(res.recentSubmissions || []);
this.loading.set(false); this.loading.set(false);
}, },
error: () => this.loading.set(false) error: () => this.loading.set(false)
......
...@@ -252,9 +252,6 @@ ...@@ -252,9 +252,6 @@
<div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)"> <div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)">
<span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span> <span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span>
<span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span> <span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span>
@if (needsEvaluation(m)) {
<span class="eval-pending-badge" title="Your evaluation is pending" style="margin-left: 4px;">⚠️ Evaluate</span>
}
</div> </div>
@if (m.finalDecision !== 'pending') { @if (m.finalDecision !== 'pending') {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
PAGE LAYOUT PAGE LAYOUT
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.page-container { padding: 32px 40px; } .page-container { padding: 32px 40px; }
.content-wrapper { max-width: 1200px; margin: 0; } .content-wrapper { max-width: 1300px; margin: 0; }
.page-header { .page-header {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px;
...@@ -193,8 +193,8 @@ ...@@ -193,8 +193,8 @@
.qep-header .material-symbols-rounded { font-size: 16px; } .qep-header .material-symbols-rounded { font-size: 16px; }
.qep-row { .qep-row {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: flex-start;
padding: 12px 16px; gap: 12px; padding: 12px 16px; gap: 40px;
border-top: 1px solid rgba(102,126,234,0.08); border-top: 1px solid rgba(102,126,234,0.08);
transition: background 0.15s; transition: background 0.15s;
} }
...@@ -202,7 +202,11 @@ ...@@ -202,7 +202,11 @@
.qep-row:hover { background: rgba(102,126,234,0.06); } .qep-row:hover { background: rgba(102,126,234,0.06); }
.qep-row.qep-done { opacity: 0.65; } .qep-row.qep-done { opacity: 0.65; }
.qep-candidate { display: flex; align-items: center; gap: 10px; } .qep-candidate {
display: flex; align-items: center; gap: 10px;
width: 280px;
flex-shrink: 0;
}
.qep-avatar { .qep-avatar {
width: 34px; height: 34px; border-radius: 9px; width: 34px; height: 34px; border-radius: 9px;
......
...@@ -135,7 +135,7 @@ ...@@ -135,7 +135,7 @@
} @else if (needsEvaluation(m)) { } @else if (needsEvaluation(m)) {
<button class="btn-evaluate" (click)="openMemberDetail(m._id, $event)"> <button class="btn-evaluate" (click)="openMemberDetail(m._id, $event)">
<span class="material-symbols-rounded">rate_review</span> <span class="material-symbols-rounded">rate_review</span>
Evaluate {{ authService.getUserRole() === 'admin' ? 'Review & Evaluate' : 'Evaluate' }}
</button> </button>
} @else { } @else {
<span class="qep-done-label"> <span class="qep-done-label">
...@@ -190,7 +190,7 @@ ...@@ -190,7 +190,7 @@
<div class="checklist-box"> <div class="checklist-box">
@for (i of interviewers(); track i._id) { @for (i of interviewers(); track i._id) {
<label class="check-item"> <label class="check-item">
<input type="checkbox" [value]="i._id" <input type="radio" [value]="i._id"
(change)="toggleSelection($event, newGroupInterview.assignedInterviewers)"> (change)="toggleSelection($event, newGroupInterview.assignedInterviewers)">
<span>{{ i.name }}</span> <span>{{ i.name }}</span>
</label> </label>
...@@ -203,7 +203,7 @@ ...@@ -203,7 +203,7 @@
<div class="checklist-box"> <div class="checklist-box">
@for (h of hrs(); track h._id) { @for (h of hrs(); track h._id) {
<label class="check-item"> <label class="check-item">
<input type="checkbox" [value]="h._id" <input type="radio" [value]="h._id"
(change)="toggleSelection($event, newGroupInterview.assignedHRs)"> (change)="toggleSelection($event, newGroupInterview.assignedHRs)">
<span>{{ h.name }}</span> <span>{{ h.name }}</span>
</label> </label>
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
<div class="checklist-box"> <div class="checklist-box">
@for (p of pms(); track p._id) { @for (p of pms(); track p._id) {
<label class="check-item"> <label class="check-item">
<input type="checkbox" [value]="p._id" <input type="radio" [value]="p._id"
(change)="toggleSelection($event, newGroupInterview.assignedPMs)"> (change)="toggleSelection($event, newGroupInterview.assignedPMs)">
<span>{{ p.name }}</span> <span>{{ p.name }}</span>
</label> </label>
...@@ -492,9 +492,6 @@ ...@@ -492,9 +492,6 @@
<div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)"> <div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)">
<span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span> <span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span>
<span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span> <span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span>
@if (needsEvaluation(m)) {
<span class="eval-pending-badge" title="Your evaluation is pending" style="margin-left: 4px;">⚠️ Evaluate</span>
}
</div> </div>
@if (m.finalDecision !== 'pending') { @if (m.finalDecision !== 'pending') {
...@@ -633,8 +630,8 @@ ...@@ -633,8 +630,8 @@
<p class="text-muted">No evaluations yet</p> <p class="text-muted">No evaluations yet</p>
} }
<!-- Add evaluation form (HR / PM / Interviewer, not if already submitted or completed) --> <!-- Add evaluation form (Admin / HR / PM / Interviewer, not if already submitted or completed) -->
@if (!hasMemberEvaluated() && selectedMember().status !== 'completed' && authService.getUserRole() !== 'admin') { @if (!hasMemberEvaluated() && selectedMember().status !== 'completed') {
<div class="eval-form"> <div class="eval-form">
<h4>Add Your Evaluation</h4> <h4>Add Your Evaluation</h4>
<div class="form-group"> <div class="form-group">
......
...@@ -329,10 +329,8 @@ export class GroupInterviewComponent implements OnInit { ...@@ -329,10 +329,8 @@ export class GroupInterviewComponent implements OnInit {
/** True when THIS user still needs to evaluate this member */ /** True when THIS user still needs to evaluate this member */
needsEvaluation(m: any): boolean { needsEvaluation(m: any): boolean {
if (m.status === 'completed') return false; if (m.status === 'completed' || (m.finalDecision && m.finalDecision !== 'pending')) return false;
const userId = this.authService.currentUser()?.id; const userId = this.authService.currentUser()?.id;
const role = this.authService.getUserRole();
if (role === 'admin') return false; // admin gives final decision, not evaluation
const evaluated = m.evaluations?.some( const evaluated = m.evaluations?.some(
(e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId (e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId
); );
......
.page-container { max-width: 1400px; padding: 32px 40px; margin: 0 auto; height: calc(100vh - 64px); display: flex; box-sizing: border-box; } .page-container { max-width: 1250px; padding: 24px 30px; margin: 0 auto; height: calc(100vh - 64px); display: flex; box-sizing: border-box; }
.split-view { gap: 32px; align-items: stretch; } .split-view { gap: 40px; align-items: stretch; }
.main-workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow-y: auto; padding-right: 16px; } .main-workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow-y: auto; padding-right: 10px; }
.page-header { margin-bottom: 28px; } .page-header { margin-bottom: 28px; }
.page-header h1 { font-size: 26px; font-weight: 700; color: var(--text-primary); margin: 0 0 8px; } .page-header h1 { font-size: 26px; font-weight: 700; color: var(--text-primary); margin: 0 0 8px; }
...@@ -32,16 +32,17 @@ ...@@ -32,16 +32,17 @@
.text-success:hover { color: #22c55e; background: rgba(34, 197, 94, 0.1); } .text-success:hover { color: #22c55e; background: rgba(34, 197, 94, 0.1); }
/* DRAG AND DROP GROUPS */ /* DRAG AND DROP GROUPS */
.groups-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px; } .groups-grid { column-count: 2; column-gap: 24px; align-items: start; }
@media (max-width: 1100px) { .groups-grid { column-count: 1; } }
.group-lane { display: flex; flex-direction: column; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; min-height: 80px; transition: all 0.25s ease-in-out; cursor: pointer; } .group-lane { display: inline-flex; width: 100%; flex-direction: column; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; min-height: 60px; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor: pointer; margin-bottom: 24px; break-inside: avoid; }
.group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-2px); box-shadow: var(--shadow-md); } .group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); }
.group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; } .group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; }
.group-lane-header { padding: 16px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: border-bottom 0.2s; pointer-events: none; } .group-lane-header { padding: 18px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: background 0.3s; pointer-events: none; }
.group-lane:has(.group-lane-body) .group-lane-header { border-bottom: 1px solid var(--border-color); } .group-lane.expanded .group-lane-header { background: rgba(102,126,234,0.1); border-bottom: 1px solid var(--border-color); }
.group-actions { pointer-events: auto; } .group-actions { pointer-events: auto; }
.group-lane-body { cursor: default; } .group-lane-body { cursor: default; overflow: hidden; }
.group-header-info { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } .group-header-info { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.lane-title { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0; color: var(--text-primary); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .lane-title { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0; color: var(--text-primary); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
...@@ -49,9 +50,25 @@ ...@@ -49,9 +50,25 @@
.member-count { font-size: 12px; color: var(--text-muted); font-weight: 500; } .member-count { font-size: 12px; color: var(--text-muted); font-weight: 500; }
.group-actions { display: flex; gap: 4px; } .group-actions { display: flex; gap: 4px; }
.group-lane-body { flex: 1; padding: 16px; display: flex; flex-direction: column; gap: 10px; background: rgba(0,0,0,0.01); min-height: 100px; } .group-lane-body {
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(0,0,0,0.01);
max-height: 0;
padding: 0 16px;
opacity: 0;
transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), padding 0.4s ease, opacity 0.3s ease;
pointer-events: none;
}
.group-lane.expanded .group-lane-body {
max-height: 2000px; /* Allow it to grow */
padding: 16px;
opacity: 1;
pointer-events: auto;
}
.empty-body { justify-content: center; align-items: center; } .empty-body { justify-content: center; align-items: center; }
.drop-placeholder { text-align: center; font-size: 13px; color: var(--text-muted); font-style: italic; opacity: 0.6; padding: 30px; border: 1px dashed var(--border-color); border-radius: 12px; width: 100%; box-sizing: border-box; } .drop-placeholder { text-align: center; font-size: 13px; color: var(--text-muted); font-style: italic; opacity: 0.6; padding: 20px; border: 1px dashed var(--border-color); border-radius: 12px; width: 100%; box-sizing: border-box; }
.student-pill { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border-color); border-radius: 10px; cursor: grab; box-shadow: 0 2px 5px rgba(0,0,0,0.02); transition: transform 0.2s, box-shadow 0.2s; } .student-pill { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border-color); border-radius: 10px; cursor: grab; box-shadow: 0 2px 5px rgba(0,0,0,0.02); transition: transform 0.2s, box-shadow 0.2s; }
.student-pill:active { cursor: grabbing; } .student-pill:active { cursor: grabbing; }
...@@ -60,7 +77,7 @@ ...@@ -60,7 +77,7 @@
.pill-name { display: block; font-size: 14px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pill-name { display: block; font-size: 14px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* UNASSIGNED SIDEBAR */ /* UNASSIGNED SIDEBAR */
.unassigned-sidebar { width: 340px; display: flex; flex-direction: column; flex-shrink: 0; height: 100%; overflow: hidden; background: var(--surface); } .unassigned-sidebar { width: 360px; display: flex; flex-direction: column; flex-shrink: 0; height: 100%; overflow: hidden; background: var(--surface); }
.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; background: rgba(102,126,234,0.05); } .sidebar-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; background: rgba(102,126,234,0.05); }
.sidebar-title { display: flex; align-items: center; gap: 10px; } .sidebar-title { display: flex; align-items: center; gap: 10px; }
.sidebar-title h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text-primary); } .sidebar-title h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text-primary); }
......
...@@ -48,12 +48,12 @@ ...@@ -48,12 +48,12 @@
<!-- Outer wrapper handles the drag contexts collectively --> <!-- Outer wrapper handles the drag contexts collectively -->
<div class="groups-grid"> <div class="groups-grid">
@for (group of groups(); track group) { @for (group of groups(); track group) {
<div class="group-lane card" cdkDropList [cdkDropListData]="group" (cdkDropListDropped)="onDrop($event)" (click)="toggleGroupExpansion(group)"> <div class="group-lane card" [class.expanded]="expandedGroup() === group" cdkDropList [cdkDropListData]="group" (cdkDropListDropped)="onDrop($event)" (click)="toggleGroupExpansion(group)">
<div class="group-lane-header"> <div class="group-lane-header">
<div class="group-header-info"> <div class="group-header-info">
@if (editingGroup() === group) { @if (editingGroup() === group) {
<input type="text" class="form-input form-input-small" [ngModel]="editName()" (ngModelChange)="editName.set($event)" autofocus> <input type="text" class="form-input form-input-small" [ngModel]="editName()" (ngModelChange)="editName.set($event)" autofocus (click)="$event.stopPropagation()">
} @else { } @else {
<h4 class="lane-title"><span class="material-symbols-rounded">workspaces</span> {{ group }}</h4> <h4 class="lane-title"><span class="material-symbols-rounded">workspaces</span> {{ group }}</h4>
<span class="member-count">{{ getStudentsInGroup(group).length }} Members</span> <span class="member-count">{{ getStudentsInGroup(group).length }} Members</span>
...@@ -71,7 +71,6 @@ ...@@ -71,7 +71,6 @@
</div> </div>
<!-- Drop Zone Body (Displayed on Expansion) --> <!-- Drop Zone Body (Displayed on Expansion) -->
@if (expandedGroup() === group) {
<div class="group-lane-body" [class.empty-body]="getStudentsInGroup(group).length === 0" (click)="$event.stopPropagation()"> <div class="group-lane-body" [class.empty-body]="getStudentsInGroup(group).length === 0" (click)="$event.stopPropagation()">
@if (getStudentsInGroup(group).length === 0) { @if (getStudentsInGroup(group).length === 0) {
<div class="drop-placeholder">No candidates found in {{group}}</div> <div class="drop-placeholder">No candidates found in {{group}}</div>
...@@ -90,7 +89,6 @@ ...@@ -90,7 +89,6 @@
</div> </div>
} }
</div> </div>
}
</div> </div>
} }
</div> </div>
......
...@@ -339,32 +339,50 @@ ...@@ -339,32 +339,50 @@
} }
.history-table th { .history-table th {
padding: 14px 20px; padding: 16px 20px;
text-align: left; text-align: left;
color: var(--text-secondary); color: var(--text-muted);
font-size: 12px; font-size: 11px;
font-weight: 600; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 1px;
white-space: nowrap;
} }
.history-table td { .history-table td {
padding: 16px 20px; padding: 16px 24px;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
vertical-align: middle;
white-space: nowrap; /* Force everything into a single line */
} }
.history-table tbody tr { /* Ensure badges and cells don't wrap internally */
transition: background 0.2s; .quiz-count-badge, .status-badge, .interviewer-cell, .view-btn, .btn-eval-download {
white-space: nowrap;
} }
.history-table tbody tr:hover { /* Alignment and spacing */
background: var(--bg-hover); .history-table th:nth-child(4), .history-table td:nth-child(4),
.history-table th:nth-child(5), .history-table td:nth-child(5) {
text-align: center;
}
.history-table th:last-child, .history-table td:last-child {
text-align: left; /* User requested left alignment */
} }
.quiz-name { .history-table td:last-child > div {
justify-content: flex-start;
flex-wrap: nowrap !important; /* Force buttons onto a single line */
gap: 16px !important;
}
.interview-position {
font-weight: 600; font-weight: 600;
color: var(--text-primary);
min-width: 180px;
} }
.score-badge { .score-badge {
...@@ -457,8 +475,8 @@ ...@@ -457,8 +475,8 @@
} }
.content-wrapper { .content-wrapper {
max-width: 1100px; max-width: 1300px;
margin: 0; /* force left alignment */ margin: 0;
} }
.student-level-control { .student-level-control {
......
...@@ -31,8 +31,8 @@ export class UserHistoryComponent implements OnInit { ...@@ -31,8 +31,8 @@ export class UserHistoryComponent implements OnInit {
levels = [ levels = [
{ value: 'beginner', label: 'Fresher' }, { value: 'beginner', label: 'Fresher' },
{ value: 'intermediate', label: 'Intern' }, { value: 'intermediate', label: 'Intern' },
{ value: 'advanced', label: 'Intermediate' }, { value: 'advanced', label: 'Pre final year' },
{ value: 'expert', label: 'Expert' } { value: 'expert', label: 'Final year' }
]; ];
/** Difficulty → level code mapping */ /** Difficulty → level code mapping */
...@@ -46,7 +46,7 @@ export class UserHistoryComponent implements OnInit { ...@@ -46,7 +46,7 @@ export class UserHistoryComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
public authService: AuthService, public authService: AuthService,
private quizService: QuizService private quizService: QuizService
) {} ) { }
ngOnInit(): void { ngOnInit(): void {
this.userId = this.route.snapshot.params['userId']; this.userId = this.route.snapshot.params['userId'];
...@@ -213,17 +213,24 @@ export class UserHistoryComponent implements OnInit { ...@@ -213,17 +213,24 @@ export class UserHistoryComponent implements OnInit {
const ivName = ivEval?.evaluatorId?.name || 'Interviewer'; const ivName = ivEval?.evaluatorId?.name || 'Interviewer';
const pmName = pmEval?.evaluatorId?.name || 'Project Manager'; const pmName = pmEval?.evaluatorId?.name || 'Project Manager';
const hrName = hrEval?.evaluatorId?.name || 'HR'; const hrName = hrEval?.evaluatorId?.name || 'HR';
const adminName = adminEval?.evaluatorId?.name || 'Administrator';
const overallRec = adminEval?.recommendation || interview.finalDecision || ''; const overallRec = adminEval?.recommendation || interview.finalDecision || '';
const ITL_LOGO = 'data:image/gif;base64,R0lGODlhtgBCAPcAAPqsTtTimLXVfMHah/y6Yf7QfP/snt7oov/ikf/ejb/f8b/a6r/Y6L/W5vqvUtrmnvu2XP7Ebf/UgPqwVPu4XsXciv7Jc9jv+r/q+Mzfkefsq7/n9+HppfigP//SftDhlc7gk+jy2vqyV8rejvidO9jknPuzWarQcvmpSviiQv3ky/3lzLTUev3jyfeZNveXM//XhP7Mdv7KdP7Gb7nZjr3Yg7jWf6rPcfH24v/nmePqp9Pil8Pbif7Hcf3BabvXgbPUeaXObPaTL3+83b/e7kCbzQCFzNXV1oKCg6urrODg4Orq6t/u9/X19e/3+2JiZJ/N5iCLxIrN8ZTR8nd3eQCDyp7V9BCCwKja9svLy21tb7Hd98/m8rnh+MDAwcHl+X/C5a/V6jCTyXC02ZeXmGCs1Y/F4le86wCg4AB3uuDy+0W46QCAxgB/wQCd3wCk4X/I8ABztgB6vwBnqABjpABvsQGr5ABcnQCn4xOv5nLE7i206AB9wgBfoGXA7ABqrKGhokCh1gCCyFhYWra2t1Ck0c/o9fmoSRCNz0Ck2f29ZPqtUP7NePu1W/3o0IyMjvmrTf7Cav3AZ/3p0f/bifmnSP2+ZfmqTL/c7fL3477ZhM3gkv3nz7/d7rDTd6/Sdv7q07/o99Xu+r/e77/f78Tr+f7Da/3mzfqxVvmlRfquUdLhlvmmR8jdjf7Pesrs+dzw+/u0WdDt+fihQLzakq3RdPikRPmkRf/Wg/3nzvy7Yv748cLk+LTXirbYjMjn+feaN+vur/mlRt/v+Pu5YPy8Y+vvsP/ml3C54Mnejv2/Zp/R7Mfn+Ofy2fu3XvPzuP/ciurz3LLTeO/14KDMZ9Xr5v3p0v3q0vqwU/u2W/7Qe8/glNzmn7zi96LNav7Bae314P/YhqjPb8Tbif7Fbcjeje3wsf7Oec7p+vmqSzCa0hCKyxCLzfD24bXf9tPs+/ibOdXjmfiePP3o0fDxtPaQK0Ci1+Xrqv7Icf/ZiOz03e303qfObrrakAB6vP///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjEgNjQuMTQwOTQ5LCAyMDEwLzEyLzA3LTEwOjU3OjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InV1aWQ6M0VFQTQ2RTUwQjUxRTIxMTg3NjBBREQ5OUJDNDQyNkYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RkQzNEU5MjI5NDI2MTFFMzlFMzJCNTI0NkY5NTYwMUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkQzNEU5MjE5NDI2MTFFMzlFMzJCNTI0NkY5NTYwMUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgSWxsdXN0cmF0b3IgQ1M1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ODYxQjI3MzY3Mjc2RTMxMUEzQjZERTY4M0Y2RDE5QTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6ODYxQjI3MzY3Mjc2RTMxMUEzQjZERTY4M0Y2RDE5QTEiLz4gPGRjOnRpdGxlPiA8cmRmOkFsdD4gPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5JVExfQkNfQVc8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAAAAAAALAAAAAC2AEIAAAj/AP8JHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEMO3NWihYoVK06dysXJkct5k0TKnEkzY4t6Ql64gCevQwpbwiodGlqzqNGjCVXkBEbCZ6pKKC5BArBoEdKrWGuqeMF0li1WKCAtcjABlQgRWdOq7bgCmLxZtw5dWoRNhIlGEJxRWMu3L8VTJGalOgRJFapYEIgRKKZIkd/HkBfm6hAXgAMRjSjosiTJRyRTkUOLFsgpRSVIlyEQUOTDFLkZ+PCNnv3YEat0qkSothSJXA8ZMRgxot187aRDi1A1IsB7hoUYrgp48ICxiPUxBK1r314kTJiDULiL/9eOHTz5hOGtcynOcNIlByYoKPo2QwajAhIk4IKB0Z//IgT5J+CAAkZBREFDEKiggAAeJMaATCCUoH8HsqeQNQCgko0ukkRgwX0SwJAPJdD091+ACyoIBUETpjhggwUxQaAZEgpYoYUIXaOKCc4oEkkPMWgTIiUJIICAif7A+M+L2ykYoUAtRjHeeQaZQaAYNVKIY0KgOBALMcp4eI4HuBCJwDE5IKnkgAU5McaAZQzU4hASPUjgkwjauCVCoEywnCTkyOCKBOEUmYMBBqiJon8HteiPnALSCREXApYhII0GtXjjnlcxuOijBjkx4Hr/zBnRm/5xcYV/UTSqJ6dYef86EJsHFfGqqRBFweo/lqaa6auwIiWrQLQaZKuWpUY6qYDYhcHsr8iO9sW01FZr7bVfXGDQM/QYowEH3MTzwQjjaGKDNCfQktCwSwqI0LH+VNhiEUPUa6+9B6Ea7z+i7ppntKJ1IfDABBdssMDvFGROMBrocMADO4DQCg81COBJuuueOKu7td7qYrEErerPFQMVAiyUJ4e2xcust+zyyyyjQ5AG9zj8gLgZVDDADwJ8ckM/GSf56bsef8xoQc76F6dAUFT6776zYSH11FRXbTXVvwzEAQcPx7PKJuXwYC4QtfADNELsgpxd0UYb1Ks/3wnU78hPbxqaFXjnrffefO//zYtA3DxQgtcgjKAzzxcHQUPQa3JsLNuSNiSyP06s7V/cKAMc2hScd+7556CD3s0/8QSwwwebJKNzDTaQLY43vjA+dMfI4spQ0/5dwZ2uSrOYcmRSBC/88MQXb7w722yTwQitrG4DCz4HQU0vsm98tEG8Q237QiYbPXLlmUM9mhrkl2//...AAAAAAAAA7'; const getSigHtml = (evalObj: any, fallbackName: string) => {
if (evalObj && evalObj.evaluatorId && evalObj.evaluatorId.signature) {
return `<img src="http://localhost:5000${evalObj.evaluatorId.signature}" alt="Signature"/>`;
}
return fallbackName || '';
};
const ITL_LOGO = '/logo.jpeg';
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/> const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/>
<title>ITL Evaluation - ${candidateName}</title><style> <title>ITL Evaluation - ${candidateName}</title><style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-size:13px;} body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-size:13px;}
.page-wrap{max-width:780px;margin:0 auto;border:1px solid #ccc;} .page-wrap{max-width:780px;margin:0 auto;border:1px solid #ccc;}
.header{background:#4472C4;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;} .header{background:#4472C4;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;}
.header h1{color:#fff;font-size:16px;font-weight:bold;font-style:italic;text-decoration:underline;} .header h1{color:#fff;font-size:26px;font-weight:bold;}
.logo-wrap img{height:52px;object-fit:contain;} .logo-wrap img{height:52px;object-fit:contain;}
.info-table{width:100%;border-collapse:collapse;} .info-table{width:100%;border-collapse:collapse;}
.info-table td{border:1px solid #bbb;padding:6px 10px;vertical-align:middle;} .info-table td{border:1px solid #bbb;padding:6px 10px;vertical-align:middle;}
...@@ -234,11 +241,61 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -234,11 +241,61 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
.comments-text{min-height:65px;font-size:13px;font-family:Arial,sans-serif;color:#333;line-height:1.5;padding:4px 0;} .comments-text{min-height:65px;font-size:13px;font-family:Arial,sans-serif;color:#333;line-height:1.5;padding:4px 0;}
.rec-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;} .rec-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;}
.rec-label{font-weight:bold;white-space:nowrap;} .rec-label{font-weight:bold;white-space:nowrap;}
.sig-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:flex-end;gap:40px;}
.sig-field{display:flex;align-items:flex-end;gap:8px;} .sig-row{
.sig-field label{font-weight:bold;white-space:nowrap;} border:1px solid #bbb;
.sig-line{border-bottom:1px solid #000;min-width:200px;height:18px;font-style:italic;color:#555;font-size:13px;} border-top:none;
.sig-line-date{min-width:140px;} padding:14px 16px 10px;
display:flex;
justify-content:space-between;
align-items:flex-end;
gap:40px;
page-break-inside: avoid;
break-inside: avoid;
}
.sig-field{
display:flex;
align-items:flex-end;
gap:10px;
flex:1;
}
.sig-field label{
font-weight:bold;
white-space:nowrap;
font-size:14px;
}
.signature-box{
border-bottom:2px solid #222;
width:165px;
height:26px;
display:flex;
justify-content:center;
align-items:flex-end;
padding-bottom:4px;
}
.signature-box img{
max-height:18px;
max-width:90px;
object-fit:contain;
}
.date-box{
border-bottom:2px solid #222;
width:130px;
height:22px;
display:flex;
align-items:flex-end;
padding-bottom:3px;
font-style:italic;
color:#444;
}
.actions{margin-top:16px;display:flex;justify-content:flex-end;gap:10px;} .actions{margin-top:16px;display:flex;justify-content:flex-end;gap:10px;}
.btn{font-size:13px;font-family:Arial,sans-serif;padding:7px 18px;border-radius:3px;cursor:pointer;border:1px solid #aaa;} .btn{font-size:13px;font-family:Arial,sans-serif;padding:7px 18px;border-radius:3px;cursor:pointer;border:1px solid #aaa;}
.btn-print{background:#4472C4;color:#fff;border-color:#3360b0;} .btn-print{background:#4472C4;color:#fff;border-color:#3360b0;}
...@@ -246,7 +303,7 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -246,7 +303,7 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
</style></head><body> </style></head><body>
<div class="page-wrap"> <div class="page-wrap">
<div class="header"> <div class="header">
<h1>Intern Interview Evaluation Form</h1> <h1>Candidate Evaluation Form</h1>
<div class="logo-wrap"><img src="${ITL_LOGO}" alt="ITL Logo"/></div> <div class="logo-wrap"><img src="${ITL_LOGO}" alt="ITL Logo"/></div>
</div> </div>
<table class="info-table"><tbody> <table class="info-table"><tbody>
...@@ -277,10 +334,21 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -277,10 +334,21 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
<div class="comments-text">${ivEval?.comments || ''}</div> <div class="comments-text">${ivEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line">${ivName}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(ivEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">
${getSigHtml(ivEval, ivName)}
</div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">
${fmtDate(ivEval?.date)}
</div>
</div> </div>
</div>
<div class="comments-section"> <div class="comments-section">
<div class="comments-label">Project Manager Comments (${pmName}):</div> <div class="comments-label">Project Manager Comments (${pmName}):</div>
...@@ -288,26 +356,64 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -288,26 +356,64 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line">${pmName}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(pmEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">
${getSigHtml(pmEval, pmName)}
</div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">
${fmtDate(pmEval?.date)}
</div>
</div> </div>
</div>
<div class="comments-section"> <div class="comments-section">
<div class="comments-label">HR Comments (${hrName}):</div> <div class="comments-label">HR Comments (${hrName}):</div>
<div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div> <div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line">${hrName}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(hrEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">
${getSigHtml(hrEval, hrName)}
</div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">
${fmtDate(hrEval?.date)}
</div> </div>
</div>
</div>
<div class="comments-section"><div class="comments-label">Overall Recommendation:</div></div> <div class="comments-section">
<div class="comments-label">Overall Comments (${adminEval?.evaluatorId?.name}):</div>
<div class="comments-text" style="min-height:52px;">${adminEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Authorized Signature:</label><div class="sig-line">${adminEval?.evaluatorId?.name || ''}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(adminEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">
${getSigHtml(adminEval, adminName)}
</div> </div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">${fmtDate(adminEval?.date)}</div>
</div>
</div>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-print" onclick="window.print()">&#128438; Print / Save as PDF</button> <button class="btn btn-print" onclick="window.print()">&#128438; Print / Save as PDF</button>
......
...@@ -33,8 +33,8 @@ ...@@ -33,8 +33,8 @@
<div class="user-card-info"> <div class="user-card-info">
<div class="name-row"> <div class="name-row">
<h4>{{ user.name }}</h4> <h4>{{ user.name }}</h4>
<span class="level-badge" [attr.data-level]="user.level || 'Fresher'"> <span class="level-badge" [attr.data-level]="(user.level || 'beginner').toLowerCase()">
{{ (user.level || 'Fresher') | titlecase }} {{ getLevelLabel(user.level || 'beginner') }}
</span> </span>
</div> </div>
<p>{{ user.email }}</p> <p>{{ user.email }}</p>
......
...@@ -45,6 +45,16 @@ export class AdminUsersComponent implements OnInit { ...@@ -45,6 +45,16 @@ export class AdminUsersComponent implements OnInit {
this.loadUsers(); this.loadUsers();
} }
getLevelLabel(level: string): string {
const map: Record<string, string> = {
beginner: 'Fresher',
intermediate: 'Intern',
advanced: 'Pre final year',
expert: 'Final year'
};
return map[level?.toLowerCase()] || 'Fresher';
}
deleteUser(userId: string): void { deleteUser(userId: string): void {
if (confirm('Are you sure you want to permanently delete this student user?')) { if (confirm('Are you sure you want to permanently delete this student user?')) {
this.quizService.deleteUser(userId).subscribe({ this.quizService.deleteUser(userId).subscribe({
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
PAGE LAYOUT PAGE LAYOUT
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
.page-container { padding: 32px 40px; } .page-container { padding: 32px 40px; }
.content-wrapper { max-width: 1200px; margin: 0; } .content-wrapper { max-width: 1300px; margin: 0; }
.page-header { .page-header {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px;
...@@ -181,8 +181,8 @@ ...@@ -181,8 +181,8 @@
.qep-header .material-symbols-rounded { font-size: 16px; } .qep-header .material-symbols-rounded { font-size: 16px; }
.qep-row { .qep-row {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: flex-start;
padding: 12px 16px; gap: 12px; padding: 12px 16px; gap: 40px;
border-top: 1px solid rgba(102,126,234,0.08); border-top: 1px solid rgba(102,126,234,0.08);
transition: background 0.15s; transition: background 0.15s;
} }
...@@ -190,7 +190,11 @@ ...@@ -190,7 +190,11 @@
.qep-row:hover { background: rgba(102,126,234,0.06); } .qep-row:hover { background: rgba(102,126,234,0.06); }
.qep-row.qep-done { opacity: 0.65; } .qep-row.qep-done { opacity: 0.65; }
.qep-candidate { display: flex; align-items: center; gap: 10px; } .qep-candidate {
display: flex; align-items: center; gap: 10px;
width: 280px;
flex-shrink: 0;
}
.qep-avatar { .qep-avatar {
width: 34px; height: 34px; border-radius: 9px; width: 34px; height: 34px; border-radius: 9px;
......
...@@ -493,9 +493,6 @@ ...@@ -493,9 +493,6 @@
<div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)"> <div style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 4px; border-radius: 4px;" (click)="openMemberDetail(m._id, $event)">
<span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span> <span style="font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Current Status:</span>
<span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span> <span class="badge" [ngClass]="getStatusClass(m.status)">{{ formatStatus(m.status) }}</span>
@if (needsEvaluation(m)) {
<span class="eval-pending-badge" title="Your evaluation is pending" style="margin-left: 4px;">⚠️ Evaluate</span>
}
</div> </div>
@if (m.finalDecision !== 'pending') { @if (m.finalDecision !== 'pending') {
......
.page-container { max-width: 1400px; padding: 32px 40px; margin: 0 auto; height: calc(100vh - 64px); display: flex; box-sizing: border-box; } .page-container { max-width: 1250px; padding: 24px 30px; margin: 0 auto; height: calc(100vh - 64px); display: flex; box-sizing: border-box; }
.split-view { gap: 32px; align-items: stretch; } .split-view { gap: 40px; align-items: stretch; }
.main-workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow-y: auto; padding-right: 16px; } .main-workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow-y: auto; padding-right: 10px; }
.page-header { margin-bottom: 28px; } .page-header { margin-bottom: 28px; }
.page-header h1 { font-size: 26px; font-weight: 700; color: var(--text-primary); margin: 0 0 8px; } .page-header h1 { font-size: 26px; font-weight: 700; color: var(--text-primary); margin: 0 0 8px; }
...@@ -32,16 +32,17 @@ ...@@ -32,16 +32,17 @@
.text-success:hover { color: #22c55e; background: rgba(34, 197, 94, 0.1); } .text-success:hover { color: #22c55e; background: rgba(34, 197, 94, 0.1); }
/* DRAG AND DROP GROUPS */ /* DRAG AND DROP GROUPS */
.groups-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px; } .groups-grid { column-count: 2; column-gap: 24px; align-items: start; }
@media (max-width: 1100px) { .groups-grid { column-count: 1; } }
.group-lane { display: flex; flex-direction: column; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; min-height: 80px; transition: all 0.25s ease-in-out; cursor: pointer; } .group-lane { display: inline-flex; width: 100%; flex-direction: column; background: var(--bg-card); border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; min-height: 60px; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor: pointer; margin-bottom: 24px; break-inside: avoid; }
.group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-2px); box-shadow: var(--shadow-md); } .group-lane:hover { border-color: rgba(102,126,234,0.4); transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.1); }
.group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; } .group-lane.cdk-drop-list-receiving { border-color: var(--accent-primary); background: rgba(102,126,234,0.05); box-shadow: 0 0 0 2px rgba(102,126,234,0.2); transform: scale(1.02); z-index: 10; }
.group-lane-header { padding: 16px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: border-bottom 0.2s; pointer-events: none; } .group-lane-header { padding: 18px 20px; background: rgba(102,126,234,0.05); border-bottom: 1px solid transparent; display: flex; justify-content: space-between; align-items: center; transition: background 0.3s; pointer-events: none; }
.group-lane:has(.group-lane-body) .group-lane-header { border-bottom: 1px solid var(--border-color); } .group-lane.expanded .group-lane-header { background: rgba(102,126,234,0.1); border-bottom: 1px solid var(--border-color); }
.group-actions { pointer-events: auto; } .group-actions { pointer-events: auto; }
.group-lane-body { cursor: default; } .group-lane-body { cursor: default; overflow: hidden; }
.group-header-info { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } .group-header-info { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.lane-title { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0; color: var(--text-primary); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .lane-title { display: flex; align-items: center; gap: 8px; font-size: 16px; margin: 0; color: var(--text-primary); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
...@@ -49,9 +50,25 @@ ...@@ -49,9 +50,25 @@
.member-count { font-size: 12px; color: var(--text-muted); font-weight: 500; } .member-count { font-size: 12px; color: var(--text-muted); font-weight: 500; }
.group-actions { display: flex; gap: 4px; } .group-actions { display: flex; gap: 4px; }
.group-lane-body { flex: 1; padding: 16px; display: flex; flex-direction: column; gap: 10px; background: rgba(0,0,0,0.01); min-height: 100px; } .group-lane-body {
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(0,0,0,0.01);
max-height: 0;
padding: 0 16px;
opacity: 0;
transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), padding 0.4s ease, opacity 0.3s ease;
pointer-events: none;
}
.group-lane.expanded .group-lane-body {
max-height: 2000px; /* Allow it to grow */
padding: 16px;
opacity: 1;
pointer-events: auto;
}
.empty-body { justify-content: center; align-items: center; } .empty-body { justify-content: center; align-items: center; }
.drop-placeholder { text-align: center; font-size: 13px; color: var(--text-muted); font-style: italic; opacity: 0.6; padding: 30px; border: 1px dashed var(--border-color); border-radius: 12px; width: 100%; box-sizing: border-box; } .drop-placeholder { text-align: center; font-size: 13px; color: var(--text-muted); font-style: italic; opacity: 0.6; padding: 20px; border: 1px dashed var(--border-color); border-radius: 12px; width: 100%; box-sizing: border-box; }
.student-pill { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border-color); border-radius: 10px; cursor: grab; box-shadow: 0 2px 5px rgba(0,0,0,0.02); transition: transform 0.2s, box-shadow 0.2s; } .student-pill { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border-color); border-radius: 10px; cursor: grab; box-shadow: 0 2px 5px rgba(0,0,0,0.02); transition: transform 0.2s, box-shadow 0.2s; }
.student-pill:active { cursor: grabbing; } .student-pill:active { cursor: grabbing; }
...@@ -60,7 +77,7 @@ ...@@ -60,7 +77,7 @@
.pill-name { display: block; font-size: 14px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pill-name { display: block; font-size: 14px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* UNASSIGNED SIDEBAR */ /* UNASSIGNED SIDEBAR */
.unassigned-sidebar { width: 340px; display: flex; flex-direction: column; flex-shrink: 0; height: 100%; overflow: hidden; background: var(--surface); } .unassigned-sidebar { width: 360px; display: flex; flex-direction: column; flex-shrink: 0; height: 100%; overflow: hidden; background: var(--surface); }
.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; background: rgba(102,126,234,0.05); } .sidebar-header { padding: 20px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; background: rgba(102,126,234,0.05); }
.sidebar-title { display: flex; align-items: center; gap: 10px; } .sidebar-title { display: flex; align-items: center; gap: 10px; }
.sidebar-title h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text-primary); } .sidebar-title h2 { margin: 0; font-size: 16px; font-weight: 600; color: var(--text-primary); }
......
...@@ -43,12 +43,12 @@ ...@@ -43,12 +43,12 @@
<!-- Outer wrapper handles the drag contexts collectively --> <!-- Outer wrapper handles the drag contexts collectively -->
<div class="groups-grid"> <div class="groups-grid">
@for (group of groups(); track group) { @for (group of groups(); track group) {
<div class="group-lane card" cdkDropList [cdkDropListData]="group" (cdkDropListDropped)="onDrop($event)" (click)="toggleGroupExpansion(group)"> <div class="group-lane card" [class.expanded]="expandedGroup() === group" cdkDropList [cdkDropListData]="group" (cdkDropListDropped)="onDrop($event)" (click)="toggleGroupExpansion(group)">
<div class="group-lane-header"> <div class="group-lane-header">
<div class="group-header-info"> <div class="group-header-info">
@if (editingGroup() === group) { @if (editingGroup() === group) {
<input type="text" class="form-input form-input-small" [ngModel]="editName()" (ngModelChange)="editName.set($event)" autofocus> <input type="text" class="form-input form-input-small" [ngModel]="editName()" (ngModelChange)="editName.set($event)" autofocus (click)="$event.stopPropagation()">
} @else { } @else {
<h4 class="lane-title"><span class="material-symbols-rounded">workspaces</span> {{ group }}</h4> <h4 class="lane-title"><span class="material-symbols-rounded">workspaces</span> {{ group }}</h4>
<span class="member-count">{{ getStudentsInGroup(group).length }} Members</span> <span class="member-count">{{ getStudentsInGroup(group).length }} Members</span>
...@@ -66,7 +66,6 @@ ...@@ -66,7 +66,6 @@
</div> </div>
<!-- Drop Zone Body (Displayed on Expansion) --> <!-- Drop Zone Body (Displayed on Expansion) -->
@if (expandedGroup() === group) {
<div class="group-lane-body" [class.empty-body]="getStudentsInGroup(group).length === 0" (click)="$event.stopPropagation()"> <div class="group-lane-body" [class.empty-body]="getStudentsInGroup(group).length === 0" (click)="$event.stopPropagation()">
@if (getStudentsInGroup(group).length === 0) { @if (getStudentsInGroup(group).length === 0) {
<div class="drop-placeholder">No candidates found in {{group}}</div> <div class="drop-placeholder">No candidates found in {{group}}</div>
...@@ -85,7 +84,6 @@ ...@@ -85,7 +84,6 @@
</div> </div>
} }
</div> </div>
}
</div> </div>
} }
</div> </div>
......
...@@ -339,32 +339,50 @@ ...@@ -339,32 +339,50 @@
} }
.history-table th { .history-table th {
padding: 14px 20px; padding: 16px 20px;
text-align: left; text-align: left;
color: var(--text-secondary); color: var(--text-muted);
font-size: 12px; font-size: 11px;
font-weight: 600; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 1px;
white-space: nowrap;
} }
.history-table td { .history-table td {
padding: 16px 20px; padding: 16px 24px;
border-top: 1px solid var(--border-color); border-top: 1px solid var(--border-color);
color: var(--text-primary); color: var(--text-primary);
font-size: 14px; font-size: 14px;
vertical-align: middle;
white-space: nowrap; /* Force everything into a single line */
} }
.history-table tbody tr { /* Ensure badges and cells don't wrap internally */
transition: background 0.2s; .quiz-count-badge, .status-badge, .interviewer-cell, .view-btn, .btn-eval-download {
white-space: nowrap;
} }
.history-table tbody tr:hover { /* Alignment and spacing */
background: var(--bg-hover); .history-table th:nth-child(4), .history-table td:nth-child(4),
.history-table th:nth-child(5), .history-table td:nth-child(5) {
text-align: center;
}
.history-table th:last-child, .history-table td:last-child {
text-align: left; /* User requested left alignment */
} }
.quiz-name { .history-table td:last-child > div {
justify-content: flex-start;
flex-wrap: nowrap !important; /* Force buttons onto a single line */
gap: 16px !important;
}
.interview-position {
font-weight: 600; font-weight: 600;
color: var(--text-primary);
min-width: 180px;
} }
.score-badge { .score-badge {
...@@ -457,8 +475,8 @@ ...@@ -457,8 +475,8 @@
} }
.content-wrapper { .content-wrapper {
max-width: 1100px; max-width: 1300px;
margin: 0; /* force left alignment */ margin: 0;
} }
.student-level-control { .student-level-control {
......
...@@ -31,8 +31,8 @@ export class HRUserHistoryComponent implements OnInit { ...@@ -31,8 +31,8 @@ export class HRUserHistoryComponent implements OnInit {
levels = [ levels = [
{ value: 'beginner', label: 'Fresher' }, { value: 'beginner', label: 'Fresher' },
{ value: 'intermediate', label: 'Intern' }, { value: 'intermediate', label: 'Intern' },
{ value: 'advanced', label: 'Intermediate' }, { value: 'advanced', label: 'Pre final year' },
{ value: 'expert', label: 'Expert' } { value: 'expert', label: 'Final year' }
]; ];
/** Difficulty → level code mapping */ /** Difficulty → level code mapping */
...@@ -213,8 +213,16 @@ export class HRUserHistoryComponent implements OnInit { ...@@ -213,8 +213,16 @@ export class HRUserHistoryComponent implements OnInit {
const ivName = ivEval?.evaluatorId?.name || 'Interviewer'; const ivName = ivEval?.evaluatorId?.name || 'Interviewer';
const pmName = pmEval?.evaluatorId?.name || 'Project Manager'; const pmName = pmEval?.evaluatorId?.name || 'Project Manager';
const hrName = hrEval?.evaluatorId?.name || 'HR'; const hrName = hrEval?.evaluatorId?.name || 'HR';
const adminName = adminEval?.evaluatorId?.name || 'Administrator';
const overallRec = adminEval?.recommendation || interview.finalDecision || ''; const overallRec = adminEval?.recommendation || interview.finalDecision || '';
const getSigHtml = (evalObj: any, fallbackName: string) => {
if (evalObj && evalObj.evaluatorId && evalObj.evaluatorId.signature) {
return `<img src="http://localhost:5000${evalObj.evaluatorId.signature}" style="max-height:35px; width:auto; object-fit:contain; vertical-align:bottom; display:block; margin:auto;" alt="${fallbackName}'s Signature"/>`;
}
return fallbackName || '';
};
const ITL_LOGO = 'data:image/gif;base64,R0lGODlhtgBCAPcAAPqsTtTimLXVfMHah/y6Yf7QfP/snt7oov/ikf/ejb/f8b/a6r/Y6L/W5vqvUtrmnvu2XP7Ebf/UgPqwVPu4XsXciv7Jc9jv+r/q+Mzfkefsq7/n9+HppfigP//SftDhlc7gk+jy2vqyV8rejvidO9jknPuzWarQcvmpSviiQv3ky/3lzLTUev3jyfeZNveXM//XhP7Mdv7KdP7Gb7nZjr3Yg7jWf6rPcfH24v/nmePqp9Pil8Pbif7Hcf3BabvXgbPUeaXObPaTL3+83b/e7kCbzQCFzNXV1oKCg6urrODg4Orq6t/u9/X19e/3+2JiZJ/N5iCLxIrN8ZTR8nd3eQCDyp7V9BCCwKja9svLy21tb7Hd98/m8rnh+MDAwcHl+X/C5a/V6jCTyXC02ZeXmGCs1Y/F4le86wCg4AB3uuDy+0W46QCAxgB/wQCd3wCk4X/I8ABztgB6vwBnqABjpABvsQGr5ABcnQCn4xOv5nLE7i206AB9wgBfoGXA7ABqrKGhokCh1gCCyFhYWra2t1Ck0c/o9fmoSRCNz0Ck2f29ZPqtUP7NePu1W/3o0IyMjvmrTf7Cav3AZ/3p0f/bifmnSP2+ZfmqTL/c7fL3477ZhM3gkv3nz7/d7rDTd6/Sdv7q07/o99Xu+r/e77/f78Tr+f7Da/3mzfqxVvmlRfquUdLhlvmmR8jdjf7Pesrs+dzw+/u0WdDt+fihQLzakq3RdPikRPmkRf/Wg/3nzvy7Yv748cLk+LTXirbYjMjn+feaN+vur/mlRt/v+Pu5YPy8Y+vvsP/ml3C54Mnejv2/Zp/R7Mfn+Ofy2fu3XvPzuP/ciurz3LLTeO/14KDMZ9Xr5v3p0v3q0vqwU/u2W/7Qe8/glNzmn7zi96LNav7Bae314P/YhqjPb8Tbif7Fbcjeje3wsf7Oec7p+vmqSzCa0hCKyxCLzfD24bXf9tPs+/ibOdXjmfiePP3o0fDxtPaQK0Ci1+Xrqv7Icf/ZiOz03e303qfObrrakAB6vP///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjEgNjQuMTQwOTQ5LCAyMDEwLzEyLzA3LTEwOjU3OjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InV1aWQ6M0VFQTQ2RTUwQjUxRTIxMTg3NjBBREQ5OUJDNDQyNkYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RkQzNEU5MjI5NDI2MTFFMzlFMzJCNTI0NkY5NTYwMUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkQzNEU5MjE5NDI2MTFFMzlFMzJCNTI0NkY5NTYwMUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgSWxsdXN0cmF0b3IgQ1M1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ODYxQjI3MzY3Mjc2RTMxMUEzQjZERTY4M0Y2RDE5QTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6ODYxQjI3MzY3Mjc2RTMxMUEzQjZERTY4M0Y2RDE5QTEiLz4gPGRjOnRpdGxlPiA8cmRmOkFsdD4gPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5JVExfQkNfQVc8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAAAAAAALAAAAAC2AEIAAAj/AP8JHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEMO3NWihYoVK06dysXJkct5k0TKnEkzY4t6Ql64gCevQwpbwiodGlqzqNGjCVXkBEbCZ6pKKC5BArBoEdKrWGuqeMF0li1WKCAtcjABlQgRWdOq7bgCmLxZtw5dWoRNhIlGEJxRWMu3L8VTJGalOgRJFapYEIgRKKZIkd/HkBfm6hAXgAMRjSjosiTJRyRTkUOLFsgpRSVIlyEQUOTDFLkZ+PCNnv3YEat0qkSothSJXA8ZMRgxot187aRDi1A1IsB7hoUYrgp48ICxiPUxBK1r314kTJiDULiL/9eOHTz5hOGtcynOcNIlByYoKPo2QwajAhIk4IKB0Z//IgT5J+CAAkZBREFDEKiggAAeJMaATCCUoH8HsqeQNQCgko0ukkRgwX0SwJAPJdD091+ACyoIBUETpjhggwUxQaAZEgpYoYUIXaOKCc4oEkkPMWgTIiUJIICAif7A+M+L2ykYoUAtRjHeeQaZQaAYNVKIY0KgOBALMcp4eI4HuBCJwDE5IKnkgAU5McaAZQzU4hASPUjgkwjauCVCoEywnCTkyOCKBOEUmYMBBqiJon8HteiPnALSCREXApYhII0GtXjjnlcxuOijBjkx4Hr/zBnRm/5xcYV/UTSqJ6dYef86EJsHFfGqqRBFweo/lqaa6auwIiWrQLQaZKuWpUY6qYDYhcHsr8iO9sW01FZr7bVfXGDQM/QYowEH3MTzwQjjaGKDNCfQktCwSwqI0LH+VNhiEUPUa6+9B6Ea7z+i7ppntKJ1IfDABBdssMDvFGROMBrocMADO4DQCg81COBJuuueOKu7td7qYrEErerPFQMVAiyUJ4e2xcust+zyyyyjQ5AG9zj8gLgZVDDADwJ8ckM/GSf56bsef8xoQc76F6dAUFT6776zYSH11FRXbTXVvwzEAQcPx7PKJuXwYC4QtfADNELsgpxd0UYb1Ks/3wnU78hPbxqaFXjnrffefO//zYtA3DxQgtcgjKAzzxcHQUPQa3JsLNuSNiSyP06s7V/cKAMc2hScd+7556CD3s0/8QSwwwebJKNzDTaQLY43vjA+dMfI4spQ0/5dwZ2uSrOYcmRSBC/88MQXb7w722yTwQitrG4DCz4HQU0vsm98tEG8Q237QiYbPXLlmUM9mhrkl2//...AAAAAAAAA7'; const ITL_LOGO = 'data:image/gif;base64,R0lGODlhtgBCAPcAAPqsTtTimLXVfMHah/y6Yf7QfP/snt7oov/ikf/ejb/f8b/a6r/Y6L/W5vqvUtrmnvu2XP7Ebf/UgPqwVPu4XsXciv7Jc9jv+r/q+Mzfkefsq7/n9+HppfigP//SftDhlc7gk+jy2vqyV8rejvidO9jknPuzWarQcvmpSviiQv3ky/3lzLTUev3jyfeZNveXM//XhP7Mdv7KdP7Gb7nZjr3Yg7jWf6rPcfH24v/nmePqp9Pil8Pbif7Hcf3BabvXgbPUeaXObPaTL3+83b/e7kCbzQCFzNXV1oKCg6urrODg4Orq6t/u9/X19e/3+2JiZJ/N5iCLxIrN8ZTR8nd3eQCDyp7V9BCCwKja9svLy21tb7Hd98/m8rnh+MDAwcHl+X/C5a/V6jCTyXC02ZeXmGCs1Y/F4le86wCg4AB3uuDy+0W46QCAxgB/wQCd3wCk4X/I8ABztgB6vwBnqABjpABvsQGr5ABcnQCn4xOv5nLE7i206AB9wgBfoGXA7ABqrKGhokCh1gCCyFhYWra2t1Ck0c/o9fmoSRCNz0Ck2f29ZPqtUP7NePu1W/3o0IyMjvmrTf7Cav3AZ/3p0f/bifmnSP2+ZfmqTL/c7fL3477ZhM3gkv3nz7/d7rDTd6/Sdv7q07/o99Xu+r/e77/f78Tr+f7Da/3mzfqxVvmlRfquUdLhlvmmR8jdjf7Pesrs+dzw+/u0WdDt+fihQLzakq3RdPikRPmkRf/Wg/3nzvy7Yv748cLk+LTXirbYjMjn+feaN+vur/mlRt/v+Pu5YPy8Y+vvsP/ml3C54Mnejv2/Zp/R7Mfn+Ofy2fu3XvPzuP/ciurz3LLTeO/14KDMZ9Xr5v3p0v3q0vqwU/u2W/7Qe8/glNzmn7zi96LNav7Bae314P/YhqjPb8Tbif7Fbcjeje3wsf7Oec7p+vmqSzCa0hCKyxCLzfD24bXf9tPs+/ibOdXjmfiePP3o0fDxtPaQK0Ci1+Xrqv7Icf/ZiOz03e303qfObrrakAB6vP///yH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjEgNjQuMTQwOTQ5LCAyMDEwLzEyLzA3LTEwOjU3OjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InV1aWQ6M0VFQTQ2RTUwQjUxRTIxMTg3NjBBREQ5OUJDNDQyNkYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RkQzNEU5MjI5NDI2MTFFMzlFMzJCNTI0NkY5NTYwMUYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkQzNEU5MjE5NDI2MTFFMzlFMzJCNTI0NkY5NTYwMUYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgSWxsdXN0cmF0b3IgQ1M1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ODYxQjI3MzY3Mjc2RTMxMUEzQjZERTY4M0Y2RDE5QTEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6ODYxQjI3MzY3Mjc2RTMxMUEzQjZERTY4M0Y2RDE5QTEiLz4gPGRjOnRpdGxlPiA8cmRmOkFsdD4gPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5JVExfQkNfQVc8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAAAAAAALAAAAAC2AEIAAAj/AP8JHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEMO3NWihYoVK06dysXJkct5k0TKnEkzY4t6Ql64gCevQwpbwiodGlqzqNGjCVXkBEbCZ6pKKC5BArBoEdKrWGuqeMF0li1WKCAtcjABlQgRWdOq7bgCmLxZtw5dWoRNhIlGEJxRWMu3L8VTJGalOgRJFapYEIgRKKZIkd/HkBfm6hAXgAMRjSjosiTJRyRTkUOLFsgpRSVIlyEQUOTDFLkZ+PCNnv3YEat0qkSothSJXA8ZMRgxot187aRDi1A1IsB7hoUYrgp48ICxiPUxBK1r314kTJiDULiL/9eOHTz5hOGtcynOcNIlByYoKPo2QwajAhIk4IKB0Z//IgT5J+CAAkZBREFDEKiggAAeJMaATCCUoH8HsqeQNQCgko0ukkRgwX0SwJAPJdD091+ACyoIBUETpjhggwUxQaAZEgpYoYUIXaOKCc4oEkkPMWgTIiUJIICAif7A+M+L2ykYoUAtRjHeeQaZQaAYNVKIY0KgOBALMcp4eI4HuBCJwDE5IKnkgAU5McaAZQzU4hASPUjgkwjauCVCoEywnCTkyOCKBOEUmYMBBqiJon8HteiPnALSCREXApYhII0GtXjjnlcxuOijBjkx4Hr/zBnRm/5xcYV/UTSqJ6dYef86EJsHFfGqqRBFweo/lqaa6auwIiWrQLQaZKuWpUY6qYDYhcHsr8iO9sW01FZr7bVfXGDQM/QYowEH3MTzwQjjaGKDNCfQktCwSwqI0LH+VNhiEUPUa6+9B6Ea7z+i7ppntKJ1IfDABBdssMDvFGROMBrocMADO4DQCg81COBJuuueOKu7td7qYrEErerPFQMVAiyUJ4e2xcust+zyyyyjQ5AG9zj8gLgZVDDADwJ8ckM/GSf56bsef8xoQc76F6dAUFT6776zYSH11FRXbTXVvwzEAQcPx7PKJuXwYC4QtfADNELsgpxd0UYb1Ks/3wnU78hPbxqaFXjnrffefO//zYtA3DxQgtcgjKAzzxcHQUPQa3JsLNuSNiSyP06s7V/cKAMc2hScd+7556CD3s0/8QSwwwebJKNzDTaQLY43vjA+dMfI4spQ0/5dwZ2uSrOYcmRSBC/88MQXb7w722yTwQitrG4DCz4HQU0vsm98tEG8Q237QiYbPXLlmUM9mhrkl2//...AAAAAAAAA7';
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/> const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/>
...@@ -277,7 +285,7 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -277,7 +285,7 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line">${ivName}</div></div> <div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(ivEval, ivName)}</div></div>
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(ivEval?.date)}</div></div> <div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(ivEval?.date)}</div></div>
</div> </div>
<div class="comments-section"> <div class="comments-section">
...@@ -286,7 +294,7 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -286,7 +294,7 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line">${pmName}</div></div> <div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(pmEval, pmName)}</div></div>
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(pmEval?.date)}</div></div> <div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(pmEval?.date)}</div></div>
</div> </div>
<div class="comments-section"> <div class="comments-section">
...@@ -295,13 +303,16 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -295,13 +303,16 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line">${hrName}</div></div> <div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(hrEval, hrName)}</div></div>
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(hrEval?.date)}</div></div> <div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(hrEval?.date)}</div></div>
</div> </div>
<div class="comments-section"><div class="comments-label">Overall Recommendation:</div></div> <div class="comments-section">
<div class="comments-label">Overall Comments (${adminName}):</div>
<div class="comments-text" style="min-height:52px;">${adminEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Authorized Signature:</label><div class="sig-line">${adminEval?.evaluatorId?.name || ''}</div></div> <div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(adminEval, adminName)}</div></div>
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(adminEval?.date)}</div></div> <div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(adminEval?.date)}</div></div>
</div> </div>
</div> </div>
......
...@@ -28,8 +28,8 @@ ...@@ -28,8 +28,8 @@
<div class="user-card-info"> <div class="user-card-info">
<div class="name-row"> <div class="name-row">
<h4>{{ user.name }}</h4> <h4>{{ user.name }}</h4>
<span class="level-badge" [attr.data-level]="user.level || 'beginner'"> <span class="level-badge" [attr.data-level]="(user.level || 'beginner').toLowerCase()">
{{ (user.level || 'beginner') | titlecase }} {{ getLevelLabel(user.level || 'beginner') }}
</span> </span>
</div> </div>
<p>{{ user.email }}</p> <p>{{ user.email }}</p>
......
...@@ -39,6 +39,16 @@ export class Users { ...@@ -39,6 +39,16 @@ export class Users {
this.loadUsers(); this.loadUsers();
} }
getLevelLabel(level: string): string {
const map: Record<string, string> = {
beginner: 'Fresher',
intermediate: 'Intern',
advanced: 'Pre final year',
expert: 'Final year'
};
return map[level?.toLowerCase()] || 'Fresher';
}
deleteUser(userId: string): void { deleteUser(userId: string): void {
if (confirm('Are you sure you want to permanently delete this student user?')) { if (confirm('Are you sure you want to permanently delete this student user?')) {
this.quizService.deleteUser(userId).subscribe({ this.quizService.deleteUser(userId).subscribe({
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
} }
getUserInterviews(userId: string): Observable<any> { getUserInterviews(userId: string): Observable<any> {
return this.http.get(`${this.adminUrl}/users/${userId}/interviews`); return this.http.get(`${this.getBaseUrl()}/users/${userId}/interviews`);
} }
updateUserLevel(userId: string, level: string, role: string = 'admin'): Observable<any> { updateUserLevel(userId: string, level: string, role: string = 'admin'): Observable<any> {
......
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