313 lines
12 KiB
TypeScript
313 lines
12 KiB
TypeScript
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' });
|
|
}
|
|
};
|