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: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",
|
||||
"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",
|
||||
"sync:hierarchy": "tsx scripts/sync-all-hierarchy.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 { 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() {
|
||||
console.log('--- Seeding Normalized Denormalized Data ---');
|
||||
async function seed() {
|
||||
console.log('--- Seeding Comprehensive Golden Path Data ---');
|
||||
|
||||
await db.sequelize.authenticate();
|
||||
// Use sync with alter false to match main app behavior
|
||||
await db.sequelize.sync({ alter: false });
|
||||
await db.sequelize.authenticate();
|
||||
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
|
||||
const roles = [
|
||||
{ roleCode: 'NBH', roleName: 'National Business Head', category: 'NATIONAL' },
|
||||
{ roleCode: 'DD Head', roleName: 'DD Head', category: 'NATIONAL' },
|
||||
{ roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' },
|
||||
{ roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' },
|
||||
{ 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: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' },
|
||||
{ roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' },
|
||||
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
|
||||
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
|
||||
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
|
||||
{ roleCode: 'Legal Admin', roleName: 'Legal Admin', category: 'DEPARTMENT' }
|
||||
];
|
||||
// 1. Ensure Roles exist
|
||||
const roles = [
|
||||
{ roleCode: 'NBH', roleName: 'National Business Head', category: 'NATIONAL' },
|
||||
{ roleCode: 'DD Head', roleName: 'DD Head', category: 'NATIONAL' },
|
||||
{ roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' },
|
||||
{ roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' },
|
||||
{ roleCode: 'RBM', roleName: 'Regional Business Manager', category: 'REGIONAL' },
|
||||
{ roleCode: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' },
|
||||
{ roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' },
|
||||
{ roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' },
|
||||
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
|
||||
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
|
||||
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' }
|
||||
];
|
||||
|
||||
for (const r of roles) {
|
||||
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 ---');
|
||||
for (const r of roles) {
|
||||
await Role.findOrCreate({ where: { roleCode: r.roleCode }, defaults: r });
|
||||
}
|
||||
|
||||
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 => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@ -24,7 +24,6 @@ export interface ApplicationAttributes {
|
||||
description: string | null;
|
||||
address: string | null;
|
||||
pincode: string | null;
|
||||
locationType: string | null;
|
||||
currentStage: string;
|
||||
overallStatus: string;
|
||||
progressPercentage: number;
|
||||
@ -142,10 +141,6 @@ export default (sequelize: Sequelize) => {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
locationType: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
currentStage: {
|
||||
type: DataTypes.ENUM(...Object.values(APPLICATION_STAGES)),
|
||||
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 whereClause: any = {};
|
||||
|
||||
if (roleCode) {
|
||||
// Handle both single string and array of role codes (if passed as multiple params)
|
||||
if (Array.isArray(roleCode)) {
|
||||
whereClause.roleCode = { [Op.in]: roleCode };
|
||||
} else {
|
||||
whereClause.roleCode = roleCode;
|
||||
let rawRoleCode: any = roleCode || req.query['roleCode[]'];
|
||||
let finalRoleCodes: string[] = [];
|
||||
|
||||
if (rawRoleCode) {
|
||||
if (Array.isArray(rawRoleCode)) {
|
||||
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 isNationalRole = (typeof roleCode === 'string' && nationalRoles.includes(roleCode)) ||
|
||||
(Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r)));
|
||||
const isNationalRole = finalRoleCodes.some(r => nationalRoles.includes(r));
|
||||
|
||||
if (!isNationalRole && locationId) {
|
||||
const district: any = await db.District.findByPk(locationId as string, {
|
||||
@ -196,7 +201,15 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
||||
|
||||
if (district) {
|
||||
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 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 };
|
||||
}
|
||||
|
||||
@ -80,7 +102,7 @@ const processInterviewApprovalDecision = async (params: {
|
||||
interviewId,
|
||||
stageCode: policy.stageCode,
|
||||
actorUserId: userId,
|
||||
actorRole: roleCode,
|
||||
actorRole: assignedRole || roleCode, // Use assigned role if available (e.g. ZBH acting as ZM)
|
||||
decision,
|
||||
remarks: remarks || null
|
||||
});
|
||||
@ -89,35 +111,63 @@ const processInterviewApprovalDecision = async (params: {
|
||||
where: { interviewId, stageCode: policy.stageCode }
|
||||
});
|
||||
|
||||
const uniqueApprovalsByRole = new Set(
|
||||
actions.filter((a: any) => a.decision === 'Approved').map((a: any) => a.actorRole)
|
||||
);
|
||||
const approvedActions = actions.filter((a: any) => a.decision === 'Approved');
|
||||
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 hasAllRequiredRoleApprovals = requiredRoles.length === 0
|
||||
? true
|
||||
: requiredRoles.every((role: string) => uniqueApprovalsByRole.has(role));
|
||||
|
||||
const meetsMinApprovals = uniqueApprovalsByRole.size >= (policy.minApprovals || 1);
|
||||
|
||||
if (hasRejection) {
|
||||
await interview.update({ status: 'Completed' });
|
||||
await db.Application.update({
|
||||
overallStatus: 'Rejected',
|
||||
currentStage: 'Rejected'
|
||||
}, { 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) {
|
||||
console.log(`[debug] hasAllRequiredRoleApprovals: ${hasAllRequiredRoleApprovals}`);
|
||||
console.log(`[debug] meetsMinApprovals: ${meetsMinApprovals}`);
|
||||
console.log(`[debug] hasRejection: ${hasRejection}`);
|
||||
|
||||
if (hasRejection) {
|
||||
await interview.update({ status: 'Completed' });
|
||||
|
||||
const application: any = await db.Application.findByPk(interview.applicationId);
|
||||
let rejectionProgress = application?.progressPercentage || 0;
|
||||
|
||||
// Marker progress values to show which stage was last reached
|
||||
if (policy.stageCode.includes('LEVEL1')) rejectionProgress = Math.max(rejectionProgress, 35);
|
||||
if (policy.stageCode.includes('LEVEL2')) rejectionProgress = Math.max(rejectionProgress, 50);
|
||||
if (policy.stageCode.includes('LEVEL3')) rejectionProgress = Math.max(rejectionProgress, 65);
|
||||
|
||||
await db.Application.update({
|
||||
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' });
|
||||
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 application = await db.Application.findByPk(interview.applicationId);
|
||||
const newProgress = progressMap[interview.level] || (application?.progressPercentage || 0);
|
||||
|
||||
await db.Application.update({
|
||||
overallStatus: newStatus,
|
||||
currentStage: newStatus
|
||||
currentStage: newStatus,
|
||||
progressPercentage: newProgress
|
||||
}, { where: { id: interview.applicationId } });
|
||||
await db.ApplicationStatusHistory.create({
|
||||
applicationId: interview.applicationId,
|
||||
@ -291,17 +341,17 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
|
||||
const application = await db.Application.findByPk(applicationId);
|
||||
let participantIds: string[] = Array.isArray(participants) ? participants : [];
|
||||
|
||||
// Auto-include relevant ZBH by location hierarchy when interviewer list is omitted.
|
||||
if (participantIds.length === 0 && (application?.districtId || application?.locationId)) {
|
||||
const ancestorLocationIds = await getLocationAncestors(application.districtId || application.locationId);
|
||||
const zonalHeads = await User.findAll({
|
||||
// Auto-fill participants from pre-assigned RequestParticipants if not provided
|
||||
if (participantIds.length === 0) {
|
||||
const preAssigned = await db.RequestParticipant.findAll({
|
||||
where: {
|
||||
roleCode: 'ZBH',
|
||||
locationId: { [Op.in]: ancestorLocationIds }
|
||||
requestId: applicationId,
|
||||
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)];
|
||||
|
||||
@ -464,6 +514,19 @@ export const submitKTMatrix = async (req: AuthRequest, res: Response) => {
|
||||
}));
|
||||
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 });
|
||||
} catch (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);
|
||||
}
|
||||
|
||||
// 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 });
|
||||
} catch (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' });
|
||||
}
|
||||
const { interviewId, recommendation } = req.body; // recommendation: 'Recommended' | 'Not Recommended'
|
||||
const normalizedDecision = (recommendation === 'Recommended' || recommendation === 'Selected' || recommendation === 'Approved')
|
||||
? 'Approved'
|
||||
: 'Rejected';
|
||||
const normalizedDecision = (['Recommended', 'Approved', 'Selected', 'Approve'].includes(recommendation))
|
||||
? 'Approved' : 'Rejected';
|
||||
|
||||
const result: any = await processInterviewApprovalDecision({
|
||||
interviewId,
|
||||
@ -672,7 +746,7 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) =
|
||||
return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
}
|
||||
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({
|
||||
interviewId,
|
||||
decision: normalizedDecision,
|
||||
|
||||
@ -6,14 +6,12 @@ import { AuthRequest } from '../../types/express.types.js';
|
||||
export const getChecklist = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { applicationId } = req.params;
|
||||
// Could auto-create if not exists?
|
||||
let checklist = await EorChecklist.findOne({
|
||||
where: { applicationId },
|
||||
include: [{ model: EorChecklistItem, as: 'items', include: ['proofDocument'] }]
|
||||
});
|
||||
|
||||
if (!checklist) {
|
||||
// Optional: Return empty or create new
|
||||
res.status(404).json({ success: false, message: 'Checklist not found' });
|
||||
return;
|
||||
}
|
||||
@ -63,7 +61,8 @@ export const createChecklist = async (req: AuthRequest, res: Response) => {
|
||||
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 });
|
||||
} catch (error) {
|
||||
@ -111,6 +110,16 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
|
||||
{ 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' });
|
||||
} catch (error) {
|
||||
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 });
|
||||
} catch (error) {
|
||||
@ -130,7 +133,11 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
if (action === 'Rejected' || hasRejection) {
|
||||
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' });
|
||||
}
|
||||
|
||||
@ -145,7 +152,10 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
|
||||
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' });
|
||||
} else {
|
||||
res.json({
|
||||
|
||||
@ -54,7 +54,10 @@ export const acknowledgeRequest = async (req: AuthRequest, res: Response) => {
|
||||
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' });
|
||||
} 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 });
|
||||
} catch (error) {
|
||||
@ -174,7 +180,11 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
|
||||
// 2. Handle Logic based on Action
|
||||
if (action === 'Rejected' || hasRejection) {
|
||||
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' });
|
||||
}
|
||||
|
||||
@ -191,7 +201,10 @@ export const approveRequest = async (req: AuthRequest, res: Response) => {
|
||||
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' });
|
||||
} else {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Request, Response } from 'express';
|
||||
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 { ROLES } from '../../common/config/constants.js';
|
||||
const { User } = db;
|
||||
|
||||
// --- 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.findOne({
|
||||
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 });
|
||||
} catch (error: any) {
|
||||
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' });
|
||||
} catch (error: any) {
|
||||
console.error('Update region error:', error);
|
||||
@ -395,7 +404,8 @@ export const getZones = async (req: Request, res: Response) => {
|
||||
include: [
|
||||
{ model: db.Region, as: 'regions', 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']]
|
||||
});
|
||||
@ -411,28 +421,42 @@ export const getZones = async (req: Request, res: Response) => {
|
||||
|
||||
const [zbhAssignment, zms] = await Promise.all([
|
||||
db.UserRole.findOne({
|
||||
where: { roleId: { [db.Sequelize.Op.in]: zbhRoleIds }, zoneId: zone.id, isPrimary: true, isActive: true },
|
||||
include: [{ model: db.User, as: 'user' }]
|
||||
where: { roleId: { [db.Sequelize.Op.in]: zbhRoleIds }, zoneId: zone.id, isActive: true },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId'] }],
|
||||
order: [['assignedAt', 'DESC']]
|
||||
}),
|
||||
db.UserRole.findAll({
|
||||
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 districts = await db.District.findAll({
|
||||
where: { zmId: zmRole.user.id, zoneId: zone.id }, // ZMs usually managed regions or specific district sets
|
||||
attributes: ['name']
|
||||
// Fetch all active regions assigned to this ZM in this zone
|
||||
const assignedRoles = await db.UserRole.findAll({
|
||||
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 {
|
||||
id: zmRole.user.id,
|
||||
name: zmRole.user.fullName || zmRole.user.name,
|
||||
email: zmRole.user.email,
|
||||
phone: zmRole.user.mobileNumber || '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.zonalBusinessHead = zbhAssignment ? {
|
||||
id: zbhAssignment.user.id,
|
||||
name: zbhAssignment.user.fullName || zbhAssignment.user.name,
|
||||
name: zbhAssignment.user.fullName,
|
||||
email: zbhAssignment.user.email,
|
||||
phone: zbhAssignment.user.mobileNumber || '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;
|
||||
return zoneJson;
|
||||
}));
|
||||
@ -464,11 +494,21 @@ export const createZone = async (req: Request, res: Response) => {
|
||||
|
||||
const zone = await db.Zone.create({ name, code });
|
||||
|
||||
// 1. Assign ZBH
|
||||
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) {
|
||||
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({
|
||||
userId: managerId,
|
||||
roleId: zbhRole.id,
|
||||
@ -476,6 +516,10 @@ export const createZone = async (req: Request, res: Response) => {
|
||||
isActive: 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 });
|
||||
|
||||
// 1. Update ZBH
|
||||
if (managerId) {
|
||||
const zbhRole = await db.Role.findOne({ where: { roleCode: 'ZBH' } });
|
||||
const { syncZoneManager } = await import('./syncHierarchy.service.js');
|
||||
|
||||
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) {
|
||||
// Deactivate old ZBHs for this zone
|
||||
// 1. Deactivate old ZBHs for this zone
|
||||
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({
|
||||
userId: managerId,
|
||||
roleId: zbhRole.id,
|
||||
@ -516,7 +571,30 @@ export const updateZone = async (req: Request, res: Response) => {
|
||||
isActive: 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)) {
|
||||
@ -866,6 +944,15 @@ export const saveZM = async (req: Request, res: Response) => {
|
||||
// Create new role assignments for each region
|
||||
if (Array.isArray(regionIds) && regionIds.length > 0) {
|
||||
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({
|
||||
userId,
|
||||
roleId: zmRole.id,
|
||||
@ -892,6 +979,13 @@ export const saveZM = async (req: Request, res: Response) => {
|
||||
// Cleanup: ZMs no longer manage districts directly
|
||||
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' });
|
||||
} catch (error) {
|
||||
console.error('Save ZM error:', error);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
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.
|
||||
@ -14,12 +15,17 @@ export const syncLocationManagers = async (districtId: string) => {
|
||||
if (!district) return;
|
||||
|
||||
// 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({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ districtId, isActive: true },
|
||||
{ regionId: district.regionId, isActive: true }
|
||||
]
|
||||
[Op.or]: orConditions
|
||||
},
|
||||
include: [
|
||||
{ model: Role, as: 'role', attributes: ['roleCode'] },
|
||||
@ -28,14 +34,24 @@ export const syncLocationManagers = async (districtId: string) => {
|
||||
});
|
||||
|
||||
// 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');
|
||||
const ddAm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'DD-AM' || (a.role as any)?.roleCode === 'AREA MANAGER');
|
||||
// ASM and DD-AM are STRICTLY mapped to the District level to prevent territory explosion
|
||||
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)
|
||||
// We prioritize the Region-level assignment if multiple exist
|
||||
// ZM can be assigned to the District (legacy/override) or the Region (standard) or Zone (broad)
|
||||
// Order of priority: 1. District level, 2. Region level, 3. Zone level
|
||||
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.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) =>
|
||||
((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,
|
||||
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'] }
|
||||
],
|
||||
@ -119,3 +141,33 @@ export const syncZoneManager = async (zoneId: string) => {
|
||||
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 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { Op } from 'sequelize';
|
||||
import { AuthRequest } from '../../types/express.types.js';
|
||||
|
||||
import { sendOpportunityEmail, sendNonOpportunityEmail } from '../../common/utils/email.service.js';
|
||||
import { syncLocationManagers } from '../master/syncHierarchy.service.js';
|
||||
|
||||
const normalizeLocationType = (rawType?: string | null): string | null => {
|
||||
if (!rawType) return null;
|
||||
const normalized = String(rawType).trim().toLowerCase();
|
||||
const supportedTypes = new Set(['area', 'district', 'state', 'region', 'zone']);
|
||||
return supportedTypes.has(normalized) ? normalized : null;
|
||||
// Helper to find district by name and state name combination
|
||||
const findDistrictByName = async (districtName: string, stateName?: string) => {
|
||||
if (!districtName) return 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) => {
|
||||
@ -42,52 +50,27 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
||||
}
|
||||
|
||||
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
|
||||
|
||||
// Resolve location using canonical id/type first, then backward-compatible state+district names.
|
||||
let locationId = null;
|
||||
let isOpportunityAvailable = false;
|
||||
const normalizedType = normalizeLocationType(locationType);
|
||||
|
||||
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 } }
|
||||
}] : []
|
||||
});
|
||||
let districtId = null;
|
||||
|
||||
// Primary Mapping: Resolve district by Name (State + District combination)
|
||||
// This is robust for external sources where ID mapping is difficult.
|
||||
if (req.body.district) {
|
||||
const districtRecord: any = await findDistrictByName(req.body.district, req.body.state);
|
||||
if (districtRecord) {
|
||||
locationId = districtRecord.id;
|
||||
isOpportunityAvailable = true;
|
||||
districtId = districtRecord.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!locationId && req.body.state) {
|
||||
const stateRecord = await State.findOne({
|
||||
where: { name: { [Op.iLike]: req.body.state } }
|
||||
});
|
||||
if (stateRecord) {
|
||||
locationId = stateRecord.id;
|
||||
isOpportunityAvailable = true;
|
||||
// Secondary Fallback: If ID is explicitly provided (Legacy/Internal use)
|
||||
if (!districtId && req.body.districtId) {
|
||||
const selectedDistrict = await District.findByPk(req.body.districtId);
|
||||
if (selectedDistrict) {
|
||||
districtId = selectedDistrict.id;
|
||||
}
|
||||
}
|
||||
|
||||
const isOpportunityAvailable = !!districtId;
|
||||
|
||||
const application = await Application.create({
|
||||
opportunityId: null, // De-coupled from Opportunity table as per user request
|
||||
applicationId,
|
||||
@ -104,8 +87,10 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
||||
currentStage: APPLICATION_STAGES.DD,
|
||||
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
|
||||
progressPercentage: isOpportunityAvailable ? 10 : 0,
|
||||
locationId,
|
||||
districtId: locationId
|
||||
districtId,
|
||||
score: 0,
|
||||
documents: [],
|
||||
timeline: []
|
||||
});
|
||||
|
||||
// Log Status History
|
||||
@ -413,6 +398,7 @@ export const bulkShortlist = async (req: AuthRequest, res: Response) => {
|
||||
ddLeadShortlisted: true,
|
||||
isShortlisted: true,
|
||||
overallStatus: 'Shortlisted',
|
||||
progressPercentage: 30,
|
||||
assignedTo: primaryAssigneeId,
|
||||
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
|
||||
@ -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) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
@ -3,7 +3,8 @@ const router = express.Router();
|
||||
import {
|
||||
submitApplication, getApplications, getApplicationById, updateApplicationStatus,
|
||||
uploadDocuments, getApplicationDocuments, bulkShortlist,
|
||||
assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes
|
||||
assignArchitectureTeam, updateArchitectureStatus, generateDealerCodes,
|
||||
retriggerEvaluators
|
||||
} from './onboarding.controller.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.put('/applications/:id/architecture-status', updateArchitectureStatus);
|
||||
router.post('/applications/:id/generate-codes', generateDealerCodes);
|
||||
router.post('/applications/:id/retrigger-evaluators', retriggerEvaluators);
|
||||
|
||||
|
||||
// Questionnaire Routes
|
||||
|
||||
@ -119,7 +119,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "Are you an existing dealer/vendor of Royal Enfield?",
|
||||
type: "radio",
|
||||
type: "yesno",
|
||||
section: "Basic Information",
|
||||
options: [
|
||||
{ text: "Yes", score: 0 },
|
||||
@ -132,7 +132,7 @@ const seedQuestionnaire = async () => {
|
||||
// Section 2: Profile & Background (Scoring Starts)
|
||||
{
|
||||
text: "Educational Qualification",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Profile & Background",
|
||||
options: [
|
||||
{ text: "Under Graduate", score: 2 },
|
||||
@ -158,7 +158,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "Are you a native of the Proposed Location?",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Location",
|
||||
options: [
|
||||
{ text: "Native", score: 10 },
|
||||
@ -168,17 +168,9 @@ const seedQuestionnaire = async () => {
|
||||
weight: 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?",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Strategy",
|
||||
options: [
|
||||
{ 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?",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Business Structure",
|
||||
options: [
|
||||
{ text: "Immediate Family", score: 5 },
|
||||
@ -203,7 +195,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "Who will be managing the Royal Enfield dealership",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Business Structure",
|
||||
options: [
|
||||
{ text: "I will be managing full time", score: 10 },
|
||||
@ -215,7 +207,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "Proposed Firm Type",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Business Structure",
|
||||
options: [
|
||||
{ text: "Proprietorship", score: 3 },
|
||||
@ -228,7 +220,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "What are you currently doing?",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Experience",
|
||||
options: [
|
||||
{ text: "Running automobile dealership", score: 10 },
|
||||
@ -241,7 +233,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "Do you own a property in proposed location?",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Location",
|
||||
options: [
|
||||
{ text: "Yes", score: 10 },
|
||||
@ -252,7 +244,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "How are you planning to invest in the Royal Enfield business",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Financials",
|
||||
options: [
|
||||
{ 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?",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Strategy",
|
||||
options: [
|
||||
{ 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?",
|
||||
type: "radio",
|
||||
type: "yesno",
|
||||
section: "Strategy",
|
||||
options: [
|
||||
{ text: "Yes", score: 0 },
|
||||
@ -287,7 +279,7 @@ const seedQuestionnaire = async () => {
|
||||
},
|
||||
{
|
||||
text: "Do you own a Royal Enfield ?",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Brand Loyalty",
|
||||
options: [
|
||||
{ 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",
|
||||
type: "radio",
|
||||
type: "select",
|
||||
section: "Brand Loyalty",
|
||||
options: [
|
||||
{ text: "Yes, with the Royal Enfield riders", score: 3 },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user