hirarchchy changed

This commit is contained in:
laxmanhalaki 2026-03-23 20:12:10 +05:30
parent 6dc506077f
commit d20e573d69
39 changed files with 1009 additions and 1506 deletions

16
check_db_roles.ts Normal file
View File

@ -0,0 +1,16 @@
import db from './src/database/models/index';
const { Role } = db;
async function check() {
try {
const roles = await Role.findAll();
console.log('--- Database Roles Check ---');
console.log('Roles Data:', JSON.stringify(roles, null, 2));
} catch (error) {
console.error('Check failed:', error);
} finally {
process.exit();
}
}
check();

25
check_db_users.ts Normal file
View File

@ -0,0 +1,25 @@
import db from './src/database/models/index';
const { User } = db;
async function check() {
try {
const users = await User.findAll({
attributes: ['id', 'roleCode', 'zoneId', 'regionId', 'areaId']
});
console.log('--- Database User Check ---');
console.log('Users Data:', JSON.stringify(users, null, 2));
const nbhUsers = users.filter((u: any) => u.roleCode === 'NBH');
console.log('NBH Users count:', nbhUsers.length);
const zmUsers = users.filter((u: any) => u.roleCode === 'DD-ZM');
console.log('DD-ZM Users count:', zmUsers.length);
} catch (error) {
console.error('Check failed:', error);
} finally {
process.exit();
}
}
check();

39
check_questions.ts Normal file
View File

@ -0,0 +1,39 @@
import db from './src/database/models/index.js';
async function checkQuestions() {
try {
const questionnaire = await db.Questionnaire.findOne({
where: { isActive: true },
include: [{
model: db.QuestionnaireQuestion,
as: 'questions',
include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }]
}]
});
if (!questionnaire) {
console.log('No active questionnaire found');
return;
}
console.log(`Active Questionnaire: ${questionnaire.version} (${questionnaire.id})`);
questionnaire.questions.forEach((q: any) => {
console.log(`- [${q.order}] ${q.questionText} (Weight: ${q.weight}, Type: ${q.inputType})`);
if (q.questionOptions && q.questionOptions.length > 0) {
q.questionOptions.forEach((opt: any) => {
console.log(` * ${opt.optionText} (Score: ${opt.score})`);
});
} else {
console.log(` (No options)`);
}
});
process.exit(0);
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
checkQuestions();

View File

@ -11,6 +11,7 @@
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"migrate": "tsx scripts/migrate.ts", "migrate": "tsx scripts/migrate.ts",
"seed": "tsx scripts/seed-geo.ts", "seed": "tsx scripts/seed-geo.ts",
"seed-normalized": "tsx scripts/seed_normalized_data.ts",
"test": "jest", "test": "jest",
"test:coverage": "jest --coverage", "test:coverage": "jest --coverage",
"clear-logs": "rm -rf logs/*.log" "clear-logs": "rm -rf logs/*.log"

View File

@ -15,6 +15,7 @@ const rolesToSeed = [
{ roleCode: ROLES.FINANCE, roleName: 'Finance', category: 'DEPARTMENT', description: 'Finance Department' }, { roleCode: ROLES.FINANCE, roleName: 'Finance', category: 'DEPARTMENT', description: 'Finance Department' },
{ roleCode: ROLES.LEGAL_ADMIN, roleName: 'Legal Admin', category: 'DEPARTMENT', description: 'Legal Department' }, { roleCode: ROLES.LEGAL_ADMIN, roleName: 'Legal Admin', category: 'DEPARTMENT', description: 'Legal Department' },
{ roleCode: ROLES.NBH, roleName: 'NBH', category: 'NATIONAL', description: 'National Business Head' }, { roleCode: ROLES.NBH, roleName: 'NBH', category: 'NATIONAL', description: 'National Business Head' },
{ roleCode: ROLES.ASM, roleName: 'ASM', category: 'SALES', description: 'Area Sales Manager' },
{ roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' } { roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' }
]; ];

View File

@ -0,0 +1,97 @@
import db from '../src/database/models/index.js';
const { Role, Location, LocationHierarchy, User, UserRole, Permission } = db;
async function seed() {
console.log('--- Seeding Normalized Graph Data ---');
// 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: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' },
{ roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' },
{ roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' }
];
for (const r of roles) {
await Role.findOrCreate({ where: { roleCode: r.roleCode }, defaults: r });
}
console.log('Roles seeded.');
// 2. Create Locations
const zone1 = await Location.create({ name: 'North Zone', type: 'zone' });
const region1 = await Location.create({ name: 'Delhi Region', type: 'region' });
const area1 = await Location.create({ name: 'South Delhi Area', type: 'area' });
const zone2 = await Location.create({ name: 'South Zone', type: 'zone' });
const region2 = await Location.create({ name: 'Bangalore Region', type: 'region' });
console.log('Locations created.');
// 3. Create Hierarchies (Bridge Table)
await LocationHierarchy.create({ locationId: region1.id, parentId: zone1.id });
await LocationHierarchy.create({ locationId: area1.id, parentId: region1.id });
// Example of multiple parents if needed
// await LocationHierarchy.create({ locationId: area1.id, parentId: someOtherParent.id });
await LocationHierarchy.create({ locationId: region2.id, parentId: zone2.id });
console.log('Hierarchies seeded.');
// 4. Create Users and Map them
// NBH (Global)
const nbhUser = await User.findOrCreate({
where: { email: 'nbh@example.com' },
defaults: { fullName: 'National Head', roleCode: 'NBH' }
});
await UserRole.create({ userId: nbhUser[0].id, roleId: (await Role.findOne({ where: { roleCode: 'NBH' } })).id });
// ZBH (North Zone)
const zbhUser = await User.findOrCreate({
where: { email: 'zbh.north@example.com' },
defaults: { fullName: 'North Zonal Head', roleCode: 'ZBH' }
});
const zbhRole = await Role.findOne({ where: { roleCode: 'ZBH' } });
await UserRole.create({
userId: zbhUser[0].id,
roleId: zbhRole.id,
locationId: zone1.id
});
// RBM (Delhi Region)
const rbmUser = await User.findOrCreate({
where: { email: 'rbm.delhi@example.com' },
defaults: { fullName: 'Delhi Regional Manager', roleCode: 'RBM' }
});
const rbmRole = await Role.findOne({ where: { roleCode: 'RBM' } });
await UserRole.create({
userId: rbmUser[0].id,
roleId: rbmRole.id,
locationId: region1.id
});
// ASM (South Delhi Area)
const asmUser = await User.findOrCreate({
where: { email: 'asm.sdelhi@example.com' },
defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM' }
});
const asmRole = await Role.findOne({ where: { roleCode: 'ASM' } });
await UserRole.create({
userId: asmUser[0].id,
roleId: asmRole.id,
locationId: area1.id
});
console.log('Users and Mappings seeded.');
console.log('--- Seeding Complete ---');
}
seed().catch(err => {
console.error(err);
process.exit(1);
}).then(() => process.exit(0));

View File

@ -10,8 +10,7 @@ export const generateToken = (user: any): string => {
userId: user.id, userId: user.id,
email: user.email, email: user.email,
role: user.roleCode, role: user.roleCode,
region: user.regionId, locationId: user.locationId
zone: user.zoneId
}; };
return jwt.sign(payload, JWT_SECRET, { return jwt.sign(payload, JWT_SECRET, {

View File

@ -11,6 +11,7 @@ export const ROLES = {
LEGAL_ADMIN: 'Legal Admin', LEGAL_ADMIN: 'Legal Admin',
SUPER_ADMIN: 'Super Admin', SUPER_ADMIN: 'Super Admin',
DD_AM: 'DD AM', DD_AM: 'DD AM',
ASM: 'ASM',
FINANCE: 'Finance', FINANCE: 'Finance',
DEALER: 'Dealer' DEALER: 'Dealer'
} as const; } as const;

View File

@ -34,9 +34,7 @@ export interface ApplicationAttributes {
architectureAssignedTo: string | null; architectureAssignedTo: string | null;
architectureStatus: string | null; architectureStatus: string | null;
submittedBy: string | null; submittedBy: string | null;
zoneId: string | null; locationId: string | null;
regionId: string | null;
areaId: string | null;
architectureAssignedDate: Date | null; architectureAssignedDate: Date | null;
architectureDocumentDate: Date | null; architectureDocumentDate: Date | null;
architectureCompletionDate: Date | null; architectureCompletionDate: Date | null;
@ -202,27 +200,11 @@ export default (sequelize: Sequelize) => {
key: 'id' key: 'id'
} }
}, },
zoneId: { locationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true, allowNull: true,
references: { references: {
model: 'zones', model: 'locations',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
areaId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'areas',
key: 'id' key: 'id'
} }
}, },
@ -263,9 +245,7 @@ export default (sequelize: Sequelize) => {
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' }); Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' }); Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' });
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' }); Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
Application.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); Application.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
Application.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
Application.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' });
Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' }); Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' });
Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' }); Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' });

View File

@ -1,141 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface AreaAttributes {
id: string;
regionId: string;
stateId: string;
zoneId: string;
districtId: string;
managerId: string | null;
areaCode: string;
areaName: string;
city: string | null;
pincode: string | null;
isActive: boolean;
activeFrom?: string | null;
activeTo?: string | null;
}
export interface AreaInstance extends Model<AreaAttributes>, AreaAttributes { }
export default (sequelize: Sequelize) => {
const Area = sequelize.define<AreaInstance>('Area', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
regionId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'regions',
key: 'id'
}
},
stateId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'states',
key: 'id'
}
},
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
},
districtId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'districts',
key: 'id'
}
},
managerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
areaCode: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
areaName: {
type: DataTypes.STRING,
allowNull: false
},
city: {
type: DataTypes.STRING,
allowNull: true
},
pincode: {
type: DataTypes.STRING,
allowNull: true
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
activeFrom: {
type: DataTypes.DATEONLY,
allowNull: true
},
activeTo: {
type: DataTypes.DATEONLY,
allowNull: true
}
}, {
tableName: 'areas',
timestamps: true
});
(Area as any).associate = (models: any) => {
Area.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
Area.belongsTo(models.State, {
foreignKey: 'stateId',
as: 'state'
});
Area.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
Area.belongsTo(models.District, {
foreignKey: 'districtId',
as: 'district'
});
Area.belongsTo(models.User, {
foreignKey: 'managerId',
as: 'manager'
});
Area.hasMany(models.Application, {
foreignKey: 'areaId',
as: 'applications'
});
// Dedicated Manager Table Associations
Area.hasMany(models.AreaManager, {
foreignKey: 'areaId',
as: 'areaManagers'
});
Area.belongsToMany(models.User, {
through: models.AreaManager,
foreignKey: 'areaId',
otherKey: 'userId',
as: 'assignedManagers'
});
};
return Area;
};

