email emplate added fo opportunity and non opportunity and new enums added for the application status
This commit is contained in:
parent
5959a6a225
commit
859416a9a9
49
debug_opp_bijapur.js
Normal file
49
debug_opp_bijapur.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import pg from 'pg';
|
||||||
|
const { Client } = pg;
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
user: 'laxman',
|
||||||
|
host: 'localhost',
|
||||||
|
database: 'royal_enfield_onboarding',
|
||||||
|
password: 'Admin@123',
|
||||||
|
port: 5432,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function debugOpportunity() {
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const districtName = 'Bijapur';
|
||||||
|
console.log(`Checking for District: ${districtName}`);
|
||||||
|
|
||||||
|
// Note: ILIKE is Postgres specific, checking matches
|
||||||
|
const districtRes = await client.query(`SELECT * FROM districts WHERE "districtName" ILIKE $1`, [`%${districtName}%`]);
|
||||||
|
|
||||||
|
if (districtRes.rows.length === 0) {
|
||||||
|
console.log('No district found matching "Bijapur"');
|
||||||
|
} else {
|
||||||
|
console.log(`Found ${districtRes.rows.length} districts:`);
|
||||||
|
for (const d of districtRes.rows) {
|
||||||
|
console.log(`- ID: ${d.id}, Name: ${d.districtName}, StateID: ${d.stateId}`);
|
||||||
|
|
||||||
|
// Check opportunities
|
||||||
|
const oppRes = await client.query(`SELECT * FROM opportunities WHERE "districtId" = $1`, [d.id]);
|
||||||
|
if (oppRes.rows.length === 0) {
|
||||||
|
console.log(` -> No opportunities found for this district.`);
|
||||||
|
} else {
|
||||||
|
for (const o of oppRes.rows) {
|
||||||
|
console.log(` -> Opportunity Found: ID=${o.id}, Status=${o.status}, Type=${o.opportunityType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugOpportunity();
|
||||||
@ -8,6 +8,48 @@ erDiagram
|
|||||||
%% ============================================
|
%% ============================================
|
||||||
%% USER MANAGEMENT & AUTHENTICATION
|
%% USER MANAGEMENT & AUTHENTICATION
|
||||||
%% ============================================
|
%% ============================================
|
||||||
|
%% ============================================
|
||||||
|
%% FINANCE & PAYMENTS
|
||||||
|
%% ============================================
|
||||||
|
FINANCE_PAYMENTS {
|
||||||
|
uuid payment_id PK
|
||||||
|
uuid application_id FK
|
||||||
|
string payment_type
|
||||||
|
decimal amount
|
||||||
|
string payment_status
|
||||||
|
string transaction_id
|
||||||
|
date payment_date
|
||||||
|
uuid verified_by FK
|
||||||
|
timestamp verification_date
|
||||||
|
text remarks
|
||||||
|
timestamp created_at
|
||||||
|
timestamp updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
%% ============================================
|
||||||
|
%% EXIT FEEDBACK & SLA BREACHES
|
||||||
|
%% ============================================
|
||||||
|
EXIT_FEEDBACK {
|
||||||
|
uuid feedback_id PK
|
||||||
|
uuid resignation_id FK
|
||||||
|
uuid termination_request_id FK
|
||||||
|
string feedback_type
|
||||||
|
json ratings
|
||||||
|
text comments
|
||||||
|
timestamp submitted_at
|
||||||
|
uuid submitted_by FK
|
||||||
|
}
|
||||||
|
|
||||||
|
SLA_BREACHES {
|
||||||
|
uuid breach_id PK
|
||||||
|
uuid tracking_id FK
|
||||||
|
timestamp breached_at
|
||||||
|
string notified_to
|
||||||
|
string status
|
||||||
|
text action_taken
|
||||||
|
timestamp created_at
|
||||||
|
}
|
||||||
|
|
||||||
USERS {
|
USERS {
|
||||||
uuid user_id PK
|
uuid user_id PK
|
||||||
string employee_id UK
|
string employee_id UK
|
||||||
@ -614,6 +656,29 @@ erDiagram
|
|||||||
timestamp updated_at
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%% ============================================
|
||||||
|
%% OUTLET MANAGEMENT
|
||||||
|
%% ============================================
|
||||||
|
OUTLETS {
|
||||||
|
uuid outlet_id PK
|
||||||
|
string code UK
|
||||||
|
string name
|
||||||
|
string type
|
||||||
|
text address
|
||||||
|
string city
|
||||||
|
string state
|
||||||
|
string pincode
|
||||||
|
decimal latitude
|
||||||
|
decimal longitude
|
||||||
|
string status
|
||||||
|
date established_date
|
||||||
|
uuid dealer_id FK
|
||||||
|
string region
|
||||||
|
string zone
|
||||||
|
timestamp created_at
|
||||||
|
timestamp updated_at
|
||||||
|
}
|
||||||
|
|
||||||
%% ============================================
|
%% ============================================
|
||||||
%% DEALER CODE GENERATION
|
%% DEALER CODE GENERATION
|
||||||
%% ============================================
|
%% ============================================
|
||||||
@ -666,14 +731,19 @@ erDiagram
|
|||||||
|
|
||||||
DEALER_CONSTITUTION_CHANGES {
|
DEALER_CONSTITUTION_CHANGES {
|
||||||
uuid constitution_change_id PK
|
uuid constitution_change_id PK
|
||||||
|
string request_id UK
|
||||||
|
uuid outlet_id FK
|
||||||
uuid dealer_id FK
|
uuid dealer_id FK
|
||||||
string current_constitution
|
string change_type
|
||||||
string proposed_constitution
|
text description
|
||||||
text reason
|
string current_stage
|
||||||
json new_partners_details
|
|
||||||
json shareholding_pattern
|
|
||||||
string status
|
string status
|
||||||
|
integer progress_percentage
|
||||||
|
json documents
|
||||||
|
json timeline
|
||||||
timestamp submitted_at
|
timestamp submitted_at
|
||||||
|
timestamp created_at
|
||||||
|
timestamp updated_at
|
||||||
}
|
}
|
||||||
|
|
||||||
%% ============================================
|
%% ============================================
|
||||||
@ -1206,4 +1276,16 @@ erDiagram
|
|||||||
USERS ||--o{ FNF_LINE_ITEMS : "added"
|
USERS ||--o{ FNF_LINE_ITEMS : "added"
|
||||||
USERS ||--o{ APPLICATIONS : "currently_assigned"
|
USERS ||--o{ APPLICATIONS : "currently_assigned"
|
||||||
|
|
||||||
|
USERS ||--o{ OUTLETS : "has_outlets"
|
||||||
|
OUTLETS ||--o{ DEALER_CONSTITUTION_CHANGES : "requests_change"
|
||||||
|
|
||||||
|
APPLICATIONS ||--o{ FINANCE_PAYMENTS : "has_payments"
|
||||||
|
USERS ||--o{ FINANCE_PAYMENTS : "verifies_payments"
|
||||||
|
|
||||||
|
DEALER_RESIGNATIONS ||--o{ EXIT_FEEDBACK : "has_feedback"
|
||||||
|
TERMINATION_REQUESTS ||--o{ EXIT_FEEDBACK : "has_feedback"
|
||||||
|
USERS ||--o{ EXIT_FEEDBACK : "submitted_by"
|
||||||
|
|
||||||
|
SLA_TRACKING ||--o{ SLA_BREACHES : "has_breaches"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,10 @@ export const APPLICATION_STAGES = {
|
|||||||
// Application Status
|
// Application Status
|
||||||
export const APPLICATION_STATUS = {
|
export const APPLICATION_STATUS = {
|
||||||
PENDING: 'Pending',
|
PENDING: 'Pending',
|
||||||
|
SUBMITTED: 'Submitted',
|
||||||
|
QUESTIONNAIRE_PENDING: 'Questionnaire Pending',
|
||||||
|
QUESTIONNAIRE_COMPLETED: 'Questionnaire Completed',
|
||||||
|
SHORTLISTED: 'Shortlisted',
|
||||||
IN_REVIEW: 'In Review',
|
IN_REVIEW: 'In Review',
|
||||||
APPROVED: 'Approved',
|
APPROVED: 'Approved',
|
||||||
REJECTED: 'Rejected',
|
REJECTED: 'Rejected',
|
||||||
|
|||||||
74
src/common/utils/email.service.ts
Normal file
74
src/common/utils/email.service.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Create test account (or use env vars in production)
|
||||||
|
let transporter: nodemailer.Transporter;
|
||||||
|
|
||||||
|
nodemailer.createTestAccount().then((account) => {
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
host: account.smtp.host,
|
||||||
|
port: account.smtp.port,
|
||||||
|
secure: account.smtp.secure,
|
||||||
|
auth: {
|
||||||
|
user: account.user,
|
||||||
|
pass: account.pass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('Email Transporter Ready (Ethereal Test Account)');
|
||||||
|
console.log(`Preview URL: https://ethereal.email/messages`);
|
||||||
|
}).catch(err => console.error('Failed to create test account:', err));
|
||||||
|
|
||||||
|
const readTemplate = (templateName: string, replacements: Record<string, string>) => {
|
||||||
|
const templatePath = path.join(__dirname, '../../emailtemplates', `${templateName}.html`);
|
||||||
|
let html = fs.readFileSync(templatePath, 'utf-8');
|
||||||
|
for (const key in replacements) {
|
||||||
|
html = html.replace(new RegExp(`{{${key}}}`, 'g'), replacements[key]);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendOpportunityEmail = async (to: string, applicantName: string, location: string, applicationId: string) => {
|
||||||
|
const link = `http://localhost:5173/questionnaire/${applicationId}`;
|
||||||
|
const html = readTemplate('opportunity', {
|
||||||
|
applicantName,
|
||||||
|
location,
|
||||||
|
applicationId,
|
||||||
|
link,
|
||||||
|
year: new Date().getFullYear().toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!transporter) return;
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: '"Royal Enfield Onboarding" <no-reply@royalenfield.com>',
|
||||||
|
to,
|
||||||
|
subject: 'Action Required: Royal Enfield Dealership Opportunity',
|
||||||
|
html
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Email Sent] Opportunity: ${nodemailer.getTestMessageUrl(info)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendNonOpportunityEmail = async (to: string, applicantName: string, location: string) => {
|
||||||
|
const html = readTemplate('non_opportunity', {
|
||||||
|
applicantName,
|
||||||
|
location,
|
||||||
|
year: new Date().getFullYear().toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!transporter) return;
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: '"Royal Enfield Onboarding" <no-reply@royalenfield.com>',
|
||||||
|
to,
|
||||||
|
subject: 'Update on your Royal Enfield Dealership Application',
|
||||||
|
html
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Email Sent] Non-Opportunity: ${nodemailer.getTestMessageUrl(info)}`);
|
||||||
|
};
|
||||||
59
src/emailtemplates/non_opportunity.html
Normal file
59
src/emailtemplates/non_opportunity.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Royal Enfield Dealership Application</h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Dear {{applicantName}},</p>
|
||||||
|
<p>Thank you for showing interest in becoming a Royal Enfield dealer.</p>
|
||||||
|
<p>We have reviewed our current network plan for <strong>{{location}}</strong>, and currently, there are no
|
||||||
|
open opportunities available in this area.</p>
|
||||||
|
<p>We have saved your details in our database and will contact you should an opportunity arise in the
|
||||||
|
future.</p>
|
||||||
|
<p>We appreciate your enthusiasm for the brand.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© {{year}} Royal Enfield. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
36
src/emailtemplates/opportunity.html
Normal file
36
src/emailtemplates/opportunity.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background-color: #000; color: #fff; padding: 15px; text-align: center; }
|
||||||
|
.content { padding: 20px; background-color: #f9f9f9; }
|
||||||
|
.button { display: inline-block; padding: 10px 20px; background-color: #d32f2f; color: white; text-decoration: none; border-radius: 5px; margin-top: 20px; }
|
||||||
|
.footer { font-size: 12px; color: #777; margin-top: 30px; text-align: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Royal Enfield Dealership Opportunity</h2>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Dear {{applicantName}},</p>
|
||||||
|
<p>Thank you for your interest in partnering with Royal Enfield.</p>
|
||||||
|
<p>We are pleased to inform you that there is an open opportunity for a dealership in your requested location (<strong>{{location}}</strong>).</p>
|
||||||
|
<p>To proceed with your application, please complete the detailed Dealership Assessment Questionnaire by clicking the link below:</p>
|
||||||
|
|
||||||
|
<a href="{{link}}" class="button">Start Questionnaire</a>
|
||||||
|
|
||||||
|
<p style="margin-top: 20px;">Or copy this link to your browser:</p>
|
||||||
|
<p>{{link}}</p>
|
||||||
|
|
||||||
|
<p><strong>Note:</strong> No login credentials are required at this stage. Your Application ID is <strong>{{applicationId}}</strong>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© {{year}} Royal Enfield. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,11 +1,13 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone } = db;
|
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone, Area } = db;
|
||||||
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
|
|
||||||
|
import { sendOpportunityEmail, sendNonOpportunityEmail } from '../../common/utils/email.service.js';
|
||||||
|
|
||||||
export const submitApplication = async (req: AuthRequest, res: Response) => {
|
export const submitApplication = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
@ -23,40 +25,70 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
|
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
|
||||||
|
|
||||||
// Fetch hierarchy from Opportunity if available
|
// Fetch hierarchy from Auto-detected Area
|
||||||
// Fetch hierarchy from Opportunity if available, OR resolve from Location
|
|
||||||
let zoneId, regionId, areaId;
|
let zoneId, regionId, areaId;
|
||||||
|
let isOpportunityAvailable = false;
|
||||||
|
|
||||||
if (opportunityId) {
|
// Auto-detect Area from District
|
||||||
const opportunity = await Opportunity.findByPk(opportunityId);
|
if (req.body.district) {
|
||||||
if (opportunity) {
|
|
||||||
zoneId = opportunity.zoneId;
|
|
||||||
regionId = opportunity.regionId;
|
|
||||||
}
|
|
||||||
} else if (req.body.district) {
|
|
||||||
// Resolve hierarchy from submitted District
|
|
||||||
const districtName = req.body.district;
|
const districtName = req.body.district;
|
||||||
|
|
||||||
|
// 1. Find District ID by Name
|
||||||
const districtRecord = await District.findOne({
|
const districtRecord = await District.findOne({
|
||||||
where: {
|
where: { districtName: { [Op.iLike]: districtName } }
|
||||||
districtName: { [Op.iLike]: districtName } // Case-insensitive match
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{ model: Region, as: 'region', attributes: ['id', 'zoneId'] },
|
|
||||||
{ model: Zone, as: 'zone', attributes: ['id'] }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
if (districtRecord) {
|
||||||
regionId = districtRecord.regionId;
|
regionId = districtRecord.regionId;
|
||||||
zoneId = districtRecord.zoneId || (districtRecord.region ? districtRecord.region.zoneId : null);
|
zoneId = districtRecord.zoneId;
|
||||||
console.log(`Auto-assigned Application to Region: ${regionId}, Zone: ${zoneId} based on District: ${districtName}`);
|
|
||||||
} else {
|
|
||||||
console.log(`Could not find District: ${districtName} for auto-assignment.`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const application = await Application.create({
|
const application = await Application.create({
|
||||||
opportunityId,
|
opportunityId: null, // De-coupled from Opportunity table as per user request
|
||||||
applicationId,
|
applicationId,
|
||||||
applicantName,
|
applicantName,
|
||||||
email,
|
email,
|
||||||
@ -69,29 +101,30 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
investmentCapacity,
|
investmentCapacity,
|
||||||
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
|
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
|
||||||
currentStage: APPLICATION_STAGES.DD,
|
currentStage: APPLICATION_STAGES.DD,
|
||||||
overallStatus: APPLICATION_STATUS.PENDING,
|
overallStatus: initialStatus,
|
||||||
progressPercentage: 10,
|
progressPercentage: isOpportunityAvailable ? 10 : 0,
|
||||||
zoneId,
|
zoneId,
|
||||||
regionId
|
regionId,
|
||||||
|
areaId // Link to Area
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log Status History
|
// Log Status History
|
||||||
await ApplicationStatusHistory.create({
|
await ApplicationStatusHistory.create({
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
previousStatus: null,
|
previousStatus: null,
|
||||||
newStatus: APPLICATION_STATUS.PENDING,
|
newStatus: initialStatus,
|
||||||
changedBy: req.user?.id,
|
changedBy: req.user?.id || null,
|
||||||
reason: 'Initial Submission'
|
reason: 'Initial Submission'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Progress (Optional, or created as needed per stage)
|
// Send Email (Async)
|
||||||
await ApplicationProgress.create({
|
if (isOpportunityAvailable) {
|
||||||
applicationId: application.id,
|
sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId)
|
||||||
stageName: 'Application',
|
.catch(err => console.error('Error sending opportunity email', err));
|
||||||
stageOrder: 1,
|
} else {
|
||||||
status: 'Completed',
|
sendNonOpportunityEmail(email, applicantName, city || preferredLocation)
|
||||||
completionPercentage: 100
|
.catch(err => console.error('Error sending non-opportunity email', err));
|
||||||
});
|
}
|
||||||
|
|
||||||
await AuditLog.create({
|
await AuditLog.create({
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
|
|||||||
@ -13,6 +13,15 @@ router.use(authenticate as any);
|
|||||||
router.get('/applications', onboardingController.getApplications);
|
router.get('/applications', onboardingController.getApplications);
|
||||||
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.post('/applications/:id/documents', onboardingController.uploadDocuments); // Moving to DMS module
|
// router.post('/applications/:id/documents', onboardingController.uploadDocuments); // Moving to DMS module
|
||||||
|
|
||||||
|
// Questionnaire Routes
|
||||||
|
router.get('/questionnaires', (req, res, next) => {
|
||||||
|
import('./questionnaire.controller.js').then(c => c.getAllQuestionnaires(req, res)).catch(next);
|
||||||
|
});
|
||||||
|
router.get('/questionnaires/:id', (req, res, next) => {
|
||||||
|
import('./questionnaire.controller.js').then(c => c.getQuestionnaireById(req, res)).catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -100,3 +100,98 @@ export const submitResponse = async (req: AuthRequest, res: Response) => {
|
|||||||
res.status(500).json({ success: false, message: 'Error submitting responses' });
|
res.status(500).json({ success: false, message: 'Error submitting responses' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAllQuestionnaires = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const questionnaires = await Questionnaire.findAll({
|
||||||
|
order: [['createdAt', 'DESC']],
|
||||||
|
attributes: ['id', 'version', 'isActive', 'createdAt']
|
||||||
|
});
|
||||||
|
res.json({ success: true, data: questionnaires });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get all questionnaires error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error fetching questionnaires' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestionnaireById = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const questionnaire = await Questionnaire.findByPk(id, {
|
||||||
|
include: [{
|
||||||
|
model: QuestionnaireQuestion,
|
||||||
|
as: 'questions',
|
||||||
|
order: [['order', 'ASC']]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!questionnaire) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Questionnaire not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, data: questionnaire });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get questionnaire by id error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPublicQuestionnaire = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { applicationId } = req.params;
|
||||||
|
|
||||||
|
// Verify valid application exists
|
||||||
|
const application = await Application.findOne({ where: { applicationId } });
|
||||||
|
if (!application) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch active questionnaire
|
||||||
|
const questionnaire = await Questionnaire.findOne({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: [{
|
||||||
|
model: QuestionnaireQuestion,
|
||||||
|
as: 'questions',
|
||||||
|
order: [['order', 'ASC']]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!questionnaire) {
|
||||||
|
return res.status(404).json({ success: false, message: 'No active questionnaire found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, data: { applicationName: application.applicantName, ...questionnaire.toJSON() } });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get public questionnaire error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error fetching questionnaire' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const submitPublicResponse = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { applicationId, responses } = req.body;
|
||||||
|
|
||||||
|
const application = await Application.findOne({ where: { applicationId } });
|
||||||
|
if (!application) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Invalid Application ID' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionnaire = await Questionnaire.findOne({ where: { isActive: true } });
|
||||||
|
if (!questionnaire) return res.status(400).json({ success: false, message: 'No active questionnaire' });
|
||||||
|
|
||||||
|
const responseRecords = responses.map((r: any) => ({
|
||||||
|
applicationId: application.id, // Use UUID from database
|
||||||
|
questionnaireId: questionnaire.id,
|
||||||
|
questionId: r.questionId,
|
||||||
|
responseValue: r.value,
|
||||||
|
attachmentUrl: r.attachmentUrl || null
|
||||||
|
}));
|
||||||
|
|
||||||
|
await QuestionnaireResponse.bulkCreate(responseRecords);
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Responses submitted successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Submit public response error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error submitting responses' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -5,9 +5,17 @@ import { authenticate } from '../../common/middleware/auth.js';
|
|||||||
import { checkRole } from '../../common/middleware/roleCheck.js';
|
import { checkRole } from '../../common/middleware/roleCheck.js';
|
||||||
import { ROLES } from '../../common/config/constants.js';
|
import { ROLES } from '../../common/config/constants.js';
|
||||||
|
|
||||||
|
// Public routes (No Auth Required)
|
||||||
|
router.get('/public/:applicationId', questionnaireController.getPublicQuestionnaire);
|
||||||
|
router.post('/public/submit', questionnaireController.submitPublicResponse);
|
||||||
|
|
||||||
|
// Public routes (No Auth Required)
|
||||||
|
router.get('/public/:applicationId', questionnaireController.getPublicQuestionnaire);
|
||||||
|
router.post('/public/submit', questionnaireController.submitPublicResponse);
|
||||||
|
|
||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
|
|
||||||
// Public/Dealer routes (Application context)
|
// Dealer routes (Application context)
|
||||||
router.get('/latest', questionnaireController.getLatestQuestionnaire);
|
router.get('/latest', questionnaireController.getLatestQuestionnaire);
|
||||||
router.post('/response', questionnaireController.submitResponse);
|
router.post('/response', questionnaireController.submitResponse);
|
||||||
|
|
||||||
|
|||||||
48
update_enums_raw.js
Normal file
48
update_enums_raw.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import pg from 'pg';
|
||||||
|
const { Client } = pg;
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
user: 'laxman',
|
||||||
|
host: 'localhost',
|
||||||
|
database: 'royal_enfield_onboarding',
|
||||||
|
password: 'Admin@123',
|
||||||
|
port: 5432,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateEnums() {
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
console.log('Connected to database.');
|
||||||
|
|
||||||
|
const enums = [
|
||||||
|
'Submitted',
|
||||||
|
'Questionnaire Pending',
|
||||||
|
'Questionnaire Completed',
|
||||||
|
'Shortlisted',
|
||||||
|
'Level 1 Pending',
|
||||||
|
'Level 1 Approved',
|
||||||
|
'Level 2 Pending',
|
||||||
|
'Level 2 Approved',
|
||||||
|
'EOR In Progress'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const val of enums) {
|
||||||
|
try {
|
||||||
|
// Quotes around value are crucial for case sensitivity if the enum type was created with quotes,
|
||||||
|
// but usually enum values are string literals.
|
||||||
|
await client.query(`ALTER TYPE "enum_applications_overallStatus" ADD VALUE IF NOT EXISTS '${val}'`);
|
||||||
|
console.log(`Added enum value: ${val}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Could not add ${val}:`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Connection error:', err);
|
||||||
|
} finally {
|
||||||
|
await client.end();
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEnums();
|
||||||
Loading…
Reference in New Issue
Block a user