hirarchcy made orte stable and and tested upto interview levl 3
This commit is contained in:
parent
574fc84ba4
commit
feeb613136
31
check_history.ts
Normal file
31
check_history.ts
Normal 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();
|
||||||
@ -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
26
scripts/check_app.ts
Normal 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
20
scripts/debug_roles.ts
Normal 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();
|
||||||
@ -3,166 +3,207 @@ import db from '../src/database/models/index.js';
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js';
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js';
|
||||||
|
|
||||||
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 [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 } });
|
|
||||||
if (role) {
|
|
||||||
await UserRole.findOrCreate({
|
|
||||||
where: {
|
|
||||||
userId: userRec.id,
|
|
||||||
roleId: role.id,
|
|
||||||
...assignment
|
|
||||||
},
|
|
||||||
defaults: {
|
|
||||||
userId: userRec.id,
|
|
||||||
roleId: role.id,
|
|
||||||
...assignment,
|
|
||||||
isActive: true,
|
|
||||||
isPrimary: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4. Create Users and Map them
|
|
||||||
const nbhResult = await User.findOrCreate({
|
|
||||||
where: { email: 'nbh@example.com' },
|
|
||||||
defaults: { fullName: 'National Head', roleCode: 'NBH', password: hashedPassword }
|
|
||||||
});
|
|
||||||
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: {} }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const m of mockUsers) {
|
|
||||||
const [u] = await User.findOrCreate({
|
|
||||||
where: { email: m.email },
|
|
||||||
defaults: {
|
|
||||||
fullName: m.name,
|
|
||||||
roleCode: m.roleCode,
|
|
||||||
password: hashedPassword,
|
|
||||||
isExternal: (m as any).isExt || false,
|
|
||||||
status: 'active'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await mapUserRole(u, m.roleCode, m.assignment);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Users and Mappings seeded.');
|
|
||||||
|
|
||||||
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'] });
|
|
||||||
for (const d of districtList) await syncLocationManagers(d.id);
|
|
||||||
|
|
||||||
const regionList = await Region.findAll({ attributes: ['id'] });
|
|
||||||
for (const r of regionList) await syncRegionManager(r.id);
|
|
||||||
|
|
||||||
const zoneList = await Zone.findAll({ attributes: ['id'] });
|
|
||||||
for (const z of zoneList) await syncZoneManager(z.id);
|
|
||||||
|
|
||||||
console.log('--- Seeding & Synchronization Complete ---');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapUserRole = async (userRec: any, roleCode: string, assignment: any = {}) => {
|
||||||
|
const role = await Role.findOne({ where: { roleCode } });
|
||||||
|
if (role) {
|
||||||
|
await UserRole.findOrCreate({
|
||||||
|
where: { userId: userRec.id, roleId: role.id, ...assignment },
|
||||||
|
defaults: { userId: userRec.id, roleId: role.id, ...assignment, isActive: true, isPrimary: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Create Hierarchical Structure
|
||||||
|
// Zones
|
||||||
|
const zones = [
|
||||||
|
{ name: 'North Zone', code: 'ZONE-N' },
|
||||||
|
{ name: 'South Zone', code: 'ZONE-S' }
|
||||||
|
];
|
||||||
|
const zoneMap: Record<string, any> = {};
|
||||||
|
for (const z of zones) {
|
||||||
|
const [zone] = await Zone.findOrCreate({ where: { name: z.name }, defaults: z });
|
||||||
|
zoneMap[z.name] = zone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
});
|
||||||
|
regionMap[r.name] = region;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 ---');
|
||||||
|
const districtList = await District.findAll({ attributes: ['id'] });
|
||||||
|
for (const d of districtList) await syncLocationManagers(d.id);
|
||||||
|
|
||||||
|
const regionList = await Region.findAll({ attributes: ['id'] });
|
||||||
|
for (const r of regionList) await syncRegionManager(r.id);
|
||||||
|
|
||||||
|
const zoneList = await Zone.findAll({ attributes: ['id'] });
|
||||||
|
for (const z of zoneList) await syncZoneManager(z.id);
|
||||||
|
|
||||||
|
console.log('--- Golden Path Seeding Complete ---');
|
||||||
|
}
|
||||||
|
|
||||||
seed().catch(err => {
|
seed().catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@ -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
65
src/diag_zbh.ts
Normal 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
39
src/diag_zm.ts
Normal 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();
|
||||||
@ -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 } }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,35 +111,63 @@ 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);
|
||||||
|
|
||||||
if (hasRejection) {
|
console.log(`[debug] hasAllRequiredRoleApprovals: ${hasAllRequiredRoleApprovals}`);
|
||||||
await interview.update({ status: 'Completed' });
|
console.log(`[debug] meetsMinApprovals: ${meetsMinApprovals}`);
|
||||||
await db.Application.update({
|
console.log(`[debug] hasRejection: ${hasRejection}`);
|
||||||
overallStatus: 'Rejected',
|
|
||||||
currentStage: 'Rejected'
|
if (hasRejection) {
|
||||||
}, { where: { id: interview.applicationId } });
|
await interview.update({ status: 'Completed' });
|
||||||
await db.ApplicationStatusHistory.create({
|
|
||||||
applicationId: interview.applicationId,
|
const application: any = await db.Application.findByPk(interview.applicationId);
|
||||||
previousStatus: 'Interview Pending',
|
let rejectionProgress = application?.progressPercentage || 0;
|
||||||
newStatus: 'Rejected',
|
|
||||||
changedBy: userId,
|
// Marker progress values to show which stage was last reached
|
||||||
reason: 'Rejected in interview approval workflow'
|
if (policy.stageCode.includes('LEVEL1')) rejectionProgress = Math.max(rejectionProgress, 35);
|
||||||
});
|
if (policy.stageCode.includes('LEVEL2')) rejectionProgress = Math.max(rejectionProgress, 50);
|
||||||
} else if (hasAllRequiredRoleApprovals && meetsMinApprovals) {
|
if (policy.stageCode.includes('LEVEL3')) rejectionProgress = Math.max(rejectionProgress, 65);
|
||||||
|
|
||||||
|
await db.Application.update({
|
||||||
|
overallStatus: 'Rejected',
|
||||||
|
currentStage: 'Rejected',
|
||||||
|
progressPercentage: rejectionProgress
|
||||||
|
}, { where: { id: interview.applicationId } });
|
||||||
|
|
||||||
|
await db.ApplicationStatusHistory.create({
|
||||||
|
applicationId: interview.applicationId,
|
||||||
|
previousStatus: 'Interview Pending',
|
||||||
|
newStatus: 'Rejected',
|
||||||
|
changedBy: userId,
|
||||||
|
reason: 'Rejected in interview approval workflow'
|
||||||
|
});
|
||||||
|
} 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,
|
||||||
|
|||||||
@ -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' });
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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') {
|
||||||
|
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) {
|
if (zbhRole) {
|
||||||
// Deactivate old ZBHs for this zone
|
// 1. Deactivate old ZBHs for this zone
|
||||||
await db.UserRole.update({ isActive: false }, {
|
await db.UserRole.update({ isActive: false }, {
|
||||||
where: { zoneId: id, roleId: zbhRole.id }
|
where: { zoneId: id, roleId: zbhRole.id, isActive: true }
|
||||||
});
|
});
|
||||||
// Assign new ZBH
|
|
||||||
|
// 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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -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.
|
|
||||||
let locationId = null;
|
// Primary Mapping: Resolve district by Name (State + District combination)
|
||||||
let isOpportunityAvailable = false;
|
// This is robust for external sources where ID mapping is difficult.
|
||||||
const normalizedType = normalizeLocationType(locationType);
|
if (req.body.district) {
|
||||||
|
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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user