View File

@ -1,72 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface AreaManagerAttributes {
id: string;
areaId: string;
userId: string;
managerType: string;
asmCode?: string;
isActive: boolean;
assignedAt: Date;
}
export interface AreaManagerInstance extends Model<AreaManagerAttributes>, AreaManagerAttributes { }
export default (sequelize: Sequelize) => {
const AreaManager = sequelize.define<AreaManagerInstance>('AreaManager', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
areaId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'areas',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
managerType: {
type: DataTypes.STRING,
allowNull: false
},
asmCode: {
type: DataTypes.STRING,
allowNull: true
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
assignedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'area_managers',
timestamps: true,
updatedAt: false
});
(AreaManager as any).associate = (models: any) => {
AreaManager.belongsTo(models.Area, {
foreignKey: 'areaId',
as: 'area'
});
AreaManager.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return AreaManager;
};

View File

@ -1,78 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface DistrictAttributes {
id: string;
stateId: string;
zoneId: string;
regionId: string;
districtName: string;
isActive: boolean;
}
export interface DistrictInstance extends Model<DistrictAttributes>, DistrictAttributes { }
export default (sequelize: Sequelize) => {
const District = sequelize.define<DistrictInstance>('District', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
stateId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'states',
key: 'id'
}
},
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
districtName: {
type: DataTypes.STRING,
allowNull: false
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'districts',
timestamps: true
});
(District as any).associate = (models: any) => {
District.belongsTo(models.State, {
foreignKey: 'stateId',
as: 'state'
});
District.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
District.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
District.hasMany(models.Area, {
foreignKey: 'districtId',
as: 'areas'
});
};
return District;
};

View File

@ -1,67 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface DistrictManagerAttributes {
id: string;
districtId: string;
userId: string;
managerType: string;
isActive: boolean;
assignedAt: Date;
}
export interface DistrictManagerInstance extends Model<DistrictManagerAttributes>, DistrictManagerAttributes { }
export default (sequelize: Sequelize) => {
const DistrictManager = sequelize.define<DistrictManagerInstance>('DistrictManager', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
districtId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'districts',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
managerType: {
type: DataTypes.STRING,
allowNull: false
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
assignedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'district_managers',
timestamps: true,
updatedAt: false
});
(DistrictManager as any).associate = (models: any) => {
DistrictManager.belongsTo(models.District, {
foreignKey: 'districtId',
as: 'district'
});
DistrictManager.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return DistrictManager;
};

View File

@ -0,0 +1,51 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface LocationAttributes {
id: string;
name: string;
type: 'zone' | 'region' | 'area' | 'state' | 'district';
}
export interface LocationInstance extends Model<LocationAttributes>, LocationAttributes { }
export default (sequelize: Sequelize) => {
const Location = sequelize.define<LocationInstance>('Location', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
type: {
type: DataTypes.ENUM('zone', 'region', 'area', 'state', 'district'),
allowNull: false
}
}, {
tableName: 'locations',
timestamps: true
});
(Location as any).associate = (models: any) => {
// Many-to-Many hierarchy via LocationHierarchy bridge table
Location.belongsToMany(models.Location, {
through: models.LocationHierarchy,
as: 'parents',
foreignKey: 'locationId',
otherKey: 'parentId'
});
Location.belongsToMany(models.Location, {
through: models.LocationHierarchy,
as: 'children',
foreignKey: 'parentId',
otherKey: 'locationId'
});
Location.hasMany(models.UserRole, { foreignKey: 'locationId', as: 'userRoles' });
Location.hasMany(models.Application, { foreignKey: 'locationId', as: 'applications' });
};
return Location;
};

View File

@ -0,0 +1,51 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface LocationHierarchyAttributes {
id: string;
locationId: string;
parentId: string;
relationshipType: string | null;
}
export interface LocationHierarchyInstance extends Model<LocationHierarchyAttributes>, LocationHierarchyAttributes { }
export default (sequelize: Sequelize) => {
const LocationHierarchy = sequelize.define<LocationHierarchyInstance>('LocationHierarchy', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
locationId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'locations',
key: 'id'
}
},
parentId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'locations',
key: 'id'
}
},
relationshipType: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: 'direct'
}
}, {
tableName: 'location_hierarchies',
timestamps: true
});
(LocationHierarchy as any).associate = (models: any) => {
LocationHierarchy.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
LocationHierarchy.belongsTo(models.Location, { foreignKey: 'parentId', as: 'parent' });
};
return LocationHierarchy;
};

View File

@ -2,10 +2,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface OpportunityAttributes { export interface OpportunityAttributes {
id: string; id: string;
zoneId: string; locationId: string;
regionId: string;
stateId: string | null;
districtId: string | null;
city: string; city: string;
opportunityType: string; opportunityType: string;
capacity: string; capacity: string;
@ -26,35 +23,11 @@ export default (sequelize: Sequelize) => {
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
primaryKey: true primaryKey: true
}, },
zoneId: { locationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: false,
references: { references: {
model: 'zones', model: 'locations',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'regions',
key: 'id'
}
},
stateId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'states',
key: 'id'
}
},
districtId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'districts',
key: 'id' key: 'id'
} }
}, },
@ -104,10 +77,7 @@ export default (sequelize: Sequelize) => {
}); });
(Opportunity as any).associate = (models: any) => { (Opportunity as any).associate = (models: any) => {
Opportunity.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); Opportunity.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
Opportunity.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
Opportunity.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
Opportunity.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
Opportunity.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' }); Opportunity.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' });
Opportunity.hasMany(models.Application, { foreignKey: 'opportunityId', as: 'applications' }); Opportunity.hasMany(models.Application, { foreignKey: 'opportunityId', as: 'applications' });
}; };

View File

@ -15,8 +15,7 @@ export interface OutletAttributes {
status: typeof OUTLET_STATUS[keyof typeof OUTLET_STATUS]; status: typeof OUTLET_STATUS[keyof typeof OUTLET_STATUS];
establishedDate: string; establishedDate: string;
dealerId: string; dealerId: string;
region: typeof REGIONS[keyof typeof REGIONS]; locationId: string;
zone: string;
} }
export interface OutletInstance extends Model<OutletAttributes>, OutletAttributes { } export interface OutletInstance extends Model<OutletAttributes>, OutletAttributes { }
@ -81,13 +80,13 @@ export default (sequelize: Sequelize) => {
key: 'id' key: 'id'
} }
}, },
region: { locationId: {
type: DataTypes.ENUM(...Object.values(REGIONS)), type: DataTypes.UUID,
allowNull: false allowNull: false,
}, references: {
zone: { model: 'locations',
type: DataTypes.STRING, key: 'id'
allowNull: false }
} }
}, { }, {
tableName: 'outlets', tableName: 'outlets',
@ -97,8 +96,7 @@ export default (sequelize: Sequelize) => {
{ fields: ['dealerId'] }, { fields: ['dealerId'] },
{ fields: ['type'] }, { fields: ['type'] },
{ fields: ['status'] }, { fields: ['status'] },
{ fields: ['region'] }, { fields: ['locationId'] }
{ fields: ['zone'] }
] ]
}); });
@ -107,6 +105,10 @@ export default (sequelize: Sequelize) => {
foreignKey: 'dealerId', foreignKey: 'dealerId',
as: 'dealer' as: 'dealer'
}); });
Outlet.belongsTo(models.Location, {
foreignKey: 'locationId',
as: 'location'
});
Outlet.hasMany(models.Resignation, { Outlet.hasMany(models.Resignation, {
foreignKey: 'outletId', foreignKey: 'outletId',
as: 'resignations' as: 'resignations'

View File

@ -1,101 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface RegionAttributes {
id: string;
zoneId: string;
// stateId: string | null; // Removed as Region covers multiple states
regionalManagerId: string | null;
regionCode: string;
regionName: string;
description: string | null;
isActive: boolean;
}
export interface RegionInstance extends Model<RegionAttributes>, RegionAttributes { }
export default (sequelize: Sequelize) => {
const Region = sequelize.define<RegionInstance>('Region', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
zoneId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'zones',
key: 'id'
}
},
// stateId: {
// type: DataTypes.UUID,
// allowNull: true,
// references: {
// model: 'states',
// key: 'id'
// }
// },
regionalManagerId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
},
regionCode: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
regionName: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'regions',
timestamps: true
});
(Region as any).associate = (models: any) => {
Region.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
// Region.belongsTo(models.State, {
// foreignKey: 'stateId',
// as: 'state'
// });
Region.hasMany(models.State, {
foreignKey: 'regionId',
as: 'states'
});
Region.hasMany(models.Area, {
foreignKey: 'regionId',
as: 'areas'
});
Region.hasMany(models.RegionManager, {
foreignKey: 'regionId',
as: 'managers'
});
Region.hasMany(models.Application, {
foreignKey: 'regionId',
as: 'applications'
});
Region.belongsTo(models.User, {
foreignKey: 'regionalManagerId',
as: 'regionalManager'
});
};
return Region;
};

View File

