Commit 954049ba authored by Aravind RK's avatar Aravind RK

Group interview bug have corrected

parent 78c91f3e
......@@ -48,8 +48,8 @@ const userSchema = new mongoose.Schema({
},
level: {
type: String,
enum: ['beginner', 'intermediate', 'advanced', 'expert'],
default: 'beginner'
enum: ['Fresher', 'Intern', 'Pre final year', 'Final year'],
default: 'Fresher'
},
topicsOfInterest: [{
topic: { type: String, required: true },
......
......@@ -428,9 +428,9 @@ router.put('/:id/evaluate', authorize('admin', 'hr', 'pm', 'interviewer'), async
// ============================================================
// @route PUT /api/interview/:id/decision
// @desc Set final decision (accepted/rejected/on_hold/2nd_round)
// @access Admin only
// @access Admin, HR
// ============================================================
router.put('/:id/decision', authorize('admin'), async (req, res) => {
router.put('/:id/decision', authorize('admin', 'hr'), async (req, res) => {
try {
const { decision } = req.body;
if (!decision) return res.status(400).json({ message: 'Decision is required' });
......@@ -515,9 +515,9 @@ router.put('/:id/validate-coding', authorize('admin', 'hr', 'pm', 'interviewer')
// ============================================================
// @route DELETE /api/interview/:id
// @desc Delete an interview
// @access Admin only
// @access Admin, HR
// ============================================================
router.delete('/:id', authorize('admin'), async (req, res) => {
router.delete('/:id', authorize('admin', 'hr'), async (req, res) => {
try {
const interview = await Interview.findById(req.params.id);
if (!interview) return res.status(404).json({ message: 'Interview not found' });
......
This diff is collapsed.
......@@ -59,12 +59,15 @@
/* Candidate chips row */
.candidate-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.candidate-chip {
width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 700; color: #fff;
padding: 4px 12px; border-radius: 16px; display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 600; color: #fff!important;
border: 2px solid var(--bg-card); background: #667eea;
transition: transform 0.15s;
white-space: nowrap;
}
.candidate-chip:hover { transform: scale(1.15); z-index: 1; }
.candidate-chip.clickable-chip { cursor: pointer; }
.candidate-chip.clickable-chip:hover { transform: scale(1.12); box-shadow: 0 4px 12px rgba(0,0,0,0.18); }
.candidate-chip.badge-warning { background: #f59e0b; }
.candidate-chip.badge-info { background: #3b82f6; }
.candidate-chip.badge-success { background: #22c55e; }
......@@ -73,6 +76,29 @@
.iv-card-bottom { display: flex; justify-content: flex-end; }
.status-summary { font-size: 12px; color: var(--text-muted); font-style: italic; }
/* ⚠ badge on chip */
.chip-alert { font-size: 11px; margin-right: 3px; }
/* Evaluation pending badge inside candidate row name */
.eval-pending-badge {
display: inline-flex; align-items: center; font-size: 11px; font-weight: 600;
padding: 2px 7px; border-radius: 10px; margin-left: 8px;
background: rgba(245,158,11,0.15); color: #f59e0b;
}
.eval-pending-badge.warn { background: rgba(102,126,234,0.1); color: #667eea; }
/* Clickable candidate left section (button reset) */
.cr-clickable {
display: flex; align-items: center; gap: 12px;
background: none; border: none; cursor: pointer; text-align: left;
padding: 6px 10px; border-radius: 10px; transition: background 0.15s;
min-width: 160px; flex-shrink: 0;
}
.cr-clickable:hover { background: rgba(102,126,234,0.08); }
/* Hint text at end of bottom row */
.cr-open-hint { font-size: 11px; color: var(--text-muted); margin-left: auto; font-style: italic; }
/* ═══════════════════════════════════════════════════════
BADGES
═══════════════════════════════════════════════════════ */
......@@ -293,20 +319,26 @@
/* Candidate detail list */
.candidate-detail-list { display: flex; flex-direction: column; gap: 12px; }
.candidate-row {
display: flex; align-items: flex-start; gap: 16px; padding: 16px;
display: flex; flex-direction: column; gap: 12px; padding: 16px;
border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-hover);
flex-wrap: wrap; transition: border-color 0.2s;
transition: border-color 0.2s;
}
.candidate-row:hover { border-color: rgba(102,126,234,0.35); }
.candidate-row-left { display: flex; align-items: center; gap: 12px; min-width: 180px; }
/* Top row: avatar+name on left, progress stepper fills the rest */
.candidate-row-top { display: flex; align-items: center; gap: 16px; }
.candidate-row-left { display: flex; align-items: center; gap: 12px; min-width: 160px; flex-shrink: 0; }
.iv-avatar { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg,#667eea,#764ba2); color:#fff; font-weight:700; font-size:16px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.iv-avatar.small { width: 32px; height: 32px; font-size: 14px; border-radius: 8px; }
.iv-avatar.small { width: 36px; height: 36px; font-size: 15px; border-radius: 9px; }
.cr-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.cr-email { font-size: 12px; color: var(--text-muted); }
.candidate-row-mid { flex: 1; }
.candidate-row-mid { flex: 1; min-width: 280px; }
.candidate-row-right { display: flex; flex-direction: column; gap: 8px; align-items: flex-end; }
/* Bottom row: status badge + quiz chips + accept/reject — all left-aligned */
.candidate-row-bottom {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
padding-top: 10px; border-top: 1px solid var(--border-color);
}
/* Progress steps (mini) */
.progress-steps { display: flex; align-items: center; }
......@@ -352,6 +384,45 @@
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
/* ═══════════════════════════════════════════════════════
EVALUATION PANEL (member detail modal)
Mirrors individual-interview styles
═══════════════════════════════════════════════════════ */
.quiz-results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
.quiz-result-card {
padding: 14px 16px; border-radius: 12px; border: 1px solid var(--border-color);
background: var(--bg-input);
}
.qr-title { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
.qr-score { font-size: 22px; font-weight: 700; color: #22c55e; }
.qr-pending { font-size: 13px; color: var(--text-muted); font-style: italic; }
.eval-list { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
.eval-card {
padding: 14px 16px; border-radius: 12px; border: 1px solid var(--border-color);
background: var(--bg-input);
}
.eval-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.eval-evaluator { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.eval-role { font-size: 10px !important; padding: 2px 6px !important; }
.eval-comments { font-size: 13px; color: var(--text-secondary); font-style: italic; margin: 6px 0 8px; }
.eval-date { font-size: 11px; color: var(--text-muted); }
.eval-form {
margin-top: 16px; padding: 18px 20px; border-radius: 14px;
border: 1px dashed rgba(102,126,234,0.35);
background: rgba(102,126,234,0.04);
display: flex; flex-direction: column; gap: 12px;
}
.eval-form h4 { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0; }
.form-textarea { resize: vertical; min-height: 80px; }
.decision-section { border-bottom: none; }
.decision-buttons { display: flex; flex-wrap: wrap; gap: 10px; }
.btn-success { background: #22c55e; color: #fff; border: none; }
.btn-warning { background: #f59e0b; color: #fff; border: none; }
/* ═══════════════════════════════════════════════════════
RESPONSIVE
═══════════════════════════════════════════════════════ */
......@@ -365,3 +436,41 @@
.candidate-row-right { align-items: flex-start; }
.da-header, .da-row { grid-template-columns: 1fr; }
}
/* ═══════════════════════════════════════════════════════
PRINT STYLES FOR EVALUATION PDF
═══════════════════════════════════════════════════════ */
.print-container {
display: none;
}
@media print {
body * {
visibility: hidden;
}
.print-container, .print-container * {
visibility: visible;
}
.print-container {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 20px;
background: white;
color: black;
font-family: Arial, sans-serif;
}
.print-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #0078d4;
padding-bottom: 10px;
margin-bottom: 20px;
}
}
......@@ -74,11 +74,16 @@ export class GroupInterviewComponent implements OnInit {
pendingSetsCount = 2;
quizSets: QuizSet[] = [];
// ── Detail modal state ────────────────────────────────────
// ── Group detail modal state ─────────────────────────────
showDetailModal = signal(false);
selectedGroup = signal<any>(null);
evalComment = signal('');
evalRecommendation = signal('');
// ── Per-candidate (member) detail modal ──────────────────
showMemberDetailModal = signal(false);
selectedMember = signal<any>(null); // full interview doc
memberEvalComment = '';
memberEvalRecommendation = '';
isMemberSubmitting = signal(false);
constructor(private quizService: QuizService, public authService: AuthService) {}
......@@ -244,7 +249,7 @@ export class GroupInterviewComponent implements OnInit {
});
}
// ── Detail modal ──────────────────────────────────────────
// ── Group detail modal ────────────────────────────────────
openDetail(group: any): void {
this.selectedGroup.set(group);
this.showDetailModal.set(true);
......@@ -255,6 +260,108 @@ export class GroupInterviewComponent implements OnInit {
this.selectedGroup.set(null);
}
// ── Member (candidate) detail modal ──────────────────────
openMemberDetail(interviewId: string, event: Event): void {
event.stopPropagation();
this.quizService.getInterviewById(interviewId).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.memberEvalComment = '';
this.memberEvalRecommendation = '';
this.showMemberDetailModal.set(true);
}
});
}
closeMemberDetail(): void {
this.showMemberDetailModal.set(false);
this.selectedMember.set(null);
}
submitMemberEvaluation(): void {
const m = this.selectedMember();
if (!m || !this.memberEvalRecommendation) return;
this.isMemberSubmitting.set(true);
this.quizService.submitEvaluation(m._id, {
comments: this.memberEvalComment,
recommendation: this.memberEvalRecommendation
}).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.memberEvalComment = '';
this.memberEvalRecommendation = '';
this.isMemberSubmitting.set(false);
// refresh the group list so chips update
this.loadInterviews();
},
error: () => this.isMemberSubmitting.set(false)
});
}
setMemberDecision(decision: string): void {
const m = this.selectedMember();
if (!m) return;
this.quizService.setInterviewDecision(m._id, decision).subscribe({
next: res => {
this.selectedMember.set(res.interview);
this.loadInterviews();
this.loadStats();
}
});
}
hasMemberEvaluated(): boolean {
const m = this.selectedMember();
if (!m) return false;
const userId = this.authService.currentUser()?.id;
return m.evaluations?.some((e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId);
}
allMemberEvaluationsDone(): boolean {
const m = this.selectedMember();
if (!m) return false;
const total = (m.assignedInterviewers?.length || 0) +
(m.assignedHRs?.length || 0) +
(m.assignedPMs?.length || 0);
if (total === 0) return false;
return (m.evaluations?.length || 0) >= total;
}
/** True when THIS user still needs to evaluate this member */
needsEvaluation(m: any): boolean {
if (m.status === 'completed') return false;
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(
(e: any) => e.evaluatorId?._id === userId || e.evaluatorId === userId
);
return !evaluated;
}
/** Whether ANY assigned staff member hasn't evaluated a given candidate interview yet */
hasPendingEvaluations(m: any): boolean {
if (m.status === 'completed') return false;
const total = (m.assignedInterviewers?.length || 0) +
(m.assignedHRs?.length || 0) +
(m.assignedPMs?.length || 0);
return (m.evaluations?.length || 0) < total;
}
isPdfGenerating = signal(false);
downloadEvaluationPdf(): void {
const m = this.selectedMember();
if (!m) return;
this.isPdfGenerating.set(true);
setTimeout(() => {
window.print();
this.isPdfGenerating.set(false);
}, 500);
}
// ── Helpers ───────────────────────────────────────────────
getStatusClass(status: string): string {
const map: any = {
......
......@@ -370,8 +370,8 @@
}
</div>
<!-- Final Decision (admin only) -->
@if (authService.getUserRole() === 'admin' && selectedInterview().status !== 'completed') {
<!-- Final Decision (admin and hr only) -->
@if ((authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') && selectedInterview().status !== 'completed') {
<div class="detail-section decision-section">
<h3 class="detail-section-title">Final Decision</h3>
<div class="decision-buttons">
......@@ -393,7 +393,7 @@
</div>
<div class="modal-footer">
@if (authService.getUserRole() === 'admin') {
@if (authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') {
<button class="btn btn-danger btn-sm" (click)="deleteInterview(selectedInterview()._id)">Delete Interview</button>
}
<button class="btn btn-outline" (click)="closeDetail()">Close</button>
......
......@@ -33,8 +33,8 @@
<div class="user-card-info">
<div class="name-row">
<h4>{{ user.name }}</h4>
<span class="level-badge" [attr.data-level]="user.level || 'beginner'">
{{ (user.level || 'beginner') | titlecase }}
<span class="level-badge" [attr.data-level]="user.level || 'Fresher'">
{{ (user.level || 'Fresher') | titlecase }}
</span>
</div>
<p>{{ user.email }}</p>
......
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