374 lines
14 KiB
TypeScript
374 lines
14 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import db from '../../database/models/index.js';
|
|
const {
|
|
Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireScore,
|
|
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User, RequestParticipant, Role
|
|
} = db;
|
|
import { AuthRequest } from '../../types/express.types.js';
|
|
import { Op } from 'sequelize';
|
|
import * as EmailService from '../../common/utils/email.service.js';
|
|
|
|
// --- Questionnaires ---
|
|
|
|
export const getQuestionnaire = async (req: Request, res: Response) => {
|
|
try {
|
|
const { version } = req.query;
|
|
const where: any = { isActive: true };
|
|
if (version) where.version = version;
|
|
|
|
const questionnaire = await Questionnaire.findOne({
|
|
where,
|
|
include: [{ model: QuestionnaireQuestion, as: 'questions' }],
|
|
order: [['createdAt', 'DESC']] // GET latest if no version
|
|
});
|
|
|
|
res.json({ success: true, data: questionnaire });
|
|
} catch (error) {
|
|
console.error('Get questionnaire error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
|
|
}
|
|
};
|
|
|
|
export const submitQuestionnaireResponse = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { applicationId, questionnaireId, responses } = req.body; // responses: [{ questionId, responseValue, attachmentUrl }]
|
|
|
|
// Calculate score logic (Placeholder)
|
|
let calculatedScore = 0;
|
|
let totalWeight = 0;
|
|
|
|
for (const resp of responses) {
|
|
await QuestionnaireResponse.create({
|
|
applicationId,
|
|
questionnaireId,
|
|
questionId: resp.questionId,
|
|
responseValue: resp.responseValue,
|
|
attachmentUrl: resp.attachmentUrl
|
|
});
|
|
// Add scoring logic here based on question type/weight
|
|
}
|
|
|
|
// Create Score Record
|
|
await QuestionnaireScore.create({
|
|
applicationId,
|
|
questionnaireId,
|
|
score: calculatedScore,
|
|
maxScore: 100, // Placeholder
|
|
status: 'Completed'
|
|
});
|
|
|
|
res.status(201).json({ success: true, message: 'Responses submitted successfully' });
|
|
} catch (error) {
|
|
console.error('Submit response error:', error);
|
|
res.status(500).json({ success: false, message: 'Error submitting responses' });
|
|
}
|
|
};
|
|
|
|
// --- Interviews ---
|
|
|
|
export const scheduleInterview = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
console.log('---------------------------------------------------');
|
|
console.log('Incoming Schedule Interview Request:', JSON.stringify(req.body, null, 2));
|
|
const { applicationId, level, scheduledAt, type, location, participants } = req.body; // participants: [userId]
|
|
|
|
// Parse level string (e.g., "level1") to integer if necessary
|
|
const levelNum = typeof level === 'string' ? parseInt(level.replace(/\D/g, ''), 10) : level;
|
|
console.log(`Parsed Level: ${level} -> ${levelNum}`);
|
|
|
|
console.log('Creating Interview record...');
|
|
const interview = await Interview.create({
|
|
applicationId,
|
|
level: levelNum || 1, // Default to 1 if parsing fails
|
|
scheduleDate: new Date(scheduledAt),
|
|
interviewType: type,
|
|
linkOrLocation: location,
|
|
status: 'Scheduled',
|
|
scheduledBy: req.user?.id
|
|
});
|
|
console.log('Interview created with ID:', interview.id);
|
|
|
|
// Update Application Status
|
|
const statusMap: any = {
|
|
1: 'Level 1 Interview Pending',
|
|
2: 'Level 2 Interview Pending',
|
|
3: 'Level 3 Interview Pending'
|
|
};
|
|
|
|
const newStatus = statusMap[levelNum] || 'Interview Scheduled';
|
|
|
|
await db.Application.update({ overallStatus: newStatus }, { where: { id: applicationId } });
|
|
|
|
if (participants && participants.length > 0) {
|
|
console.log(`Processing ${participants.length} participants...`);
|
|
for (const userId of participants) {
|
|
// 1. Add to Panel
|
|
await InterviewParticipant.create({
|
|
interviewId: interview.id,
|
|
userId,
|
|
role: 'Panelist'
|
|
});
|
|
|
|
// 2. Add as Request Participant for Collaboration
|
|
console.log(`Adding user ${userId} to RequestParticipant...`);
|
|
await RequestParticipant.findOrCreate({
|
|
where: {
|
|
requestId: applicationId,
|
|
requestType: 'application',
|
|
userId
|
|
},
|
|
defaults: {
|
|
participantType: 'contributor', // 'interviewer' is not a valid enum value
|
|
joinedMethod: 'interview'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Fetch application and user email for notification
|
|
const application = await db.Application.findByPk(applicationId);
|
|
|
|
if (application) {
|
|
await EmailService.sendInterviewScheduledEmail(
|
|
application.email,
|
|
application.name,
|
|
application.applicationId || application.id,
|
|
interview
|
|
);
|
|
}
|
|
|
|
// Notify panelists if needed
|
|
if (participants && participants.length > 0) {
|
|
for (const userId of participants) {
|
|
const panelist = await User.findByPk(userId);
|
|
if (panelist) {
|
|
await EmailService.sendInterviewScheduledEmail(
|
|
panelist.email,
|
|
panelist.fullName,
|
|
application?.applicationId || application?.id || applicationId,
|
|
interview
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Interview scheduling completed successfully.');
|
|
res.status(201).json({ success: true, message: 'Interview scheduled successfully', data: interview });
|
|
} catch (error) {
|
|
console.error('CRITICAL ERROR in scheduleInterview:', error);
|
|
// Log the full error object for inspection
|
|
console.log(JSON.stringify(error, null, 2));
|
|
res.status(500).json({ success: false, message: 'Error scheduling interview', error: String(error) });
|
|
}
|
|
};
|
|
|
|
export const updateInterview = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { status, scheduledAt, outcome } = req.body;
|
|
|
|
const interview = await Interview.findByPk(id);
|
|
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
|
|
|
|
await interview.update({ status, scheduledAt, outcome });
|
|
|
|
res.json({ success: true, message: 'Interview updated successfully' });
|
|
} catch (error) {
|
|
console.error('Update interview error:', error);
|
|
res.status(500).json({ success: false, message: 'Error updating interview' });
|
|
}
|
|
};
|
|
|
|
export const submitEvaluation = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { id } = req.params; // Interview ID
|
|
const { ktScore, feedback, recommendation, status } = req.body;
|
|
|
|
const interview = await Interview.findByPk(id);
|
|
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
|
|
|
|
const evaluation = await InterviewEvaluation.create({
|
|
interviewId: id,
|
|
evaluatorId: req.user?.id,
|
|
ktMatrixScore: ktScore,
|
|
qualitativeFeedback: feedback,
|
|
recommendation
|
|
});
|
|
|
|
// Auto update interview status if completed
|
|
if (status === 'Completed') {
|
|
await interview.update({ status: 'Completed', outcome: recommendation });
|
|
}
|
|
|
|
res.status(201).json({ success: true, message: 'Evaluation submitted successfully', data: evaluation });
|
|
} catch (error) {
|
|
console.error('Submit evaluation error:', error);
|
|
res.status(500).json({ success: false, message: 'Error submitting evaluation' });
|
|
}
|
|
};
|
|
|
|
export const submitKTMatrix = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { interviewId, criteriaScores, feedback, recommendation } = req.body;
|
|
// criteriaScores: [{ criterionName, score, maxScore, weightage }]
|
|
|
|
const interview = await Interview.findByPk(interviewId);
|
|
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
|
|
|
|
// Calculate total weighted score
|
|
let totalWeightedScore = 0;
|
|
const totalWeightage = criteriaScores.reduce((sum: number, item: any) => sum + item.weightage, 0);
|
|
|
|
const ktMatrixScoresData = criteriaScores.map((item: any) => {
|
|
const weightedScore = (item.score / item.maxScore) * item.weightage;
|
|
totalWeightedScore += weightedScore;
|
|
return {
|
|
criterionName: item.criterionName,
|
|
score: item.score,
|
|
maxScore: item.maxScore,
|
|
weightage: item.weightage,
|
|
weightedScore
|
|
};
|
|
});
|
|
|
|
// Check if evaluation exists for this user and interview, if so update, else create
|
|
let evaluation = await InterviewEvaluation.findOne({
|
|
where: { interviewId, evaluatorId: req.user?.id }
|
|
});
|
|
|
|
if (evaluation) {
|
|
await evaluation.update({
|
|
ktMatrixScore: totalWeightedScore,
|
|
qualitativeFeedback: feedback,
|
|
recommendation
|
|
});
|
|
// Remove old details to replace with new
|
|
await db.KTMatrixScore.destroy({ where: { evaluationId: evaluation.id } });
|
|
} else {
|
|
evaluation = await InterviewEvaluation.create({
|
|
interviewId,
|
|
evaluatorId: req.user?.id,
|
|
ktMatrixScore: totalWeightedScore,
|
|
qualitativeFeedback: feedback,
|
|
recommendation
|
|
});
|
|
}
|
|
|
|
// Bulk create detailed scores
|
|
const scoreRecords = ktMatrixScoresData.map((s: any) => ({
|
|
...s,
|
|
evaluationId: evaluation?.id
|
|
}));
|
|
await db.KTMatrixScore.bulkCreate(scoreRecords);
|
|
|
|
res.status(201).json({ success: true, message: 'KT Matrix submitted successfully', data: evaluation });
|
|
} catch (error) {
|
|
console.error('Submit KT Matrix error:', error);
|
|
res.status(500).json({ success: false, message: 'Error submitting KT Matrix' });
|
|
}
|
|
};
|
|
|
|
export const submitLevel2Feedback = async (req: AuthRequest, res: Response) => {
|
|
try {
|
|
const { interviewId, feedbackItems, recommendation, overallScore } = req.body;
|
|
// feedbackItems: [{ type: 'Strategic Vision', comments: '...' }]
|
|
|
|
const interview = await Interview.findByPk(interviewId);
|
|
if (!interview) return res.status(404).json({ success: false, message: 'Interview not found' });
|
|
|
|
// Check if evaluation exists for this user and interview, if so update, else create
|
|
let evaluation = await InterviewEvaluation.findOne({
|
|
where: { interviewId, evaluatorId: req.user?.id }
|
|
});
|
|
|
|
if (evaluation) {
|
|
await evaluation.update({
|
|
ktMatrixScore: overallScore, // Reusing this field for overall score (check if type matches)
|
|
recommendation
|
|
});
|
|
// Remove old details to replace with new
|
|
await db.InterviewFeedback.destroy({ where: { evaluationId: evaluation.id } });
|
|
} else {
|
|
evaluation = await InterviewEvaluation.create({
|
|
interviewId,
|
|
evaluatorId: req.user?.id,
|
|
ktMatrixScore: overallScore,
|
|
recommendation
|
|
});
|
|
}
|
|
|
|
// Bulk create detailed qualitative feedback
|
|
if (feedbackItems && feedbackItems.length > 0) {
|
|
const feedbackRecords = feedbackItems.map((item: any) => ({
|
|
evaluationId: evaluation?.id,
|
|
feedbackType: item.type,
|
|
comments: item.comments
|
|
}));
|
|
await db.InterviewFeedback.bulkCreate(feedbackRecords);
|
|
}
|
|
|
|
res.status(201).json({ success: true, message: 'Level 2 Feedback submitted successfully', data: evaluation });
|
|
} catch (error) {
|
|
console.error('Submit Level 2 Feedback error:', error);
|
|
res.status(500).json({ success: false, message: 'Error submitting Level 2 Feedback' });
|
|
}
|
|
};
|
|
|
|
// --- AI Summary ---
|
|
|
|
export const getAiSummary = async (req: Request, res: Response) => {
|
|
try {
|
|
const { applicationId } = req.params;
|
|
|
|
const summary = await AiSummary.findOne({
|
|
where: { applicationId },
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
|
|
res.json({ success: true, data: summary });
|
|
} catch (error) {
|
|
console.error('Get AI summary error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching AI summary' });
|
|
}
|
|
};
|
|
|
|
export const getInterviews = async (req: Request, res: Response) => {
|
|
try {
|
|
const { applicationId } = req.params;
|
|
const interviews = await Interview.findAll({
|
|
where: { applicationId },
|
|
include: [
|
|
{
|
|
model: InterviewParticipant,
|
|
as: 'participants',
|
|
include: [{ model: User, as: 'user' }] // Assuming association exists
|
|
},
|
|
{
|
|
model: InterviewEvaluation,
|
|
as: 'evaluations',
|
|
include: [{
|
|
model: User,
|
|
as: 'evaluator',
|
|
attributes: ['id', 'fullName', 'email'],
|
|
include: [{ model: Role, as: 'role', attributes: ['roleName', 'roleCode'] }]
|
|
},
|
|
{
|
|
model: db.InterviewFeedback,
|
|
as: 'feedbackDetails'
|
|
}]
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'scheduler',
|
|
attributes: ['id', 'fullName', 'email', 'designation']
|
|
}
|
|
],
|
|
order: [['createdAt', 'DESC']]
|
|
});
|
|
res.json({ success: true, data: interviews });
|
|
} catch (error) {
|
|
console.error('Get interviews error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching interviews' });
|
|
}
|
|
};
|