@ -1,67 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface RegionManagerAttributes {
id: string;
regionId: string;
userId: string;
managerType: string;
isActive: boolean;
assignedAt: Date;
}
export interface RegionManagerInstance extends Model<RegionManagerAttributes>, RegionManagerAttributes { }
export default (sequelize: Sequelize) => {
const RegionManager = sequelize.define<RegionManagerInstance>('RegionManager', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
regionId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'regions',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
managerType: {
type: DataTypes.STRING,
allowNull: false
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
assignedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'region_managers',
timestamps: true,
updatedAt: false
});
(RegionManager as any).associate = (models: any) => {
RegionManager.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
RegionManager.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return RegionManager;
};

View File

@ -1,70 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface StateAttributes {
id: string;
stateName: string;
zoneId: string;
regionId: string | null;
isActive: boolean;
}
export interface StateInstance extends Model<StateAttributes>, StateAttributes { }
export default (sequelize: Sequelize) => {
const State = sequelize.define<StateInstance>('State', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
stateName: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
zoneId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'zones',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'states',
timestamps: true
});
(State as any).associate = (models: any) => {
State.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
State.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
State.hasMany(models.District, {
foreignKey: 'stateId',
as: 'districts'
});
State.hasMany(models.Region, {
foreignKey: 'stateId',
as: 'regions'
});
};
return State;
};

View File

@ -11,11 +11,7 @@ export interface UserAttributes {
department: string | null; department: string | null;
designation: string | null; designation: string | null;
roleCode: string | null; roleCode: string | null;
zoneId: string | null; locationId: string | null;
regionId: string | null;
stateId: string | null;
districtId: string | null;
areaId: string | null;
dealerId: string | null; dealerId: string | null;
isActive: boolean; isActive: boolean;
isExternal: boolean; isExternal: boolean;
@ -70,43 +66,11 @@ export default (sequelize: Sequelize) => {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
}, },
zoneId: { locationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true, allowNull: true,
references: { references: {
model: 'zones', model: 'locations',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
stateId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'states',
key: 'id'
}
},
districtId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'districts',
key: 'id'
}
},
areaId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'areas',
key: 'id' key: 'id'
} }
}, },
@ -152,14 +116,9 @@ export default (sequelize: Sequelize) => {
}); });
User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' }); User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' });
User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' }); User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' });
User.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); User.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
User.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
User.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
User.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
User.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' });
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' }); User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' });
User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' }); User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' });
}; };

View File

