hirarchcy made orte stable and and tested upto interview levl 3

This commit is contained in:
laxman h 2026-03-31 21:11:08 +05:30
parent 574fc84ba4
commit feeb613136
18 changed files with 946 additions and 313 deletions

31
check_history.ts Normal file
View File

@ -0,0 +1,31 @@
import db from './src/database/models/index.js';
const { Application, ApplicationStatusHistory } = db;
async function checkApp() {
try {
const app = await Application.findOne({ order: [['updatedAt', 'DESC']] });
if (!app) {
console.log('No apps found');
return;
}
console.log('--- Application Info ---');
console.log(`ID: ${app.id}, Reg: ${app.applicationId}, Name: ${app.applicantName}`);
console.log(`Current Status: ${app.overallStatus}, Progress: ${app.progressPercentage}`);
const history = await ApplicationStatusHistory.findAll({
where: { applicationId: app.id },
order: [['createdAt', 'ASC']]
});
console.log('\n--- Status History ---');
history.forEach((h: any) => {
console.log(`[${h.createdAt.toISOString()}] ${h.previousStatus} -> ${h.newStatus} (Reason: ${h.reason})`);
});
} catch (err) {
console.error(err);
} finally {
process.exit(0);
}
}
checkApp();

View File

@ -16,7 +16,7 @@
"seed:approval-policies": "tsx scripts/seed-approval-policies.ts", "seed:approval-policies": "tsx scripts/seed-approval-policies.ts",
"seed:email-templates": "tsx src/scripts/seed-master-emails.ts", "seed:email-templates": "tsx src/scripts/seed-master-emails.ts",
"seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates", "seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates",
"setup:fresh": "npm run migrate && npm run seed:all && npm run sync:hierarchy", "setup:fresh": "npm run migrate && npm run seed:real-geo && npm run seed:all && npm run sync:hierarchy",
"seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy", "seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy",
"sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts", "sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts",
"seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts", "seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts",

26
scripts/check_app.ts Normal file
View File

@ -0,0 +1,26 @@
import db from '../src/database/models/index.js';
async function check() {
try {
const app = await (db as any).Application.findOne({
where: { email: 'test-dealer-tumkur@example.com' },
include: [{ model: (db as any).District, as: 'district' }]
});
if (app) {
console.log('Application Found:');
console.log('ID:', app.applicationId);
console.log('District Name:', app.district ? app.district.name : 'NULL');
console.log('District ID:', app.districtId);
console.log('Is Opportunity Available (Status):', app.overallStatus);
} else {
console.log('Application not found.');
}
process.exit(0);
} catch (err) {
console.error(err);
process.exit(1);
}
}
check();

20
scripts/debug_roles.ts Normal file
View File

@ -0,0 +1,20 @@
import db from '../src/database/models/index.js';
async function check() {
try {
const roles = await (db as any).Role.findAll();
console.log('--- ROLES START ---');
console.log(JSON.stringify(roles.map((r: any) => ({
name: r.roleName,
code: r.roleCode,
id: r.id
})), null, 2));
console.log('--- ROLES END ---');
process.exit(0);
} catch (error) {
console.error('Error listing roles:', error);
process.exit(1);
}
}
check();

View File

