Commit d8e24864 authored by Aravind RK's avatar Aravind RK

feat : added file type for uploading

parent e01f6a70
...@@ -43,8 +43,8 @@ ...@@ -43,8 +43,8 @@
allowedHeaders: ['Content-Type', 'Authorization'] allowedHeaders: ['Content-Type', 'Authorization']
})); }));
app.use(express.json()); app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser()); app.use(cookieParser());
// Serve static files from uploads folder (for resumes, etc) // Serve static files from uploads folder (for resumes, etc)
......
...@@ -191,6 +191,17 @@ export class UserHistoryComponent implements OnInit { ...@@ -191,6 +191,17 @@ export class UserHistoryComponent implements OnInit {
const q2Name = q2?.quizId?.title || q2?.title || ''; const q2Name = q2?.quizId?.title || q2?.title || '';
const q2Score = (q2?.score != null) ? `${q2.score}/${q2.totalMarks}` : ''; const q2Score = (q2?.score != null) ? `${q2.score}/${q2.totalMarks}` : '';
const _lcm: Record<string, string> = { easy: 'BEG', medium: 'INT', hard: 'ADV' };
const makeQuizId = (quiz: any, idx: number): string => {
if (!quiz) return '';
const qd = quiz.quizId || {};
const topic = (qd.category || 'General').replace(/\s+/g, '_');
const code = _lcm[(qd.difficulty || 'easy').toLowerCase()] || 'BEG';
return `Q_${topic}_${code}_${String(idx + 1).padStart(3, '0')}`;
};
const q1Id = makeQuizId(q1, 0);
const q2Id = makeQuizId(q2, 1);
const evals = interview.evaluations || []; const evals = interview.evaluations || [];
const ivEval = evals.find((e: any) => e.evaluatorRole === 'interviewer'); const ivEval = evals.find((e: any) => e.evaluatorRole === 'interviewer');
const pmEval = evals.find((e: any) => e.evaluatorRole === 'pm'); const pmEval = evals.find((e: any) => e.evaluatorRole === 'pm');
...@@ -228,28 +239,29 @@ export class UserHistoryComponent implements OnInit { ...@@ -228,28 +239,29 @@ export class UserHistoryComponent implements OnInit {
<title>ITL Evaluation - ${candidateName}</title><style> <title>ITL Evaluation - ${candidateName}</title><style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-size:13px;} body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-size:13px;}
.page-wrap{max-width:780px;margin:0 auto;border:1px solid #ccc;} .page-wrap{max-width:820px;margin:0 auto;border:1px solid #bbb;box-shadow: 0 4px 12px rgba(0,0,0,0.1);}
.header{background:#4472C4;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;} .header{background:#4472C4;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;}
.header h1{color:#fff;font-size:26px;font-weight:bold;} .header h1{color:#fff;font-size:24px;font-weight:bold;}
.logo-wrap img{height:52px;object-fit:contain;} .logo-wrap img{height:52px;object-fit:contain;}
.info-table{width:100%;border-collapse:collapse;} .info-table{width:100%;border-collapse:collapse;}
.info-table td{border:1px solid #bbb;padding:6px 10px;vertical-align:middle;} .info-table td{border:1px solid #bbb;padding:8px 12px;vertical-align:middle;}
.info-table .label{font-weight:bold;background:#fff;width:160px;white-space:nowrap;} .info-table .label{font-weight:bold;background:#f5f5f5;width:170px;white-space:nowrap;}
.info-table .value{background:#fff;color:#444;font-style:italic;} .info-table .value{background:#fff;color:#222;}
.comments-section{border:1px solid #bbb;border-top:none;padding:10px 16px;} .comments-section{border:1px solid #bbb;border-top:none;padding:12px 16px;background:#fff;}
.comments-label{font-weight:bold;margin-bottom:6px;} .comments-label{font-weight:bold;margin-bottom:6px;color:#111;}
.comments-text{min-height:65px;font-size:13px;font-family:Arial,sans-serif;color:#333;line-height:1.5;padding:4px 0;} .comments-text{min-height:55px;font-size:13px;font-family:Arial,sans-serif;color:#333;line-height:1.5;padding:4px 0;}
.rec-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;} .rec-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;background:#fcfcfc;}
.rec-label{font-weight:bold;white-space:nowrap;} .rec-label{font-weight:bold;white-space:nowrap;}
.sig-row{ .sig-row{
border:1px solid #bbb; border:1px solid #bbb;
border-top:none; border-top:none;
padding:14px 16px 10px; padding:12px 16px;
display:flex; display:flex;
justify-content:space-between; justify-content:space-between;
align-items:flex-end; align-items:flex-end;
gap:40px; gap:40px;
background:#fff;
page-break-inside: avoid; page-break-inside: avoid;
break-inside: avoid; break-inside: avoid;
} }
...@@ -264,42 +276,76 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -264,42 +276,76 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
.sig-field label{ .sig-field label{
font-weight:bold; font-weight:bold;
white-space:nowrap; white-space:nowrap;
font-size:14px; font-size:13px;
} }
.signature-box{ .signature-box{
border-bottom:2px solid #222; border-bottom:1.5px solid #222;
width:165px; width:180px;
height:26px; height:30px;
display:flex; display:flex;
justify-content:center; justify-content:center;
align-items:flex-end; align-items:flex-end;
padding-bottom:4px; padding-bottom:3px;
} }
.signature-box img{ .signature-box img{
max-height:18px; max-height:26px;
max-width:90px; max-width:160px;
object-fit:contain; object-fit:contain;
} }
.date-box{ .date-box{
border-bottom:2px solid #222; border-bottom:1.5px solid #222;
width:130px; width:140px;
height:22px; height:24px;
display:flex; display:flex;
align-items:flex-end; align-items:flex-end;
padding-bottom:3px; padding-bottom:2px;
font-style:italic; font-style:italic;
color:#444; color:#333;
} }
.actions{margin-top:20px;display:flex;justify-content:center;gap:10px;}
.btn{font-size:14px;font-family:Arial,sans-serif;padding:8px 24px;border-radius:4px;cursor:pointer;border:1px solid #3360b0;font-weight:bold;}
.btn-print{background:#4472C4;color:#fff;}
@media print {
.actions{margin-top:16px;display:flex;justify-content:flex-end;gap:10px;} @page {
.btn{font-size:13px;font-family:Arial,sans-serif;padding:7px 18px;border-radius:3px;cursor:pointer;border:1px solid #aaa;} size: A4 portrait;
.btn-print{background:#4472C4;color:#fff;border-color:#3360b0;} margin: 8mm 12mm;
@media print{body{padding:0;}.actions{display:none;}} }
body {
padding: 0;
font-size: 13.5px;
background: #fff;
}
.page-wrap {
width: 100%;
max-width: 100%;
margin: 0;
border: 1px solid #bbb;
box-shadow: none;
}
.info-table td {
padding: 7px 10px;
}
.comments-section {
padding: 10px 14px;
}
.comments-text {
min-height: 52px;
}
.rec-row {
padding: 8px 14px;
}
.sig-row {
padding: 10px 14px;
}
.actions {
display: none;
}
}
</style></head><body> </style></head><body>
<div class="page-wrap"> <div class="page-wrap">
<div class="header"> <div class="header">
...@@ -320,11 +366,11 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -320,11 +366,11 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
<td class="label"><strong>Interviewer:</strong></td><td class="value">${interviewerNames}</td> <td class="label"><strong>Interviewer:</strong></td><td class="value">${interviewerNames}</td>
</tr> </tr>
<tr> <tr>
<td class="label">General Aptitude Test QP Set</td><td class="value">${q1Name}</td> <td class="label">General Aptitude Test QP ID</td><td class="value">${q1Id}</td>
<td class="label"><strong>General Aptitude Test Score</strong></td><td class="value">${q1Score}</td> <td class="label"><strong>General Aptitude Test Score</strong></td><td class="value">${q1Score}</td>
</tr> </tr>
<tr> <tr>
<td class="label">Technical MCQ Test QP Set</td><td class="value">${q2Name}</td> <td class="label">Technical MCQ Test QP ID</td><td class="value">${q2Id}</td>
<td class="label"><strong>Technical MCQ Test Score</strong></td><td class="value">${q2Score}</td> <td class="label"><strong>Technical MCQ Test Score</strong></td><td class="value">${q2Score}</td>
</tr> </tr>
</tbody></table> </tbody></table>
...@@ -334,21 +380,16 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -334,21 +380,16 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
<div class="comments-text">${ivEval?.comments || ''}</div> <div class="comments-text">${ivEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"> <div class="sig-field">
<label>Evaluator's Signature:</label> <label>Evaluator's Signature:</label>
<div class="signature-box"> <div class="signature-box">${getSigHtml(ivEval, ivName)}</div>
${getSigHtml(ivEval, ivName)}
</div> </div>
</div> <div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="sig-field" style="flex:0.55;"> <div class="date-box">${fmtDate(ivEval?.date)}</div>
<label>Date:</label>
<div class="date-box">
${fmtDate(ivEval?.date)}
</div> </div>
</div> </div>
</div>
<div class="comments-section"> <div class="comments-section">
<div class="comments-label">Project Manager Comments (${pmName}):</div> <div class="comments-label">Project Manager Comments (${pmName}):</div>
...@@ -356,64 +397,47 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -356,64 +397,47 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"> <div class="sig-field">
<label>Evaluator's Signature:</label> <label>Evaluator's Signature:</label>
<div class="signature-box"> <div class="signature-box">${getSigHtml(pmEval, pmName)}</div>
${getSigHtml(pmEval, pmName)}
</div> </div>
</div> <div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="sig-field" style="flex:0.55;"> <div class="date-box">${fmtDate(pmEval?.date)}</div>
<label>Date:</label>
<div class="date-box">
${fmtDate(pmEval?.date)}
</div> </div>
</div> </div>
</div>
<div class="comments-section"> <div class="comments-section">
<div class="comments-label">HR Comments (${hrName}):</div> <div class="comments-label">HR Comments (${hrName}):</div>
<div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div> <div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"> <div class="sig-field">
<label>Evaluator's Signature:</label> <label>Evaluator's Signature:</label>
<div class="signature-box"> <div class="signature-box">${getSigHtml(hrEval, hrName)}</div>
${getSigHtml(hrEval, hrName)}
</div> </div>
</div> <div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="sig-field" style="flex:0.55;"> <div class="date-box">${fmtDate(hrEval?.date)}</div>
<label>Date:</label>
<div class="date-box">
${fmtDate(hrEval?.date)}
</div> </div>
</div> </div>
</div>
<div class="comments-section"> <div class="comments-section">
<div class="comments-label">Overall Comments (${adminEval?.evaluatorId?.name}):</div> <div class="comments-label">Overall Comments (${adminEval?.evaluatorId?.name || adminName}):</div>
<div class="comments-text" style="min-height:52px;">${adminEval?.comments || ''}</div> <div class="comments-text" style="min-height:52px;">${adminEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"> <div class="sig-field">
<label>Evaluator's Signature:</label> <label>Evaluator's Signature:</label>
<div class="signature-box"> <div class="signature-box">${getSigHtml(adminEval, adminName)}</div>
${getSigHtml(adminEval, adminName)} </div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">${fmtDate(adminEval?.date)}</div>
</div> </div>
</div> </div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">${fmtDate(adminEval?.date)}</div>
</div>
</div>
</div> </div>
<div class="actions"> <div class="actions">
<button class="btn btn-print" onclick="window.print()">&#128438; Print / Save as PDF</button> <button class="btn btn-print" onclick="window.print()">&#128438; Print / Save as PDF</button>
......
...@@ -191,6 +191,17 @@ export class HRUserHistoryComponent implements OnInit { ...@@ -191,6 +191,17 @@ export class HRUserHistoryComponent implements OnInit {
const q2Name = q2?.quizId?.title || q2?.title || ''; const q2Name = q2?.quizId?.title || q2?.title || '';
const q2Score = (q2?.score != null) ? `${q2.score}/${q2.totalMarks}` : ''; const q2Score = (q2?.score != null) ? `${q2.score}/${q2.totalMarks}` : '';
const _lcm: Record<string, string> = { easy: 'BEG', medium: 'INT', hard: 'ADV' };
const makeQuizId = (quiz: any, idx: number): string => {
if (!quiz) return '';
const qd = quiz.quizId || {};
const topic = (qd.category || 'General').replace(/\s+/g, '_');
const code = _lcm[(qd.difficulty || 'easy').toLowerCase()] || 'BEG';
return `Q_${topic}_${code}_${String(idx + 1).padStart(3, '0')}`;
};
const q1Id = makeQuizId(q1, 0);
const q2Id = makeQuizId(q2, 1);
const evals = interview.evaluations || []; const evals = interview.evaluations || [];
const ivEval = evals.find((e: any) => e.evaluatorRole === 'interviewer'); const ivEval = evals.find((e: any) => e.evaluatorRole === 'interviewer');
const pmEval = evals.find((e: any) => e.evaluatorRole === 'pm'); const pmEval = evals.find((e: any) => e.evaluatorRole === 'pm');
...@@ -229,32 +240,117 @@ export class HRUserHistoryComponent implements OnInit { ...@@ -229,32 +240,117 @@ export class HRUserHistoryComponent implements OnInit {
<title>ITL Evaluation - ${candidateName}</title><style> <title>ITL Evaluation - ${candidateName}</title><style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-size:13px;} body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-size:13px;}
.page-wrap{max-width:780px;margin:0 auto;border:1px solid #ccc;} .page-wrap{max-width:820px;margin:0 auto;border:1px solid #bbb;box-shadow: 0 4px 12px rgba(0,0,0,0.1);}
.header{background:#4472C4;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;} .header{background:#4472C4;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;}
.header h1{color:#fff;font-size:16px;font-weight:bold;font-style:italic;text-decoration:underline;} .header h1{color:#fff;font-size:24px;font-weight:bold;}
.logo-wrap img{height:52px;object-fit:contain;} .logo-wrap img{height:52px;object-fit:contain;}
.info-table{width:100%;border-collapse:collapse;} .info-table{width:100%;border-collapse:collapse;}
.info-table td{border:1px solid #bbb;padding:6px 10px;vertical-align:middle;} .info-table td{border:1px solid #bbb;padding:8px 12px;vertical-align:middle;}
.info-table .label{font-weight:bold;background:#fff;width:160px;white-space:nowrap;} .info-table .label{font-weight:bold;background:#f5f5f5;width:170px;white-space:nowrap;}
.info-table .value{background:#fff;color:#444;font-style:italic;} .info-table .value{background:#fff;color:#222;}
.comments-section{border:1px solid #bbb;border-top:none;padding:10px 16px;} .comments-section{border:1px solid #bbb;border-top:none;padding:12px 16px;background:#fff;}
.comments-label{font-weight:bold;margin-bottom:6px;} .comments-label{font-weight:bold;margin-bottom:6px;color:#111;}
.comments-text{min-height:65px;font-size:13px;font-family:Arial,sans-serif;color:#333;line-height:1.5;padding:4px 0;} .comments-text{min-height:55px;font-size:13px;font-family:Arial,sans-serif;color:#333;line-height:1.5;padding:4px 0;}
.rec-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;} .rec-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;background:#fcfcfc;}
.rec-label{font-weight:bold;white-space:nowrap;} .rec-label{font-weight:bold;white-space:nowrap;}
.sig-row{border:1px solid #bbb;border-top:none;padding:8px 16px;display:flex;align-items:flex-end;gap:40px;}
.sig-field{display:flex;align-items:flex-end;gap:8px;} .sig-row{
.sig-field label{font-weight:bold;white-space:nowrap;} border:1px solid #bbb;
.sig-line{border-bottom:1px solid #000;min-width:200px;height:18px;font-style:italic;color:#555;font-size:13px;} border-top:none;
.sig-line-date{min-width:140px;} padding:12px 16px;
.actions{margin-top:16px;display:flex;justify-content:flex-end;gap:10px;} display:flex;
.btn{font-size:13px;font-family:Arial,sans-serif;padding:7px 18px;border-radius:3px;cursor:pointer;border:1px solid #aaa;} justify-content:space-between;
.btn-print{background:#4472C4;color:#fff;border-color:#3360b0;} align-items:flex-end;
@media print{body{padding:0;}.actions{display:none;}} gap:40px;
background:#fff;
page-break-inside: avoid;
break-inside: avoid;
}
.sig-field{
display:flex;
align-items:flex-end;
gap:10px;
flex:1;
}
.sig-field label{
font-weight:bold;
white-space:nowrap;
font-size:13px;
}
.signature-box{
border-bottom:1.5px solid #222;
width:180px;
height:30px;
display:flex;
justify-content:center;
align-items:flex-end;
padding-bottom:3px;
}
.signature-box img{
max-height:26px;
max-width:160px;
object-fit:contain;
}
.date-box{
border-bottom:1.5px solid #222;
width:140px;
height:24px;
display:flex;
align-items:flex-end;
padding-bottom:2px;
font-style:italic;
color:#333;
}
.actions{margin-top:20px;display:flex;justify-content:center;gap:10px;}
.btn{font-size:14px;font-family:Arial,sans-serif;padding:8px 24px;border-radius:4px;cursor:pointer;border:1px solid #3360b0;font-weight:bold;}
.btn-print{background:#4472C4;color:#fff;}
@media print {
@page {
size: A4 portrait;
margin: 8mm 12mm;
}
body {
padding: 0;
font-size: 13.5px;
background: #fff;
}
.page-wrap {
width: 100%;
max-width: 100%;
margin: 0;
border: 1px solid #bbb;
box-shadow: none;
}
.info-table td {
padding: 7px 10px;
}
.comments-section {
padding: 10px 14px;
}
.comments-text {
min-height: 52px;
}
.rec-row {
padding: 8px 14px;
}
.sig-row {
padding: 10px 14px;
}
.actions {
display: none;
}
}
</style></head><body> </style></head><body>
<div class="page-wrap"> <div class="page-wrap">
<div class="header"> <div class="header">
<h1>Intern Interview Evaluation Form</h1> <h1>Candidate Evaluation Form</h1>
<div class="logo-wrap"><img src="${ITL_LOGO}" alt="ITL Logo"/></div> <div class="logo-wrap"><img src="${ITL_LOGO}" alt="ITL Logo"/></div>
</div> </div>
<table class="info-table"><tbody> <table class="info-table"><tbody>
...@@ -271,49 +367,77 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s ...@@ -271,49 +367,77 @@ body{font-family:Arial,sans-serif;background:#fff;color:#000;padding:24px;font-s
<td class="label"><strong>Interviewer:</strong></td><td class="value">${interviewerNames}</td> <td class="label"><strong>Interviewer:</strong></td><td class="value">${interviewerNames}</td>
</tr> </tr>
<tr> <tr>
<td class="label">General Aptitude Test QP Set</td><td class="value">${q1Name}</td> <td class="label">General Aptitude Test QP ID</td><td class="value">${q1Id}</td>
<td class="label"><strong>General Aptitude Test Score</strong></td><td class="value">${q1Score}</td> <td class="label"><strong>General Aptitude Test Score</strong></td><td class="value">${q1Score}</td>
</tr> </tr>
<tr> <tr>
<td class="label">Technical MCQ Test QP Set</td><td class="value">${q2Name}</td> <td class="label">Technical MCQ Test QP ID</td><td class="value">${q2Id}</td>
<td class="label"><strong>Technical MCQ Test Score</strong></td><td class="value">${q2Score}</td> <td class="label"><strong>Technical MCQ Test Score</strong></td><td class="value">${q2Score}</td>
</tr> </tr>
</tbody></table> </tbody></table>
<div class="comments-section" style="margin-top:0;"> <div class="comments-section" style="margin-top:0;">
<div class="comments-label">Interviewer's Comments (${ivName}):</div> <div class="comments-label">Interviewer's Comments (${ivName}):</div>
<div class="comments-text">${ivEval?.comments || ''}</div> <div class="comments-text">${ivEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec1', ivEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(ivEval, ivName)}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(ivEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">${getSigHtml(ivEval, ivName)}</div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">${fmtDate(ivEval?.date)}</div>
</div>
</div> </div>
<div class="comments-section"> <div class="comments-section">
<div class="comments-label">Project Manager Comments (${pmName}):</div> <div class="comments-label">Project Manager Comments (${pmName}):</div>
<div class="comments-text">${pmEval?.comments || ''}</div> <div class="comments-text">${pmEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec2', pmEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(pmEval, pmName)}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(pmEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">${getSigHtml(pmEval, pmName)}</div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">${fmtDate(pmEval?.date)}</div>
</div>
</div> </div>
<div class="comments-section"> <div class="comments-section">
<div class="comments-label">HR Comments (${hrName}):</div> <div class="comments-label">HR Comments (${hrName}):</div>
<div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div> <div class="comments-text" style="min-height:52px;">${hrEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec3', hrEval?.recommendation || '')}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(hrEval, hrName)}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(hrEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">${getSigHtml(hrEval, hrName)}</div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">${fmtDate(hrEval?.date)}</div>
</div>
</div> </div>
<div class="comments-section">
<div class="comments-section">
<div class="comments-label">Overall Comments (${adminName}):</div> <div class="comments-label">Overall Comments (${adminName}):</div>
<div class="comments-text" style="min-height:52px;">${adminEval?.comments || ''}</div> <div class="comments-text" style="min-height:52px;">${adminEval?.comments || ''}</div>
</div> </div>
<div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div> <div class="rec-row"><span class="rec-label">Recommendation:</span>${buildCheckboxes('rec-overall', overallRec)}</div>
<div class="sig-row"> <div class="sig-row">
<div class="sig-field"><label>Evaluator's Signature:</label><div class="sig-line" style="text-align:center;">${getSigHtml(adminEval, adminName)}</div></div> <div class="sig-field">
<div class="sig-field"><label>Date:</label><div class="sig-line sig-line-date">${fmtDate(adminEval?.date)}</div></div> <label>Evaluator's Signature:</label>
<div class="signature-box">${getSigHtml(adminEval, adminName)}</div>
</div>
<div class="sig-field" style="flex:0.55;">
<label>Date:</label>
<div class="date-box">${fmtDate(adminEval?.date)}</div>
</div>
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
......
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
<input type="file" accept="image/*" style="display: none;" (change)="uploadSignature($event)"> <input type="file" accept="image/*" style="display: none;" (change)="uploadSignature($event)">
</label> </label>
</div> </div>
<p style="margin-top: 8px; font-size: 12px; color: #666;">Valid formats: JPG, PNG, GIF, WebP (Max 5MB)</p>
} @else { } @else {
<div class="empty-state" style="padding: 40px 20px;"> <div class="empty-state" style="padding: 40px 20px;">
<span class="material-symbols-rounded">draw</span> <span class="material-symbols-rounded">draw</span>
...@@ -57,6 +58,7 @@ ...@@ -57,6 +58,7 @@
@else { <span class="material-symbols-rounded">upload</span> Upload Signature } @else { <span class="material-symbols-rounded">upload</span> Upload Signature }
<input type="file" accept="image/*" style="display: none;" (change)="uploadSignature($event)"> <input type="file" accept="image/*" style="display: none;" (change)="uploadSignature($event)">
</label> </label>
<p style="margin-top: 8px; font-size: 12px; color: #666;">Valid formats: JPG, PNG, GIF, WebP (Max 5MB)</p>
</div> </div>
} }
</div> </div>
......
...@@ -85,27 +85,72 @@ export class StaffProfileComponent implements OnInit { ...@@ -85,27 +85,72 @@ export class StaffProfileComponent implements OnInit {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
// Validate file type
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
alert('Please upload an image file (JPG, PNG, GIF, or WebP)');
return;
}
// Validate file size (max 5MB before compression)
if (file.size > 5 * 1024 * 1024) {
alert('File size must be less than 5MB');
return;
}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = (e: any) => {
const base64String = reader.result as string; const img = new Image();
this.isUploadingSignature.set(true); img.onload = () => {
// Resize to signature-appropriate dimensions and compress as JPEG
this.authService.uploadSignature({ signature: base64String }).subscribe({ const MAX_WIDTH = 600;
next: (res) => { const MAX_HEIGHT = 200;
this.isUploadingSignature.set(false); let width = img.width;
if (res.signature) { let height = img.height;
// Update user signal
this.user.update(u => ({ ...u, signature: res.signature })); // Scale down proportionally if needed
} if (width > MAX_WIDTH) {
}, height = Math.round((height * MAX_WIDTH) / width);
error: (err) => { width = MAX_WIDTH;
this.isUploadingSignature.set(false); }
alert(err.error?.message || 'Error uploading signature'); if (height > MAX_HEIGHT) {
width = Math.round((width * MAX_HEIGHT) / height);
height = MAX_HEIGHT;
} }
});
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
// White background so transparent PNGs look clean
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
// Export as JPEG at 80% quality (much smaller than raw PNG base64)
const base64String = canvas.toDataURL('image/jpeg', 0.8);
this.isUploadingSignature.set(true);
this.authService.uploadSignature({ signature: base64String }).subscribe({
next: (res) => {
this.isUploadingSignature.set(false);
if (res.signature) {
this.user.update(u => ({ ...u, signature: res.signature }));
}
},
error: (err) => {
this.isUploadingSignature.set(false);
alert(err.error?.message || 'Error uploading signature. Please try again.');
}
});
};
img.onerror = () => {
alert('Failed to load image. Please try a different file.');
};
img.src = e.target.result;
}; };
reader.onerror = () => { reader.onerror = () => {
alert('Failed to read file'); alert('Failed to read file. Please try again.');
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
......
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