From 31109d6109e3109848ee3313bbd738f65a27cdbd Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Wed, 18 Feb 2026 20:03:42 +0530 Subject: [PATCH] enhanced the questionnaraire ui and added upto interview lvel 3 implemented --- scripts/add-level3-enum.ts | 28 +++ scripts/update-enum.ts | 42 ++++ src/common/config/constants.ts | 9 +- src/common/middleware/auth.ts | 38 ++++ src/controllers/ProspectiveLoginController.ts | 63 ++++++ src/database/models/Interview.ts | 10 + src/modules/admin/admin.routes.ts | 4 +- .../assessment/assessment.controller.ts | 203 +++++++++++++++++- src/modules/assessment/assessment.routes.ts | 3 + src/modules/auth/auth.controller.ts | 15 ++ .../onboarding/onboarding.controller.ts | 109 +++++++++- src/modules/onboarding/onboarding.routes.ts | 5 +- .../prospective-login.controller.ts | 101 +++++++++ .../prospective-login.routes.ts | 9 + src/server.ts | 2 + 15 files changed, 615 insertions(+), 26 deletions(-) create mode 100644 scripts/add-level3-enum.ts create mode 100644 scripts/update-enum.ts create mode 100644 src/controllers/ProspectiveLoginController.ts create mode 100644 src/modules/prospective-login/prospective-login.controller.ts create mode 100644 src/modules/prospective-login/prospective-login.routes.ts diff --git a/scripts/add-level3-enum.ts b/scripts/add-level3-enum.ts new file mode 100644 index 0000000..0611898 --- /dev/null +++ b/scripts/add-level3-enum.ts @@ -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(); diff --git a/scripts/update-enum.ts b/scripts/update-enum.ts new file mode 100644 index 0000000..c18dece --- /dev/null +++ b/scripts/update-enum.ts @@ -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(); diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index 5ef078f..5d1ec6a 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -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', diff --git a/src/common/middleware/auth.ts b/src/common/middleware/auth.ts index 04b21c8..f30554c 100644 --- a/src/common/middleware/auth.ts +++ b/src/common/middleware/auth.ts @@ -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 }; diff --git a/src/controllers/ProspectiveLoginController.ts b/src/controllers/ProspectiveLoginController.ts new file mode 100644 index 0000000..aef13dc --- /dev/null +++ b/src/controllers/ProspectiveLoginController.ts @@ -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' }); + } + } +} diff --git a/src/database/models/Interview.ts b/src/database/models/Interview.ts index e08b10b..a50db82 100644 --- a/src/database/models/Interview.ts +++ b/src/database/models/Interview.ts @@ -8,6 +8,7 @@ export interface InterviewAttributes { interviewType: string; linkOrLocation: string | null; status: string; + scheduledBy: string | null; } export interface InterviewInstance extends Model, 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' }); }; diff --git a/src/modules/admin/admin.routes.ts b/src/modules/admin/admin.routes.ts index bcfef5a..36ba09a 100644 --- a/src/modules/admin/admin.routes.ts +++ b/src/modules/admin/admin.routes.ts @@ -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); diff --git a/src/modules/assessment/assessment.controller.ts b/src/modules/assessment/assessment.controller.ts index f4af7a5..8ad5af8 100644 --- a/src/modules/assessment/assessment.controller.ts +++ b/src/modules/assessment/assessment.controller.ts @@ -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' }); + } +}; diff --git a/src/modules/assessment/assessment.routes.ts b/src/modules/assessment/assessment.routes.ts index 085f6df..3f7737f 100644 --- a/src/modules/assessment/assessment.routes.ts +++ b/src/modules/assessment/assessment.routes.ts @@ -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); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 8359793..7fc1974 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -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'] }); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index 8d18e22..571953f 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -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' }); } }; diff --git a/src/modules/onboarding/onboarding.routes.ts b/src/modules/onboarding/onboarding.routes.ts index d20ed5d..01ad244 100644 --- a/src/modules/onboarding/onboarding.routes.ts +++ b/src/modules/onboarding/onboarding.routes.ts @@ -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) => { diff --git a/src/modules/prospective-login/prospective-login.controller.ts b/src/modules/prospective-login/prospective-login.controller.ts new file mode 100644 index 0000000..4167c84 --- /dev/null +++ b/src/modules/prospective-login/prospective-login.controller.ts @@ -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' }); + } + } +} diff --git a/src/modules/prospective-login/prospective-login.routes.ts b/src/modules/prospective-login/prospective-login.routes.ts new file mode 100644 index 0000000..c3aa07f --- /dev/null +++ b/src/modules/prospective-login/prospective-login.routes.ts @@ -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; diff --git a/src/server.ts b/src/server.ts index 8f05cf5..fb84302 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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);