@ -6,151 +6,192 @@ import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src
const { Role, Zone, Region, State, District, User, UserRole } = db; const { Role, Zone, Region, State, District, User, UserRole } = db;
async function seed() { async function seed() {
console.log('--- Seeding Normalized Denormalized Data ---'); console.log('--- Seeding Comprehensive Golden Path Data ---');
await db.sequelize.authenticate(); await db.sequelize.authenticate();
// Use sync with alter false to match main app behavior
await db.sequelize.sync({ alter: false }); await db.sequelize.sync({ alter: false });
const hashedPassword = await bcrypt.hash('Admin@123', 10); const hashedPassword = await bcrypt.hash('Admin@123', 10);
// 1. Create Roles // 1. Ensure Roles exist
const roles = [ const roles = [
{ roleCode: 'NBH', roleName: 'National Business Head', category: 'NATIONAL' }, { roleCode: 'NBH', roleName: 'National Business Head', category: 'NATIONAL' },
{ roleCode: 'DD Head', roleName: 'DD Head', category: 'NATIONAL' }, { roleCode: 'DD Head', roleName: 'DD Head', category: 'NATIONAL' },
{ roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' }, { roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' },
{ roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' }, { roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' },
{ roleCode: 'RBM', roleName: 'Regional Business Manager', category: 'REGIONAL' }, { roleCode: 'RBM', roleName: 'Regional Business Manager', category: 'REGIONAL' },
{ roleCode: 'RM', roleName: 'Regional Manager', category: 'REGIONAL' },
{ roleCode: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' }, { roleCode: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' },
{ roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' }, { roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' },
{ roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' }, { roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' },
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' }, { roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' }, { roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' }, { roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' }
{ roleCode: 'Legal Admin', roleName: 'Legal Admin', category: 'DEPARTMENT' }
]; ];
for (const r of roles) { for (const r of roles) {
await Role.findOrCreate({ where: { roleCode: r.roleCode }, defaults: r }); await Role.findOrCreate({ where: { roleCode: r.roleCode }, defaults: r });
} }
console.log('Roles seeded.');
// 2. Create Districts (Hierarchy) const mapUserRole = async (userRec: any, roleCode: string, assignment: any = {}) => {
const [zone1] = await Zone.findOrCreate({
where: { name: 'North Zone' },
defaults: { name: 'North Zone', code: 'ZONE-N' }
});
const [zone2] = await Zone.findOrCreate({
where: { name: 'South Zone' },
defaults: { name: 'South Zone', code: 'ZONE-S' }
});
const [state1] = await State.findOrCreate({
where: { name: 'Delhi' },
defaults: { name: 'Delhi', zoneId: zone1.id }
});
const [region1] = await Region.findOrCreate({
where: { name: 'NCR Region' },
defaults: { name: 'NCR Region', zoneId: zone1.id }
});
const [region2] = await Region.findOrCreate({
where: { name: 'Bangalore Region' },
defaults: { name: 'Bangalore Region', zoneId: zone2.id }
});
const [district1] = await District.findOrCreate({
where: { name: 'South Delhi District' },
defaults: { name: 'South Delhi District', stateId: state1.id, regionId: region1.id, zoneId: zone1.id }
});
console.log('Geographical Hierarchy seeded.');
const mapUserRole = async (userRec: any, roleCode: string, assignment: { zoneId?: string | null, regionId?: string | null, districtId?: string | null } = {}) => {
const role = await Role.findOne({ where: { roleCode } }); const role = await Role.findOne({ where: { roleCode } });
if (role) { if (role) {
await UserRole.findOrCreate({ await UserRole.findOrCreate({
where: { where: { userId: userRec.id, roleId: role.id, ...assignment },
userId: userRec.id, defaults: { userId: userRec.id, roleId: role.id, ...assignment, isActive: true, isPrimary: true }
roleId: role.id,
...assignment
},
defaults: {
userId: userRec.id,
roleId: role.id,
...assignment,
isActive: true,
isPrimary: true
}
}); });
} }
}; };
// 4. Create Users and Map them // 2. Create Hierarchical Structure
const nbhResult = await User.findOrCreate({ // Zones
where: { email: 'nbh@example.com' }, const zones = [
defaults: { fullName: 'National Head', roleCode: 'NBH', password: hashedPassword } { name: 'North Zone', code: 'ZONE-N' },
}); { name: 'South Zone', code: 'ZONE-S' }
await mapUserRole(nbhResult[0], 'NBH');
const zbhResult = await User.findOrCreate({
where: { email: 'zbh.north@example.com' },
defaults: { fullName: 'North Zonal Head', roleCode: 'ZBH', password: hashedPassword, employeeId: 'ZBH001' }
});
await mapUserRole(zbhResult[0], 'ZBH', { zoneId: zone1.id });
const rmResult = await User.findOrCreate({
where: { email: 'rbm.delhi@example.com' },
defaults: { fullName: 'Delhi Regional Manager', roleCode: 'RM', password: hashedPassword, employeeId: 'RBM001' }
});
await mapUserRole(rmResult[0], 'RM', { regionId: region1.id });
// ZM is now mapped to Regions (not Districts) — multi-region support
const zmResult = await User.findOrCreate({
where: { email: 'zm.north@example.com' },
defaults: { fullName: 'North Zonal Manager', roleCode: 'DD-ZM', password: hashedPassword, employeeId: 'ZM001' }
});
// One UserRole entry per region managed by this ZM
await mapUserRole(zmResult[0], 'DD-ZM', { zoneId: zone1.id, regionId: region1.id });
const asmResult = await User.findOrCreate({
where: { email: 'asm.sdelhi@example.com' },
defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM', password: hashedPassword, employeeId: 'ASM001' }
});
await mapUserRole(asmResult[0], 'ASM', { districtId: district1.id });
// Mock Users alignment
const mockUsers = [
{ email: 'ddlead@royalenfield.com', name: 'Meera Iyer', roleCode: 'DD Lead', assignment: { zoneId: zone1.id } },
{ email: 'finance@royalenfield.com', name: 'Rahul Verma', roleCode: 'Finance', assignment: {} },
{ email: 'dealer@royalenfield.com', name: 'Amit Sharma', roleCode: 'Dealer', assignment: { districtId: district1.id }, isExt: true },
{ email: 'admin@royalenfield.com', name: 'Laxman H', roleCode: 'DD Lead', assignment: { zoneId: zone2.id } },
{ email: 'yashwin@gmail.com', name: 'Yashwin', roleCode: 'ZBH', assignment: { zoneId: zone1.id } },
{ email: 'kenil@gmail.com', name: 'Kenil', roleCode: 'DD Lead', assignment: { zoneId: zone1.id } },
{ email: 'lince@gmail.com', name: 'Lince', roleCode: 'DD Admin', assignment: {} }
]; ];
const zoneMap: Record<string, any> = {};
for (const m of mockUsers) { for (const z of zones) {
const [u] = await User.findOrCreate({ const [zone] = await Zone.findOrCreate({ where: { name: z.name }, defaults: z });
where: { email: m.email }, zoneMap[z.name] = zone;
defaults: {
fullName: m.name,
roleCode: m.roleCode,
password: hashedPassword,
isExternal: (m as any).isExt || false,
status: 'active'
} }
// Regions
const regions = [
{ name: 'NCR Region', zoneName: 'North Zone' },
{ name: 'Punjab Region', zoneName: 'North Zone' },
{ name: 'Karnataka Region', zoneName: 'South Zone' },
{ name: 'Tamil Nadu Region', zoneName: 'South Zone' }
];
const regionMap: Record<string, any> = {};
for (const r of regions) {
const zone = zoneMap[r.zoneName];
const [region] = await Region.findOrCreate({
where: { name: r.name },
defaults: { name: r.name, zoneId: zone.id }
}); });
await mapUserRole(u, m.roleCode, m.assignment); regionMap[r.name] = region;
} }
console.log('Users and Mappings seeded.'); // States & Districts
const districts = [
{ name: 'South Delhi', stateName: 'DELHI', regionName: 'NCR Region' },
{ name: 'NOIDA', stateName: 'UTTAR PRADESH', regionName: 'NCR Region' },
{ name: 'Ludhiana', stateName: 'PUNJAB', regionName: 'Punjab Region' },
{ name: 'Bangalore Urban', stateName: 'KARNATAKA', regionName: 'Karnataka Region' }
];
for (const d of districts) {
const region = regionMap[d.regionName];
const [state] = await State.findOrCreate({
where: { name: d.stateName },
defaults: { name: d.stateName, zoneId: region.zoneId }
});
await District.findOrCreate({
where: { name: d.name },
defaults: { name: d.name, stateId: state.id, regionId: region.id, zoneId: region.zoneId, isActive: true }
});
}
// 3. Create Key Management Users
// National / Administrative
const nationalUsers = [
{ email: 'nbh@royalenfield.com', name: 'Alwyn John', role: 'NBH' },
{ email: 'ddhead@royalenfield.com', name: 'Vikram Singh', role: 'DD Head' },
{ email: 'finance@royalenfield.com', name: 'Rahul Verma', role: 'Finance' },
{ email: 'admin@royalenfield.com', name: 'Laxman H', role: 'Super Admin' },
{ email: 'lince@gmail.com', name: 'Lince', role: 'DD Admin' }
];
for (const u of nationalUsers) {
const [user] = await User.findOrCreate({
where: { email: u.email },
defaults: { fullName: u.name, roleCode: u.role, password: hashedPassword, status: 'active' }
});
await mapUserRole(user, u.role);
}
// Frontend Mock Users for Quick Login (Ensuring exact matches)
const frontendMocks = [
{ email: 'ddlead@royalenfield.com', name: 'Meera Iyer', role: 'DD Lead', zone: 'North Zone' },
{ email: 'yashwin@gmail.com', name: 'Yashwin', role: 'ZBH', zone: 'North Zone' },
{ email: 'kenil@gmail.com', name: 'Kenil', role: 'DD Lead', zone: 'North Zone' },
{ email: 'dealer@royalenfield.com', name: 'Amit Sharma', role: 'Dealer', district: 'South Delhi' }
];
for (const m of frontendMocks) {
const assignment: any = {};
if (m.zone) assignment.zoneId = zoneMap[m.zone].id;
if (m.district) {
const d = await District.findOne({ where: { name: m.district } });
if (d) {
assignment.districtId = d.id;
assignment.zoneId = d.zoneId;
assignment.regionId = d.regionId;
}
}
const [user] = await User.findOrCreate({
where: { email: m.email },
defaults: { fullName: m.name, roleCode: m.role, password: hashedPassword, status: 'active' }
});
await mapUserRole(user, m.role, assignment);
}
// Zonal Business Heads (Additional)
const zbhUsers = [
{ email: 'zbh.south@royalenfield.com', name: 'Srinivasan K', zone: 'South Zone' }
];
for (const u of zbhUsers) {
const zone = zoneMap[u.zone];
const [user] = await User.findOrCreate({
where: { email: u.email },
defaults: { fullName: u.name, roleCode: 'ZBH', password: hashedPassword, status: 'active' }
});
await mapUserRole(user, 'ZBH', { zoneId: zone.id });
}
// Regional Managers (RBMs)
const rbmUsers = [
{ email: 'rbm.ncr@royalenfield.com', name: 'Sanjay Dutt', region: 'NCR Region' },
{ email: 'rbm.punjab@royalenfield.com', name: 'Harpreet Singh', region: 'Punjab Region' },
{ email: 'rbm.kar@royalenfield.com', name: 'Manish Kumar', region: 'Karnataka Region' }
];
for (const u of rbmUsers) {
const region = regionMap[u.region];
const [user] = await User.findOrCreate({
where: { email: u.email },
defaults: { fullName: u.name, roleCode: 'RBM', password: hashedPassword, status: 'active' }
});
await mapUserRole(user, 'RBM', { regionId: region.id, zoneId: region.zoneId });
}
// Zonal Managers (DD-ZM) - Assigned to Regions
const zmUsers = [
{ email: 'zm.ncr@royalenfield.com', name: 'Rajesh Khanna', region: 'NCR Region' },
{ email: 'zm.south@royalenfield.com', name: 'Kartik Subbaraj', region: 'Karnataka Region' }
];
for (const u of zmUsers) {
const region = regionMap[u.region];
const [user] = await User.findOrCreate({
where: { email: u.email },
defaults: { fullName: u.name, roleCode: 'DD-ZM', password: hashedPassword, status: 'active' }
});
await mapUserRole(user, 'DD-ZM', { regionId: region.id, zoneId: region.zoneId });
}
// ASMs (Assigned to Districts)
const asmUsers = [
{ email: 'asm.sdelhi@royalenfield.com', name: 'Arun Jaitley', district: 'South Delhi' },
{ email: 'asm.noida@royalenfield.com', name: 'Kishan Reddy', district: 'NOIDA' },
{ email: 'asm.bangalore@royalenfield.com', name: 'Vishnu Dev', district: 'Bangalore Urban' }
];
for (const u of asmUsers) {
const district = await District.findOne({ where: { name: u.district } });
if (district) {
const [user] = await User.findOrCreate({
where: { email: u.email },
defaults: { fullName: u.name, roleCode: 'ASM', password: hashedPassword, status: 'active' }
});
await mapUserRole(user, 'ASM', { districtId: district.id, zoneId: district.zoneId, regionId: district.regionId });
}
}
console.log('--- Triggering Hierarchy Synchronization ---'); console.log('--- Triggering Hierarchy Synchronization ---');
// syncLocationManagers now resolves zmId from the region parent — so districts get updated automatically
const districtList = await District.findAll({ attributes: ['id'] }); const districtList = await District.findAll({ attributes: ['id'] });
for (const d of districtList) await syncLocationManagers(d.id); for (const d of districtList) await syncLocationManagers(d.id);
@ -160,7 +201,7 @@ import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src
const zoneList = await Zone.findAll({ attributes: ['id'] }); const zoneList = await Zone.findAll({ attributes: ['id'] });
for (const z of zoneList) await syncZoneManager(z.id); for (const z of zoneList) await syncZoneManager(z.id);
console.log('--- Seeding & Synchronization Complete ---'); console.log('--- Golden Path Seeding Complete ---');
} }
seed().catch(err => { seed().catch(err => {

View File

@ -24,7 +24,6 @@ export interface ApplicationAttributes {
description: string | null; description: string | null;
address: string | null; address: string | null;
pincode: string | null; pincode: string | null;
locationType: string | null;
currentStage: string; currentStage: string;
overallStatus: string; overallStatus: string;
progressPercentage: number; progressPercentage: number;
@ -142,10 +141,6 @@ export default (sequelize: Sequelize) => {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
}, },
locationType: {
type: DataTypes.STRING,
allowNull: true
},
currentStage: { currentStage: {
type: DataTypes.ENUM(...Object.values(APPLICATION_STAGES)), type: DataTypes.ENUM(...Object.values(APPLICATION_STAGES)),
defaultValue: APPLICATION_STAGES.DD defaultValue: APPLICATION_STAGES.DD

65
src/diag_zbh.ts Normal file
View File

@ -0,0 +1,65 @@
import db from '../src/database/models/index.js';
import { ROLES } from '../src/common/config/constants.js';
async function diagnoseZBH() {
try {
console.log('--- Diagnosis Start ---');
// 1. Check Role table
const roles = await db.Role.findAll();
console.log('Available Roles:');
roles.forEach(r => console.log(`- ID: ${r.id}, Code: ${r.roleCode}, Name: ${r.roleName}`));
const zbhRole = await db.Role.findOne({
where: {
[db.Sequelize.Op.or]: [
{ roleCode: ROLES.ZBH },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zonal Business Head%' } },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zone Business Head%' } }
]
}
});
if (zbhRole) {
console.log(`\nIdentified ZBH Role: ID=${zbhRole.id}, Code=${zbhRole.roleCode}`);
} else {
console.error('\nFAILED to identify ZBH Role using current logic!');
}
// 2. Check Zones
const zones = await db.Zone.findAll({
include: [{ model: db.User, as: 'zonalBusinessHead' }]
});
console.log('\nZones Status:');
zones.forEach(z => {
console.log(`- Zone: ${z.name} (ID: ${z.id})`);
console.log(` zbhId (Zone table): ${z.zbhId}`);
console.log(` zbhCode (Zone table): ${z.zbhCode}`);
console.log(` zonalBusinessHead (Association): ${z.zonalBusinessHead ? z.zonalBusinessHead.fullName : 'None'}`);
});
// 3. Check UserRoles for ZBH
if (zbhRole) {
const zbhAssignments = await db.UserRole.findAll({
where: { roleId: zbhRole.id, isActive: true },
include: [
{ model: db.User, as: 'user' },
{ model: db.Zone, as: 'zone' }
]
});
console.log('\nActive ZBH UserRole Assignments:');
zbhAssignments.forEach(a => {
console.log(`- User: ${a.user.fullName} (ID: ${a.userId}) -> Zone: ${a.zone?.name || 'NULL'} (ID: ${a.zoneId})`);
});
}
console.log('\n--- Diagnosis End ---');
process.exit(0);
} catch (error) {
console.error('Diagnosis Failed:', error);
process.exit(1);
}
}
diagnoseZBH();

39
src/diag_zm.ts Normal file
View File

@ -0,0 +1,39 @@
import db from '../src/database/models/index.js';
import { ROLES } from '../src/common/config/constants.js';
async function diagnoseZM() {
try {
console.log('--- ZM Diagnosis Start ---');
const zones = await db.Zone.findAll();
const roles = await db.Role.findAll({
where: { roleCode: { [db.Sequelize.Op.in]: ['ZBH', 'DD-ZM'] } }
});
const zmRoleIds = roles.filter((r: any) => r.roleCode === 'DD-ZM').map((r: any) => r.id);
console.log(`ZM Role IDs: ${zmRoleIds.join(', ')}`);
for (const zone of zones) {
console.log(`\nZone: ${zone.name} (ID: ${zone.id})`);
const zms = await db.UserRole.findAll({
where: { roleId: { [db.Sequelize.Op.in]: zmRoleIds }, zoneId: zone.id, isActive: true },
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId'] }]
});
console.log(`Active ZM Count: ${zms.length}`);
zms.forEach(z => {
console.log(`- ZM: ${z.user.fullName} (ID: ${z.user.id})`);
});
}
console.log('\n--- ZM Diagnosis End ---');
process.exit(0);
} catch (error) {
console.error('ZM Diagnosis Failed:', error);
process.exit(1);
}
}
diagnoseZM();

View File

@ -176,18 +176,23 @@ export const getAllUsers = async (req: Request, res: Response) => {
const { roleCode, locationId } = req.query; const { roleCode, locationId } = req.query;
const whereClause: any = {}; const whereClause: any = {};
if (roleCode) { let rawRoleCode: any = roleCode || req.query['roleCode[]'];
// Handle both single string and array of role codes (if passed as multiple params) let finalRoleCodes: string[] = [];
if (Array.isArray(roleCode)) {
whereClause.roleCode = { [Op.in]: roleCode }; if (rawRoleCode) {
} else { if (Array.isArray(rawRoleCode)) {
whereClause.roleCode = roleCode; finalRoleCodes = rawRoleCode;
} else if (typeof rawRoleCode === 'string') {
finalRoleCodes = rawRoleCode.split(',').map(r => r.trim());
} }
} }
if (finalRoleCodes.length > 0) {
whereClause.roleCode = { [Op.in]: finalRoleCodes };
}
const nationalRoles = ['NBH', 'DD Head', 'Super Admin']; const nationalRoles = ['NBH', 'DD Head', 'Super Admin'];
const isNationalRole = (typeof roleCode === 'string' && nationalRoles.includes(roleCode)) || const isNationalRole = finalRoleCodes.some(r => nationalRoles.includes(r));
(Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r)));
if (!isNationalRole && locationId) { if (!isNationalRole && locationId) {
const district: any = await db.District.findByPk(locationId as string, { const district: any = await db.District.findByPk(locationId as string, {
@ -196,7 +201,15 @@ export const getAllUsers = async (req: Request, res: Response) => {
if (district) { if (district) {
const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean); const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean);
whereClause.districtId = { [Op.in]: relevantIds }; whereClause[Op.or] = [
{ districtId: { [Op.in]: relevantIds } },
{ zoneId: { [Op.in]: relevantIds } },
{ regionId: { [Op.in]: relevantIds } },
{ stateId: { [Op.in]: relevantIds } },
{ '$userRoles.districtId$': { [Op.in]: relevantIds } },
{ '$userRoles.zoneId$': { [Op.in]: relevantIds } },
{ '$userRoles.regionId$': { [Op.in]: relevantIds } }
];
} }
} }

View File

@ -55,7 +55,29 @@ const processInterviewApprovalDecision = async (params: {
const policy = await ensureInterviewPolicy(interview.level); const policy = await ensureInterviewPolicy(interview.level);
const requiredRoles: string[] = Array.isArray(policy.requiredRoles) ? policy.requiredRoles : []; const requiredRoles: string[] = Array.isArray(policy.requiredRoles) ? policy.requiredRoles : [];
if (requiredRoles.length > 0 && !requiredRoles.includes(roleCode) && roleCode !== 'Super Admin') { // Check if user is an assigned participant for this specific level
const userAssignments = await db.RequestParticipant.findAll({
where: {
requestId: interview.applicationId,
requestType: 'application',
userId: userId
}
});
const assignedParticipant = userAssignments.find((p: any) =>
p.metadata && Number(p.metadata.interviewLevel) === Number(interview.level)
);
const isAssigned = !!assignedParticipant;
const assignedRole = assignedParticipant?.metadata?.role;
console.log(`[debug] User ID: ${userId}, Role: ${roleCode}, isAssigned: ${isAssigned}, assignedRole: ${assignedRole}`);
if (isAssigned) {
console.log(`[debug] Assigned Participant Metadata: ${JSON.stringify(assignedParticipant.metadata)}`);
}
// Forbidden if not Super Admin AND not in required roles AND not an assigned participant
if (roleCode !== 'Super Admin' && requiredRoles.length > 0 && !requiredRoles.includes(roleCode) && !isAssigned) {
return { forbidden: true, policy, requiredRoles, currentRole: roleCode }; return { forbidden: true, policy, requiredRoles, currentRole: roleCode };
} }
@ -80,7 +102,7 @@ const processInterviewApprovalDecision = async (params: {
interviewId, interviewId,
stageCode: policy.stageCode, stageCode: policy.stageCode,
actorUserId: userId, actorUserId: userId,
actorRole: roleCode, actorRole: assignedRole || roleCode, // Use assigned role if available (e.g. ZBH acting as ZM)
decision, decision,
remarks: remarks || null remarks: remarks || null
}); });
@ -89,21 +111,43 @@ const processInterviewApprovalDecision = async (params: {
where: { interviewId, stageCode: policy.stageCode } where: { interviewId, stageCode: policy.stageCode }
}); });
const uniqueApprovalsByRole = new Set( const approvedActions = actions.filter((a: any) => a.decision === 'Approved');
actions.filter((a: any) => a.decision === 'Approved').map((a: any) => a.actorRole) const uniqueApprovalsByRole = new Set(approvedActions.map((a: any) => a.actorRole));
);
console.log(`[debug] Interview Level: ${interview.level}, Stage: ${policy.stageCode}`);
console.log(`[debug] Required Roles: ${JSON.stringify(requiredRoles)}`);
console.log(`[debug] Approved Roles: ${JSON.stringify(Array.from(uniqueApprovalsByRole))}`);
console.log(`[debug] Approved Actions Count: ${approvedActions.length}`);
console.log(`[debug] Min Approvals Required: ${policy.minApprovals}`);
const hasRejection = actions.some((a: any) => a.decision === 'Rejected'); const hasRejection = actions.some((a: any) => a.decision === 'Rejected');
const hasAllRequiredRoleApprovals = requiredRoles.length === 0 const hasAllRequiredRoleApprovals = requiredRoles.length === 0
? true ? true
: requiredRoles.every((role: string) => uniqueApprovalsByRole.has(role)); : requiredRoles.every((role: string) => uniqueApprovalsByRole.has(role));
const meetsMinApprovals = uniqueApprovalsByRole.size >= (policy.minApprovals || 1); const meetsMinApprovals = uniqueApprovalsByRole.size >= (policy.minApprovals || 1);
console.log(`[debug] hasAllRequiredRoleApprovals: ${hasAllRequiredRoleApprovals}`);
console.log(`[debug] meetsMinApprovals: ${meetsMinApprovals}`);
console.log(`[debug] hasRejection: ${hasRejection}`);
if (hasRejection) { if (hasRejection) {
await interview.update({ status: 'Completed' }); await interview.update({ status: 'Completed' });
const application: any = await db.Application.findByPk(interview.applicationId);
let rejectionProgress = application?.progressPercentage || 0;
// Marker progress values to show which stage was last reached
if (policy.stageCode.includes('LEVEL1')) rejectionProgress = Math.max(rejectionProgress, 35);
if (policy.stageCode.includes('LEVEL2')) rejectionProgress = Math.max(rejectionProgress, 50);
if (policy.stageCode.includes('LEVEL3')) rejectionProgress = Math.max(rejectionProgress, 65);
await db.Application.update({ await db.Application.update({
overallStatus: 'Rejected', overallStatus: 'Rejected',
currentStage: 'Rejected' currentStage: 'Rejected',
progressPercentage: rejectionProgress
}, { where: { id: interview.applicationId } }); }, { where: { id: interview.applicationId } });
await db.ApplicationStatusHistory.create({ await db.ApplicationStatusHistory.create({
applicationId: interview.applicationId, applicationId: interview.applicationId,
previousStatus: 'Interview Pending', previousStatus: 'Interview Pending',
@ -114,10 +158,16 @@ const processInterviewApprovalDecision = async (params: {
} else if (hasAllRequiredRoleApprovals && meetsMinApprovals) { } else if (hasAllRequiredRoleApprovals && meetsMinApprovals) {
await interview.update({ status: 'Completed', outcome: 'Selected' }); await interview.update({ status: 'Completed', outcome: 'Selected' });
const nextStatusMap: any = { 1: 'Level 1 Approved', 2: 'Level 2 Approved', 3: 'Level 3 Approved' }; const nextStatusMap: any = { 1: 'Level 1 Approved', 2: 'Level 2 Approved', 3: 'Level 3 Approved' };
const progressMap: any = { 1: 40, 2: 55, 3: 70 };
const newStatus = nextStatusMap[interview.level] || 'Approved'; const newStatus = nextStatusMap[interview.level] || 'Approved';
const application = await db.Application.findByPk(interview.applicationId);
const newProgress = progressMap[interview.level] || (application?.progressPercentage || 0);
await db.Application.update({ await db.Application.update({
overallStatus: newStatus, overallStatus: newStatus,
currentStage: newStatus currentStage: newStatus,
progressPercentage: newProgress
}, { where: { id: interview.applicationId } }); }, { where: { id: interview.applicationId } });
await db.ApplicationStatusHistory.create({ await db.ApplicationStatusHistory.create({
applicationId: interview.applicationId, applicationId: interview.applicationId,
@ -291,17 +341,17 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
const application = await db.Application.findByPk(applicationId); const application = await db.Application.findByPk(applicationId);
let participantIds: string[] = Array.isArray(participants) ? participants : []; let participantIds: string[] = Array.isArray(participants) ? participants : [];
// Auto-include relevant ZBH by location hierarchy when interviewer list is omitted. // Auto-fill participants from pre-assigned RequestParticipants if not provided
if (participantIds.length === 0 && (application?.districtId || application?.locationId)) { if (participantIds.length === 0) {
const ancestorLocationIds = await getLocationAncestors(application.districtId || application.locationId); const preAssigned = await db.RequestParticipant.findAll({
const zonalHeads = await User.findAll({
where: { where: {
roleCode: 'ZBH', requestId: applicationId,
locationId: { [Op.in]: ancestorLocationIds } requestType: 'application',
'metadata.interviewLevel': levelNum
}, },
attributes: ['id'] attributes: ['userId']
}); });
participantIds = zonalHeads.map((user: any) => user.id); participantIds = preAssigned.map((p: any) => p.userId);
} }
participantIds = [...new Set(participantIds)]; participantIds = [...new Set(participantIds)];
@ -464,6 +514,19 @@ export const submitKTMatrix = async (req: AuthRequest, res: Response) => {
})); }));
await db.KTMatrixScore.bulkCreate(scoreRecords); await db.KTMatrixScore.bulkCreate(scoreRecords);
// Auto-process approval if recommendation is provided
if (recommendation && req.user?.id && req.user?.roleCode) {
const normalizedDecision = (['Recommended', 'Approved', 'Selected', 'Approve'].includes(recommendation))
? 'Approved' : 'Rejected';
await processInterviewApprovalDecision({
interviewId,
decision: normalizedDecision,
remarks: feedback,
userId: req.user.id,
roleCode: req.user.roleCode
});
}
res.status(201).json({ success: true, message: 'KT Matrix submitted successfully', data: evaluation }); res.status(201).json({ success: true, message: 'KT Matrix submitted successfully', data: evaluation });
} catch (error) { } catch (error) {
console.error('Submit KT Matrix error:', error); console.error('Submit KT Matrix error:', error);
@ -510,6 +573,18 @@ export const submitLevel2Feedback = async (req: AuthRequest, res: Response) => {
await db.InterviewFeedback.bulkCreate(feedbackRecords); await db.InterviewFeedback.bulkCreate(feedbackRecords);
} }
// Auto-process approval if recommendation is provided
if (recommendation && req.user?.id && req.user?.roleCode) {
const normalizedDecision = (['Recommended', 'Approved', 'Selected', 'Approve'].includes(recommendation))
? 'Approved' : 'Rejected';
await processInterviewApprovalDecision({
interviewId,
decision: normalizedDecision,
userId: req.user.id,
roleCode: req.user.roleCode
});
}
res.status(201).json({ success: true, message: 'Level 2 Feedback submitted successfully', data: evaluation }); res.status(201).json({ success: true, message: 'Level 2 Feedback submitted successfully', data: evaluation });
} catch (error) { } catch (error) {
console.error('Submit Level 2 Feedback error:', error); console.error('Submit Level 2 Feedback error:', error);
@ -627,9 +702,8 @@ export const updateRecommendation = async (req: AuthRequest, res: Response) => {
return res.status(401).json({ success: false, message: 'Unauthorized' }); return res.status(401).json({ success: false, message: 'Unauthorized' });
} }
const { interviewId, recommendation } = req.body; // recommendation: 'Recommended' | 'Not Recommended' const { interviewId, recommendation } = req.body; // recommendation: 'Recommended' | 'Not Recommended'
const normalizedDecision = (recommendation === 'Recommended' || recommendation === 'Selected' || recommendation === 'Approved') const normalizedDecision = (['Recommended', 'Approved', 'Selected', 'Approve'].includes(recommendation))
? 'Approved' ? 'Approved' : 'Rejected';
: 'Rejected';
const result: any = await processInterviewApprovalDecision({ const result: any = await processInterviewApprovalDecision({
interviewId, interviewId,
@ -672,7 +746,7 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) =
return res.status(401).json({ success: false, message: 'Unauthorized' }); return res.status(401).json({ success: false, message: 'Unauthorized' });
} }
const { interviewId, decision, remarks } = req.body; // decision: 'Approved' | 'Rejected' const { interviewId, decision, remarks } = req.body; // decision: 'Approved' | 'Rejected'
const normalizedDecision = decision === 'Approved' ? 'Approved' : 'Rejected'; const normalizedDecision = (decision === 'Approved' || decision === 'Approve') ? 'Approved' : 'Rejected';
const result: any = await processInterviewApprovalDecision({ const result: any = await processInterviewApprovalDecision({
interviewId, interviewId,
decision: normalizedDecision, decision: normalizedDecision,

View File

@ -6,14 +6,12 @@ import { AuthRequest } from '../../types/express.types.js';
export const getChecklist = async (req: Request, res: Response) => { export const getChecklist = async (req: Request, res: Response) => {
try { try {
const { applicationId } = req.params; const { applicationId } = req.params;
// Could auto-create if not exists?
let checklist = await EorChecklist.findOne({ let checklist = await EorChecklist.findOne({
where: { applicationId }, where: { applicationId },
include: [{ model: EorChecklistItem, as: 'items', include: ['proofDocument'] }] include: [{ model: EorChecklistItem, as: 'items', include: ['proofDocument'] }]
}); });
if (!checklist) { if (!checklist) {
// Optional: Return empty or create new
res.status(404).json({ success: false, message: 'Checklist not found' }); res.status(404).json({ success: false, message: 'Checklist not found' });
return; return;
} }
@ -63,7 +61,8 @@ export const createChecklist = async (req: AuthRequest, res: Response) => {
await EorChecklistItem.bulkCreate(itemsData); await EorChecklistItem.bulkCreate(itemsData);
} }
await application.update({ overallStatus: 'EOR In Progress' }); // Status transition will be handled by the global handleApprove workflow or explicit trigger
// await application.update({ overallStatus: 'EOR In Progress' });
res.status(201).json({ success: true, message: 'EOR Checklist initiated with default items', data: checklist }); res.status(201).json({ success: true, message: 'EOR Checklist initiated with default items', data: checklist });
} catch (error) { } catch (error) {
@ -111,6 +110,16 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
{ where: { id: checklistId } } { where: { id: checklistId } }
); );
if (status === 'Completed') {
const checklist = await EorChecklist.findByPk(checklistId);
if (checklist) {
await db.Application.update({
overallStatus: 'Approved',
progressPercentage: 100
}, { where: { id: checklist.applicationId } });
}
}
res.json({ success: true, message: 'EOR Audit submitted' }); res.json({ success: true, message: 'EOR Audit submitted' });
} catch (error) { } catch (error) {
res.status(500).json({ success: false, message: 'Error submitting audit' }); res.status(500).json({ success: false, message: 'Error submitting audit' });

View File

@ -63,7 +63,10 @@ export const createRequest = async (req: AuthRequest, res: Response) => {
} }
}); });
await application.update({ overallStatus: 'LOA Pending' }); await application.update({
overallStatus: 'LOA Pending',
progressPercentage: 92
});
res.status(201).json({ success: true, message: 'LOA Request initiated with DD Head approval', data: request }); res.status(201).json({ success: true, message: 'LOA Request initiated with DD Head approval', data: request });
} catch (error) { } catch (error) {
@ -130,7 +133,11 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
if (action === 'Rejected' || hasRejection) { if (action === 'Rejected' || hasRejection) {
await request.update({ status: 'Rejected' }); await request.update({ status: 'Rejected' });
await db.Application.update({ overallStatus: 'LOA Rejected' }, { where: { id: request.applicationId } }); await db.Application.update({
overallStatus: 'LOA Rejected',
currentStage: 'Rejected',
progressPercentage: 92
}, { where: { id: request.applicationId } });
return res.json({ success: true, message: 'LOA Request rejected' }); return res.json({ success: true, message: 'LOA Request rejected' });
} }
@ -145,7 +152,10 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
filePath: `/uploads/loa/${mockFile}` filePath: `/uploads/loa/${mockFile}`
}); });
await db.Application.update({ overallStatus: 'Authorized for Operations' }, { where: { id: request.applicationId } }); await db.Application.update({
overallStatus: 'Authorized for Operations',
progressPercentage: 97
}, { where: { id: request.applicationId } });
res.json({ success: true, message: 'LOA fully approved and issued' }); res.json({ success: true, message: 'LOA fully approved and issued' });
} else { } else {
res.json({ res.json({

View File

@ -54,7 +54,10 @@ export const acknowledgeRequest = async (req: AuthRequest, res: Response) => {
status: 'Acknowledged' status: 'Acknowledged'
}); });
await db.Application.update({ overallStatus: 'Dealer Code Generation' }, { where: { id: request.applicationId } }); await db.Application.update({
overallStatus: 'Dealer Code Generation',
progressPercentage: 90
}, { where: { id: request.applicationId } });
res.json({ success: true, message: 'LOI Acknowledged by applicant' }); res.json({ success: true, message: 'LOI Acknowledged by applicant' });
} catch (error) { } catch (error) {
@ -87,7 +90,10 @@ export const createRequest = async (req: AuthRequest, res: Response) => {
} }
}); });
await application.update({ overallStatus: 'LOI In Progress' }); await application.update({
overallStatus: 'LOI In Progress',
progressPercentage: 75
});
res.status(201).json({ success: true, message: 'LOI Request initiated with Finance approval', data: request }); res.status(201).json({ success: true, message: 'LOI Request initiated with Finance approval', data: request });
} catch (error) { } catch (error) {
@ -174,7 +180,11 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
// 2. Handle Logic based on Action // 2. Handle Logic based on Action
if (action === 'Rejected' || hasRejection) { if (action === 'Rejected' || hasRejection) {
await request.update({ status: 'Rejected' }); await request.update({ status: 'Rejected' });
await db.Application.update({ overallStatus: 'LOI Rejected' }, { where: { id: request.applicationId } }); await db.Application.update({
overallStatus: 'LOI Rejected',
currentStage: 'Rejected',
progressPercentage: 75
}, { where: { id: request.applicationId } });
return res.json({ success: true, message: 'LOI Request rejected' }); return res.json({ success: true, message: 'LOI Request rejected' });
} }
@ -191,7 +201,10 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
filePath: `/uploads/loi/${mockFile}` filePath: `/uploads/loi/${mockFile}`
}); });
await db.Application.update({ overallStatus: 'LOI Issued' }, { where: { id: request.applicationId } }); await db.Application.update({
overallStatus: 'LOI Issued',
progressPercentage: 85
}, { where: { id: request.applicationId } });
res.json({ success: true, message: 'LOI Request fully approved and document generated' }); res.json({ success: true, message: 'LOI Request fully approved and document generated' });
} else { } else {

View File

@ -1,7 +1,8 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import { syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js'; import { syncDistrictsByRegion, syncDistrictsByZone, syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
import { ROLES } from '../../common/config/constants.js';
const { User } = db; const { User } = db;
// --- Areas (Granular Locations) --- // --- Areas (Granular Locations) ---
@ -219,7 +220,7 @@ export const getRegions = async (req: Request, res: Response) => {
db.UserRole.count({ where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true }, distinct: true, col: 'userId' }), db.UserRole.count({ where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true }, distinct: true, col: 'userId' }),
db.UserRole.findOne({ db.UserRole.findOne({
where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true }, where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true },
include: [{ model: db.User, as: 'user' }] include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId'] }]
}) })
]); ]);
@ -297,6 +298,11 @@ export const createRegion = async (req: Request, res: Response) => {
); );
} }
await syncRegionManager(region.id);
if (Array.isArray(targetDistrictIds) && targetDistrictIds.length > 0) {
await syncDistrictsByRegion(region.id);
}
res.status(201).json({ success: true, message: 'Region created', data: region }); res.status(201).json({ success: true, message: 'Region created', data: region });
} catch (error: any) { } catch (error: any) {
console.error('Create region error:', error); console.error('Create region error:', error);
@ -373,6 +379,9 @@ export const updateRegion = async (req: Request, res: Response) => {
} }
} }
await syncRegionManager(id as string);
await syncDistrictsByRegion(id as string);
res.json({ success: true, message: 'Region updated' }); res.json({ success: true, message: 'Region updated' });
} catch (error: any) { } catch (error: any) {
console.error('Update region error:', error); console.error('Update region error:', error);
@ -395,7 +404,8 @@ export const getZones = async (req: Request, res: Response) => {
include: [ include: [
{ model: db.Region, as: 'regions', attributes: ['id', 'name'] }, { model: db.Region, as: 'regions', attributes: ['id', 'name'] },
{ model: db.State, as: 'states', attributes: ['id', 'name'] }, { model: db.State, as: 'states', attributes: ['id', 'name'] },
{ model: db.District, as: 'districts', attributes: ['id'] } { model: db.District, as: 'districts', attributes: ['id'] },
{ model: db.User, as: 'zonalBusinessHead', attributes: ['id', 'fullName', 'email', 'employeeId'] }
], ],
order: [['name', 'ASC']] order: [['name', 'ASC']]
}); });
@ -411,28 +421,42 @@ export const getZones = async (req: Request, res: Response) => {
const [zbhAssignment, zms] = await Promise.all([ const [zbhAssignment, zms] = await Promise.all([
db.UserRole.findOne({ db.UserRole.findOne({
where: { roleId: { [db.Sequelize.Op.in]: zbhRoleIds }, zoneId: zone.id, isPrimary: true, isActive: true }, where: { roleId: { [db.Sequelize.Op.in]: zbhRoleIds }, zoneId: zone.id, isActive: true },
include: [{ model: db.User, as: 'user' }] include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId'] }],
order: [['assignedAt', 'DESC']]
}), }),
db.UserRole.findAll({ db.UserRole.findAll({
where: { roleId: { [db.Sequelize.Op.in]: zmRoleIds }, zoneId: zone.id, isActive: true }, where: { roleId: { [db.Sequelize.Op.in]: zmRoleIds }, zoneId: zone.id, isActive: true },
include: [{ model: db.User, as: 'user' }] include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId'] }]
}) })
]); ]);
// For each ZM, fetch their assigned districts in this zone // For each ZM, fetch their assigned regions in this zone
const zonalManagers = await Promise.all(zms.map(async (zmRole: any) => { const zonalManagers = await Promise.all(zms.map(async (zmRole: any) => {
const districts = await db.District.findAll({ // Fetch all active regions assigned to this ZM in this zone
where: { zmId: zmRole.user.id, zoneId: zone.id }, // ZMs usually managed regions or specific district sets const assignedRoles = await db.UserRole.findAll({
attributes: ['name'] where: {
userId: zmRole.user.id,
zoneId: zone.id,
isActive: true,
roleId: { [db.Sequelize.Op.in]: zmRoleIds }
},
include: [{ model: db.Region, as: 'region', attributes: ['id', 'name'] }]
}); });
const regionNames = Array.from(new Set(
assignedRoles
.filter((r: any) => r.region?.name)
.map((r: any) => r.region.name)
));
return { return {
id: zmRole.user.id, id: zmRole.user.id,
name: zmRole.user.fullName || zmRole.user.name, name: zmRole.user.fullName || zmRole.user.name,
email: zmRole.user.email, email: zmRole.user.email,
phone: zmRole.user.mobileNumber || 'N/A', phone: zmRole.user.mobileNumber || 'N/A',
code: zmRole.managerCode || zmRole.user.employeeId || 'N/A', code: zmRole.managerCode || zmRole.user.employeeId || 'N/A',
districts: districts.map((d: any) => d.name) regions: regionNames
}; };
})); }));
@ -442,11 +466,17 @@ export const getZones = async (req: Request, res: Response) => {
zoneJson.zmCount = zms.length; zoneJson.zmCount = zms.length;
zoneJson.zonalBusinessHead = zbhAssignment ? { zoneJson.zonalBusinessHead = zbhAssignment ? {
id: zbhAssignment.user.id, id: zbhAssignment.user.id,
name: zbhAssignment.user.fullName || zbhAssignment.user.name, name: zbhAssignment.user.fullName,
email: zbhAssignment.user.email, email: zbhAssignment.user.email,
phone: zbhAssignment.user.mobileNumber || 'N/A', phone: zbhAssignment.user.mobileNumber || 'N/A',
code: zone.zbhCode || 'N/A' code: zone.zbhCode || 'N/A'
} : null; } : (zone.zonalBusinessHead ? {
id: zone.zonalBusinessHead.id,
name: zone.zonalBusinessHead.fullName,
email: zone.zonalBusinessHead.email,
phone: zone.zonalBusinessHead.mobileNumber || 'N/A',
code: zone.zbhCode || 'N/A'
} : null);
zoneJson.zonalManagers = zonalManagers; zoneJson.zonalManagers = zonalManagers;
return zoneJson; return zoneJson;
})); }));
@ -464,11 +494,21 @@ export const createZone = async (req: Request, res: Response) => {
const zone = await db.Zone.create({ name, code }); const zone = await db.Zone.create({ name, code });
// 1. Assign ZBH if (managerId && managerId !== 'none') {
if (managerId) { const zbhRole = await db.Role.findOne({
const zbhRole = await db.Role.findOne({ where: { roleCode: 'ZBH' } }); where: {
[db.Sequelize.Op.or]: [
{ roleCode: ROLES.ZBH },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zonal Business Head%' } },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zone Business Head%' } }
]
}
});
if (zbhRole) { if (zbhRole) {
await db.UserRole.update({ isActive: false }, { where: { zoneId: zone.id, roleId: zbhRole.id } }); // Deactivate any existing active ZBH roles for this zone (unlikely for new zone but safe)
await db.UserRole.update({ isActive: false }, { where: { zoneId: zone.id, roleId: zbhRole.id, isActive: true } });
await db.UserRole.create({ await db.UserRole.create({
userId: managerId, userId: managerId,
roleId: zbhRole.id, roleId: zbhRole.id,
@ -476,6 +516,10 @@ export const createZone = async (req: Request, res: Response) => {
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });
// Sync to Zone model
const { syncZoneManager } = await import('./syncHierarchy.service.js');
await syncZoneManager(zone.id as string);
} }
} }
@ -500,15 +544,26 @@ export const updateZone = async (req: Request, res: Response) => {
await zone.update({ name, code }); await zone.update({ name, code });
// 1. Update ZBH const { syncZoneManager } = await import('./syncHierarchy.service.js');
if (managerId) {
const zbhRole = await db.Role.findOne({ where: { roleCode: 'ZBH' } }); if (managerId && managerId !== 'none') {
if (zbhRole) { const zbhRole = await db.Role.findOne({
// Deactivate old ZBHs for this zone where: {
await db.UserRole.update({ isActive: false }, { [db.Sequelize.Op.or]: [
where: { zoneId: id, roleId: zbhRole.id } { roleCode: ROLES.ZBH },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zonal Business Head%' } },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zone Business Head%' } }
]
}
}); });
// Assign new ZBH
if (zbhRole) {
// 1. Deactivate old ZBHs for this zone
await db.UserRole.update({ isActive: false }, {
where: { zoneId: id, roleId: zbhRole.id, isActive: true }
});
// 2. Assign new ZBH role
await db.UserRole.create({ await db.UserRole.create({
userId: managerId, userId: managerId,
roleId: zbhRole.id, roleId: zbhRole.id,
@ -516,7 +571,30 @@ export const updateZone = async (req: Request, res: Response) => {
isActive: true, isActive: true,
isPrimary: true isPrimary: true
}); });
// 3. Sync to Zone model
await syncZoneManager(id as string);
} }
} else if (managerId === null || managerId === 'none') {
// Find ZBH role to deactivate
const zbhRole = await db.Role.findOne({
where: {
[db.Sequelize.Op.or]: [
{ roleCode: ROLES.ZBH },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zonal Business Head%' } },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zone Business Head%' } }
]
}
});
if (zbhRole) {
await db.UserRole.update({ isActive: false }, {
where: { zoneId: id, roleId: zbhRole.id, isActive: true }
});
}
// Sync to Zone model (will set zbhId to null)
await syncZoneManager(id as string);
} }
if (Array.isArray(stateIds)) { if (Array.isArray(stateIds)) {
@ -866,6 +944,15 @@ export const saveZM = async (req: Request, res: Response) => {
// Create new role assignments for each region // Create new role assignments for each region
if (Array.isArray(regionIds) && regionIds.length > 0) { if (Array.isArray(regionIds) && regionIds.length > 0) {
for (const regionId of regionIds) { for (const regionId of regionIds) {
// Ensure exclusivity: Deactivate ANY existing ZM role assigned to this region
await db.UserRole.update({ isActive: false }, {
where: {
regionId: regionId,
roleId: zmRole.id,
isActive: true
}
});
await db.UserRole.create({ await db.UserRole.create({
userId, userId,
roleId: zmRole.id, roleId: zmRole.id,
@ -892,6 +979,13 @@ export const saveZM = async (req: Request, res: Response) => {
// Cleanup: ZMs no longer manage districts directly // Cleanup: ZMs no longer manage districts directly
await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: userId } }); await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: userId } });
// Trigger sync for all affected regions to update district.zmId
if (Array.isArray(regionIds) && regionIds.length > 0) {
for (const regionId of regionIds) {
await syncDistrictsByRegion(regionId);
}
}
res.json({ success: true, message: 'Zonal Manager saved successfully' }); res.json({ success: true, message: 'Zonal Manager saved successfully' });
} catch (error) { } catch (error) {
console.error('Save ZM error:', error); console.error('Save ZM error:', error);

View File

@ -1,4 +1,5 @@
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
import { ROLES } from '../../common/config/constants.js';
/** /**
* Synchronizes the Location (District) table's manager IDs with the UserRole table. * Synchronizes the Location (District) table's manager IDs with the UserRole table.
@ -14,12 +15,17 @@ export const syncLocationManagers = async (districtId: string) => {
if (!district) return; if (!district) return;
// Fetch active assignments for this district PLUS any region-level assignments for its parent region // Fetch active assignments for this district PLUS any region-level assignments for its parent region
const orConditions: any[] = [{ districtId, isActive: true }];
if (district.regionId) {
orConditions.push({ regionId: district.regionId, isActive: true });
}
if (district.zoneId) {
orConditions.push({ zoneId: district.zoneId, isActive: true });
}
const activeAssignments = await UserRole.findAll({ const activeAssignments = await UserRole.findAll({
where: { where: {
[Op.or]: [ [Op.or]: orConditions
{ districtId, isActive: true },
{ regionId: district.regionId, isActive: true }
]
}, },
include: [ include: [
{ model: Role, as: 'role', attributes: ['roleCode'] }, { model: Role, as: 'role', attributes: ['roleCode'] },
@ -28,14 +34,24 @@ export const syncLocationManagers = async (districtId: string) => {
}); });
// Find primary/last assigned manager for each type // Find primary/last assigned manager for each type
const asm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'ASM' || (a.role as any)?.roleCode === 'AREA SALES MANAGER'); // ASM and DD-AM are STRICTLY mapped to the District level to prevent territory explosion
const ddAm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'DD-AM' || (a.role as any)?.roleCode === 'AREA MANAGER'); const asm = activeAssignments.find((a: any) =>
((a.role as any)?.roleCode === 'ASM' || (a.role as any)?.roleCode === 'AREA SALES MANAGER') &&
a.districtId === districtId
);
const ddAm = activeAssignments.find((a: any) =>
((a.role as any)?.roleCode === 'DD-AM' || (a.role as any)?.roleCode === 'AREA MANAGER') &&
a.districtId === districtId
);
// ZM can be assigned to the District (legacy) or the Region (new) // ZM can be assigned to the District (legacy/override) or the Region (standard) or Zone (broad)
// We prioritize the Region-level assignment if multiple exist // Order of priority: 1. District level, 2. Region level, 3. Zone level
const zm = activeAssignments.find((a: any) => const zm = activeAssignments.find((a: any) =>
((a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER') && ((a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER') &&
a.regionId === district.regionId a.districtId === districtId
) || activeAssignments.find((a: any) =>
((a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER') &&
a.regionId === district.regionId && !a.districtId
) || activeAssignments.find((a: any) => ) || activeAssignments.find((a: any) =>
((a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER') ((a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER')
); );
@ -100,7 +116,13 @@ export const syncZoneManager = async (zoneId: string) => {
{ {
model: db.Role, model: db.Role,
as: 'role', as: 'role',
where: { roleCode: { [db.Sequelize.Op.in]: ['ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'] } } where: {
[db.Sequelize.Op.or]: [
{ roleCode: ROLES.ZBH },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zonal Business Head%' } },
{ roleName: { [db.Sequelize.Op.iLike]: '%Zone Business Head%' } }
]
}
}, },
{ model: db.User, as: 'user', attributes: ['employeeId'] } { model: db.User, as: 'user', attributes: ['employeeId'] }
], ],
@ -119,3 +141,33 @@ export const syncZoneManager = async (zoneId: string) => {
console.error(`[Sync] Error synchronizing Zone ${zoneId}:`, error); console.error(`[Sync] Error synchronizing Zone ${zoneId}:`, error);
} }
}; };
/**
* Synchronizes all districts within a specific region.
*/
export const syncDistrictsByRegion = async (regionId: string) => {
try {
const districts = await db.District.findAll({ where: { regionId }, attributes: ['id'] });
for (const district of districts) {
await syncLocationManagers(district.id);
}
console.log(`[Sync] All districts in Region ${regionId} synchronized successfully`);
} catch (error) {
console.error(`[Sync] Error synchronizing districts for Region ${regionId}:`, error);
}
};
/**
* Synchronizes all districts within a specific zone.
*/
export const syncDistrictsByZone = async (zoneId: string) => {
try {
const districts = await db.District.findAll({ where: { zoneId }, attributes: ['id'] });
for (const district of districts) {
await syncLocationManagers(district.id);
}
console.log(`[Sync] All districts in Zone ${zoneId} synchronized successfully`);
} catch (error) {
console.error(`[Sync] Error synchronizing districts for Zone ${zoneId}:`, error);
}
};

View File

@ -1,18 +1,26 @@
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, State, Region } = db; const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, State, Region, Zone } = 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'; import { sendOpportunityEmail, sendNonOpportunityEmail } from '../../common/utils/email.service.js';
import { syncLocationManagers } from '../master/syncHierarchy.service.js';
const normalizeLocationType = (rawType?: string | null): string | null => { // Helper to find district by name and state name combination
if (!rawType) return null; const findDistrictByName = async (districtName: string, stateName?: string) => {
const normalized = String(rawType).trim().toLowerCase(); if (!districtName) return null;
const supportedTypes = new Set(['area', 'district', 'state', 'region', 'zone']);
return supportedTypes.has(normalized) ? normalized : null; return await District.findOne({
where: { name: { [Op.iLike]: districtName.trim() } },
include: stateName ? [{
model: State,
as: 'state',
where: { name: { [Op.iLike]: stateName.trim() } }
}] : []
});
}; };
export const submitApplication = async (req: AuthRequest, res: Response) => { export const submitApplication = async (req: AuthRequest, res: Response) => {
@ -42,52 +50,27 @@ 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()}`;
let districtId = null;
// Resolve location using canonical id/type first, then backward-compatible state+district names. // Primary Mapping: Resolve district by Name (State + District combination)
let locationId = null; // This is robust for external sources where ID mapping is difficult.
let isOpportunityAvailable = false; if (req.body.district) {
const normalizedType = normalizeLocationType(locationType); const districtRecord: any = await findDistrictByName(req.body.district, req.body.state);
if (req.body.locationId && normalizedType === 'district') {
const selectedDistrict = await District.findByPk(req.body.locationId);
if (selectedDistrict) {
locationId = selectedDistrict.id;
isOpportunityAvailable = true;
}
} else if (req.body.locationId && normalizedType === 'state') {
const selectedState = await State.findByPk(req.body.locationId);
if (selectedState) {
locationId = selectedState.id;
isOpportunityAvailable = true;
}
}
// Backward-compatible fallback path for older payloads that send only names.
if (!locationId && req.body.district) {
const districtRecord: any = await District.findOne({
where: { name: { [Op.iLike]: req.body.district } },
include: req.body.state ? [{
model: State,
as: 'state',
where: { name: { [Op.iLike]: req.body.state } }
}] : []
});
if (districtRecord) { if (districtRecord) {
locationId = districtRecord.id; districtId = districtRecord.id;
isOpportunityAvailable = true;
} }
} }
if (!locationId && req.body.state) { // Secondary Fallback: If ID is explicitly provided (Legacy/Internal use)
const stateRecord = await State.findOne({ if (!districtId && req.body.districtId) {
where: { name: { [Op.iLike]: req.body.state } } const selectedDistrict = await District.findByPk(req.body.districtId);
}); if (selectedDistrict) {
if (stateRecord) { districtId = selectedDistrict.id;
locationId = stateRecord.id;
isOpportunityAvailable = true;
} }
} }
const isOpportunityAvailable = !!districtId;
const application = await Application.create({ const application = await Application.create({
opportunityId: null, // De-coupled from Opportunity table as per user request opportunityId: null, // De-coupled from Opportunity table as per user request
applicationId, applicationId,
@ -104,8 +87,10 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
currentStage: APPLICATION_STAGES.DD, currentStage: APPLICATION_STAGES.DD,
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED, overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
progressPercentage: isOpportunityAvailable ? 10 : 0, progressPercentage: isOpportunityAvailable ? 10 : 0,
locationId, districtId,
districtId: locationId score: 0,
documents: [],
timeline: []
}); });
// Log Status History // Log Status History
@ -413,6 +398,7 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
ddLeadShortlisted: true, ddLeadShortlisted: true,
isShortlisted: true, isShortlisted: true,
overallStatus: 'Shortlisted', overallStatus: 'Shortlisted',
progressPercentage: 30,
assignedTo: primaryAssigneeId, assignedTo: primaryAssigneeId,
updatedAt: new Date(), updatedAt: new Date(),
}, { }, {
@ -436,6 +422,9 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
} }
}); });
} }
// AUTO-FILL Interview Evaluators for all 3 levels
await assignStageEvaluators(appId);
} }
// Create Status History Entries // Create Status History Entries
@ -468,6 +457,174 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
} }
}; };
/**
/**
* Helper to assign default evaluators for all 3 interview levels based on location
*/
/**
* Helper to assign default evaluators for all 3 interview levels, LOI, and LOA based on location
*/
const assignStageEvaluators = async (applicationId: string) => {
try {
console.log(`[debug] Starting stage evaluator assignment for App: ${applicationId}`);
const application = await Application.findByPk(applicationId, {
include: [
{
model: District,
as: 'district',
include: [
{ model: Region, as: 'region' },
{ model: Zone, as: 'zone' }
]
}
]
});
if (!application) {
console.log(`[debug] Application ${applicationId} not found`);
return;
}
if (!application.district) {
console.log(`[debug] Application ${applicationId} has NO district linked. Skipping auto-assign.`);
return;
}
const district = application.district;
const region = district.region;
const zone = district.zone;
console.log(`[debug] Mapping for District: ${district.name}, Region: ${region?.name}, Zone: ${zone?.name}`);
const evaluatorMappings: any = {
1: [], // Level 1 Interview: DD-ZM + RBM
2: [], // Level 2 Interview: DD Lead + ZBH
3: [], // Level 3 Interview: NBH + DD Head
'LOI_APPROVAL': [], // LOI: Finance, DD Head, NBH
'LOA_APPROVAL': [] // LOA: DD Head, NBH
};
// --- INTERVIEWS ---
// Level 1: DD-ZM (District manager) + RBM (Region manager)
if (district.zmId) evaluatorMappings[1].push({ id: district.zmId, role: 'DD-ZM' });
if (region && region.rbmId) evaluatorMappings[1].push({ id: region.rbmId, role: 'RBM' });
// Level 2: ZBH (Zone manager) + DD Lead (Filtered by Zone)
if (zone && zone.zbhId) evaluatorMappings[2].push({ id: zone.zbhId, role: 'ZBH' });
if (zone) {
const ddLead = await db.User.findOne({
where: { roleCode: 'DD Lead', status: 'active' },
include: [{
model: db.UserRole,
as: 'userRoles',
where: { zoneId: zone.id, isActive: true }
}]
});
if (ddLead) evaluatorMappings[2].push({ id: ddLead.id, role: 'DD Lead' });
}
// Level 3: NBH + DD Head (National Level Roles)
const level3Roles = ['NBH', 'DD Head'];
for (const roleCode of level3Roles) {
const user = await db.User.findOne({ where: { roleCode, status: 'active' } });
if (user) evaluatorMappings[3].push({ id: user.id, role: roleCode });
}
// --- LOI & LOA ---
// National roles for LOI / LOA
const nationalRoles = ['NBH', 'DD Head', 'Finance'];
const nationalUsers: Record<string, string> = {};
for (const r of nationalRoles) {
const user = await db.User.findOne({ where: { roleCode: r, status: 'active' } });
if (user) nationalUsers[r] = user.id;
}
// LOI: Finance, DD Head, NBH
if (nationalUsers['Finance']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['Finance'], role: 'Finance' });
if (nationalUsers['DD Head']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['DD Head'], role: 'DD Head' });
if (nationalUsers['NBH']) evaluatorMappings['LOI_APPROVAL'].push({ id: nationalUsers['NBH'], role: 'NBH' });
// LOA: DD Head, NBH
if (nationalUsers['DD Head']) evaluatorMappings['LOA_APPROVAL'].push({ id: nationalUsers['DD Head'], role: 'DD Head' });
if (nationalUsers['NBH']) evaluatorMappings['LOA_APPROVAL'].push({ id: nationalUsers['NBH'], role: 'NBH' });
// Persistence logic: Store in RequestParticipant with metadata
const allStages = [1, 2, 3, 'LOI_APPROVAL', 'LOA_APPROVAL'];
for (const stage of allStages) {
const assignments = evaluatorMappings[stage];
for (const assign of assignments) {
const { id: userId, role } = assign;
const whereClause: any = {
requestId: applicationId,
requestType: 'application',
userId,
participantType: 'contributor'
};
const existing = await db.RequestParticipant.findOne({ where: whereClause });
// If interview level, check metadata match. If stageCode, check metadata match.
const isInterview = typeof stage === 'number';
if (existing) {
const match = isInterview
? (existing.metadata?.interviewLevel === stage)
: (existing.metadata?.stageCode === stage);
if (match) continue;
}
await db.RequestParticipant.create({
requestId: applicationId,
requestType: 'application',
userId,
participantType: 'contributor',
joinedMethod: 'auto',
metadata: isInterview
? { interviewLevel: stage, role, autoMapped: true }
: { stageCode: stage, role, autoMapped: true }
});
}
}
} catch (error) {
console.error(`Error assigning stage evaluators for application ${applicationId}:`, error);
}
};
export const retriggerEvaluators = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const application = await Application.findByPk(id);
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
// Remove existing auto-mapped participants (Interviews, LOI, LOA)
// Using a more robust Postgres-compatible JSON path check
await db.RequestParticipant.destroy({
where: {
requestId: id,
requestType: 'application',
joinedMethod: 'auto',
[Op.and]: [
db.sequelize.literal(`"metadata"->>'autoMapped' = 'true'`)
]
}
});
// Sync district data before re-assignment to ensure fresh manager mapping
if (application.districtId) {
await syncLocationManagers(application.districtId);
}
await assignStageEvaluators(id as string);
res.json({ success: true, message: 'All stage evaluators (Interviews, LOI, LOA) have been re-assigned successfully.' });
} catch (error) {
console.error('Retrigger evaluators error:', error);
res.status(500).json({ success: false, message: 'Error re-triggering evaluator assignment' });
}
};
export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => { export const assignArchitectureTeam = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;

View File

@ -3,7 +3,8 @@ const router = express.Router();
import { import {
submitApplication, getApplications, getApplicationById, updateApplicationStatus, submitApplication, getApplications, getApplicationById, updateApplicationStatus,
uploadDocuments, getApplicationDocuments, bulkShortlist, uploadDocuments, getApplicationDocuments, bulkShortlist,
assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes,
retriggerEvaluators
} from './onboarding.controller.js'; } from './onboarding.controller.js';
import { authenticate } from '../../common/middleware/auth.js'; import { authenticate } from '../../common/middleware/auth.js';
@ -27,6 +28,7 @@ router.get('/applications/:id/documents', getApplicationDocuments); // Existing
router.post('/applications/:id/assign-architecture', assignArchitectureTeam); router.post('/applications/:id/assign-architecture', assignArchitectureTeam);
router.put('/applications/:id/architecture-status', updateArchitectureStatus); router.put('/applications/:id/architecture-status', updateArchitectureStatus);
router.post('/applications/:id/generate-codes', generateDealerCodes); router.post('/applications/:id/generate-codes', generateDealerCodes);
router.post('/applications/:id/retrigger-evaluators', retriggerEvaluators);
// Questionnaire Routes // Questionnaire Routes

View File

@ -119,7 +119,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Are you an existing dealer/vendor of Royal Enfield?", text: "Are you an existing dealer/vendor of Royal Enfield?",
type: "radio", type: "yesno",
section: "Basic Information", section: "Basic Information",
options: [ options: [
{ text: "Yes", score: 0 }, { text: "Yes", score: 0 },
@ -132,7 +132,7 @@ const seedQuestionnaire = async () => {
// Section 2: Profile & Background (Scoring Starts) // Section 2: Profile & Background (Scoring Starts)
{ {
text: "Educational Qualification", text: "Educational Qualification",
type: "radio", type: "select",
section: "Profile & Background", section: "Profile & Background",
options: [ options: [
{ text: "Under Graduate", score: 2 }, { text: "Under Graduate", score: 2 },
@ -158,7 +158,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Are you a native of the Proposed Location?", text: "Are you a native of the Proposed Location?",
type: "radio", type: "select",
section: "Location", section: "Location",
options: [ options: [
{ text: "Native", score: 10 }, { text: "Native", score: 10 },
@ -168,17 +168,9 @@ const seedQuestionnaire = async () => {
weight: 10, weight: 10,
order: 10 order: 10
}, },
{
text: "Proposed Location Photos (If any)",
type: "file",
section: "Location",
options: null,
weight: 0,
order: 11
},
{ {
text: "Why do you want to partner with Royal Enfield?", text: "Why do you want to partner with Royal Enfield?",
type: "radio", type: "select",
section: "Strategy", section: "Strategy",
options: [ options: [
{ text: "Absence of Royal Enfield in the particular location and presence of opportunity", score: 2 }, { text: "Absence of Royal Enfield in the particular location and presence of opportunity", score: 2 },
@ -190,7 +182,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Who will be the partners in proposed company?", text: "Who will be the partners in proposed company?",
type: "radio", type: "select",
section: "Business Structure", section: "Business Structure",
options: [ options: [
{ text: "Immediate Family", score: 5 }, { text: "Immediate Family", score: 5 },
@ -203,7 +195,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Who will be managing the Royal Enfield dealership", text: "Who will be managing the Royal Enfield dealership",
type: "radio", type: "select",
section: "Business Structure", section: "Business Structure",
options: [ options: [
{ text: "I will be managing full time", score: 10 }, { text: "I will be managing full time", score: 10 },
@ -215,7 +207,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Proposed Firm Type", text: "Proposed Firm Type",
type: "radio", type: "select",
section: "Business Structure", section: "Business Structure",
options: [ options: [
{ text: "Proprietorship", score: 3 }, { text: "Proprietorship", score: 3 },
@ -228,7 +220,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "What are you currently doing?", text: "What are you currently doing?",
type: "radio", type: "select",
section: "Experience", section: "Experience",
options: [ options: [
{ text: "Running automobile dealership", score: 10 }, { text: "Running automobile dealership", score: 10 },
@ -241,7 +233,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Do you own a property in proposed location?", text: "Do you own a property in proposed location?",
type: "radio", type: "select",
section: "Location", section: "Location",
options: [ options: [
{ text: "Yes", score: 10 }, { text: "Yes", score: 10 },
@ -252,7 +244,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "How are you planning to invest in the Royal Enfield business", text: "How are you planning to invest in the Royal Enfield business",
type: "radio", type: "select",
section: "Financials", section: "Financials",
options: [ options: [
{ text: "I will be investing my own funds", score: 10 }, { text: "I will be investing my own funds", score: 10 },
@ -264,7 +256,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "What are your plans of expansion with RE?", text: "What are your plans of expansion with RE?",
type: "radio", type: "select",
section: "Strategy", section: "Strategy",
options: [ options: [
{ text: "Willing to expand with the help of partners", score: 2 }, { text: "Willing to expand with the help of partners", score: 2 },
@ -276,7 +268,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Will you be expanding to any other automobile OEM in the future?", text: "Will you be expanding to any other automobile OEM in the future?",
type: "radio", type: "yesno",
section: "Strategy", section: "Strategy",
options: [ options: [
{ text: "Yes", score: 0 }, { text: "Yes", score: 0 },
@ -287,7 +279,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Do you own a Royal Enfield ?", text: "Do you own a Royal Enfield ?",
type: "radio", type: "select",
section: "Brand Loyalty", section: "Brand Loyalty",
options: [ options: [
{ text: "Yes, it is registered in my name", score: 3 }, { text: "Yes, it is registered in my name", score: 3 },
@ -300,7 +292,7 @@ const seedQuestionnaire = async () => {
}, },
{ {
text: "Do you go for long leisure rides", text: "Do you go for long leisure rides",
type: "radio", type: "select",
section: "Brand Loyalty", section: "Brand Loyalty",
options: [ options: [
{ text: "Yes, with the Royal Enfield riders", score: 3 }, { text: "Yes, with the Royal Enfield riders", score: 3 },