Commit 831930da authored by Aravind RK's avatar Aravind RK

feat : Combined all the evaluators evaluation and put it in a pdf

parent 9caafb31
...@@ -221,9 +221,13 @@ ...@@ -221,9 +221,13 @@
const interviews = await Interview.find({ candidateId: userId }) const interviews = await Interview.find({ candidateId: userId })
.populate('interviewerId', 'name email role') .populate('interviewerId', 'name email role')
.populate('assignedInterviewers', 'name email')
.populate('assignedHRs', 'name email')
.populate('assignedPMs', 'name email')
.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')
.sort({ createdAt: -1 }); .sort({ createdAt: -1 });
res.json({ user, interviews }); res.json({ user, interviews });
......
Initial chunk files | Names  |  Raw size | Estimated transfer size
chunk-MIRC6W6E.js  | -  | 246.95 kB |  67.80 kB
styles-4EG7DNWJ.css | styles  |  11.07 kB |  776 bytes
main-PZJZRLKZ.js  | main  |  2.51 kB |  802 bytes
chunk-RTK3EC7A.js  | -  |  1.35 kB |  548 bytes
   | Initial total  | 261.89 kB |  69.92 kB
Lazy chunk files  | Names  |  Raw size | Estimated transfer size
chunk-ZWSSD7ZD.js  | -  |  29.23 kB |  6.50 kB
chunk-F5TCWNSW.js  | generate-quiz  |  14.44 kB |  3.89 kB
chunk-T4IU55DP.js  | take-quiz  |  13.50 kB |  3.73 kB
chunk-GPZWIEOZ.js  | results  |  12.05 kB |  3.13 kB
chunk-D642GFQ6.js  | submission-detail |  11.32 kB |  3.04 kB
chunk-7KH36PB3.js  | profile  |  11.30 kB |  3.03 kB
chunk-JIQZZPSH.js  | user-history  |  10.56 kB |  2.90 kB
chunk-NDRVNEE6.js  | dashboard  |  9.60 kB |  2.70 kB
chunk-5TGG3WZ6.js  | users  |  9.40 kB |  2.56 kB
chunk-RPGMXBIO.js  | register  |  8.49 kB |  2.47 kB
chunk-JD4GHZ43.js  | login  |  7.61 kB |  2.33 kB
chunk-DX5QOEI4.js  | dashboard  |  7.39 kB |  1.99 kB
chunk-CJBJHMWZ.js  | -  |  1.25 kB |  412 bytes
Application bundle generation complete. [3.660 seconds] - 2026-04-07T13:08:03.882Z
▲ [WARNING] src/app/pages/admin/users/users.css exceeded maximum budget. Budget 4.00 kB was not met by 283 bytes with a total of 4.28 kB.
▲ [WARNING] src/app/pages/admin/submission-detail/submission-detail.css exceeded maximum budget. Budget 4.00 kB was not met by 746 bytes with a total of 4.75 kB.
▲ [WARNING] src/app/pages/admin/user-history/user-history.css exceeded maximum budget. Budget 4.00 kB was not met by 582 bytes with a total of 4.58 kB.
▲ [WARNING] src/app/pages/student/results/results.css exceeded maximum budget. Budget 4.00 kB was not met by 1.11 kB with a total of 5.12 kB.
▲ [WARNING] src/app/pages/student/dashboard/dashboard.css exceeded maximum budget. Budget 4.00 kB was not met by 364 bytes with a total of 4.36 kB.
▲ [WARNING] src/app/pages/admin/generate-quiz/generate-quiz.css exceeded maximum budget. Budget 4.00 kB was not met by 1.71 kB with a total of 5.71 kB.
▲ [WARNING] src/app/pages/student/profile/profile.css exceeded maximum budget. Budget 4.00 kB was not met by 937 bytes with a total of 4.94 kB.
▲ [WARNING] src/app/pages/student/take-quiz/take-quiz.css exceeded maximum budget. Budget 4.00 kB was not met by 1.05 kB with a total of 5.05 kB.

Output location: C:\Users\Ramesh\Aravind\Studies\intern\Evaluation_App3\Frontend\dist\quiz-app
> quiz-app@0.0.0 build
> ng build
❯ Building...
✔ Building...
Initial chunk files | Names  |  Raw size | Estimated transfer size
chunk-MIRC6W6E.js  | -  | 246.95 kB |  67.80 kB
styles-4EG7DNWJ.css | styles  |  11.07 kB |  776 bytes
main-PZJZRLKZ.js  | main  |  2.51 kB |  802 bytes
chunk-RTK3EC7A.js  | -  |  1.35 kB |  548 bytes
   | Initial total  | 261.89 kB |  69.92 kB
