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({ ...@@ -48,8 +48,8 @@ const userSchema = new mongoose.Schema({
}, },
level: { level: {
type: String, type: String,
enum: ['beginner', 'intermediate', 'advanced', 'expert'], enum: ['Fresher', 'Intern', 'Pre final year', 'Final year'],
default: 'beginner' default: 'Fresher'
}, },
topicsOfInterest: [{ topicsOfInterest: [{
topic: { type: String, required: true }, topic: { type: String, required: true },
......
...@@ -428,9 +428,9 @@ router.put('/:id/evaluate', authorize('admin', 'hr', 'pm', 'interviewer'), async ...@@ -428,9 +428,9 @@ router.put('/:id/evaluate', authorize('admin', 'hr', 'pm', 'interviewer'), async
// ============================================================ // ============================================================
// @route PUT /api/interview/:id/decision // @route PUT /api/interview/:id/decision
// @desc Set final decision (accepted/rejected/on_hold/2nd_round) // @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 { try {
const { decision } = req.body; const { decision } = req.body;
if (!decision) return res.status(400).json({ message: 'Decision is required' }); 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') ...@@ -515,9 +515,9 @@ router.put('/:id/validate-coding', authorize('admin', 'hr', 'pm', 'interviewer')
// ============================================================ // ============================================================
// @route DELETE /api/interview/:id // @route DELETE /api/interview/:id
// @desc Delete an interview // @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 { try {
const interview = await Interview.findById(req.params.id); const interview = await Interview.findById(req.params.id);
if (!interview) return res.status(404).json({ message: 'Interview not found' }); if (!interview) return res.status(404).json({ message: 'Interview not found' });
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -59,12 +59,15 @@ ...@@ -59,12 +59,15 @@
/* Candidate chips row */ /* Candidate chips row */
.candidate-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; } .candidate-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.candidate-chip { .candidate-chip {
width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; padding: 4px 12px; border-radius: 16px; display: flex; align-items: center;
justify-content: center; font-size: 12px; font-weight: 700; color: #fff; justify-content: center; font-size: 12px; font-weight: 600; color: #fff!important;
border: 2px solid var(--bg-card); background: #667eea; border: 2px solid var(--bg-card); background: #667eea;
transition: transform 0.15s; transition: transform 0.15s;
white-space: nowrap;
} }
.candidate-chip:hover { transform: scale(1.15); z-index: 1; } .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-warning { background: #f59e0b; }
.candidate-chip.badge-info { background: #3b82f6; } .candidate-chip.badge-info { background: #3b82f6; }
.candidate-chip.badge-success { background: #22c55e; } .candidate-chip.badge-success { background: #22c55e; }
...@@ -73,6 +76,29 @@ ...@@ -73,6 +76,29 @@
.iv-card-bottom { display: flex; justify-content: flex-end; } .iv-card-bottom { display: flex; justify-content: flex-end; }
.status-summary { font-size: 12px; color: var(--text-muted); font-style: italic; } .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 BADGES
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
...@@ -293,20 +319,26 @@ ...@@ -293,20 +319,26 @@
/* Candidate detail list */ /* Candidate detail list */
.candidate-detail-list { display: flex; flex-direction: column; gap: 12px; } .candidate-detail-list { display: flex; flex-direction: column; gap: 12px; }
.candidate-row { .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); 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: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 { 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-name { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.cr-email { font-size: 12px; color: var(--text-muted); } .cr-email { font-size: 12px; color: var(--text-muted); }
.candidate-row-mid { flex: 1; }
.candidate-row-mid { flex: 1; min-width: 280px; } /* Bottom row: status badge + quiz chips + accept/reject — all left-aligned */
.candidate-row-right { display: flex; flex-direction: column; gap: 8px; align-items: flex-end; } .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 (mini) */
.progress-steps { display: flex; align-items: center; } .progress-steps { display: flex; align-items: center; }
...@@ -352,6 +384,45 @@ ...@@ -352,6 +384,45 @@
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(24px); opacity: 0; } to { transform: translateY(0); 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 RESPONSIVE
═══════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════ */
...@@ -365,3 +436,41 @@ ...@@ -365,3 +436,41 @@
.candidate-row-right { align-items: flex-start; } .candidate-row-right { align-items: flex-start; }
.da-header, .da-row { grid-template-columns: 1fr; } .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 { ...@@ -74,11 +74,16 @@ export class GroupInterviewComponent implements OnInit {
pendingSetsCount = 2; pendingSetsCount = 2;
quizSets: QuizSet[] = []; quizSets: QuizSet[] = [];
// ── Detail modal state ──────────────────────────────────── // ── Group detail modal state ─────────────────────────────
showDetailModal = signal(false); showDetailModal = signal(false);
selectedGroup = signal<any>(null); 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) {} constructor(private quizService: QuizService, public authService: AuthService) {}
...@@ -244,7 +249,7 @@ export class GroupInterviewComponent implements OnInit { ...@@ -244,7 +249,7 @@ export class GroupInterviewComponent implements OnInit {
}); });
} }
// ── Detail modal ────────────────────────────────────────── // ── Group detail modal ────────────────────────────────────
openDetail(group: any): void { openDetail(group: any): void {
this.selectedGroup.set(group); this.selectedGroup.set(group);
this.showDetailModal.set(true); this.showDetailModal.set(true);
...@@ -255,6 +260,108 @@ export class GroupInterviewComponent implements OnInit { ...@@ -255,6 +260,108 @@ export class GroupInterviewComponent implements OnInit {
this.selectedGroup.set(null); 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 ─────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────
getStatusClass(status: string): string { getStatusClass(status: string): string {
const map: any = { const map: any = {
......
...@@ -370,8 +370,8 @@ ...@@ -370,8 +370,8 @@
} }
</div> </div>
<!-- Final Decision (admin only) --> <!-- Final Decision (admin and hr only) -->
@if (authService.getUserRole() === 'admin' && selectedInterview().status !== 'completed') { @if ((authService.getUserRole() === 'admin' || authService.getUserRole() === 'hr') && selectedInterview().status !== 'completed') {
<div class="detail-section decision-section"> <div class="detail-section decision-section">
<h3 class="detail-section-title">Final Decision</h3> <h3 class="detail-section-title">Final Decision</h3>
<div class="decision-buttons"> <div class="decision-buttons">
...@@ -393,7 +393,7 @@ ...@@ -393,7 +393,7 @@
</div> </div>
<div class="modal-footer"> <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-danger btn-sm" (click)="deleteInterview(selectedInterview()._id)">Delete Interview</button>
} }
<button class="btn btn-outline" (click)="closeDetail()">Close</button> <button class="btn btn-outline" (click)="closeDetail()">Close</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 || 'beginner'"> <span class="level-badge" [attr.data-level]="user.level || 'Fresher'">
{{ (user.level || 'beginner') | titlecase }} {{ (user.level || 'Fresher') | titlecase }}
</span> </span>
</div> </div>
<p>{{ user.email }}</p> <p>{{ user.email }}</p>
......
import { Component } from '@angular/core'; import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { QuizService } from '../../../services/quiz.service';
import { AuthService } from '../../../services/auth.service';
@Component({ @Component({
selector: 'app-individual-interview', selector: 'app-individual-interview',
imports: [], standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './individual-interview.html', templateUrl: './individual-interview.html',
styleUrl: './individual-interview.css', styleUrl: './individual-interview.css'
}) })
export class IndividualInterview {} export class IndividualInterviewComponent implements OnInit {
interviews = signal<any[]>([]);
loading = signal(true);
showCreateModal = signal(false);
showDetailModal = signal(false);
selectedInterview = signal<any>(null);
// Create form data
candidates = signal<any[]>([]);
interviewers = signal<any[]>([]);
hrs = signal<any[]>([]);
pms = signal<any[]>([]);
quizzes = signal<any[]>([]);
newInterview = {
candidateId: '',
assignedInterviewers: [] as string[],
assignedHRs: [] as string[],
assignedPMs: [] as string[],
position: '',
techStack: '',
source: '',
dateOfInterview: new Date().toISOString().split('T')[0],
quizIds: [] as string[]
};
// Evaluation form
evalComment = signal('');
evalRecommendation = signal('');
// Stats
stats = signal<any>({ total: 0, pending: 0, completed: 0, accepted: 0, rejected: 0 });
// Filter
filterStatus = signal('');
isSubmitting = signal(false);
constructor(private quizService: QuizService, public authService: AuthService) {}
ngOnInit(): void {
this.loadInterviews();
this.loadStats();
}
loadInterviews(): void {
this.loading.set(true);
const params: any = {};
if (this.filterStatus()) params.status = this.filterStatus();
params.type = 'individual';
this.quizService.getInterviews(params).subscribe({
next: (res) => {
this.interviews.set(res.interviews || []);
this.loading.set(false);
},
error: () => this.loading.set(false)
});
}
loadStats(): void {
this.quizService.getInterviewStats().subscribe({
next: (res) => this.stats.set(res)
});
}
openCreateModal(): void {
// Load dropdown data
this.quizService.getInterviewCandidates().subscribe({
next: (res) => this.candidates.set(res.candidates || [])
});
this.quizService.getInterviewers().subscribe({
next: (res) => {
const staff = res.interviewers || [];
this.interviewers.set(staff.filter((s: any) => s.role === 'interviewer'));
this.pms.set(staff.filter((s: any) => s.role === 'pm'));
this.hrs.set(staff.filter((s: any) => s.role === 'hr'));
}
});
this.quizService.getAdminQuizzes().subscribe({
next: (res) => this.quizzes.set(res.quizzes || [])
});
this.newInterview = {
candidateId: '', assignedInterviewers: [], assignedHRs: [], assignedPMs: [], position: '', techStack: '',
source: '', dateOfInterview: new Date().toISOString().split('T')[0], quizIds: []
};
this.showCreateModal.set(true);
}
closeCreateModal(): void {
this.showCreateModal.set(false);
}
toggleQuizSelection(quizId: string): void {
const idx = this.newInterview.quizIds.indexOf(quizId);
if (idx >= 0) {
this.newInterview.quizIds.splice(idx, 1);
} else {
this.newInterview.quizIds.push(quizId);
}
}
toggleSelection(event: any, array: string[]): void {
const val = event.target.value;
if (event.target.checked) {
array.push(val);
} else {
const idx = array.indexOf(val);
if (idx >= 0) array.splice(idx, 1);
}
}
isQuizSelected(quizId: string): boolean {
return this.newInterview.quizIds.includes(quizId);
}
createInterview(): void {
if (!this.newInterview.candidateId || !this.newInterview.position) return;
this.isSubmitting.set(true);
this.quizService.createInterview(this.newInterview).subscribe({
next: () => {
this.isSubmitting.set(false);
this.closeCreateModal();
this.loadInterviews();
this.loadStats();
},
error: (err) => {
this.isSubmitting.set(false);
alert(err.error?.message || 'Failed to create interview');
}
});
}
openDetail(interview: any): void {
this.quizService.getInterviewById(interview._id).subscribe({
next: (res) => {
this.selectedInterview.set(res.interview);
this.evalComment.set('');
this.evalRecommendation.set('');
this.showDetailModal.set(true);
}
});
}
closeDetail(): void {
this.showDetailModal.set(false);
this.selectedInterview.set(null);
}
submitEvaluation(): void {
const interview = this.selectedInterview();
if (!interview || !this.evalRecommendation()) return;
this.isSubmitting.set(true);
this.quizService.submitEvaluation(interview._id, {
comments: this.evalComment(),
recommendation: this.evalRecommendation()
}).subscribe({
next: (res) => {
this.selectedInterview.set(res.interview);
this.evalComment.set('');
this.evalRecommendation.set('');
this.isSubmitting.set(false);
},
error: () => this.isSubmitting.set(false)
});
}
setDecision(decision: string): void {
const interview = this.selectedInterview();
if (!interview) return;
this.quizService.setInterviewDecision(interview._id, decision).subscribe({
next: (res) => {
this.selectedInterview.set(res.interview);
this.loadInterviews();
this.loadStats();
}
});
}
deleteInterview(id: string): void {
if (confirm('Are you sure you want to delete this interview?')) {
this.quizService.deleteInterview(id).subscribe({
next: () => {
this.loadInterviews();
this.loadStats();
this.closeDetail();
}
});
}
}
onFilterChange(): void {
this.loadInterviews();
}
getStatusClass(status: string): string {
switch (status) {
case 'pending': return 'badge-warning';
case 'quiz_phase': return 'badge-info';
case 'coding_phase': return 'badge-info';
case 'evaluation': return 'badge-purple';
case 'completed': return 'badge-success';
default: return '';
}
}
getDecisionClass(decision: string): string {
switch (decision) {
case 'accepted': return 'badge-success';
case 'rejected': return 'badge-danger';
case 'on_hold': return 'badge-warning';
case '2nd_round': return 'badge-info';
default: return 'badge-muted';
}
}
formatStatus(status: string): string {
return status.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
formatDecision(decision: string): string {
const map: any = {
pending: 'Pending', accepted: 'Accepted', rejected: 'Rejected',
on_hold: 'On Hold', '2nd_round': '2nd Round'
};
return map[decision] || decision;
}
hasUserEvaluated(): boolean {
const interview = this.selectedInterview();
if (!interview) return false;
const userId = this.authService.currentUser()?.id;
return interview.evaluations?.some((e: any) => e.evaluatorId?._id === userId);
}
isPdfGenerating = signal(false);
allEvaluationsDone(): boolean {
const iv = this.selectedInterview();
if (!iv) return false;
const numInterviewers = iv.assignedInterviewers?.length || 0;
const numHrs = iv.assignedHRs?.length || 0;
const numPms = iv.assignedPMs?.length || 0;
const totalExpected = numInterviewers + numHrs + numPms;
if (totalExpected === 0) return false;
const numEvaluations = iv.evaluations?.length || 0;
return numEvaluations >= totalExpected;
}
downloadEvaluationPdf(): void {
const iv = this.selectedInterview();
if (!iv) return;
this.isPdfGenerating.set(true);
setTimeout(() => {
window.print();
this.isPdfGenerating.set(false);
}, 500);
}
}
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