@ -4,9 +4,7 @@ export interface UserRoleAttributes {
id: string; id: string;
userId: string; userId: string;
roleId: string; roleId: string;
zoneId: string | null; locationId: string | null;
regionId: string | null;
areaId: string | null;
assignedAt: Date; assignedAt: Date;
assignedBy: string | null; assignedBy: string | null;
} }
@ -36,27 +34,11 @@ export default (sequelize: Sequelize) => {
key: 'id' key: 'id'
} }
}, },
zoneId: { locationId: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true, allowNull: true,
references: { references: {
model: 'zones', model: 'locations',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
areaId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'areas',
key: 'id' key: 'id'
} }
}, },
@ -87,17 +69,9 @@ export default (sequelize: Sequelize) => {
foreignKey: 'roleId', foreignKey: 'roleId',
as: 'role' as: 'role'
}); });
UserRole.belongsTo(models.Zone, { UserRole.belongsTo(models.Location, {
foreignKey: 'zoneId', foreignKey: 'locationId',
as: 'zone' as: 'location'
});
UserRole.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
UserRole.belongsTo(models.Area, {
foreignKey: 'areaId',
as: 'area'
}); });
UserRole.belongsTo(models.User, { UserRole.belongsTo(models.User, {
foreignKey: 'assignedBy', foreignKey: 'assignedBy',

View File

@ -1,75 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface ZoneAttributes {
id: string;
zoneCode: string;
zoneName: string;
description: string | null;
isActive: boolean;
zonalBusinessHeadId: string | null;
}
export interface ZoneInstance extends Model<ZoneAttributes>, ZoneAttributes { }
export default (sequelize: Sequelize) => {
const Zone = sequelize.define<ZoneInstance>('Zone', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
zoneCode: {
type: DataTypes.STRING,
unique: true,
allowNull: false
},
zoneName: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
zonalBusinessHeadId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}
}, {
tableName: 'zones',
timestamps: true
});
(Zone as any).associate = (models: any) => {
Zone.belongsTo(models.User, {
foreignKey: 'zonalBusinessHeadId',
as: 'zonalBusinessHead'
});
Zone.hasMany(models.Region, {
foreignKey: 'zoneId',
as: 'regions'
});
Zone.hasMany(models.State, {
foreignKey: 'zoneId',
as: 'states'
});
Zone.hasMany(models.ZoneManager, {
foreignKey: 'zoneId',
as: 'managers'
});
Zone.hasMany(models.Application, {
foreignKey: 'zoneId',
as: 'applications'
});
};
return Zone;
};

View File

@ -1,67 +0,0 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface ZoneManagerAttributes {
id: string;
zoneId: string;
userId: string;
managerType: string;
isActive: boolean;
assignedAt: Date;
}
export interface ZoneManagerInstance extends Model<ZoneManagerAttributes>, ZoneManagerAttributes { }
export default (sequelize: Sequelize) => {
const ZoneManager = sequelize.define<ZoneManagerInstance>('ZoneManager', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
zoneId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'zones',
key: 'id'
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
managerType: {
type: DataTypes.STRING,
allowNull: false
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true
},
assignedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'zone_managers',
timestamps: true,
updatedAt: false
});
(ZoneManager as any).associate = (models: any) => {
ZoneManager.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
ZoneManager.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
};
return ZoneManager;
};

View File

@ -14,26 +14,19 @@ import createAuditLog from './AuditLog.js';
import createFinancePayment from './FinancePayment.js'; import createFinancePayment from './FinancePayment.js';
import createFnF from './FnF.js'; import createFnF from './FnF.js';
import createFnFLineItem from './FnFLineItem.js'; import createFnFLineItem from './FnFLineItem.js';
import createRegion from './Region.js';
import createZone from './Zone.js';
import createSLAConfiguration from './SLAConfiguration.js'; import createSLAConfiguration from './SLAConfiguration.js';
import createSLAReminder from './SLAReminder.js'; import createSLAReminder from './SLAReminder.js';
import createSLAEscalationConfig from './SLAEscalationConfig.js'; import createSLAEscalationConfig from './SLAEscalationConfig.js';
import createWorkflowStageConfig from './WorkflowStageConfig.js'; import createWorkflowStageConfig from './WorkflowStageConfig.js';
import createNotification from './Notification.js'; import createNotification from './Notification.js';
import createLocation from './Location.js';
import createLocationHierarchy from './LocationHierarchy.js';
// Batch 1: Organizational Hierarchy & User Management // Batch 1: Organizational Hierarchy & User Management
import createRole from './Role.js'; import createRole from './Role.js';
import createPermission from './Permission.js'; import createPermission from './Permission.js';
import createRolePermission from './RolePermission.js'; import createRolePermission from './RolePermission.js';
import createState from './State.js';
import createDistrict from './District.js';
import createArea from './Area.js';
import createUserRole from './UserRole.js'; import createUserRole from './UserRole.js';
import createZoneManager from './ZoneManager.js';
import createRegionManager from './RegionManager.js';
import createAreaManager from './AreaManager.js';
import createDistrictManager from './DistrictManager.js';
// Batch 2: Opportunity & Application Framework // Batch 2: Opportunity & Application Framework
import createOpportunity from './Opportunity.js'; import createOpportunity from './Opportunity.js';
@ -121,26 +114,19 @@ db.AuditLog = createAuditLog(sequelize);
db.FinancePayment = createFinancePayment(sequelize); db.FinancePayment = createFinancePayment(sequelize);
db.FnF = createFnF(sequelize); db.FnF = createFnF(sequelize);
db.FnFLineItem = createFnFLineItem(sequelize); db.FnFLineItem = createFnFLineItem(sequelize);
db.Region = createRegion(sequelize);
db.Zone = createZone(sequelize);
db.SLAConfiguration = createSLAConfiguration(sequelize); db.SLAConfiguration = createSLAConfiguration(sequelize);
db.SLAReminder = createSLAReminder(sequelize); db.SLAReminder = createSLAReminder(sequelize);
db.SLAEscalationConfig = createSLAEscalationConfig(sequelize); db.SLAEscalationConfig = createSLAEscalationConfig(sequelize);
db.WorkflowStageConfig = createWorkflowStageConfig(sequelize); db.WorkflowStageConfig = createWorkflowStageConfig(sequelize);
db.Notification = createNotification(sequelize); db.Notification = createNotification(sequelize);
db.Location = createLocation(sequelize);
db.LocationHierarchy = createLocationHierarchy(sequelize);
// Batch 1: Organizational Hierarchy & User Management // Batch 1: Organizational Hierarchy & User Management
db.Role = createRole(sequelize); db.Role = createRole(sequelize);
db.Permission = createPermission(sequelize); db.Permission = createPermission(sequelize);
db.RolePermission = createRolePermission(sequelize); db.RolePermission = createRolePermission(sequelize);
db.State = createState(sequelize);
db.District = createDistrict(sequelize);
db.Area = createArea(sequelize);
db.UserRole = createUserRole(sequelize); db.UserRole = createUserRole(sequelize);
db.ZoneManager = createZoneManager(sequelize);
db.RegionManager = createRegionManager(sequelize);
db.AreaManager = createAreaManager(sequelize);
db.DistrictManager = createDistrictManager(sequelize);
// Batch 2: Opportunity & Application Framework // Batch 2: Opportunity & Application Framework
db.Opportunity = createOpportunity(sequelize); db.Opportunity = createOpportunity(sequelize);

View File

@ -1,5 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { Op } from 'sequelize';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db; const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
import { AUDIT_ACTIONS } from '../../common/config/constants.js'; import { AUDIT_ACTIONS } from '../../common/config/constants.js';
@ -120,7 +121,45 @@ export const getPermissions = async (req: Request, res: Response) => {
export const getAllUsers = async (req: Request, res: Response) => { export const getAllUsers = async (req: Request, res: Response) => {
try { try {
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;
}
}
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)));
if (!isNationalRole && locationId) {
// Find all ancestors of the given location (BFS for many-to-many support)
let ancestorIds: Set<string> = new Set([locationId as string]);
let queue: string[] = [locationId as string];
while (queue.length > 0) {
const currentId = queue.shift()!;
const hierarchies = await db.LocationHierarchy.findAll({
where: { locationId: currentId }
});
for (const h of hierarchies) {
if (!ancestorIds.has(h.parentId)) {
ancestorIds.add(h.parentId);
queue.push(h.parentId);
}
}
}
whereClause.locationId = { [Op.in]: Array.from(ancestorIds) };
}
const users = await User.findAll({ const users = await User.findAll({
where: whereClause,
attributes: { exclude: ['password'] }, attributes: { exclude: ['password'] },
include: [ include: [
{ {
@ -134,9 +173,7 @@ export const getAllUsers = async (req: Request, res: Response) => {
} }
] ]
}, },
{ model: db.Zone, as: 'zone' }, { model: db.Location, as: 'location' }
{ model: db.Region, as: 'region' },
{ model: db.Area, as: 'area' }
], ],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
@ -152,9 +189,10 @@ export const createUser = async (req: AuthRequest, res: Response) => {
const { const {
fullName, email, roleCode, fullName, email, roleCode,
employeeId, mobileNumber, department, designation, employeeId, mobileNumber, department, designation,
zoneId, regionId, stateId, districtId, areaId locationId
} = req.body; } = req.body;
// Validate required fields // Validate required fields
if (!fullName || !email || !roleCode) { if (!fullName || !email || !roleCode) {
return res.status(400).json({ return res.status(400).json({
@ -198,11 +236,7 @@ export const createUser = async (req: AuthRequest, res: Response) => {
mobileNumber, mobileNumber,
department, department,
designation, designation,
zoneId, locationId
regionId,
stateId,
districtId,
areaId
}); });
await AuditLog.create({ await AuditLog.create({
@ -259,7 +293,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
const { const {
fullName, email, roleCode, status, isActive, employeeId, fullName, email, roleCode, status, isActive, employeeId,
mobileNumber, department, designation, mobileNumber, department, designation,
zoneId, regionId, stateId, districtId, areaId, locationId,
password // Optional password update password // Optional password update
} = req.body; } = req.body;
@ -277,11 +311,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
mobileNumber: mobileNumber || user.mobileNumber, mobileNumber: mobileNumber || user.mobileNumber,
department: department || user.department, department: department || user.department,
designation: designation || user.designation, designation: designation || user.designation,
zoneId: zoneId !== undefined ? zoneId : user.zoneId, locationId: (locationId === '' ? null : (locationId !== undefined ? locationId : user.locationId))
regionId: regionId !== undefined ? regionId : user.regionId,
stateId: stateId !== undefined ? stateId : user.stateId,
districtId: districtId !== undefined ? districtId : user.districtId,
areaId: areaId !== undefined ? areaId : user.areaId
}; };
// If password is provided, hash it and update // If password is provided, hash it and update
@ -319,7 +349,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
export const generateDealerCode = async (req: AuthRequest, res: Response) => { export const generateDealerCode = async (req: AuthRequest, res: Response) => {
try { try {
const { regionId, stateId, channel } = req.body; const { locationId, channel } = req.body;
// Logic to generate unique code based on format (e.g., RE-[Region]-[State]-[Seq]) // Logic to generate unique code based on format (e.g., RE-[Region]-[State]-[Seq])
// This is a placeholder for the actual business logic // This is a placeholder for the actual business logic

View File

@ -587,35 +587,34 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) =
}); });
} }
// Always mark interview as completed when a decision is made // --- Multi-Interviewer Synchronization ---
// This ensures action buttons hide for the user // Fetch all assigned participants for this interview
await interview.update({ status: 'Completed' }); const participants = await db.InterviewParticipant.findAll({
where: { interviewId }
});
// Update Application Status // Fetch all evaluations submitted for this interview
if (decision === 'Rejected') { const evaluations = await db.InterviewEvaluation.findAll({
await db.Application.update({ where: { interviewId }
overallStatus: 'Rejected', });
currentStage: 'Rejected'
}, { where: { id: interview.applicationId } });
// Log Status History const isFullyEvaluated = evaluations.length >= participants.length;
await db.ApplicationStatusHistory.create({
applicationId: interview.applicationId, if (isFullyEvaluated) {
previousStatus: 'Interview Pending', // All interviewers have responded
newStatus: 'Rejected', await interview.update({ status: 'Completed' });
changedBy: req.user?.id,
reason: remarks || 'Interview Rejected' // Determine next status based on level
});
} else {
// Determine next status based on current level
const nextStatusMap: any = { const nextStatusMap: any = {
1: 'Level 1 Approved', 1: 'Level 1 Approved',
2: 'Level 2 Approved', 2: 'Level 2 Approved',
3: 'Level 3 Approved' 3: 'Level 3 Approved'
}; };
// Check if any interviewer rejected (for logging/metadata, though we still move forward as requested)
const hasRejection = evaluations.some((e: any) => e.decision === 'Rejected');
const newStatus = nextStatusMap[interview.level] || 'Approved'; const newStatus = nextStatusMap[interview.level] || 'Approved';
// Also update currentStage for better tracking
const stageMapping: any = { const stageMapping: any = {
1: 'Level 1 Approved', 1: 'Level 1 Approved',
2: 'Level 2 Approved', 2: 'Level 2 Approved',
@ -633,8 +632,13 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) =
previousStatus: 'Interview Pending', previousStatus: 'Interview Pending',
newStatus: newStatus, newStatus: newStatus,
changedBy: req.user?.id, changedBy: req.user?.id,
reason: remarks || 'Interview Approved' reason: hasRejection ? 'Interview completed with mixed recommendations' : 'Interview Approved by all'
}); });
} else {
// Still waiting for other interviewers
// We can mark the status as 'In Progress' or keep it as 'Scheduled'
// But we do NOT update the Application status yet.
console.log(`Interview ${interviewId}: Waiting for more evaluations. (${evaluations.length}/${participants.length})`);
} }
await db.AuditLog.create({ await db.AuditLog.create({

View File

@ -9,7 +9,7 @@ import { AuthRequest } from '../../types/express.types.js';
// Register new user // Register new user
export const register = async (req: Request, res: Response) => { export const register = async (req: Request, res: Response) => {
try { try {
const { email, password, fullName, role, phone, region, zone } = req.body; const { email, password, fullName, role, phone, locationId } = req.body;
// Validate input // Validate input
if (!email || !password || !fullName || !role) { if (!email || !password || !fullName || !role) {
@ -38,8 +38,7 @@ export const register = async (req: Request, res: Response) => {
fullName, fullName,
roleCode: role, roleCode: role,
mobileNumber: phone, mobileNumber: phone,
regionId: region, locationId,
zoneId: zone,
status: 'active' status: 'active'
}); });
@ -127,8 +126,7 @@ export const login = async (req: Request, res: Response) => {
email: user.email, email: user.email,
fullName: user.fullName, fullName: user.fullName,
role: user.roleCode, role: user.roleCode,
region: user.regionId, locationId: user.locationId
zone: user.zoneId
} }
}); });
} catch (error) { } catch (error) {
@ -163,7 +161,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
} }
const user = await User.findByPk(req.user.id, { const user = await User.findByPk(req.user.id, {
attributes: ['id', 'email', 'fullName', 'roleCode', 'regionId', 'zoneId', 'mobileNumber', 'createdAt'] attributes: ['id', 'email', 'fullName', 'roleCode', 'locationId', 'mobileNumber', 'createdAt']
}); });
if (!user) { if (!user) {
@ -180,8 +178,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
email: user.email, email: user.email,
fullName: user.fullName, fullName: user.fullName,
role: user.roleCode, role: user.roleCode,
region: user.regionId, locationId: user.locationId,
zone: user.zoneId,
phone: user.mobileNumber, phone: user.mobileNumber,
createdAt: (user as any).createdAt createdAt: (user as any).createdAt
} }

View File

@ -7,6 +7,7 @@ const {
} = db; } = db;
import { AuthRequest } from '../../types/express.types.js'; import { AuthRequest } from '../../types/express.types.js';
import { AUDIT_ACTIONS } from '../../common/config/constants.js'; import { AUDIT_ACTIONS } from '../../common/config/constants.js';
import { Op } from 'sequelize';
export const getDealers = async (req: Request, res: Response) => { export const getDealers = async (req: Request, res: Response) => {
try { try {
@ -110,8 +111,7 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
status: 'active', status: 'active',
isActive: true, isActive: true,
isExternal: true, // Dealers are external users isExternal: true, // Dealers are external users
zoneId: application.zoneId, locationId: application.locationId
regionId: application.regionId
}); });
console.log(`[Dealer Onboarding] Created new Dealer user account for ${user.email}.`); console.log(`[Dealer Onboarding] Created new Dealer user account for ${user.email}.`);
} }
@ -124,29 +124,18 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
newData: { roleCode: 'Dealer', dealerId: dealer.id } newData: { roleCode: 'Dealer', dealerId: dealer.id }
}); });
// --- Create Primary Outlet for the Dealer --- // --- Create or Link Outlet for this specific application ---
// Check if outlet already exists // We use the application's unique ID to ensure we don't duplicate THE SAME application's outlet,
let outlet = await Outlet.findOne({ where: { dealerId: user.id } }); // but we allow multiple outlets for the same User ID.
let outlet = await Outlet.findOne({
where: {
dealerId: user.id,
name: { [Op.like]: `%${application.city || 'Primary'}%` }, // Rough check for same location
createdAt: { [Op.gte]: new Date(new Date().getTime() - 60000) } // Prevent double-clicks in same minute
}
});
if (!outlet) { if (!outlet) {
let regionName = 'Central';
let zoneName = 'National';
if (application.regionId) {
const reg = await Region.findByPk(application.regionId);
if (reg) regionName = reg.regionName;
}
if (application.zoneId) {
const zon = await Zone.findByPk(application.zoneId);
if (zon) zoneName = zon.zoneName;
}
// Map region name to valid ENUM values if necessary
const validRegions = ['East', 'West', 'North', 'South', 'Central'];
if (!validRegions.includes(regionName)) {
regionName = 'Central'; // Fallback
}
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId: application.id } }); const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId: application.id } });
const outletCode = `OUT-${dealerCodeRecord?.dealerCode || Date.now().toString().slice(-6)}`; const outletCode = `OUT-${dealerCodeRecord?.dealerCode || Date.now().toString().slice(-6)}`;
@ -161,11 +150,10 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
status: 'Active', status: 'Active',
establishedDate: new Date(), establishedDate: new Date(),
dealerId: user.id, dealerId: user.id,
region: regionName as any, locationId: application.locationId
zone: zoneName
}); });
console.log(`[Dealer Onboarding] Created primary outlet ${outlet.code} for dealer ${user.email}.`); console.log(`[Dealer Onboarding] Created outlet ${outlet.code} for application ${application.applicationId} linked to user ${user.email}.`);
await AuditLog.create({ await AuditLog.create({
userId: req.user?.id, userId: req.user?.id,

View File

@ -1,60 +1,56 @@
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 { Region, Zone, State, District, Area, User, AreaManager } = db; const { User } = db;
// --- Generic Location Fetching ---
const getLocationsByType = async (type: string, req: Request, res: Response) => {
try {
const locations = await db.Location.findAll({
where: { type },
include: [
{
model: db.Location,
as: 'parents',
through: { attributes: [] }
},
{
model: db.Location,
as: 'children',
through: { attributes: [] }
}
],
order: [['name', 'ASC']]
});
res.json({ success: true, data: locations });
} catch (error) {
console.error(`Get ${type} list error:`, error);
res.status(500).json({ success: false, message: `Error fetching ${type} list` });
}
};
// --- Regions --- // --- Regions ---
export const getRegions = async (req: Request, res: Response) => { export const getRegions = async (req: Request, res: Response) => {
try { return getLocationsByType('region', req, res);
const regions = await Region.findAll({
include: [
{
model: State,
as: 'states',
attributes: ['id', 'stateName']
},
{
model: Zone,
as: 'zone',
attributes: ['id', 'zoneName']
},
{
model: User,
as: 'regionalManager',
attributes: ['id', 'fullName', 'email', 'mobileNumber']
}
],
order: [['regionName', 'ASC']]
});
res.json({ success: true, data: regions });
} catch (error) {
console.error('Get regions error:', error);
res.status(500).json({ success: false, message: 'Error fetching regions' });
}
}; };
export const createRegion = async (req: Request, res: Response) => { export const createRegion = async (req: Request, res: Response) => {
try { try {
const { zoneId, regionCode, regionName, description, stateIds, regionalManagerId } = req.body; const { zoneId, regionName } = req.body;
if (!zoneId || !regionName || !regionCode) { if (!regionName) {
return res.status(400).json({ success: false, message: 'Zone ID, region name and code are required' }); return res.status(400).json({ success: false, message: 'Region name is required' });
} }
const region = await Region.create({ const region = await db.Location.create({
zoneId, name: regionName,
regionCode, type: 'region'
regionName,
description,
regionalManagerId: regionalManagerId || null
}); });
// Assign states if provided if (zoneId) {
if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) { await db.LocationHierarchy.create({
await State.update( locationId: region.id,
{ regionId: region.id, zoneId }, // Also ensure State belongs to the Zone (hierarchy) parentId: zoneId
{ where: { id: stateIds } } });
);
} }
res.status(201).json({ success: true, message: 'Region created successfully', data: region }); res.status(201).json({ success: true, message: 'Region created successfully', data: region });
@ -64,122 +60,31 @@ export const createRegion = async (req: Request, res: Response) => {
} }
}; };
export const updateRegion = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { zoneId, regionCode, regionName, description, isActive, stateIds, regionalManagerId } = req.body;
const region = await Region.findByPk(id);
if (!region) {
return res.status(404).json({ success: false, message: 'Region not found' });
}
const updates: any = {};
if (zoneId) updates.zoneId = zoneId;
if (regionCode) updates.regionCode = regionCode;
if (regionName) updates.regionName = regionName;
if (description !== undefined) updates.description = description;
if (isActive !== undefined) updates.isActive = isActive;
if (regionalManagerId !== undefined) updates.regionalManagerId = regionalManagerId;
await region.update(updates);
// Handle State reassignment
if (stateIds && Array.isArray(stateIds)) {
// 1. Unassign states currently assigned to this region but NOT in the new list?
// Or just simpler: Assign the new ones. Old ones stay?
// Standard behavior for "List of items in a container": Sync list.
// We should set regionId=null for states previously in this region but not in stateIds.
// But let's check safety. If I uncheck a state, I want it removed from the region.
// First, find states currently in this region
// Actually, simplest 'Reset and Set' approach:
// 1. Set regionId=null for all states where regionId = this.id
// 2. Set regionId=this.id for states in stateIds.
// Note: We should probably also enforce zoneId match?
// If a user moves a state to this Region, the State must conceptually belong to the Region's Zone.
// So we update both regionId and zoneId for the target states.
// Step 1: Remove States from this Region (if they are NOT in the new list)
// We can do this by:
// await State.update({ regionId: null }, { where: { regionId: id } });
// But wait, if I am only ADDING, I don't want to nuke everything.
// But "update" implies "this is the new state of the world".
// Assuming frontend sends the FULL list of selected states.
await State.update({ regionId: null }, { where: { regionId: id } });
if (stateIds.length > 0) {
await State.update(
{
regionId: id,
zoneId: zoneId || region.zoneId // Ensure state moves to the region's zone
},
{ where: { id: stateIds } }
);
}
}
res.json({ success: true, message: 'Region updated successfully' });
} catch (error) {
console.error('Update region error:', error);
res.status(500).json({ success: false, message: 'Error updating region' });
}
};
// --- Zones --- // --- Zones ---
export const getZones = async (req: Request, res: Response) => { export const getZones = async (req: Request, res: Response) => {
try { return getLocationsByType('zone', req, res);
const { regionId } = req.query as { regionId?: string };
const where: any = {};
if (regionId) {
where.regionId = regionId;
}
const zones = await Zone.findAll({
where,
include: [
{
model: Region,
as: 'regions',
attributes: ['regionName']
},
{
model: User,
as: 'zonalBusinessHead',
attributes: ['fullName', 'email', 'mobileNumber']
},
{
model: State,
as: 'states',
attributes: ['stateName']
}
],
order: [['zoneName', 'ASC']]
});
res.json({ success: true, data: zones });
} catch (error) {
console.error('Get zones error:', error);
res.status(500).json({ success: false, message: 'Error fetching zones' });
}
}; };
export const createZone = async (req: Request, res: Response) => { export const createZone = async (req: Request, res: Response) => {
try { try {
const { regionId, zoneName } = req.body; const { regionId, zoneName } = req.body;
if (!regionId || !zoneName) { if (!zoneName) {
return res.status(400).json({ success: false, message: 'Region ID and zone name are required' }); return res.status(400).json({ success: false, message: 'Zone name is required' });
} }
const zone = await Zone.create({ const zone = await db.Location.create({
regionId, // Wait, Zone Model doesn't have regionId. It's the other way around? name: zoneName,
zoneName type: 'zone'
}); });
if (regionId) {
await db.LocationHierarchy.create({
locationId: zone.id,
parentId: regionId
});
}
res.status(201).json({ success: true, message: 'Zone created successfully', data: zone }); res.status(201).json({ success: true, message: 'Zone created successfully', data: zone });
} catch (error) { } catch (error) {
console.error('Create zone error:', error); console.error('Create zone error:', error);
@ -187,69 +92,53 @@ export const createZone = async (req: Request, res: Response) => {
} }
}; };
export const updateZone = async (req: Request, res: Response) => { export const updateLocation = async (req: Request, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { zoneName, description, isActive, zonalBusinessHeadId, stateIds } = req.body; const { name, type, parentIds } = req.body;
const zone = await Zone.findByPk(id); const location = await db.Location.findByPk(id);
if (!zone) { if (!location) {
return res.status(404).json({ success: false, message: 'Zone not found' }); return res.status(404).json({ success: false, message: 'Location not found' });
} }
const updates: any = {}; const updates: any = {};
if (zoneName) updates.zoneName = zoneName; if (name) updates.name = name;
if (description !== undefined) updates.description = description; if (type) updates.type = type;
if (isActive !== undefined) updates.isActive = isActive;
if (zonalBusinessHeadId !== undefined) updates.zonalBusinessHeadId = zonalBusinessHeadId;
await zone.update(updates); await location.update(updates);
// Handle State assignment if (parentIds && Array.isArray(parentIds)) {
if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) { // Re-sync parents
// Update all provided states to belong to this zone await db.LocationHierarchy.destroy({ where: { locationId: id } });
// We can't easily "remove" states because zoneId is non-nullable. for (const pid of parentIds) {
// States must be moved TO another zone to be removed from this one. await db.LocationHierarchy.create({ locationId: id, parentId: pid });
// So we primarily handle "bringing states into this zone". }
// However, we should check if they exist first.
await State.update(
{ zoneId: zone.id },
{ where: { id: stateIds } }
);
} }
res.json({ success: true, message: 'Zone updated successfully' }); res.json({ success: true, message: 'Location updated successfully' });
} catch (error) { } catch (error) {
console.error('Update zone error:', error); console.error('Update location error:', error);
res.status(500).json({ success: false, message: 'Error updating zone' }); res.status(500).json({ success: false, message: 'Error updating location' });
} }
}; };
// --- States --- // --- States ---
export const getStates = async (req: Request, res: Response) => { export const getStates = async (req: Request, res: Response) => {
try { return getLocationsByType('state', req, res);
const { zoneId } = req.query as { zoneId?: string };
const where: any = {};
if (zoneId) where.zoneId = zoneId;
const states = await State.findAll({
where,
include: [{ model: Zone, as: 'zone', attributes: ['zoneName'] }],
order: [['stateName', 'ASC']]
});
res.json({ success: true, states });
} catch (error) {
console.error('Get states error:', error);
res.status(500).json({ success: false, message: 'Error fetching states' });
}
}; };
export const createState = async (req: Request, res: Response) => { export const createState = async (req: Request, res: Response) => {
try { try {
const { zoneId, stateName } = req.body; const { zoneId, stateName } = req.body;
if (!zoneId || !stateName) return res.status(400).json({ success: false, message: 'Zone ID and state name required' }); if (!stateName) return res.status(400).json({ success: false, message: 'State name is required' });
const state = await State.create({ zoneId, stateName }); const state = await db.Location.create({ name: stateName, type: 'state' });
if (zoneId) {
await db.LocationHierarchy.create({ locationId: state.id, parentId: zoneId });
}
res.status(201).json({ success: true, message: 'State created', data: state }); res.status(201).json({ success: true, message: 'State created', data: state });
} catch (error) { } catch (error) {
console.error('Create state error:', error); console.error('Create state error:', error);
@ -257,46 +146,22 @@ export const createState = async (req: Request, res: Response) => {
} }
}; };
export const updateState = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { stateName, isActive } = req.body;
const state = await State.findByPk(id);
if (!state) return res.status(404).json({ success: false, message: 'State not found' });
await state.update({ stateName, isActive });
res.json({ success: true, message: 'State updated' });
} catch (error) {
console.error('Update state error:', error);
res.status(500).json({ success: false, message: 'Error updating state' });
}
};
// --- Districts --- // --- Districts ---
export const getDistricts = async (req: Request, res: Response) => { export const getDistricts = async (req: Request, res: Response) => {
try { return getLocationsByType('district', req, res);
const { stateId } = req.query as { stateId?: string };
const where: any = {};
if (stateId) where.stateId = stateId;
const districts = await District.findAll({
where,
include: [{ model: State, as: 'state', attributes: ['stateName'] }],
order: [['districtName', 'ASC']]
});
res.json({ success: true, districts });
} catch (error) {
console.error('Get districts error:', error);
res.status(500).json({ success: false, message: 'Error fetching districts' });
}
}; };
export const createDistrict = async (req: Request, res: Response) => { export const createDistrict = async (req: Request, res: Response) => {
try { try {
const { stateId, districtName } = req.body; const { stateId, districtName } = req.body;
if (!stateId || !districtName) return res.status(400).json({ success: false, message: 'State ID and district name required' }); if (!districtName) return res.status(400).json({ success: false, message: 'District name is required' });
const district = await District.create({ stateId, districtName }); const district = await db.Location.create({ name: districtName, type: 'district' });
if (stateId) {
await db.LocationHierarchy.create({ locationId: district.id, parentId: stateId });
}
res.status(201).json({ success: true, message: 'District created', data: district }); res.status(201).json({ success: true, message: 'District created', data: district });
} catch (error) { } catch (error) {
console.error('Create district error:', error); console.error('Create district error:', error);
@ -304,121 +169,20 @@ export const createDistrict = async (req: Request, res: Response) => {
} }
}; };
export const updateDistrict = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { districtName, isActive } = req.body;
const district = await District.findByPk(id);
if (!district) return res.status(404).json({ success: false, message: 'District not found' });
await district.update({ districtName, isActive });
res.json({ success: true, message: 'District updated' });
} catch (error) {
console.error('Update district error:', error);
res.status(500).json({ success: false, message: 'Error updating district' });
}
};
// --- Areas --- // --- Areas ---
export const getAreas = async (req: Request, res: Response) => { export const getAreas = async (req: Request, res: Response) => {
try { return getLocationsByType('area', req, res);
const { districtId } = req.query as { districtId?: string };
const where: any = {};
if (districtId) where.districtId = districtId;
const areas = await Area.findAll({
where,
include: [
{ model: District, as: 'district', attributes: ['districtName'] },
{ model: State, as: 'state', attributes: ['stateName'] },
{ model: Region, as: 'region', attributes: ['regionName'] },
{ model: Zone, as: 'zone', attributes: ['zoneName'] },
// Include explicit manager column (legacy/fallback)
{ model: User, as: 'manager', attributes: ['id', 'fullName', 'email', 'mobileNumber'] },
// Include active managers from dedicated table
{
model: AreaManager,
as: 'areaManagers',
where: { isActive: true },
required: false, // Left join, so we get areas even without managers
include: [{
model: User,
as: 'user',
attributes: ['id', 'fullName', 'email', 'mobileNumber']
}]
}
],
order: [['areaName', 'ASC']]
});
res.json({ success: true, areas });
} catch (error) {
console.error('Get areas error:', error);
res.status(500).json({ success: false, message: 'Error fetching areas' });
}
}; };
export const createArea = async (req: Request, res: Response) => { export const createArea = async (req: Request, res: Response) => {
try { try {
const { districtId, areaCode, areaName, city, pincode, managerId } = req.body; const { districtId, areaName } = req.body;
if (!districtId || !areaName || !pincode) return res.status(400).json({ success: false, message: 'District ID, area name, and pincode required' }); if (!areaName) return res.status(400).json({ success: false, message: 'Area name is required' });
// Need to fetch regionId from district -> state -> zone -> region? const area = await db.Location.create({ name: areaName, type: 'area' });
// Or user provides it?
// The Area model has regionId, districtId.
// It's safer to fetch relationships.
const district = await District.findByPk(districtId, {
include: [{
model: State,
as: 'state',
include: [
{ model: Zone, as: 'zone' },
{ model: Region, as: 'region' }
]
}]
});
let regionId = null; if (districtId) {
let zoneId = null; await db.LocationHierarchy.create({ locationId: area.id, parentId: districtId });
let stateId = null;
if (district) {
stateId = district.stateId;
// Access associations using the logical structure (District -> State -> Zone/Region)
if (district.state) {
if (district.state.zone) {
zoneId = district.state.zone.id;
}
if (district.state.region) {
regionId = district.state.region.id;
}
}
}
const area = await Area.create({
districtId,
stateId,
zoneId,
regionId,
areaCode,
areaName,
city,
pincode,
managerId: managerId || null, // Legacy support
isActive: req.body.isActive ?? true,
activeFrom: req.body.activeFrom || null,
activeTo: req.body.activeTo || null
});
// Create AreaManager record if manager assigned
if (managerId) {
await AreaManager.create({
areaId: area.id,
userId: managerId,
managerType: 'ASM',
isActive: true,
assignedAt: new Date(),
asmCode: req.body.asmCode || null
});
} }
res.status(201).json({ success: true, message: 'Area created', data: area }); res.status(201).json({ success: true, message: 'Area created', data: area });
@ -428,144 +192,44 @@ export const createArea = async (req: Request, res: Response) => {
} }
}; };
// --- Area Managers --- // --- Managers (Consolidated) ---
export const getAreaManagers = async (req: Request, res: Response) => { export const getManagersByRole = async (req: Request, res: Response) => {
try { try {
// Fetch Users who have active AreaManager assignments const { roleCode, locationId } = req.query as any;
// We use the User model as the primary so we get the User details naturally const where: any = {};
const managers = await User.findAll({ if (roleCode) where.roleCode = roleCode;
attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId', 'roleCode', 'zoneId', 'regionId'], if (locationId) where.locationId = locationId;
include: [
{
model: AreaManager,
as: 'areaManagers',
where: { isActive: true },
required: true, // Only return users who ARE active managers
attributes: ['asmCode'],
include: [
{
model: Area,
as: 'area',
attributes: ['id', 'areaName', 'areaCode'],
include: [
{ model: District, as: 'district', attributes: ['districtName'] },
{ model: State, as: 'state', attributes: ['stateName'] },
{ model: Region, as: 'region', attributes: ['id', 'regionName'] },
{ model: Zone, as: 'zone', attributes: ['id', 'zoneName'] }
]
}
]
},
{ model: Zone, as: 'zone', attributes: ['id', 'zoneName'] },
{ model: Region, as: 'region', attributes: ['id', 'regionName'] }
],
order: [['fullName', 'ASC']]
});
// Transform if necessary to flatten the structure for the frontend const managers = await User.findAll({
// But the user asked for "straightforward", so a clean nested JSON is usually best where,
// We can double check if they want a flat list of (User, Area) pairs or User -> [Areas] attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode', 'locationId'],
// "Arean mangers" implies the People. So User -> [Areas] is the best entity representation. include: [{
model: db.Location,
as: 'location',
attributes: ['id', 'name', 'type']
}]
});
res.json({ success: true, data: managers }); res.json({ success: true, data: managers });
} catch (error) { } catch (error) {
console.error('Get area managers error:', error); console.error('Get managers error:', error);
res.status(500).json({ success: false, message: 'Error fetching area managers' }); res.status(500).json({ success: false, message: 'Error fetching managers' });
} }
}; };
export const updateArea = async (req: Request, res: Response) => { export const getAreaManagers = async (req: Request, res: Response) => {
req.query.roleCode = 'ASM';
return getManagersByRole(req, res);
};
export const deleteLocation = async (req: Request, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { areaName, city, pincode, isActive, managerId, districtId, activeFrom, activeTo } = req.body; await db.LocationHierarchy.destroy({ where: { [db.Sequelize.Op.or]: [{ locationId: id }, { parentId: id }] } });
const area = await Area.findByPk(id); await db.Location.destroy({ where: { id } });
if (!area) return res.status(404).json({ success: false, message: 'Area not found' }); res.json({ success: true, message: 'Location deleted' });
const updates: any = {};
if (areaName) updates.areaName = areaName;
if (city) updates.city = city;
if (pincode) updates.pincode = pincode;
if (isActive !== undefined) updates.isActive = isActive;
if (activeFrom !== undefined) updates.activeFrom = activeFrom || null;
if (activeTo !== undefined) updates.activeTo = activeTo || null;
if (managerId !== undefined) updates.managerId = managerId; // Legacy support
// If district is changed, update the entire hierarchy (State, Zone, Region)
if (districtId && districtId !== area.districtId) {
updates.districtId = districtId;
const district = await District.findByPk(districtId, {
include: [{
model: State,
as: 'state',
include: [
{ model: Zone, as: 'zone' },
{ model: Region, as: 'region' }
]
}]
});
if (district) {
updates.stateId = district.stateId;
if (district.state) {
if (district.state.zone) {
updates.zoneId = district.state.zone.id;
}
if (district.state.region) {
updates.regionId = district.state.region.id;
}
}
}
}
await area.update(updates);
// Handle AreaManager Table Update
if (managerId !== undefined) {
const asmCode = req.body.asmCode;
// 1. Find currently active manager for this area
const currentActiveManager = await AreaManager.findOne({
where: {
areaId: id,
isActive: true
}
});
// If there is an active manager
if (currentActiveManager) {
// If the new managerId is different (or null, meaning unassign), deactivate the old one
if (currentActiveManager.userId !== managerId) {
await currentActiveManager.update({ isActive: false });
} else {
// If SAME user, update asmCode if provided
if (asmCode !== undefined) {
await currentActiveManager.update({ asmCode });
}
}
}
// 2. If a new manager is being assigned (and it's not null)
if (managerId) {
// Check if this specific user is already active (to avoid duplicates if logic above missed it)
const isAlreadyActive = currentActiveManager && currentActiveManager.userId === managerId;
if (!isAlreadyActive) {
await AreaManager.create({
areaId: id,
userId: managerId,
managerType: 'ASM', // Default type
isActive: true,
assignedAt: new Date(),
asmCode: asmCode || null
});
}
}
}
res.json({ success: true, message: 'Area updated successfully' });
} catch (error) { } catch (error) {
console.error('Update area error:', error); console.error('Delete location error:', error);
res.status(500).json({ success: false, message: 'Error updating area' }); res.status(500).json({ success: false, message: 'Error deleting location' });
} }
}; };

