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',
|
||||
APPROVED: 'Approved',
|
||||
REJECTED: 'Rejected',
|
||||
SUBMITTED: 'Submitted',
|
||||
QUESTIONNAIRE_PENDING: 'Questionnaire Pending',
|
||||
LEVEL_1_PENDING: 'Level 1 Pending',
|
||||
LEVEL_1_PENDING: 'Level 1 Interview Pending',
|
||||
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_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',
|
||||
PAYMENT_PENDING: 'Payment Pending',
|
||||
LOI_ISSUED: 'LOI Issued',
|
||||
|
||||
@ -19,6 +19,44 @@ export const authenticate = async (req: AuthRequest, res: Response, next: NextFu
|
||||
|
||||
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
|
||||
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;
|
||||
linkOrLocation: string | null;
|
||||
status: string;
|
||||
scheduledBy: string | null;
|
||||
}
|
||||
|
||||
export interface InterviewInstance extends Model<InterviewAttributes>, InterviewAttributes { }
|
||||
@ -46,6 +47,14 @@ export default (sequelize: Sequelize) => {
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'scheduled'
|
||||
},
|
||||
scheduledBy: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
tableName: 'interviews',
|
||||
@ -54,6 +63,7 @@ export default (sequelize: Sequelize) => {
|
||||
|
||||
(Interview as any).associate = (models: any) => {
|
||||
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.InterviewEvaluation, { foreignKey: 'interviewId', as: 'evaluations' });
|
||||
};
|
||||
|
||||
@ -10,12 +10,12 @@ import { ROLES } from '../../common/config/constants.js';
|
||||
router.use(authenticate as any);
|
||||
|
||||
// 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.put('/roles/:id', checkRole([ROLES.SUPER_ADMIN]) as any, adminController.updateRole);
|
||||
|
||||
// Permissions
|
||||
router.get('/permissions', adminController.getPermissions);
|
||||
router.get('/permissions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, adminController.getPermissions);
|
||||
|
||||
// Users (Admin View)
|
||||
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';
|
||||
const {
|
||||
Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireScore,
|
||||
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User
|
||||
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User, RequestParticipant, Role
|
||||
} = db;
|
||||
import { AuthRequest } from '../../types/express.types.js';
|
||||
import { Op } from 'sequelize';
|
||||
@ -68,25 +68,60 @@ export const submitQuestionnaireResponse = async (req: AuthRequest, res: Respons
|
||||
|
||||
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,
|
||||
scheduledAt,
|
||||
type,
|
||||
location,
|
||||
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'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
} catch (error) {
|
||||
console.error('Schedule interview error:', error);
|
||||
res.status(500).json({ success: false, message: 'Error scheduling interview' });
|
||||
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) });
|
||||
}
|
||||
};
|
||||
|
||||
@ -152,8 +190,8 @@ export const submitEvaluation = async (req: AuthRequest, res: Response) => {
|
||||
const evaluation = await InterviewEvaluation.create({
|
||||
interviewId: id,
|
||||
evaluatorId: req.user?.id,
|
||||
ktScore,
|
||||
feedback,
|
||||
ktMatrixScore: ktScore,
|
||||
qualitativeFeedback: feedback,
|
||||
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 ---
|
||||
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
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.put('/interviews/:id', assessmentController.updateInterview);
|
||||
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
|
||||
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' });
|
||||
}
|
||||
|
||||
// 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, {
|
||||
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 {
|
||||
// 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({
|
||||
where: whereClause,
|
||||
include: [{ model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] }],
|
||||
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 {
|
||||
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' });
|
||||
}
|
||||
|
||||
// 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 });
|
||||
} catch (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) => {
|
||||
// Existing logic or enhanced to use Document model
|
||||
// For now, keeping simple or stubbing.
|
||||
export const uploadDocuments = async (req: any, res: Response) => {
|
||||
try {
|
||||
// This should likely use the new Document modules/models later
|
||||
res.json({ success: true, message: 'Use Document module for uploads' });
|
||||
const { id } = req.params;
|
||||
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) {
|
||||
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 { 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)
|
||||
// Public route for application submission
|
||||
router.post('/apply', onboardingController.submitApplication);
|
||||
@ -15,7 +17,8 @@ router.post('/applications/shortlist', onboardingController.bulkShortlist);
|
||||
router.get('/applications/:id', onboardingController.getApplicationById);
|
||||
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
|
||||
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 communicationRoutes from './modules/communication/communication.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 errorHandler from './common/middleware/errorHandler.js';
|
||||
@ -106,6 +107,7 @@ app.use('/api/dealer', dealerRoutes);
|
||||
app.use('/api/sla', slaRoutes);
|
||||
app.use('/api/communication', communicationRoutes);
|
||||
app.use('/api/questionnaire', questionnaireRoutes);
|
||||
app.use('/api/prospective-login', prospectiveLoginRoutes);
|
||||
|
||||
// Backward Compatibility Aliases
|
||||
app.use('/api/applications', onboardingRoutes);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user