enhanced the questionnaraire ui and added upto interview lvel 3 implemented
This commit is contained in:
parent
74202de59b
commit
31109d6109
28
scripts/add-level3-enum.ts
Normal file
28
scripts/add-level3-enum.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import db from '../src/database/models/index.js';
|
||||||
|
|
||||||
|
const updateEnum = async () => {
|
||||||
|
try {
|
||||||
|
console.log('>>> STARTING ENUM MIGRATION (Level 3) <<<');
|
||||||
|
await db.sequelize.authenticate();
|
||||||
|
console.log('Database connection established.');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Approved';`);
|
||||||
|
console.log('Added Level 3 Approved');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Level 3 Approved likely exists or error', e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('>>> SUCCESS: Enum values updated <<<');
|
||||||
|
|
||||||
|
await db.sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('>>> ERROR: Failed to update Enum', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateEnum();
|
||||||
42
scripts/update-enum.ts
Normal file
42
scripts/update-enum.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import db from '../src/database/models/index.js';
|
||||||
|
|
||||||
|
const updateEnum = async () => {
|
||||||
|
try {
|
||||||
|
console.log('>>> STARTING ENUM MIGRATION <<<');
|
||||||
|
await db.sequelize.authenticate();
|
||||||
|
console.log('Database connection established.');
|
||||||
|
|
||||||
|
// Raw query to add values to enum
|
||||||
|
// Note: PostgreSQL cannot remove enum values, only add.
|
||||||
|
// We will add the "Interview Pending" variations.
|
||||||
|
|
||||||
|
const queryInterface = db.sequelize.getQueryInterface();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 1 Interview Pending';`);
|
||||||
|
console.log('Added Level 1 Interview Pending');
|
||||||
|
} catch (e) { console.log('Level 1 Interview Pending likely exists or error', e.message); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 2 Interview Pending';`);
|
||||||
|
console.log('Added Level 2 Interview Pending');
|
||||||
|
} catch (e) { console.log('Level 2 Interview Pending likely exists or error', e.message); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.sequelize.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS 'Level 3 Interview Pending';`);
|
||||||
|
console.log('Added Level 3 Interview Pending');
|
||||||
|
} catch (e) { console.log('Level 3 Interview Pending likely exists or error', e.message); }
|
||||||
|
|
||||||
|
console.log('>>> SUCCESS: Enum values updated <<<');
|
||||||
|
|
||||||
|
await db.sequelize.close();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('>>> ERROR: Failed to update Enum', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateEnum();
|
||||||
@ -49,14 +49,13 @@ export const APPLICATION_STATUS = {
|
|||||||
IN_REVIEW: 'In Review',
|
IN_REVIEW: 'In Review',
|
||||||
APPROVED: 'Approved',
|
APPROVED: 'Approved',
|
||||||
REJECTED: 'Rejected',
|
REJECTED: 'Rejected',
|
||||||
SUBMITTED: 'Submitted',
|
LEVEL_1_PENDING: 'Level 1 Interview Pending',
|
||||||
QUESTIONNAIRE_PENDING: 'Questionnaire Pending',
|
|
||||||
LEVEL_1_PENDING: 'Level 1 Pending',
|
|
||||||
LEVEL_1_APPROVED: 'Level 1 Approved',
|
LEVEL_1_APPROVED: 'Level 1 Approved',
|
||||||
LEVEL_2_PENDING: 'Level 2 Pending',
|
LEVEL_2_PENDING: 'Level 2 Interview Pending',
|
||||||
LEVEL_2_APPROVED: 'Level 2 Approved',
|
LEVEL_2_APPROVED: 'Level 2 Approved',
|
||||||
LEVEL_2_RECOMMENDED: 'Level 2 Recommended',
|
LEVEL_2_RECOMMENDED: 'Level 2 Recommended',
|
||||||
LEVEL_3_PENDING: 'Level 3 Pending',
|
LEVEL_3_PENDING: 'Level 3 Interview Pending',
|
||||||
|
LEVEL_3_APPROVED: 'Level 3 Approved',
|
||||||
FDD_VERIFICATION: 'FDD Verification',
|
FDD_VERIFICATION: 'FDD Verification',
|
||||||
PAYMENT_PENDING: 'Payment Pending',
|
PAYMENT_PENDING: 'Payment Pending',
|
||||||
LOI_ISSUED: 'LOI Issued',
|
LOI_ISSUED: 'LOI Issued',
|
||||||
|
|||||||
@ -19,6 +19,44 @@ export const authenticate = async (req: AuthRequest, res: Response, next: NextFu
|
|||||||
|
|
||||||
const token = authHeader.replace('Bearer ', '');
|
const token = authHeader.replace('Bearer ', '');
|
||||||
|
|
||||||
|
// Mock Prospective User handling
|
||||||
|
// Mock Prospective User handling
|
||||||
|
if (token.startsWith('mock-prospective-token-')) {
|
||||||
|
const appId = token.replace('mock-prospective-token-', '');
|
||||||
|
|
||||||
|
// Validate UUID format to prevent DB errors with legacy tokens
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
if (!uuidRegex.test(appId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid session token. Please login again.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find application to get real details
|
||||||
|
const application = await db.Application.findByPk(appId);
|
||||||
|
|
||||||
|
if (application) {
|
||||||
|
req.user = {
|
||||||
|
id: application.id,
|
||||||
|
email: application.email,
|
||||||
|
firstName: application.applicantName ? application.applicantName.split(' ')[0] : 'Prospective',
|
||||||
|
lastName: application.applicantName ? application.applicantName.split(' ').slice(1).join(' ') : 'User',
|
||||||
|
fullName: application.applicantName,
|
||||||
|
roleCode: 'Prospective Dealer',
|
||||||
|
status: 'active'
|
||||||
|
} as any;
|
||||||
|
req.token = token;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
// If app not found, fall through or error?
|
||||||
|
// Let's error to be safe as the token was specific
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid prospective user session.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Verify token
|
// Verify token
|
||||||
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
|
||||||
|
|
||||||
|
|||||||
63
src/controllers/ProspectiveLoginController.ts
Normal file
63
src/controllers/ProspectiveLoginController.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
export class ProspectiveLoginController {
|
||||||
|
|
||||||
|
static async sendOtp(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { phone } = req.body;
|
||||||
|
|
||||||
|
if (!phone) {
|
||||||
|
return res.status(400).json({ message: 'Phone number is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock logic: In a real app, we would generate a random OTP and send it via SMS
|
||||||
|
// For now, we just return success
|
||||||
|
console.log(`[Mock] OTP request for ${phone}`);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'OTP sent successfully',
|
||||||
|
data: {
|
||||||
|
phone,
|
||||||
|
// In development surfacing the OTP for easier testing
|
||||||
|
mockOtp: '123456'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send OTP error:', error);
|
||||||
|
return res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async verifyOtp(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { phone, otp } = req.body;
|
||||||
|
|
||||||
|
if (!phone || !otp) {
|
||||||
|
return res.status(400).json({ message: 'Phone and OTP are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock logic: Verify OTP
|
||||||
|
if (otp === '123456') {
|
||||||
|
// Mock success response with a fake token
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'OTP verified successfully',
|
||||||
|
data: {
|
||||||
|
token: 'mock-prospective-token-' + Date.now(),
|
||||||
|
user: {
|
||||||
|
id: 'prospective-user-id',
|
||||||
|
phone: phone,
|
||||||
|
role: 'Prospective Dealer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ message: 'Invalid OTP' });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Verify OTP error:', error);
|
||||||
|
return res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ export interface InterviewAttributes {
|
|||||||
interviewType: string;
|
interviewType: string;
|
||||||
linkOrLocation: string | null;
|
linkOrLocation: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
|
scheduledBy: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InterviewInstance extends Model<InterviewAttributes>, InterviewAttributes { }
|
export interface InterviewInstance extends Model<InterviewAttributes>, InterviewAttributes { }
|
||||||
@ -46,6 +47,14 @@ export default (sequelize: Sequelize) => {
|
|||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
defaultValue: 'scheduled'
|
defaultValue: 'scheduled'
|
||||||
|
},
|
||||||
|
scheduledBy: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'interviews',
|
tableName: 'interviews',
|
||||||
@ -54,6 +63,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
|
|
||||||
(Interview as any).associate = (models: any) => {
|
(Interview as any).associate = (models: any) => {
|
||||||
Interview.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
|
Interview.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
|
||||||
|
Interview.belongsTo(models.User, { foreignKey: 'scheduledBy', as: 'scheduler' });
|
||||||
Interview.hasMany(models.InterviewParticipant, { foreignKey: 'interviewId', as: 'participants' });
|
Interview.hasMany(models.InterviewParticipant, { foreignKey: 'interviewId', as: 'participants' });
|
||||||
Interview.hasMany(models.InterviewEvaluation, { foreignKey: 'interviewId', as: 'evaluations' });
|
Interview.hasMany(models.InterviewEvaluation, { foreignKey: 'interviewId', as: 'evaluations' });
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,12 +10,12 @@ import { ROLES } from '../../common/config/constants.js';
|
|||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
|
|
||||||
// Roles
|
// Roles
|
||||||
router.get('/roles', adminController.getRoles);
|
router.get('/roles', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.getRoles);
|
||||||
router.post('/roles', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.createRole);
|
router.post('/roles', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.createRole);
|
||||||
router.put('/roles/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateRole);
|
router.put('/roles/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateRole);
|
||||||
|
|
||||||
// Permissions
|
// Permissions
|
||||||
router.get('/permissions', adminController.getPermissions);
|
router.get('/permissions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.getPermissions);
|
||||||
|
|
||||||
// Users (Admin View)
|
// Users (Admin View)
|
||||||
router.post('/users', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.createUser);
|
router.post('/users', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.createUser);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
|||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const {
|
const {
|
||||||
Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireScore,
|
Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireScore,
|
||||||
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User
|
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User, RequestParticipant, Role
|
||||||
} = db;
|
} = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
@ -68,25 +68,60 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
|
|||||||
|
|
||||||
export const scheduleInterview = async (req: AuthRequest, res: Response) => {
|
export const scheduleInterview = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
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]
|
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({
|
const interview = await Interview.create({
|
||||||
applicationId,
|
applicationId,
|
||||||
level,
|
level: levelNum || 1, // Default to 1 if parsing fails
|
||||||
scheduledAt,
|
scheduleDate: new Date(scheduledAt),
|
||||||
type,
|
interviewType: type,
|
||||||
location,
|
linkOrLocation: location,
|
||||||
status: 'Scheduled',
|
status: 'Scheduled',
|
||||||
scheduledBy: req.user?.id
|
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) {
|
if (participants && participants.length > 0) {
|
||||||
|
console.log(`Processing ${participants.length} participants...`);
|
||||||
for (const userId of participants) {
|
for (const userId of participants) {
|
||||||
|
// 1. Add to Panel
|
||||||
await InterviewParticipant.create({
|
await InterviewParticipant.create({
|
||||||
interviewId: interview.id,
|
interviewId: interview.id,
|
||||||
userId,
|
userId,
|
||||||
role: 'Panelist'
|
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'
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,10 +152,13 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Interview scheduling completed successfully.');
|
||||||
res.status(201).json({ success: true, message: 'Interview scheduled successfully', data: interview });
|
res.status(201).json({ success: true, message: 'Interview scheduled successfully', data: interview });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Schedule interview error:', error);
|
console.error('CRITICAL ERROR in scheduleInterview:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error scheduling interview' });
|
// 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) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -152,8 +190,8 @@ export const submitEvaluation = async (req: AuthRequest, res: Response) => {
|
|||||||
const evaluation = await InterviewEvaluation.create({
|
const evaluation = await InterviewEvaluation.create({
|
||||||
interviewId: id,
|
interviewId: id,
|
||||||
evaluatorId: req.user?.id,
|
evaluatorId: req.user?.id,
|
||||||
ktScore,
|
ktMatrixScore: ktScore,
|
||||||
feedback,
|
qualitativeFeedback: feedback,
|
||||||
recommendation
|
recommendation
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -169,6 +207,113 @@ export const submitEvaluation = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 ---
|
// --- AI Summary ---
|
||||||
|
|
||||||
export const getAiSummary = async (req: Request, res: Response) => {
|
export const getAiSummary = async (req: Request, res: Response) => {
|
||||||
@ -186,3 +331,43 @@ export const getAiSummary = async (req: Request, res: Response) => {
|
|||||||
res.status(500).json({ success: false, message: 'Error fetching AI summary' });
|
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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -13,6 +13,9 @@ router.post('/questionnaire/response', assessmentController.submitQuestionnaireR
|
|||||||
router.post('/interviews', assessmentController.scheduleInterview);
|
router.post('/interviews', assessmentController.scheduleInterview);
|
||||||
router.put('/interviews/:id', assessmentController.updateInterview);
|
router.put('/interviews/:id', assessmentController.updateInterview);
|
||||||
router.post('/interviews/:id/evaluation', assessmentController.submitEvaluation);
|
router.post('/interviews/:id/evaluation', assessmentController.submitEvaluation);
|
||||||
|
router.get('/interviews/:applicationId', assessmentController.getInterviews);
|
||||||
|
router.post('/kt-matrix', assessmentController.submitKTMatrix);
|
||||||
|
router.post('/level2-feedback', assessmentController.submitLevel2Feedback);
|
||||||
|
|
||||||
// AI Summary
|
// AI Summary
|
||||||
router.get('/ai-summary/:applicationId', assessmentController.getAiSummary);
|
router.get('/ai-summary/:applicationId', assessmentController.getAiSummary);
|
||||||
|
|||||||
@ -147,6 +147,21 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
|
|||||||
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mock Prospective User handling
|
||||||
|
if (req.user.roleCode === 'Prospective Dealer') {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
id: req.user.id,
|
||||||
|
email: req.user.email,
|
||||||
|
fullName: req.user.fullName,
|
||||||
|
role: 'Prospective Dealer',
|
||||||
|
phone: (req.user as any).mobileNumber || (req.user as any).phone,
|
||||||
|
createdAt: (req.user as any).createdAt || new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.findByPk(req.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
attributes: ['id', 'email', 'fullName', 'roleCode', 'regionId', 'zoneId', 'mobileNumber', 'createdAt']
|
attributes: ['id', 'email', 'fullName', 'roleCode', 'regionId', 'zoneId', 'mobileNumber', 'createdAt']
|
||||||
});
|
});
|
||||||
|
|||||||
@ -143,10 +143,17 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApplications = async (req: Request, res: Response) => {
|
export const getApplications = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Add filtering logic here similar to Opportunity
|
const whereClause: any = {};
|
||||||
|
|
||||||
|
// Security Check: If prospective dealer, only show their own application
|
||||||
|
if (req.user?.roleCode === 'Prospective Dealer') {
|
||||||
|
whereClause.email = req.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
const applications = await Application.findAll({
|
const applications = await Application.findAll({
|
||||||
|
where: whereClause,
|
||||||
include: [{ model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] }],
|
include: [{ model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
@ -158,7 +165,7 @@ export const getApplications = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApplicationById = async (req: Request, res: Response) => {
|
export const getApplicationById = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
@ -195,6 +202,11 @@ export const getApplicationById = async (req: Request, res: Response) => {
|
|||||||
return res.status(404).json({ success: false, message: 'Application not found' });
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Security Check: Ensure prospective dealer controls data ownership
|
||||||
|
if (req.user?.roleCode === 'Prospective Dealer' && application.email !== req.user.email) {
|
||||||
|
return res.status(403).json({ success: false, message: 'Forbidden: You do not have permission to view this application' });
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ success: true, data: application });
|
res.json({ success: true, data: application });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get application error:', error);
|
console.error('Get application error:', error);
|
||||||
@ -242,14 +254,93 @@ export const updateApplicationStatus = async (req: AuthRequest, res: Response) =
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadDocuments = async (req: Request, res: Response) => {
|
export const uploadDocuments = async (req: any, res: Response) => {
|
||||||
// Existing logic or enhanced to use Document model
|
|
||||||
// For now, keeping simple or stubbing.
|
|
||||||
try {
|
try {
|
||||||
// This should likely use the new Document modules/models later
|
const { id } = req.params;
|
||||||
res.json({ success: true, message: 'Use Document module for uploads' });
|
const { documentType } = req.body;
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!documentType) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Document type is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const application = await Application.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id },
|
||||||
|
{ applicationId: id }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Document Record
|
||||||
|
const newDoc = await db.Document.create({
|
||||||
|
applicationId: application.id,
|
||||||
|
requestId: application.id,
|
||||||
|
requestType: 'application',
|
||||||
|
documentType,
|
||||||
|
fileName: file.originalname,
|
||||||
|
filePath: file.path, // Store relative path or full path as needed by your storage strategy
|
||||||
|
fileSize: file.size,
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
// For prospective users (who are applications, not in Users table), set uploadedBy to null to avoid FK violation
|
||||||
|
uploadedBy: req.user?.roleCode === 'Prospective Dealer' ? null : req.user?.id,
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Document uploaded successfully',
|
||||||
|
data: newDoc
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ success: false, message: 'Error' });
|
console.error('Upload document error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error uploading document' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApplicationDocuments = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
// Resolve ID to primary key if it's an appId string
|
||||||
|
const application = await Application.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id },
|
||||||
|
{ applicationId: id }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const documents = await db.Document.findAll({
|
||||||
|
where: {
|
||||||
|
requestId: application.id,
|
||||||
|
requestType: 'application',
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{ model: db.User, as: 'uploader', attributes: ['fullName'] }
|
||||||
|
],
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, data: documents });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get documents error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error fetching documents' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ const router = express.Router();
|
|||||||
import * as onboardingController from './onboarding.controller.js';
|
import * as onboardingController from './onboarding.controller.js';
|
||||||
import { authenticate } from '../../common/middleware/auth.js';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
|
|
||||||
|
import { uploadSingle } from '../../common/middleware/upload.js';
|
||||||
|
|
||||||
// All routes require authentication (or public for submission? Keeping auth for now)
|
// All routes require authentication (or public for submission? Keeping auth for now)
|
||||||
// Public route for application submission
|
// Public route for application submission
|
||||||
router.post('/apply', onboardingController.submitApplication);
|
router.post('/apply', onboardingController.submitApplication);
|
||||||
@ -15,7 +17,8 @@ router.post('/applications/shortlist', onboardingController.bulkShortlist);
|
|||||||
router.get('/applications/:id', onboardingController.getApplicationById);
|
router.get('/applications/:id', onboardingController.getApplicationById);
|
||||||
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
||||||
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
router.put('/applications/:id/status', onboardingController.updateApplicationStatus);
|
||||||
// router.post('/applications/:id/documents', onboardingController.uploadDocuments); // Moving to DMS module
|
router.post('/applications/:id/documents', uploadSingle, onboardingController.uploadDocuments);
|
||||||
|
router.get('/applications/:id/documents', onboardingController.getApplicationDocuments);
|
||||||
|
|
||||||
// Questionnaire Routes
|
// Questionnaire Routes
|
||||||
router.get('/questionnaires', (req, res, next) => {
|
router.get('/questionnaires', (req, res, next) => {
|
||||||
|
|||||||
101
src/modules/prospective-login/prospective-login.controller.ts
Normal file
101
src/modules/prospective-login/prospective-login.controller.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import db from '../../database/models';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
// Mock secret for now, should be in env
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret';
|
||||||
|
|
||||||
|
export class ProspectiveLoginController {
|
||||||
|
|
||||||
|
static async sendOtp(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { phone } = req.body;
|
||||||
|
|
||||||
|
if (!phone) {
|
||||||
|
return res.status(400).json({ message: 'Phone number is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[ProspectiveLogin] Received OTP request for phone: '${phone}'`);
|
||||||
|
|
||||||
|
// Check if application exists and is shortlisted
|
||||||
|
const application = await db.Application.findOne({
|
||||||
|
where: { phone: phone }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[ProspectiveLogin] DB Search Result:`, application ? `Found AppId: ${application.id}, Shortlisted: ${application.isShortlisted}, DDLeadShortlisted: ${application.ddLeadShortlisted}` : 'Not Found');
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
console.log(`[ProspectiveLogin] Application not found for ${phone}, returning 404`);
|
||||||
|
return res.status(404).json({ message: 'No application found with this phone number' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!application.isShortlisted && !application.ddLeadShortlisted) {
|
||||||
|
console.log(`[ProspectiveLogin] Application found but not shortlisted`);
|
||||||
|
return res.status(403).json({ message: 'Your application is under review. You can login only after shortlisting.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock logic: In a real app, we would generate a random OTP and send it via SMS
|
||||||
|
console.log(`[Mock] OTP request for ${phone}`);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'OTP sent successfully',
|
||||||
|
data: {
|
||||||
|
phone,
|
||||||
|
mockOtp: '123456'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send OTP error:', error);
|
||||||
|
return res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async verifyOtp(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { phone, otp } = req.body;
|
||||||
|
|
||||||
|
if (!phone || !otp) {
|
||||||
|
return res.status(400).json({ message: 'Phone and OTP are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otp === '123456') {
|
||||||
|
// Fetch application again to get details
|
||||||
|
const application = await db.Application.findOne({
|
||||||
|
where: { phone: phone }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application) {
|
||||||
|
return res.status(404).json({ message: 'Application not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a real token or a mock one that Auth middleware accepts
|
||||||
|
// Using the specific mock token format for now to bypass standard Auth middleware db check
|
||||||
|
// if it's strict, or we can issue a real JWT if `strategies` allow it.
|
||||||
|
// Reverting to the mock token format we established:
|
||||||
|
const token = 'mock-prospective-token-' + application.id;
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: 'OTP verified successfully',
|
||||||
|
data: {
|
||||||
|
token: token,
|
||||||
|
user: {
|
||||||
|
id: application.id, // Use application ID as user ID for prospective
|
||||||
|
name: application.applicantName,
|
||||||
|
email: application.email,
|
||||||
|
phone: application.phone,
|
||||||
|
role: 'Prospective Dealer',
|
||||||
|
applicationId: application.applicationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ message: 'Invalid OTP' });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Verify OTP error:', error);
|
||||||
|
return res.status(500).json({ message: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { ProspectiveLoginController } from './prospective-login.controller.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/send-otp', ProspectiveLoginController.sendOtp);
|
||||||
|
router.post('/verify-otp', ProspectiveLoginController.verifyOtp);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -32,6 +32,7 @@ import dealerRoutes from './modules/dealer/dealer.routes.js';
|
|||||||
import slaRoutes from './modules/sla/sla.routes.js';
|
import slaRoutes from './modules/sla/sla.routes.js';
|
||||||
import communicationRoutes from './modules/communication/communication.routes.js';
|
import communicationRoutes from './modules/communication/communication.routes.js';
|
||||||
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
|
import questionnaireRoutes from './modules/onboarding/questionnaire.routes.js';
|
||||||
|
import prospectiveLoginRoutes from './modules/prospective-login/prospective-login.routes.js';
|
||||||
|
|
||||||
// Import common middleware & utils
|
// Import common middleware & utils
|
||||||
import errorHandler from './common/middleware/errorHandler.js';
|
import errorHandler from './common/middleware/errorHandler.js';
|
||||||
@ -106,6 +107,7 @@ app.use('/api/dealer', dealerRoutes);
|
|||||||
app.use('/api/sla', slaRoutes);
|
app.use('/api/sla', slaRoutes);
|
||||||
app.use('/api/communication', communicationRoutes);
|
app.use('/api/communication', communicationRoutes);
|
||||||
app.use('/api/questionnaire', questionnaireRoutes);
|
app.use('/api/questionnaire', questionnaireRoutes);
|
||||||
|
app.use('/api/prospective-login', prospectiveLoginRoutes);
|
||||||
|
|
||||||
// Backward Compatibility Aliases
|
// Backward Compatibility Aliases
|
||||||
app.use('/api/applications', onboardingRoutes);
|
app.use('/api/applications', onboardingRoutes);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user