Lazy chunk files  | Names  |  Raw size | Estimated transfer size
chunk-ZWSSD7ZD.js  | -  |  29.23 kB |  6.50 kB
chunk-F5TCWNSW.js  | generate-quiz  |  14.44 kB |  3.89 kB
chunk-T4IU55DP.js  | take-quiz  |  13.50 kB |  3.73 kB
chunk-GPZWIEOZ.js  | results  |  12.05 kB |  3.13 kB
chunk-D642GFQ6.js  | submission-detail |  11.32 kB |  3.04 kB
chunk-7KH36PB3.js  | profile  |  11.30 kB |  3.03 kB
chunk-JIQZZPSH.js  | user-history  |  10.56 kB |  2.90 kB
chunk-NDRVNEE6.js  | dashboard  |  9.60 kB |  2.70 kB
chunk-5TGG3WZ6.js  | users  |  9.40 kB |  2.56 kB
chunk-RPGMXBIO.js  | register  |  8.49 kB |  2.47 kB
chunk-JD4GHZ43.js  | login  |  7.61 kB |  2.33 kB
chunk-DX5QOEI4.js  | dashboard  |  7.39 kB |  1.99 kB
chunk-CJBJHMWZ.js  | -  |  1.25 kB |  412 bytes
Application bundle generation complete. [3.462 seconds] - 2026-04-07T13:09:03.363Z
Output location: C:\Users\Ramesh\Aravind\Studies\intern\Evaluation_App3\Frontend\dist\quiz-app
Binary files a/Frontend/build_output.txt and /dev/null differ Binary files a/Frontend/build_output.txt and /dev/null differ
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
<span>Select a candidate to evaluate</span> <span>Select a candidate to evaluate</span>
</div> </div>
@for (m of g.members; track m._id) { @for (m of g.members; track m._id) {
<div class="qep-row" [class.qep-done]="!needsEvaluation(m)"> <div class="qep-row" [class.qep-done]="!['quiz_phase', 'coding_phase'].includes(m.status) && !needsEvaluation(m)">
<div class="qep-candidate"> <div class="qep-candidate">
<div class="qep-avatar" [ngClass]="getStatusClass(m.status)"> <div class="qep-avatar" [ngClass]="getStatusClass(m.status)">
{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }} {{ m.candidateId?.name?.charAt(0)?.toUpperCase() }}
...@@ -127,7 +127,12 @@ ...@@ -127,7 +127,12 @@
</div> </div>
</div> </div>
<div class="qep-action"> <div class="qep-action">
@if (needsEvaluation(m)) { @if (['quiz_phase', 'coding_phase'].includes(m.status)) {
<span class="qep-pending-label" style="color:var(--text-muted); font-size:12px; font-weight:500; display:flex; align-items:center; gap:4px;">
<span class="material-symbols-rounded" style="font-size:16px;">hourglass_empty</span>
Pending
</span>
} @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 Evaluate
......
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
<span>Select a candidate to evaluate</span> <span>Select a candidate to evaluate</span>
</div> </div>
@for (m of g.members; track m._id) { @for (m of g.members; track m._id) {
<div class="qep-row" [class.qep-done]="!needsEvaluation(m)"> <div class="qep-row" [class.qep-done]="!['quiz_phase', 'coding_phase'].includes(m.status) && !needsEvaluation(m)">
<div class="qep-candidate"> <div class="qep-candidate">
<div class="qep-avatar" [ngClass]="getStatusClass(m.status)"> <div class="qep-avatar" [ngClass]="getStatusClass(m.status)">
{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }} {{ m.candidateId?.name?.charAt(0)?.toUpperCase() }}
...@@ -127,7 +127,12 @@ ...@@ -127,7 +127,12 @@
</div> </div>
</div> </div>
<div class="qep-action"> <div class="qep-action">
@if (needsEvaluation(m)) { @if (['quiz_phase', 'coding_phase'].includes(m.status)) {
<span class="qep-pending-label" style="color:var(--text-muted); font-size:12px; font-weight:500; display:flex; align-items:center; gap:4px;">
<span class="material-symbols-rounded" style="font-size:16px;">hourglass_empty</span>
Pending
</span>
} @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 Evaluate
......
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
<span>Select a candidate to evaluate</span> <span>Select a candidate to evaluate</span>
</div> </div>
@for (m of g.members; track m._id) { @for (m of g.members; track m._id) {
<div class="qep-row" [class.qep-done]="!needsEvaluation(m)"> <div class="qep-row" [class.qep-done]="!['quiz_phase', 'coding_phase'].includes(m.status) && !needsEvaluation(m)">
<div class="qep-candidate"> <div class="qep-candidate">
<div class="qep-avatar" [ngClass]="getStatusClass(m.status)"> <div class="qep-avatar" [ngClass]="getStatusClass(m.status)">
{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }} {{ m.candidateId?.name?.charAt(0)?.toUpperCase() }}
...@@ -127,7 +127,12 @@ ...@@ -127,7 +127,12 @@
</div> </div>
</div> </div>
<div class="qep-action"> <div class="qep-action">
@if (needsEvaluation(m)) { @if (['quiz_phase', 'coding_phase'].includes(m.status)) {
<span class="qep-pending-label" style="color:var(--text-muted); font-size:12px; font-weight:500; display:flex; align-items:center; gap:4px;">
<span class="material-symbols-rounded" style="font-size:16px;">hourglass_empty</span>
Pending
</span>
} @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 Evaluate
...@@ -268,7 +273,7 @@ ...@@ -268,7 +273,7 @@
<span class="material-symbols-rounded">quiz</span> Quiz Configuration <span class="material-symbols-rounded">quiz</span> Quiz Configuration
@if (quizSets.length > 0) { @if (quizSets.length > 0) {
<button class="btn btn-outline btn-sm" style="margin-left: auto;" (click)="addMoreSet()"> <button class="btn btn-outline btn-sm" style="margin-left: auto;" (click)="addMoreSet()">
<span class="material-symbols-rounded">add</span> Add Set <span class="material-symbols-rounded">add</span> Add Quiz
</button> </button>
} }
</div> </div>
...@@ -276,7 +281,7 @@ ...@@ -276,7 +281,7 @@
@if (quizSets.length === 0 && !showQuizSetup) { @if (quizSets.length === 0 && !showQuizSetup) {
<div class="quiz-empty-hint"> <div class="quiz-empty-hint">
<span class="material-symbols-rounded">quiz</span> <span class="material-symbols-rounded">quiz</span>
<p>No quiz sets configured yet.</p> <p>No quizzes configured yet.</p>
<button class="btn btn-primary" (click)="showQuizSetup = true"> <button class="btn btn-primary" (click)="showQuizSetup = true">
<span class="material-symbols-rounded">add</span> Add Quiz <span class="material-symbols-rounded">add</span> Add Quiz
</button> </button>
...@@ -286,7 +291,7 @@ ...@@ -286,7 +291,7 @@
@if (showQuizSetup) { @if (showQuizSetup) {
<div class="quiz-setup-prompt"> <div class="quiz-setup-prompt">
<span class="material-symbols-rounded">help_outline</span> <span class="material-symbols-rounded">help_outline</span>
<span>How many quiz sets?</span> <span>How many quizzes?</span>
<input type="number" min="1" max="10" class="sets-count-input" <input type="number" min="1" max="10" class="sets-count-input"
[(ngModel)]="pendingSetsCount" placeholder="e.g. 2"> [(ngModel)]="pendingSetsCount" placeholder="e.g. 2">
<button class="btn btn-primary btn-sm" (click)="confirmSetsCount()">Confirm</button> <button class="btn btn-primary btn-sm" (click)="confirmSetsCount()">Confirm</button>
...@@ -299,11 +304,11 @@ ...@@ -299,11 +304,11 @@
<div class="quiz-set-block"> <div class="quiz-set-block">
<div class="quiz-set-header"> <div class="quiz-set-header">
<div class="quiz-set-title"> <div class="quiz-set-title">
<span class="set-badge">Set {{ si + 1 }}</span> <span class="set-badge">Quiz {{ si + 1 }}</span>
@if (isSingleQuizSet(set)) { @if (isSingleQuizSet(set)) {
<span class="set-note">📌 Assigned to all candidates</span> <span class="set-note">📌 Assigned to all candidates</span>
} @else if (isMultiQuizSet(set)) { } @else if (isMultiQuizSet(set)) {
<span class="set-note">🎯 {{ validEntries(set).length }} quizzes · {{ set.mode === 'random' ? '🎲 Random' : 'Direct' }} assignment</span> <span class="set-note">🎯 {{ validEntries(set).length }} sets · {{ set.mode === 'random' ? '🎲 Random' : 'Direct' }} assignment</span>
} }
</div> </div>
<button class="icon-btn danger" (click)="removeQuizSet(si)"> <button class="icon-btn danger" (click)="removeQuizSet(si)">
...@@ -315,7 +320,7 @@ ...@@ -315,7 +320,7 @@
@for (entry of set.quizEntries; track $index; let ei = $index) { @for (entry of set.quizEntries; track $index; let ei = $index) {
<div class="quiz-entry-row"> <div class="quiz-entry-row">
<select class="form-input" [(ngModel)]="entry.quizId"> <select class="form-input" [(ngModel)]="entry.quizId">
<option value="">— Select quiz</option> <option value="">— Select set</option>
@for (q of getAvailableQuizzes(si, ei); track q._id) { @for (q of getAvailableQuizzes(si, ei); track q._id) {
<option [value]="q._id">{{ q.title }}</option> <option [value]="q._id">{{ q.title }}</option>
} }
...@@ -327,7 +332,7 @@ ...@@ -327,7 +332,7 @@
} }
<button class="btn btn-ghost btn-sm" (click)="addQuizToSet(si)"> <button class="btn btn-ghost btn-sm" (click)="addQuizToSet(si)">
<span class="material-symbols-rounded">add</span> Add quiz to Set {{ si + 1 }} <span class="material-symbols-rounded">add</span> Add set to Quiz {{ si + 1 }}
</button> </button>
<!-- Assignment mode — only shown when multiple quizzes --> <!-- Assignment mode — only shown when multiple quizzes -->
......
...@@ -127,7 +127,7 @@ export class GroupInterviewComponent implements OnInit { ...@@ -127,7 +127,7 @@ export class GroupInterviewComponent implements OnInit {
this.hrs.set(staff.filter((s: any) => s.role === 'hr')); this.hrs.set(staff.filter((s: any) => s.role === 'hr'));
} }
}); });
this.quizService.getAdminQuizzes().subscribe({ next: r => this.quizzes.set(r.quizzes || []) }); this.quizService.getQuizzes().subscribe({ next: r => this.quizzes.set(r.quizzes || []) });
this.showCreateModal.set(true); this.showCreateModal.set(true);
} }
......
...@@ -118,11 +118,18 @@ ...@@ -118,11 +118,18 @@
</span> </span>
</td> </td>
<td class="date-cell">{{ interview.dateOfInterview | date:'dd MMM yyyy' }}</td> <td class="date-cell">{{ interview.dateOfInterview | date:'dd MMM yyyy' }}</td>
<td> <td>
<button class="view-btn" (click)="selectInterview(interview)"> <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
View Quizzes <span class="material-symbols-rounded" style="font-size:16px;vertical-align:middle;">arrow_forward</span> <button class="view-btn" (click)="selectInterview(interview)">
</button> View Quizzes <span class="material-symbols-rounded" style="font-size:16px;vertical-align:middle;">arrow_forward</span>
</td> </button>
@if (hasEvaluations(interview)) {
<button class="view-btn" style="background:var(--accent,#4472C4);color:#fff;border-color:#3360b0;" (click)="downloadEvaluationForm(interview)">
<span class="material-symbols-rounded" style="font-size:16px;vertical-align:middle;">download</span> Eval Form
</button>
}
</div>
</td>
</tr> </tr>
} }
</tbody> </tbody>
......
...@@ -164,4 +164,161 @@ export class UserHistoryComponent implements OnInit { ...@@ -164,4 +164,161 @@ export class UserHistoryComponent implements OnInit {
if (!resumePath) return ''; if (!resumePath) return '';
return `http://localhost:5000${resumePath}`; return `http://localhost:5000${resumePath}`;
} }
hasEvaluations(interview: any): boolean {
return (interview.evaluations?.length || 0) > 0;
}
downloadEvaluationForm(interview: any): void {
const u = this.user();
const candidateName = u?.name || '';
const dateStr = interview.dateOfInterview
? new Date(interview.dateOfInterview).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
: '';
const position = interview.position || '';
const source = interview.source || '';
const techStack = interview.techStack || '';
const interviewerNames = (interview.assignedInterviewers || [])
.map((i: any) => i.name || '').filter(Boolean).join(', ')
|| interview.interviewerId?.name || 'Unassigned';
const quizzes = interview.quizzes || [];
const q1 = quizzes[0];
const q2 = quizzes[1];
const q1Name = q1?.quizId?.title || q1?.title || '';
const q1Score = (q1?.score != null) ? `${q1.score}/${q1.totalMarks}` : '';
const q2Name = q2?.quizId?.title || q2?.title || '';
const q2Score = (q2?.score != null) ? `${q2.score}/${q2.totalMarks}` : '';
const evals = interview.evaluations || [];
const ivEval = evals.find((e: any) => e.evaluatorRole === 'interviewer');
const pmEval = evals.find((e: any) => e.evaluatorRole === 'pm');
const hrEval = evals.find((e: any) => e.evaluatorRole === 'hr');
const adminEval = evals.find((e: any) => e.evaluatorRole === 'admin');
const recMap: Record<string, string> = {
offer: 'Offer/Hire as Intern', on_hold: 'On Hold',
rejected: 'Rejected', '2nd_round': '2nd Round'
};
const buildCheckboxes = (groupName: string, rec: string): string =>
['offer', 'on_hold', 'rejected', '2nd_round'].map(val => {
const checked = rec === val ? 'checked' : '';
const label = val === '2nd_round' ? '2<sup>nd</sup> Round' : recMap[val];
return `<label style="display:flex;align-items:center;gap:5px;font-size:13px;"><input type="checkbox" name="${groupName}" ${checked} style="width:14px;height:14px;accent-color:#4472C4;"> ${label}</label>`;
}).join('');
const fmtDate = (d: any) => d ? new Date(d).toLocaleDateString('en-GB') : '';
const ivName = ivEval?.evaluatorId?.name || 'Interviewer';
const pmName = pmEval?.evaluatorId?.name || 'Project Manager';
const hrName = hrEval?.evaluatorId?.name || 'HR';
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 html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/>
<title>ITL Evaluation - ${candidateName}</title><style>
*,*::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;}
.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 h1{color:#fff;font-size:16px;font-weight:bold;font-style:italic;text-decoration:underline;}
.logo-wrap img{height:52px;object-fit:contain;}
.info-table{width:100%;border-collapse:collapse;}
.info-table td{border:1px solid #bbb;padding:6px 10px;vertical-align:middle;}
.info-table .label{font-weight:bold;background:#fff;width:160px;white-space:nowrap;}
.info-table .value{background:#fff;color:#444;font-style:italic;}
.comments-section{border:1px solid #bbb;border-top:none;padding:10px 16px;}
.comments-label{font-weight:bold;margin-bottom:6px;}
.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-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-field label{font-weight:bold;white-space:nowrap;}
.sig-line{border-bottom:1px solid #000;min-width:200px;height:18px;font-style:italic;color:#555;font-size:13px;}
.sig-line-date{min-width:140px;}
.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-print{background:#4472C4;color:#fff;border-color:#3360b0;}
@media print{body{padding:0;}.actions{display:none;}}
</style></head><body>
<div class="page-wrap">
<div class="header">
<h1>Intern Interview Evaluation Form</h1>
<div class="logo-wrap"><img src="${ITL_LOGO}" alt="ITL Logo"/></div>
</div>
<table class="info-table"><tbody>
<tr>
<td class="label">Candidate Name:</td><td class="value">${candidateName}</td>
<td class="label">Date of Interview:</td><td class="value">${dateStr}</td>
</tr>
<tr>
<td class="label">Position:</td><td class="value">${position}</td>
<td class="label">Source:</td><td class="value">${source}</td>
</tr>
<tr>
<td class="label">Tech Stack:</td><td class="value">${techStack}</td>
<td class="label"><strong>Interviewer:</strong></td><td class="value">${interviewerNames}</td>
</tr>
<tr>
<td class="label">General Aptitude Test QP Set</td><td class="value">${q1Name}</td>
<td class="label"><strong>General Aptitude Test Score</strong></td><td class="value">${q1Score}</td>
</tr>
<tr>
<td class="label">Technical MCQ Test QP Set</td><td class="value">${q2Name}</td>
<td class="label"><strong>Technical MCQ Test Score</strong></td><td class="value">${q2Score}</td>
</tr>
</tbody></table>
<div class="comments-section" style="margin-top:0;">
<div class="comments-label">Interviewer's Comments (${ivName}):</div>
<div class="comments-text">${ivEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(ivEval?.date)}</div></div>
</div>
<div class="comments-section">
<div class="comments-label">Project Manager Comments (${pmName}):</div>
<div class="comments-text">${pmEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(pmEval?.date)}</div></div>
</div>
<div class="comments-section">
<div class="comments-label">HR Comments (${hrName}):</div>
<div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(hrEval?.date)}</div></div>
</div>
<div class="comments-section"><div class="comments-label">Overall Recommendation:</div></div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(adminEval?.date)}</div></div>
</div>
</div>
<div class="actions">
<button class="btn btn-print" onclick="window.print()">&#128438; Print / Save as PDF</button>
</div>
</body></html>`;
const win = window.open('', '_blank');
if (win) {
win.document.write(html);
win.document.close();
win.focus();
}
}
} }
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
<span>Select a candidate to evaluate</span> <span>Select a candidate to evaluate</span>
</div> </div>
@for (m of g.members; track m._id) { @for (m of g.members; track m._id) {
<div class="qep-row" [class.qep-done]="!needsEvaluation(m)"> <div class="qep-row" [class.qep-done]="!['quiz_phase', 'coding_phase'].includes(m.status) && !needsEvaluation(m)">
<div class="qep-candidate"> <div class="qep-candidate">
<div class="qep-avatar" [ngClass]="getStatusClass(m.status)"> <div class="qep-avatar" [ngClass]="getStatusClass(m.status)">
{{ m.candidateId?.name?.charAt(0)?.toUpperCase() }} {{ m.candidateId?.name?.charAt(0)?.toUpperCase() }}
...@@ -127,7 +127,12 @@ ...@@ -127,7 +127,12 @@
</div> </div>
</div> </div>
<div class="qep-action"> <div class="qep-action">
@if (needsEvaluation(m)) { @if (['quiz_phase', 'coding_phase'].includes(m.status)) {
<span class="qep-pending-label" style="color:var(--text-muted); font-size:12px; font-weight:500; display:flex; align-items:center; gap:4px;">
<span class="material-symbols-rounded" style="font-size:16px;">hourglass_empty</span>
Pending
</span>
} @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 Evaluate
...@@ -269,7 +274,7 @@ ...@@ -269,7 +274,7 @@
<span class="material-symbols-rounded">quiz</span> Quiz Configuration <span class="material-symbols-rounded">quiz</span> Quiz Configuration
@if (quizSets.length > 0) { @if (quizSets.length > 0) {
<button class="btn btn-outline btn-sm" style="margin-left: auto;" (click)="addMoreSet()"> <button class="btn btn-outline btn-sm" style="margin-left: auto;" (click)="addMoreSet()">
<span class="material-symbols-rounded">add</span> Add Set <span class="material-symbols-rounded">add</span> Add Quiz
</button> </button>
} }
</div> </div>
...@@ -277,7 +282,7 @@ ...@@ -277,7 +282,7 @@
@if (quizSets.length === 0 && !showQuizSetup) { @if (quizSets.length === 0 && !showQuizSetup) {
<div class="quiz-empty-hint"> <div class="quiz-empty-hint">
<span class="material-symbols-rounded">quiz</span> <span class="material-symbols-rounded">quiz</span>
<p>No quiz sets configured yet.</p> <p>No quizzes configured yet.</p>
<button class="btn btn-primary" (click)="showQuizSetup = true"> <button class="btn btn-primary" (click)="showQuizSetup = true">
<span class="material-symbols-rounded">add</span> Add Quiz <span class="material-symbols-rounded">add</span> Add Quiz
</button> </button>
...@@ -287,7 +292,7 @@ ...@@ -287,7 +292,7 @@
@if (showQuizSetup) { @if (showQuizSetup) {
<div class="quiz-setup-prompt"> <div class="quiz-setup-prompt">
<span class="material-symbols-rounded">help_outline</span> <span class="material-symbols-rounded">help_outline</span>
<span>How many quiz sets?</span> <span>How many quizzes?</span>
<input type="number" min="1" max="10" class="sets-count-input" <input type="number" min="1" max="10" class="sets-count-input"
[(ngModel)]="pendingSetsCount" placeholder="e.g. 2"> [(ngModel)]="pendingSetsCount" placeholder="e.g. 2">
<button class="btn btn-primary btn-sm" (click)="confirmSetsCount()">Confirm</button> <button class="btn btn-primary btn-sm" (click)="confirmSetsCount()">Confirm</button>
...@@ -300,11 +305,11 @@ ...@@ -300,11 +305,11 @@
<div class="quiz-set-block"> <div class="quiz-set-block">
<div class="quiz-set-header"> <div class="quiz-set-header">
<div class="quiz-set-title"> <div class="quiz-set-title">
<span class="set-badge">Set {{ si + 1 }}</span> <span class="set-badge">Quiz {{ si + 1 }}</span>
@if (isSingleQuizSet(set)) { @if (isSingleQuizSet(set)) {
<span class="set-note">📌 Assigned to all candidates</span> <span class="set-note">📌 Assigned to all candidates</span>
} @else if (isMultiQuizSet(set)) { } @else if (isMultiQuizSet(set)) {
<span class="set-note">🎯 {{ validEntries(set).length }} quizzes · {{ set.mode === 'random' ? '🎲 Random' : 'Direct' }} assignment</span> <span class="set-note">🎯 {{ validEntries(set).length }} sets · {{ set.mode === 'random' ? '🎲 Random' : 'Direct' }} assignment</span>
} }
</div> </div>
<button class="icon-btn danger" (click)="removeQuizSet(si)"> <button class="icon-btn danger" (click)="removeQuizSet(si)">
...@@ -316,7 +321,7 @@ ...@@ -316,7 +321,7 @@
@for (entry of set.quizEntries; track $index; let ei = $index) { @for (entry of set.quizEntries; track $index; let ei = $index) {
<div class="quiz-entry-row"> <div class="quiz-entry-row">
<select class="form-input" [(ngModel)]="entry.quizId"> <select class="form-input" [(ngModel)]="entry.quizId">
<option value="">— Select quiz</option> <option value="">— Select set</option>
@for (q of getAvailableQuizzes(si, ei); track q._id) { @for (q of getAvailableQuizzes(si, ei); track q._id) {
<option [value]="q._id">{{ q.title }}</option> <option [value]="q._id">{{ q.title }}</option>
} }
...@@ -328,7 +333,7 @@ ...@@ -328,7 +333,7 @@
} }
<button class="btn btn-ghost btn-sm" (click)="addQuizToSet(si)"> <button class="btn btn-ghost btn-sm" (click)="addQuizToSet(si)">
<span class="material-symbols-rounded">add</span> Add quiz to Set {{ si + 1 }} <span class="material-symbols-rounded">add</span> Add set to Quiz {{ si + 1 }}
</button> </button>
<!-- Assignment mode — only shown when multiple quizzes --> <!-- Assignment mode — only shown when multiple quizzes -->
......
...@@ -127,7 +127,7 @@ export class HRGroupInterviewComponent implements OnInit { ...@@ -127,7 +127,7 @@ export class HRGroupInterviewComponent implements OnInit {
this.hrs.set(staff.filter((s: any) => s.role === 'hr')); this.hrs.set(staff.filter((s: any) => s.role === 'hr'));
} }
}); });
this.quizService.getAdminQuizzes().subscribe({ next: r => this.quizzes.set(r.quizzes || []) }); this.quizService.getQuizzes().subscribe({ next: r => this.quizzes.set(r.quizzes || []) });
this.showCreateModal.set(true); this.showCreateModal.set(true);
} }
......
...@@ -118,11 +118,18 @@ ...@@ -118,11 +118,18 @@
</span> </span>
</td> </td>
<td class="date-cell">{{ interview.dateOfInterview | date:'dd MMM yyyy' }}</td> <td class="date-cell">{{ interview.dateOfInterview | date:'dd MMM yyyy' }}</td>
<td> <td>
<button class="view-btn" (click)="selectInterview(interview)"> <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
View Quizzes <span class="material-symbols-rounded" style="font-size:16px;vertical-align:middle;">arrow_forward</span> <button class="view-btn" (click)="selectInterview(interview)">
</button> View Quizzes <span class="material-symbols-rounded" style="font-size:16px;vertical-align:middle;">arrow_forward</span>
</td> </button>
@if (hasEvaluations(interview)) {
<button class="view-btn" style="background:var(--accent,#4472C4);color:#fff;border-color:#3360b0;" (click)="downloadEvaluationForm(interview)">
<span class="material-symbols-rounded" style="font-size:16px;vertical-align:middle;">download</span> Eval Form
</button>
}
</div>
</td>
</tr> </tr>
} }
</tbody> </tbody>
......
...@@ -164,4 +164,157 @@ export class HRUserHistoryComponent implements OnInit { ...@@ -164,4 +164,157 @@ export class HRUserHistoryComponent implements OnInit {
if (!resumePath) return ''; if (!resumePath) return '';
return `http://localhost:5000${resumePath}`; return `http://localhost:5000${resumePath}`;
} }
hasEvaluations(interview: any): boolean {
return (interview.evaluations?.length || 0) > 0;
}
downloadEvaluationForm(interview: any): void {
const u = this.user();
const candidateName = u?.name || '';
const dateStr = interview.dateOfInterview
? new Date(interview.dateOfInterview).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
: '';
const position = interview.position || '';
const source = interview.source || '';
const techStack = interview.techStack || '';
const interviewerNames = (interview.assignedInterviewers || [])
.map((i: any) => i.name || '').filter(Boolean).join(', ')
|| interview.interviewerId?.name || 'Unassigned';
const quizzes = interview.quizzes || [];
const q1 = quizzes[0];
const q2 = quizzes[1];
const q1Name = q1?.quizId?.title || q1?.title || '';
const q1Score = (q1?.score != null) ? `${q1.score}/${q1.totalMarks}` : '';
const q2Name = q2?.quizId?.title || q2?.title || '';
const q2Score = (q2?.score != null) ? `${q2.score}/${q2.totalMarks}` : '';
const evals = interview.evaluations || [];
const ivEval = evals.find((e: any) => e.evaluatorRole === 'interviewer');
const pmEval = evals.find((e: any) => e.evaluatorRole === 'pm');
const hrEval = evals.find((e: any) => e.evaluatorRole === 'hr');
const adminEval = evals.find((e: any) => e.evaluatorRole === 'admin');
const recMap: Record<string, string> = {
offer: 'Offer/Hire as Intern', on_hold: 'On Hold',
rejected: 'Rejected', '2nd_round': '2nd Round'
};
const buildCheckboxes = (groupName: string, rec: string): string =>
['offer', 'on_hold', 'rejected', '2nd_round'].map(val => {
const checked = rec === val ? 'checked' : '';
const label = val === '2nd_round' ? '2<sup>nd</sup> Round' : recMap[val];
return `<label style="display:flex;align-items:center;gap:5px;font-size:13px;"><input type="checkbox" name="${groupName}" ${checked} style="width:14px;height:14px;accent-color:#4472C4;"> ${label}</label>`;
}).join('');
const fmtDate = (d: any) => d ? new Date(d).toLocaleDateString('en-GB') : '';
const ivName = ivEval?.evaluatorId?.name || 'Interviewer';
const pmName = pmEval?.evaluatorId?.name || 'Project Manager';
const hrName = hrEval?.evaluatorId?.name || 'HR';
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 html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/>
<title>ITL Evaluation - ${candidateName}</title><style>
*,*::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;}
.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 h1{color:#fff;font-size:16px;font-weight:bold;font-style:italic;text-decoration:underline;}
.logo-wrap img{height:52px;object-fit:contain;}
.info-table{width:100%;border-collapse:collapse;}
.info-table td{border:1px solid #bbb;padding:6px 10px;vertical-align:middle;}
.info-table .label{font-weight:bold;background:#fff;width:160px;white-space:nowrap;}
.info-table .value{background:#fff;color:#444;font-style:italic;}
.comments-section{border:1px solid #bbb;border-top:none;padding:10px 16px;}
.comments-label{font-weight:bold;margin-bottom:6px;}
.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-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-field label{font-weight:bold;white-space:nowrap;}
.sig-line{border-bottom:1px solid #000;min-width:200px;height:18px;font-style:italic;color:#555;font-size:13px;}
.sig-line-date{min-width:140px;}
.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-print{background:#4472C4;color:#fff;border-color:#3360b0;}
@media print{body{padding:0;}.actions{display:none;}}
</style></head><body>
<div class="page-wrap">
<div class="header">
<h1>Intern Interview Evaluation Form</h1>
<div class="logo-wrap"><img src="${ITL_LOGO}" alt="ITL Logo"/></div>
</div>
<table class="info-table"><tbody>
<tr>
<td class="label">Candidate Name:</td><td class="value">${candidateName}</td>
<td class="label">Date of Interview:</td><td class="value">${dateStr}</td>
</tr>
<tr>
<td class="label">Position:</td><td class="value">${position}</td>
<td class="label">Source:</td><td class="value">${source}</td>
</tr>
<tr>
<td class="label">Tech Stack:</td><td class="value">${techStack}</td>
<td class="label"><strong>Interviewer:</strong></td><td class="value">${interviewerNames}</td>
</tr>
<tr>
<td class="label">General Aptitude Test QP Set</td><td class="value">${q1Name}</td>
<td class="label"><strong>General Aptitude Test Score</strong></td><td class="value">${q1Score}</td>
</tr>
<tr>
<td class="label">Technical MCQ Test QP Set</td><td class="value">${q2Name}</td>
<td class="label"><strong>Technical MCQ Test Score</strong></td><td class="value">${q2Score}</td>
</tr>
</tbody></table>
<div class="comments-section" style="margin-top:0;">
<div class="comments-label">Interviewer's Comments (${ivName}):</div>
<div class="comments-text">${ivEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(ivEval?.date)}</div></div>
</div>
<div class="comments-section">
<div class="comments-label">Project Manager Comments (${pmName}):</div>
<div class="comments-text">${pmEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(pmEval?.date)}</div></div>
</div>
<div class="comments-section">
<div class="comments-label">HR Comments (${hrName}):</div>
<div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div>
</div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(hrEval?.date)}</div></div>
</div>
<div class="comments-section"><div class="comments-label">Overall Recommendation:</div></div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div>
<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>Date:</label><div class="sig-line sig-line-date">${fmtDate(adminEval?.date)}</div></div>
</div>
</div>
<div class="actions">
<button class="btn btn-print" onclick="window.print()">&#128438; Print / Save as PDF</button>
</div>
</body></html>`;
const win = window.open('', '_blank');
if (win) {
win.document.write(html);
win.document.close();
win.focus();
}
}
} }
...@@ -214,6 +214,10 @@ ...@@ -214,6 +214,10 @@
return this.adminUrl; return this.adminUrl;
} }
getQuizzes(): Observable<any> {
return this.http.get(`${this.getBaseUrl()}/quizzes`);
}
createGroup(name: string): Observable<any> { createGroup(name: string): Observable<any> {
return this.http.post(`${this.getBaseUrl()}/groups`, { name }); return this.http.post(`${this.getBaseUrl()}/groups`, { name });
} }
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Intern Interview Evaluation Form</title>
<style>
*, *::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;
}
.page-wrap {
max-width: 780px;
margin: 0 auto;
border: 1px solid #ccc;
}
/* ── HEADER ── */
.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;
}
.logo-wrap img {
height: 52px;
object-fit: contain;
}
/* ── INFO TABLE ── */
.info-table {
width: 100%;
border-collapse: collapse;
}
.info-table td {
border: 1px solid #bbb;
padding: 6px 10px;
vertical-align: middle;
}
.info-table .label {
font-weight: bold;
background: #fff;
width: 160px;
white-space: nowrap;
}
.info-table .value {
background: #fff;
color: #555;
font-style: italic;
width: 160px;
}
.info-table input[type="text"] {
border: none;
outline: none;
width: 100%;
font-size: 13px;
font-style: italic;
color: #555;
background: transparent;
}
/* ── COMMENTS SECTION ── */
.comments-section {
border: 1px solid #bbb;
border-top: none;
padding: 10px 16px;
}
.comments-label {
font-weight: bold;
margin-bottom: 6px;
}
.comments-section textarea {
width: 100%;
height: 72px;
border: none;
resize: none;
outline: none;
font-size: 13px;
font-family: Arial, sans-serif;
}
/* ── RECOMMENDATION ROW ── */
.rec-row {
border: 1px solid #bbb;
border-top: none;
padding: 8px 16px;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.rec-row .rec-label {
font-weight: bold;
white-space: nowrap;
}
.rec-option {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
font-size: 13px;
}
.rec-option input[type="checkbox"] {
width: 14px;
height: 14px;
accent-color: #4472C4;
cursor: pointer;
}
/* ── SIGNATURE ROW ── */
.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-field label {
font-weight: bold;
white-space: nowrap;
}
.sig-line {
border-bottom: 1px solid #000;
min-width: 200px;
height: 18px;
}
.sig-line-date {
min-width: 140px;
}
.sig-field input[type="text"] {
border: none;
border-bottom: 1px solid #000;
outline: none;
min-width: 200px;
font-size: 13px;
padding: 0 2px;
background: transparent;
}
.sig-field input.date-input {
min-width: 140px;
}
/* ── SECTION DIVIDER ── */
.section-gap {
height: 8px;
background: #fff;
border-left: 1px solid #bbb;
border-right: 1px solid #bbb;
}
/* ── ACTIONS ── */
.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-clear {
background: #f5f5f5;
color: #333;
}
.btn-print {
background: #4472C4;
color: #fff;
border-color: #3360b0;
}
@media print {
body { padding: 0; }
.actions { display: none; }
}
</style>
</head>
<body>
<div class="page-wrap">
<!-- HEADER -->
<div class="header">
<h1>Intern Interview Evaluation Form</h1>
<div class="logo-wrap">
<img src="./logo.jpeg" alt="ITL Logo" />
</div>
</div>
<!-- INFO TABLE -->
<table class="info-table">
<tbody>
<tr>
<td class="label">Candidate Name:</td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
<td class="label">Date of Interview:</td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
</tr>
<tr>
<td class="label">Position:</td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
<td class="label">Source</td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
</tr>
<tr>
<td class="label">Tech Stack:</td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
<td class="label"><strong>Interviewer:</strong></td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
</tr>
<tr>
<td class="label">General Aptitude Test QP Set</td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
<td class="label"><strong>General Aptitude Test Score</strong></td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
</tr>
<tr>
<td class="label">Technical MCQ Test QP Set</td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
<td class="label"><strong>Technical MCQ Test Score</strong></td>
<td class="value"><input type="text" placeholder="Click here to enter text." /></td>
</tr>
</tbody>
</table>
<!-- INTERVIEWER COMMENTS -->
<div class="comments-section" style="margin-top:0;">
<div class="comments-label">Interviewer's Comments (Interviewer Name):</div>
<textarea placeholder=""></textarea>
</div>
<div class="rec-row">
<span class="rec-label">Recommendation:</span>
<label class="rec-option"><input type="checkbox" name="rec1" /> Offer/Hire as Intern</label>
<label class="rec-option"><input type="checkbox" name="rec1" /> On Hold</label>
<label class="rec-option"><input type="checkbox" name="rec1" /> Rejected</label>
<label class="rec-option"><input type="checkbox" name="rec1" /> 2<sup>nd</sup> Round</label>
</div>
<div class="sig-row">
<div class="sig-field">
<label>Evaluator's Signature:</label>
<input type="text" style="min-width:200px;" />
</div>
<div class="sig-field">
<label>Date:</label>
<input type="text" class="date-input" style="min-width:140px;" />
</div>
</div>
<!-- PROJECT MANAGER COMMENTS -->
<div class="comments-section">
<div class="comments-label">Project Manager Comments (Project Manager Name):</div>
<textarea placeholder=""></textarea>
</div>
<div class="rec-row">
<span class="rec-label">Recommendation:</span>
<label class="rec-option"><input type="checkbox" name="rec2" /> Offer/Hire as Intern</label>
<label class="rec-option"><input type="checkbox" name="rec2" /> On Hold</label>
<label class="rec-option"><input type="checkbox" name="rec2" /> Rejected</label>
<label class="rec-option"><input type="checkbox" name="rec2" /> 2<sup>nd</sup> Round</label>
</div>
<div class="sig-row">
<div class="sig-field">
<label>Evaluator's Signature:</label>
<input type="text" style="min-width:200px;" />
</div>
<div class="sig-field">
<label>Date:</label>
<input type="text" class="date-input" style="min-width:140px;" />
</div>
</div>
<!-- HR COMMENTS -->
<div class="comments-section">
<div class="comments-label">HR Comments (HR Name):</div>
<textarea style="height:52px;" placeholder=""></textarea>
</div>
<div class="rec-row">
<span class="rec-label">Recommendation:</span>
<label class="rec-option"><input type="checkbox" name="rec3" /> Offer/Hire as Intern</label>
<label class="rec-option"><input type="checkbox" name="rec3" /> On Hold</label>
<label class="rec-option"><input type="checkbox" name="rec3" /> Rejected</label>
<label class="rec-option"><input type="checkbox" name="rec3" /> 2<sup>nd</sup> Round</label>
</div>
<div class="sig-row">
<div class="sig-field">
<label>Evaluator's Signature:</label>
<input type="text" style="min-width:200px;" />
</div>
<div class="sig-field">
<label>Date:</label>
<input type="text" class="date-input" style="min-width:140px;" />
</div>
</div>
<!-- OVERALL RECOMMENDATION -->
<div class="comments-section">
<div class="comments-label">Overall Recommendation:</div>
</div>
<div class="rec-row">
<span class="rec-label">Recommendation:</span>
<label class="rec-option"><input type="checkbox" name="rec-overall" /> Offer/Hire as Intern</label>
<label class="rec-option"><input type="checkbox" name="rec-overall" /> On Hold</label>
<label class="rec-option"><input type="checkbox" name="rec-overall" /> Rejected</label>
<label class="rec-option"><input type="checkbox" name="rec-overall" /> 2<sup>nd</sup> Round</label>
</div>
<div class="sig-row">
<div class="sig-field">
<label>Authorized Signature:</label>
<input type="text" style="min-width:200px;" />
</div>
<div class="sig-field">
<label>Date:</label>
<input type="text" class="date-input" style="min-width:140px;" />
</div>
</div>
</div><!-- /.page-wrap -->
<!-- ACTIONS -->
<div class="actions">
<button class="btn btn-clear" onclick="clearForm()">Clear Form</button>
<button class="btn btn-print" onclick="window.print()">🖨 Print / Save as PDF</button>
</div>
<script>
function clearForm() {
if (!confirm('Clear all fields? This cannot be undone.')) return;
document.querySelectorAll('input[type="text"], textarea').forEach(el => el.value = '');
document.querySelectorAll('input[type="checkbox"]').forEach(el => el.checked = false);
}
</script>
</body>
</html>
logo.jpeg

10.5 KB

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