View File

@ -17,25 +17,26 @@ router.use(authenticate as any);
// Regions // Regions
router.get('/regions', masterController.getRegions); router.get('/regions', masterController.getRegions);
router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createRegion); router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createRegion);
router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateRegion); router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// Zones // Zones
router.get('/zones', masterController.getZones); router.get('/zones', masterController.getZones);
router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createZone); router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createZone);
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateZone); router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// States (Update only) // States (Update only)
router.post('/states', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createState); router.post('/states', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createState);
router.put('/states/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateState); router.put('/states/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// Districts (Update only) // Districts (Update only)
router.post('/districts', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createDistrict); router.post('/districts', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createDistrict);
router.put('/districts/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateDistrict); router.put('/districts/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// Areas // Areas
router.get('/areas', masterController.getAreas); router.get('/areas', masterController.getAreas);
router.post('/areas', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createArea); router.post('/areas', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createArea);
router.put('/areas/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateArea); router.put('/areas/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
router.delete('/locations/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.deleteLocation);
// Area Managers // Area Managers
router.get('/area-managers', masterController.getAreaManagers); router.get('/area-managers', masterController.getAreaManagers);

View File

@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone, Area } = db; const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, Location } = 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';
@ -17,72 +17,44 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode
} = req.body; } = req.body;
// Check for duplicate email // Check for duplicate application for SAME location
const existingApp = await Application.findOne({ where: { email } }); const existingApp = await Application.findOne({
where: {
email,
city: city || null,
preferredLocation: preferredLocation || null,
overallStatus: { [Op.ne]: 'Rejected' } // Don't block if previous was rejected
}
});
if (existingApp) { if (existingApp) {
return res.status(400).json({ success: false, message: 'Application with this email already exists' }); return res.status(400).json({
success: false,
message: 'An active application for this location already exists with this email address.'
});
} }
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`; const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
// Fetch hierarchy from Auto-detected Area // Fetch hierarchy from Auto-detected Location
let zoneId, regionId, areaId; let locationId = null;
let isOpportunityAvailable = false; let isOpportunityAvailable = false;
// Auto-detect Area from District // Auto-detect Location from District
if (req.body.district) { if (req.body.district) {
const districtName = req.body.district; const districtName = req.body.district;
// 1. Find District ID by Name // Find Location (type: district) match
const districtRecord = await District.findOne({ const districtRecord = await Location.findOne({
where: { districtName: { [Op.iLike]: districtName } } where: {
}); name: { [Op.iLike]: districtName },
type: 'district'
if (districtRecord) {
// 2. Find Active Area for this District
const today = new Date();
const validArea = await Area.findOne({
where: {
districtId: districtRecord.id,
isActive: true,
[Op.and]: [
{
[Op.or]: [
{ activeFrom: { [Op.eq]: null } },
{ activeFrom: { [Op.lte]: today } }
]
},
{
[Op.or]: [
{ activeTo: { [Op.eq]: null } },
{ activeTo: { [Op.gte]: today } }
]
}
]
}
});
if (validArea) {
areaId = validArea.id;
zoneId = validArea.zoneId;
regionId = validArea.regionId;
isOpportunityAvailable = true;
} }
}
}
// Determine Initial Status
let initialStatus = isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED;
// Auto-assign Zone/Region from District if still null (even if no opportunity found)
if (!zoneId && req.body.district) {
const districtRecord = await District.findOne({
where: { districtName: { [Op.iLike]: req.body.district } },
include: [{ model: Region, as: 'region' }, { model: Zone, as: 'zone' }]
}); });
if (districtRecord) { if (districtRecord) {
regionId = districtRecord.regionId; locationId = districtRecord.id;
zoneId = districtRecord.zoneId; isOpportunityAvailable = true; // For now, assume if district exists, it's an opportunity
} }
} }
@ -100,18 +72,16 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
investmentCapacity, investmentCapacity,
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
currentStage: APPLICATION_STAGES.DD, currentStage: APPLICATION_STAGES.DD,
overallStatus: initialStatus, overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
progressPercentage: isOpportunityAvailable ? 10 : 0, progressPercentage: isOpportunityAvailable ? 10 : 0,
zoneId, locationId
regionId,
areaId // Link to Area
}); });
// Log Status History // Log Status History
await ApplicationStatusHistory.create({ await ApplicationStatusHistory.create({
applicationId: application.id, applicationId: application.id,
previousStatus: null, previousStatus: null,
newStatus: initialStatus, newStatus: application.overallStatus,
changedBy: req.user?.id || null, changedBy: req.user?.id || null,
reason: 'Initial Submission' reason: 'Initial Submission'
}); });

View File

@ -7,12 +7,11 @@ import { AUDIT_ACTIONS } from '../../common/config/constants.js';
export const getOpportunities = async (req: Request, res: Response) => { export const getOpportunities = async (req: Request, res: Response) => {
try { try {
const { status, regionId, zoneId } = req.query as any; const { status, locationId } = req.query as any;
const where: any = {}; const where: any = {};
if (status) where.status = status; if (status) where.status = status;
if (regionId) where.regionId = regionId; if (locationId) where.locationId = locationId;
if (zoneId) where.zoneId = zoneId;
const opportunities = await Opportunity.findAll({ const opportunities = await Opportunity.findAll({
where, where,
@ -33,7 +32,7 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => {
try { try {
const { const {
leadSource, leadName, contactNumber, email, leadSource, leadName, contactNumber, email,
zoneId, regionId, stateId, districtId, locationId,
opportunityType, priority opportunityType, priority
} = req.body; } = req.body;
@ -42,10 +41,7 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => {
leadName, leadName,
contactNumber, contactNumber,
email, email,
zoneId, locationId,
regionId,
stateId,
districtId,
opportunityType, opportunityType,
priority, priority,
status: 'New', status: 'New',

View File

@ -116,6 +116,17 @@ const seedQuestionnaire = async () => {
weight: 0, weight: 0,
order: 7 order: 7
}, },
{
text: "Are you an existing dealer/vendor of Royal Enfield?",
type: "radio",
section: "Basic Information",
options: [
{ text: "Yes", score: 0 },
{ text: "No", score: 0 }
],
weight: 0,
order: 8
},
// Section 2: Profile & Background (Scoring Starts) // Section 2: Profile & Background (Scoring Starts)
{ {

View File

@ -0,0 +1,169 @@
import db from '../database/models/index.js';
const { Application, User, Outlet, Dealer, DealerCode } = db;
import { Op } from 'sequelize';
async function test() {
console.log('--- Starting Multi-Outlet Support Test ---');
const email = `test_dealer_multi_${Date.now()}@example.com`;
const phone = '9876543210';
try {
// 1. Initial Application for City X
console.log('1. Submitting first application for City X...');
const app1 = await Application.create({
applicationId: `APP-X-${Date.now()}`,
applicantName: 'Test Dealer',
email,
phone,
businessType: 'Dealership',
city: 'City X',
preferredLocation: 'Location X',
overallStatus: 'Pending',
currentStage: 'DD',
zoneId: null,
regionId: null
});
console.log('Success: Application 1 created.');
// 2. Duplicate Application for SAME Location
console.log('2. Attempting duplicate application for City X...');
const existingApp = await Application.findOne({
where: {
email,
city: 'City X',
preferredLocation: 'Location X',
overallStatus: { [Op.ne]: 'Rejected' }
}
});
if (existingApp) {
console.log('Correct: Detected duplicate application for same location.');
} else {
console.error('Error: Failed to detect duplicate location!');
}
// 3. New Application for DIFFERENT Location (City Y)
console.log('3. Submitting application for City Y...');
const app2 = await Application.create({
applicationId: `APP-Y-${Date.now()}`,
applicantName: 'Test Dealer',
email,
phone,
businessType: 'Dealership',
city: 'City Y',
preferredLocation: 'Location Y',
overallStatus: 'Pending',
currentStage: 'DD',
zoneId: null,
regionId: null
});
console.log('Success: Application 2 created for different location.');
// 4. Onboard Application 1 (Simulated step)
console.log('4. Onboarding Application 1...');
const dCode1 = await DealerCode.create({
dealerCode: `CODE-X-${Date.now()}`,
applicationId: app1.id
});
const dealer1 = await Dealer.create({
applicationId: app1.id,
dealerCodeId: dCode1.id,
legalName: 'Test Dealer X Ltd',
businessName: 'Test Dealer - X',
constitutionType: 'Partnership',
status: 'Active'
});
const user = await User.create({
fullName: 'Test Dealer',
email,
password: 'hashed_password',
roleCode: 'Dealer',
dealerId: dealer1.id,
status: 'active'
});
// Current Logic in dealer.controller.ts for outlet 1
const outlet1 = await Outlet.create({
code: `OUT-X-${Date.now()}`,
name: `${app1.applicantName} - ${app1.city}`,
type: 'Dealership',
address: 'Address X',
city: 'City X',
state: 'State X',
pincode: '123456',
status: 'Active',
establishedDate: new Date(),
dealerId: user.id,
region: 'Central',
zone: 'National'
});
console.log('Success: Application 1 onboarded.');
// 5. Onboard Application 2 (Simulation of our fix)
console.log('5. Onboarding Application 2 (sharing same user)...');
const dCode2 = await DealerCode.create({
dealerCode: `CODE-Y-${Date.now()}`,
applicationId: app2.id
});
const dealer2 = await Dealer.create({
applicationId: app2.id,
dealerCodeId: dCode2.id,
legalName: 'Test Dealer Y Ltd',
businessName: 'Test Dealer - Y',
constitutionType: 'Partnership',
status: 'Active'
});
// Update existing user with new dealerId (Latest)
await user.update({ dealerId: dealer2.id });
// Our Fix in dealer.controller.ts: Check for outlet for THIS application/city
let existingOutletForThisApp = await Outlet.findOne({
where: {
dealerId: user.id,
name: { [Op.like]: `%${app2.city}%` }
}
});
if (!existingOutletForThisApp) {
const outlet2 = await Outlet.create({
code: `OUT-Y-${Date.now()}`,
name: `${app2.applicantName} - ${app2.city}`,
type: 'Dealership',
address: 'Address Y',
city: 'City Y',
state: 'State Y',
pincode: '654321',
status: 'Active',
establishedDate: new Date(),
dealerId: user.id,
region: 'Central',
zone: 'National'
});
console.log('Success: Outlet 2 created for City Y.');
} else {
console.error('Error: Outlet for City Y already exists wrongly!');
}
// 6. Final Check
const outletCount = await Outlet.count({ where: { dealerId: user.id } });
console.log(`Final Verification: User ${email} has ${outletCount} outlets total.`);
if (outletCount === 2) {
console.log('TEST PASSED: Multi-outlet support verified.');
} else {
console.error(`TEST FAILED: Expected 2 outlets, found ${outletCount}.`);
}
} catch (error) {
console.error('Test error:', error);
} finally {
process.exit();
}
}
test();

View File

@ -0,0 +1,74 @@
import db from '../database/models/index.js';
const { User, Region, Zone } = db;
import bcrypt from 'bcryptjs';
async function verifyFix() {
console.log('--- Starting UUID Sanitization Verification ---');
const testEmail = `test_uuid_fix_${Date.now()}@example.com`;
const password = await bcrypt.hash('Test@123', 10);
try {
// 1. Attempt to create a user with empty string for regionId
// This simulates what the createUser controller now does after sanitization
console.log('1. Testing User creation with empty-string sanitized to null...');
const userData = {
fullName: 'Test UUID User',
email: testEmail,
password: password,
roleCode: 'ZBH',
status: 'active',
isActive: true,
employeeId: `EMP-${Date.now()}`,
mobileNumber: '1234567890',
zoneId: null, // If frontend sends "", my controller now converts to null
regionId: null,
stateId: null,
districtId: null,
areaId: null
};
const user = await User.create(userData);
console.log('Success: User created with null UUID fields.');
// 2. Simulate the Circular Dependency Workflow
console.log('2. Testing Circular Dependency Workflow...');
// a. Create a Zone first (needed for Region)
const zone = await Zone.create({
zoneCode: `ZNE-${Date.now().toString().slice(-4)}`,
zoneName: `Test Zone ${Date.now()}`
});
// b. Create a Region and assign the user as manager
console.log('Creating Region with User as Manager...');
const region = await Region.create({
zoneId: zone.id,
regionCode: `REG-${Date.now().toString().slice(-4)}`,
regionName: 'Test Region',
regionalManagerId: user.id
});
console.log('Success: Region created.');
// c. Link User back to the Region
console.log('Linking User back to the new Region...');
await user.update({ regionId: region.id });
console.log('Success: User updated with regionId.');
// 3. Verify final state
const updatedUser = await User.findByPk(user.id);
if (updatedUser?.regionId === region.id) {
console.log('VERIFICATION PASSED: User-Region cycle completed successfully.');
} else {
console.error('VERIFICATION FAILED: user.regionId was not updated correctly.');
}
} catch (error) {
console.error('VERIFICATION FAILED with error:', error);
} finally {
process.exit();
}
}
verifyFix();

View File

@ -2,6 +2,5 @@ export interface TokenPayload {
userId: string; userId: string;
email: string; email: string;
role: string; role: string;
region: string | null; locationId: string | null;
zone: string | null;
} }

112
verify_approval_sync.ts Normal file
View File

@ -0,0 +1,112 @@
import 'dotenv/config';
import db from './src/database/models/index';
const { Application, Interview, InterviewParticipant, InterviewEvaluation } = db;
import { Op } from 'sequelize';
async function testApprovalSync() {
console.log('--- Testing Synchronized Approval Logic (Direct DB Simulation) ---');
try {
// Setup IDs for test (must exist in DB)
const user1Id = 'fb9ba702-233b-4b3f-ba21-33bedec6209a'; // DD-ZM
const user2Id = 'c3602348-c9bd-4938-83b2-acc3c62791aa'; // RBM
// 1. Setup a test application
console.log('\n1. Creating test application...');
const app = await Application.create({
applicationId: 'APP-' + Date.now(),
registrationNumber: 'TEST-' + Date.now(),
fullName: 'Test Applicant',
applicantName: 'Test Applicant',
email: 'test' + Date.now() + '@example.com',
phone: '1234567890',
businessType: 'Dealership',
overallStatus: 'Level 1 Interview Pending',
currentStage: 'Level 1 Approved', // Mocking stage
zoneId: 'aedf5f64-3f95-4fa7-ac64-52c97cb330e7',
regionId: 'a5ea0aa2-c26c-49a9-95fe-0dbd7283c125'
});
// 2. Create an interview with 2 participants
console.log('2. Creating interview for Level 1...');
const interview = await Interview.create({
applicationId: app.id,
level: 1,
status: 'Scheduled',
interviewType: 'Level 1 Interview'
});
const participant1 = await InterviewParticipant.create({
interviewId: interview.id,
userId: user1Id,
roleInPanel: 'Interviewer'
});
const participant2 = await InterviewParticipant.create({
interviewId: interview.id,
userId: user2Id,
roleInPanel: 'Interviewer'
});
console.log(`Interview created with ID: ${interview.id}`);
// Decision logic
async function submitDecision(userId: string, decision: string) {
console.log(`\nSubmitting ${decision} for User: ${userId}...`);
await InterviewEvaluation.create({
interviewId: interview.id,
evaluatorId: userId,
recommendation: decision,
decision: decision,
remarks: 'Testing'
});
// Replicate the sync check from controller
const participants = await InterviewParticipant.findAll({ where: { interviewId: interview.id } });
const evaluations = await InterviewEvaluation.findAll({ where: { interviewId: interview.id } });
const isFullyEvaluated = evaluations.length >= participants.length;
console.log(`Evaluations: ${evaluations.length}/${participants.length}. Fully Evaluated: ${isFullyEvaluated}`);
if (isFullyEvaluated) {
console.log('Finalizing interview and updating application status...');
await interview.update({ status: 'Completed' });
const nextStatus = 'Level 1 Approved';
await Application.update({
overallStatus: nextStatus,
currentStage: 'Level 1 Approved'
}, { where: { id: app.id } });
console.log(`Application status updated to: ${nextStatus}`);
} else {
console.log('Waiting for more evaluations. Application status remains unchanged.');
}
}
// 3. First participant rejects
await submitDecision(user1Id, 'Rejected');
let currentApp = await Application.findByPk(app.id);
console.log(`Current App Overall Status: ${currentApp.overallStatus}`);
// 4. Second participant approves
await submitDecision(user2Id, 'Approved');
currentApp = await Application.findByPk(app.id);
console.log(`Final App Overall Status: ${currentApp.overallStatus}`);
// Cleanup
console.log('\nCleaning up...');
await InterviewEvaluation.destroy({ where: { interviewId: interview.id } });
await InterviewParticipant.destroy({ where: { interviewId: interview.id } });
await Interview.destroy({ where: { id: interview.id } });
await Application.destroy({ where: { id: app.id } });
} catch (error) {
console.error('Test failed:', error);
} finally {
process.exit(0);
}
}
testApprovalSync();

74
verify_db_logic.ts Normal file
View File

@ -0,0 +1,74 @@
import 'dotenv/config';
import db from './src/database/models/index';
const { User, Role } = db;
import { Op } from 'sequelize';
async function testFiltering() {
console.log('--- Testing Backend Filtering Logic (Direct DB) ---');
const testCasesMap = [
{
name: 'National Role (NBH) - Should ignore location',
params: { roleCode: 'NBH', zoneId: 'some-random-id' },
expectedRoles: ['NBH']
},
{
name: 'National Role (DD Head) - Should ignore location',
params: { roleCode: 'DD Head', zoneId: 'some-random-id' },
expectedRoles: ['DD Head']
},
{
name: 'Zonal Role (RBM) - Should respect location (Matching)',
params: { roleCode: 'RBM', zoneId: 'aedf5f64-3f95-4fa7-ac64-52c97cb330e7' }, // Known RBM zone from check_db_users
expectedCount: 1
},
{
name: 'Zonal Role (RBM) - Should respect location (Mismatch)',
params: { roleCode: 'RBM', zoneId: '741e0e70-32d3-40d6-987f-5d2ffd54f152' }, // ZBH zone
expectedCount: 0
}
];
for (const testCase of testCasesMap) {
console.log(`\nTesting: ${testCase.name}`);
const { roleCode, zoneId, regionId, areaId } = testCase.params;
const whereClause: any = {};
// Replicating logic from admin.controller.ts
if (roleCode) {
if (Array.isArray(roleCode)) {
whereClause.roleCode = { [Op.in]: roleCode };
} else {
whereClause.roleCode = roleCode;
}
}
const nationalRoles = ['NBH', 'DD Head'];
const isNationalRole = (typeof roleCode === 'string' && nationalRoles.includes(roleCode)) ||
(Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r)));
if (!isNationalRole) {
if (zoneId) whereClause.zoneId = zoneId;
if (regionId) whereClause.regionId = regionId;
if (areaId) whereClause.areaId = areaId;
}
const users = await User.findAll({
where: whereClause,
attributes: ['id', 'roleCode', 'zoneId']
});
console.log(`Result: ${users.length} users found.`);
if (users.length > 0) {
console.log('Found Roles:', Array.from(new Set(users.map(u => u.roleCode))));
}
}
process.exit(0);
}
testFiltering().catch(err => {
console.error(err);
process.exit(1);
});