import { Request, Response } from 'express'; import db from '../../database/models/index.js'; const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone, Area } = db; import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js'; import { v4 as uuidv4 } from 'uuid'; import { Op } from 'sequelize'; import { AuthRequest } from '../../types/express.types.js'; import { sendOpportunityEmail, sendNonOpportunityEmail } from '../../common/utils/email.service.js'; export const submitApplication = async (req: AuthRequest, res: Response) => { try { const { opportunityId, applicantName, email, phone, businessType, locationType, preferredLocation, city, state, experienceYears, investmentCapacity, age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode } = req.body; // Check for duplicate email const existingApp = await Application.findOne({ where: { email } }); if (existingApp) { return res.status(400).json({ success: false, message: 'Application with this email already exists' }); } const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`; // Fetch hierarchy from Auto-detected Area let zoneId, regionId, areaId; let isOpportunityAvailable = false; // Auto-detect Area from District if (req.body.district) { const districtName = req.body.district; // 1. Find District ID by Name const districtRecord = await District.findOne({ where: { districtName: { [Op.iLike]: districtName } } }); if (districtRecord) { // 2. Find Active Area for this District const today = new Date(); const validArea = await Area.findOne({ where: { districtId: districtRecord.id, isActive: true, [Op.and]: [ { [Op.or]: [ { activeFrom: { [Op.eq]: null } }, { activeFrom: { [Op.lte]: today } } ] }, { [Op.or]: [ { activeTo: { [Op.eq]: null } }, { activeTo: { [Op.gte]: today } } ] } ] } }); if (validArea) { areaId = validArea.id; zoneId = validArea.zoneId; regionId = validArea.regionId; isOpportunityAvailable = true; console.log(`[Auto-Match] Found Active Area ${validArea.areaName} for District: ${districtName}`); } } } // Determine Initial Status let initialStatus = isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED; // Auto-assign Zone/Region from District if still null (even if no opportunity found) if (!zoneId && req.body.district) { const districtRecord = await District.findOne({ where: { districtName: { [Op.iLike]: req.body.district } }, include: [{ model: Region, as: 'region' }, { model: Zone, as: 'zone' }] }); if (districtRecord) { regionId = districtRecord.regionId; zoneId = districtRecord.zoneId; } } const application = await Application.create({ opportunityId: null, // De-coupled from Opportunity table as per user request applicationId, applicantName, email, phone, businessType, preferredLocation, city, state, experienceYears, investmentCapacity, age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, currentStage: APPLICATION_STAGES.DD, overallStatus: initialStatus, progressPercentage: isOpportunityAvailable ? 10 : 0, zoneId, regionId, areaId // Link to Area }); // Log Status History await ApplicationStatusHistory.create({ applicationId: application.id, previousStatus: null, newStatus: initialStatus, changedBy: req.user?.id || null, reason: 'Initial Submission' }); // Send Email (Async) if (isOpportunityAvailable) { sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId) .catch(err => console.error('Error sending opportunity email', err)); } else { sendNonOpportunityEmail(email, applicantName, city || preferredLocation) .catch(err => console.error('Error sending non-opportunity email', err)); } await AuditLog.create({ userId: req.user?.id, action: AUDIT_ACTIONS.CREATED, entityType: 'application', entityId: application.id }); res.status(201).json({ success: true, message: 'Application submitted successfully', data: application }); } catch (error) { console.error('Submit application error:', error); res.status(500).json({ success: false, message: 'Error submitting application' }); } }; export const getApplications = async (req: Request, res: Response) => { try { // Add filtering logic here similar to Opportunity const applications = await Application.findAll({ include: [{ model: Opportunity, as: 'opportunity', attributes: ['opportunityType', 'city', 'id'] }], order: [['createdAt', 'DESC']] }); res.json({ success: true, data: applications }); } catch (error) { console.error('Get applications error:', error); res.status(500).json({ success: false, message: 'Error fetching applications' }); } }; export const getApplicationById = async (req: Request, res: Response) => { try { const { id } = req.params; const application = await Application.findOne({ where: { [Op.or]: [ { id }, { applicationId: id } ] }, include: [ { model: ApplicationStatusHistory, as: 'statusHistory' }, { model: ApplicationProgress, as: 'progressTracking' }, { model: db.QuestionnaireResponse, as: 'questionnaireResponses', include: [ { model: db.QuestionnaireQuestion, as: 'question', include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }] } ] } ] }); if (!application) { return res.status(404).json({ success: false, message: 'Application not found' }); } res.json({ success: true, data: application }); } catch (error) { console.error('Get application error:', error); res.status(500).json({ success: false, message: 'Error fetching application' }); } }; export const updateApplicationStatus = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { status, stage, reason } = req.body; const application = await Application.findByPk(id); if (!application) return res.status(404).json({ success: false, message: 'Application not found' }); const previousStatus = application.overallStatus; await application.update({ overallStatus: status, currentStage: stage || application.currentStage, updatedAt: new Date() }); // Log Status History await ApplicationStatusHistory.create({ applicationId: application.id, previousStatus, newStatus: status, changedBy: req.user?.id, reason }); await AuditLog.create({ userId: req.user?.id, action: AUDIT_ACTIONS.UPDATED, entityType: 'application', entityId: application.id, newData: { status, stage } }); res.json({ success: true, message: 'Application status updated successfully' }); } catch (error) { console.error('Update application status error:', error); res.status(500).json({ success: false, message: 'Error updating application status' }); } }; export const uploadDocuments = async (req: Request, res: Response) => { // Existing logic or enhanced to use Document model // For now, keeping simple or stubbing. try { // This should likely use the new Document modules/models later res.json({ success: true, message: 'Use Document module for uploads' }); } catch (error) { res.status(500).json({ success: false, message: 'Error' }); } }; export const bulkShortlist = async (req: AuthRequest, res: Response) => { try { const { applicationIds, assignedTo, remarks } = req.body; if (!applicationIds || !Array.isArray(applicationIds) || applicationIds.length === 0) { return res.status(400).json({ success: false, message: 'No applications selected' }); } // assignedTo is expected to be an array of User IDs from frontend now // But database only supports single UUID for assignedTo. // Strategy: Assign the first user as primary assignee. const primaryAssigneeId = Array.isArray(assignedTo) && assignedTo.length > 0 ? assignedTo[0] : null; // Verify primaryAssigneeId is a valid UUID if strictly enforced by DB, but Sequelize might handle null. // Update Applications const updateData: any = { ddLeadShortlisted: true, overallStatus: 'Shortlisted', updatedAt: new Date(), }; if (primaryAssigneeId) { updateData.assignedTo = primaryAssigneeId; } await Application.update(updateData, { where: { id: { [Op.in]: applicationIds } } }); // Create Status History Entries const historyEntries = applicationIds.map(appId => ({ applicationId: appId, previousStatus: 'Questionnaire Completed', newStatus: 'Shortlisted', changedBy: req.user?.id, reason: remarks ? `${remarks} (Assignees: ${Array.isArray(assignedTo) ? assignedTo.join(', ') : assignedTo})` : 'Bulk Shortlist' })); await ApplicationStatusHistory.bulkCreate(historyEntries); // Audit Log const auditEntries = applicationIds.map(appId => ({ userId: req.user?.id, action: AUDIT_ACTIONS.UPDATED, entityType: 'application', entityId: appId, newData: { ddLeadShortlisted: true, assignedTo: primaryAssigneeId, remarks } })); await AuditLog.bulkCreate(auditEntries); res.json({ success: true, message: `Successfully shortlisted ${applicationIds.length} application(s)` }); } catch (error) { console.error('Bulk shortlist error:', error); res.status(500).json({ success: false, message: 'Error processing shortlist' }); } };