From d20e573d693f66a896ecc508769efe9f31bb1fef Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Mon, 23 Mar 2026 20:12:10 +0530 Subject: [PATCH] hirarchchy changed --- check_db_roles.ts | 16 + check_db_users.ts | 25 + check_questions.ts | 39 ++ package.json | 1 + scripts/seed-roles.ts | 1 + scripts/seed_normalized_data.ts | 97 +++ src/common/config/auth.ts | 3 +- src/common/config/constants.ts | 1 + src/database/models/Application.ts | 28 +- src/database/models/Area.ts | 141 ----- src/database/models/AreaManager.ts | 72 --- src/database/models/District.ts | 78 --- src/database/models/DistrictManager.ts | 67 -- src/database/models/Location.ts | 51 ++ src/database/models/LocationHierarchy.ts | 51 ++ src/database/models/Opportunity.ts | 38 +- src/database/models/Outlet.ts | 24 +- src/database/models/Region.ts | 101 --- src/database/models/RegionManager.ts | 67 -- src/database/models/State.ts | 70 --- src/database/models/User.ts | 49 +- src/database/models/UserRole.ts | 38 +- src/database/models/Zone.ts | 75 --- src/database/models/ZoneManager.ts | 67 -- src/database/models/index.ts | 22 +- src/modules/admin/admin.controller.ts | 62 +- .../assessment/assessment.controller.ts | 48 +- src/modules/auth/auth.controller.ts | 13 +- src/modules/dealer/dealer.controller.ts | 42 +- src/modules/master/master.controller.ts | 574 ++++-------------- src/modules/master/master.routes.ts | 11 +- .../onboarding/onboarding.controller.ts | 88 +-- .../opportunity/opportunity.controller.ts | 12 +- src/scripts/seedQuestionnaire.ts | 11 + src/scripts/test_multi_outlet.ts | 169 ++++++ src/scripts/verify_uuid_fix.ts | 74 +++ src/types/auth.types.ts | 3 +- verify_approval_sync.ts | 112 ++++ verify_db_logic.ts | 74 +++ 39 files changed, 1009 insertions(+), 1506 deletions(-) create mode 100644 check_db_roles.ts create mode 100644 check_db_users.ts create mode 100644 check_questions.ts create mode 100644 scripts/seed_normalized_data.ts delete mode 100644 src/database/models/Area.ts delete mode 100644 src/database/models/AreaManager.ts delete mode 100644 src/database/models/District.ts delete mode 100644 src/database/models/DistrictManager.ts create mode 100644 src/database/models/Location.ts create mode 100644 src/database/models/LocationHierarchy.ts delete mode 100644 src/database/models/Region.ts delete mode 100644 src/database/models/RegionManager.ts delete mode 100644 src/database/models/State.ts delete mode 100644 src/database/models/Zone.ts delete mode 100644 src/database/models/ZoneManager.ts create mode 100644 src/scripts/test_multi_outlet.ts create mode 100644 src/scripts/verify_uuid_fix.ts create mode 100644 verify_approval_sync.ts create mode 100644 verify_db_logic.ts diff --git a/check_db_roles.ts b/check_db_roles.ts new file mode 100644 index 0000000..0aad7f9 --- /dev/null +++ b/check_db_roles.ts @@ -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(); diff --git a/check_db_users.ts b/check_db_users.ts new file mode 100644 index 0000000..0b505bc --- /dev/null +++ b/check_db_users.ts @@ -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(); diff --git a/check_questions.ts b/check_questions.ts new file mode 100644 index 0000000..3342b09 --- /dev/null +++ b/check_questions.ts @@ -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(); diff --git a/package.json b/package.json index 5832f0c..41aad96 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "type-check": "tsc --noEmit", "migrate": "tsx scripts/migrate.ts", "seed": "tsx scripts/seed-geo.ts", + "seed-normalized": "tsx scripts/seed_normalized_data.ts", "test": "jest", "test:coverage": "jest --coverage", "clear-logs": "rm -rf logs/*.log" diff --git a/scripts/seed-roles.ts b/scripts/seed-roles.ts index ccc2ec5..79e5923 100644 --- a/scripts/seed-roles.ts +++ b/scripts/seed-roles.ts @@ -15,6 +15,7 @@ const rolesToSeed = [ { roleCode: ROLES.FINANCE, roleName: 'Finance', category: 'DEPARTMENT', description: 'Finance 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.ASM, roleName: 'ASM', category: 'SALES', description: 'Area Sales Manager' }, { roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' } ]; diff --git a/scripts/seed_normalized_data.ts b/scripts/seed_normalized_data.ts new file mode 100644 index 0000000..1b3f45d --- /dev/null +++ b/scripts/seed_normalized_data.ts @@ -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)); diff --git a/src/common/config/auth.ts b/src/common/config/auth.ts index e6096e1..c9ebee8 100644 --- a/src/common/config/auth.ts +++ b/src/common/config/auth.ts @@ -10,8 +10,7 @@ export const generateToken = (user: any): string => { userId: user.id, email: user.email, role: user.roleCode, - region: user.regionId, - zone: user.zoneId + locationId: user.locationId }; return jwt.sign(payload, JWT_SECRET, { diff --git a/src/common/config/constants.ts b/src/common/config/constants.ts index a0ebf8c..212ecd3 100644 --- a/src/common/config/constants.ts +++ b/src/common/config/constants.ts @@ -11,6 +11,7 @@ export const ROLES = { LEGAL_ADMIN: 'Legal Admin', SUPER_ADMIN: 'Super Admin', DD_AM: 'DD AM', + ASM: 'ASM', FINANCE: 'Finance', DEALER: 'Dealer' } as const; diff --git a/src/database/models/Application.ts b/src/database/models/Application.ts index 6714af5..3d2d116 100644 --- a/src/database/models/Application.ts +++ b/src/database/models/Application.ts @@ -34,9 +34,7 @@ export interface ApplicationAttributes { architectureAssignedTo: string | null; architectureStatus: string | null; submittedBy: string | null; - zoneId: string | null; - regionId: string | null; - areaId: string | null; + locationId: string | null; architectureAssignedDate: Date | null; architectureDocumentDate: Date | null; architectureCompletionDate: Date | null; @@ -202,27 +200,11 @@ export default (sequelize: Sequelize) => { key: 'id' } }, - zoneId: { + locationId: { type: DataTypes.UUID, allowNull: true, references: { - model: 'zones', - key: 'id' - } - }, - regionId: { - type: DataTypes.UUID, - allowNull: true, - references: { - model: 'regions', - key: 'id' - } - }, - areaId: { - type: DataTypes.UUID, - allowNull: true, - references: { - model: 'areas', + model: 'locations', key: 'id' } }, @@ -263,9 +245,7 @@ export default (sequelize: Sequelize) => { Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' }); Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' }); Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' }); - Application.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); - Application.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' }); - Application.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' }); + Application.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' }); Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' }); Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' }); diff --git a/src/database/models/Area.ts b/src/database/models/Area.ts deleted file mode 100644 index 4495d7f..0000000 --- a/src/database/models/Area.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const Area = sequelize.define('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; -}; diff --git a/src/database/models/AreaManager.ts b/src/database/models/AreaManager.ts deleted file mode 100644 index 9c60692..0000000 --- a/src/database/models/AreaManager.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const AreaManager = sequelize.define('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; -}; diff --git a/src/database/models/District.ts b/src/database/models/District.ts deleted file mode 100644 index 3a5946f..0000000 --- a/src/database/models/District.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const District = sequelize.define('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; -}; diff --git a/src/database/models/DistrictManager.ts b/src/database/models/DistrictManager.ts deleted file mode 100644 index df6d26d..0000000 --- a/src/database/models/DistrictManager.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const DistrictManager = sequelize.define('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; -}; diff --git a/src/database/models/Location.ts b/src/database/models/Location.ts new file mode 100644 index 0000000..d00ca26 --- /dev/null +++ b/src/database/models/Location.ts @@ -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 { } + +export default (sequelize: Sequelize) => { + const Location = sequelize.define('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; +}; diff --git a/src/database/models/LocationHierarchy.ts b/src/database/models/LocationHierarchy.ts new file mode 100644 index 0000000..85ddd2e --- /dev/null +++ b/src/database/models/LocationHierarchy.ts @@ -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 { } + +export default (sequelize: Sequelize) => { + const LocationHierarchy = sequelize.define('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; +}; diff --git a/src/database/models/Opportunity.ts b/src/database/models/Opportunity.ts index 96e6854..fecc012 100644 --- a/src/database/models/Opportunity.ts +++ b/src/database/models/Opportunity.ts @@ -2,10 +2,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize'; export interface OpportunityAttributes { id: string; - zoneId: string; - regionId: string; - stateId: string | null; - districtId: string | null; + locationId: string; city: string; opportunityType: string; capacity: string; @@ -26,35 +23,11 @@ export default (sequelize: Sequelize) => { defaultValue: DataTypes.UUIDV4, primaryKey: true }, - zoneId: { + locationId: { type: DataTypes.UUID, allowNull: false, references: { - model: 'zones', - 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', + model: 'locations', key: 'id' } }, @@ -104,10 +77,7 @@ export default (sequelize: Sequelize) => { }); (Opportunity as any).associate = (models: any) => { - Opportunity.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); - 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.Location, { foreignKey: 'locationId', as: 'location' }); Opportunity.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' }); Opportunity.hasMany(models.Application, { foreignKey: 'opportunityId', as: 'applications' }); }; diff --git a/src/database/models/Outlet.ts b/src/database/models/Outlet.ts index 801300b..e33887a 100644 --- a/src/database/models/Outlet.ts +++ b/src/database/models/Outlet.ts @@ -15,8 +15,7 @@ export interface OutletAttributes { status: typeof OUTLET_STATUS[keyof typeof OUTLET_STATUS]; establishedDate: string; dealerId: string; - region: typeof REGIONS[keyof typeof REGIONS]; - zone: string; + locationId: string; } export interface OutletInstance extends Model, OutletAttributes { } @@ -81,13 +80,13 @@ export default (sequelize: Sequelize) => { key: 'id' } }, - region: { - type: DataTypes.ENUM(...Object.values(REGIONS)), - allowNull: false - }, - zone: { - type: DataTypes.STRING, - allowNull: false + locationId: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'locations', + key: 'id' + } } }, { tableName: 'outlets', @@ -97,8 +96,7 @@ export default (sequelize: Sequelize) => { { fields: ['dealerId'] }, { fields: ['type'] }, { fields: ['status'] }, - { fields: ['region'] }, - { fields: ['zone'] } + { fields: ['locationId'] } ] }); @@ -107,6 +105,10 @@ export default (sequelize: Sequelize) => { foreignKey: 'dealerId', as: 'dealer' }); + Outlet.belongsTo(models.Location, { + foreignKey: 'locationId', + as: 'location' + }); Outlet.hasMany(models.Resignation, { foreignKey: 'outletId', as: 'resignations' diff --git a/src/database/models/Region.ts b/src/database/models/Region.ts deleted file mode 100644 index db173a5..0000000 --- a/src/database/models/Region.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const Region = sequelize.define('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; -}; diff --git a/src/database/models/RegionManager.ts b/src/database/models/RegionManager.ts deleted file mode 100644 index ab61ec2..0000000 --- a/src/database/models/RegionManager.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const RegionManager = sequelize.define('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; -}; diff --git a/src/database/models/State.ts b/src/database/models/State.ts deleted file mode 100644 index fa04e83..0000000 --- a/src/database/models/State.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const State = sequelize.define('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; -}; diff --git a/src/database/models/User.ts b/src/database/models/User.ts index 900313e..600c04d 100644 --- a/src/database/models/User.ts +++ b/src/database/models/User.ts @@ -11,11 +11,7 @@ export interface UserAttributes { department: string | null; designation: string | null; roleCode: string | null; - zoneId: string | null; - regionId: string | null; - stateId: string | null; - districtId: string | null; - areaId: string | null; + locationId: string | null; dealerId: string | null; isActive: boolean; isExternal: boolean; @@ -70,43 +66,11 @@ export default (sequelize: Sequelize) => { type: DataTypes.STRING, allowNull: true }, - zoneId: { + locationId: { type: DataTypes.UUID, allowNull: true, references: { - model: 'zones', - 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', + model: 'locations', key: 'id' } }, @@ -152,14 +116,9 @@ export default (sequelize: Sequelize) => { }); User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' }); User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' }); - User.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); - 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.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' }); User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' }); - User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' }); User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' }); }; diff --git a/src/database/models/UserRole.ts b/src/database/models/UserRole.ts index 3f08215..15561d1 100644 --- a/src/database/models/UserRole.ts +++ b/src/database/models/UserRole.ts @@ -4,9 +4,7 @@ export interface UserRoleAttributes { id: string; userId: string; roleId: string; - zoneId: string | null; - regionId: string | null; - areaId: string | null; + locationId: string | null; assignedAt: Date; assignedBy: string | null; } @@ -36,27 +34,11 @@ export default (sequelize: Sequelize) => { key: 'id' } }, - zoneId: { + locationId: { type: DataTypes.UUID, allowNull: true, references: { - model: 'zones', - key: 'id' - } - }, - regionId: { - type: DataTypes.UUID, - allowNull: true, - references: { - model: 'regions', - key: 'id' - } - }, - areaId: { - type: DataTypes.UUID, - allowNull: true, - references: { - model: 'areas', + model: 'locations', key: 'id' } }, @@ -87,17 +69,9 @@ export default (sequelize: Sequelize) => { foreignKey: 'roleId', as: 'role' }); - UserRole.belongsTo(models.Zone, { - foreignKey: 'zoneId', - as: 'zone' - }); - UserRole.belongsTo(models.Region, { - foreignKey: 'regionId', - as: 'region' - }); - UserRole.belongsTo(models.Area, { - foreignKey: 'areaId', - as: 'area' + UserRole.belongsTo(models.Location, { + foreignKey: 'locationId', + as: 'location' }); UserRole.belongsTo(models.User, { foreignKey: 'assignedBy', diff --git a/src/database/models/Zone.ts b/src/database/models/Zone.ts deleted file mode 100644 index 2b4b16b..0000000 --- a/src/database/models/Zone.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const Zone = sequelize.define('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; -}; diff --git a/src/database/models/ZoneManager.ts b/src/database/models/ZoneManager.ts deleted file mode 100644 index ea8887d..0000000 --- a/src/database/models/ZoneManager.ts +++ /dev/null @@ -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 { } - -export default (sequelize: Sequelize) => { - const ZoneManager = sequelize.define('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; -}; diff --git a/src/database/models/index.ts b/src/database/models/index.ts index 86d00fd..0aa42bf 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -14,26 +14,19 @@ import createAuditLog from './AuditLog.js'; import createFinancePayment from './FinancePayment.js'; import createFnF from './FnF.js'; import createFnFLineItem from './FnFLineItem.js'; -import createRegion from './Region.js'; -import createZone from './Zone.js'; import createSLAConfiguration from './SLAConfiguration.js'; import createSLAReminder from './SLAReminder.js'; import createSLAEscalationConfig from './SLAEscalationConfig.js'; import createWorkflowStageConfig from './WorkflowStageConfig.js'; import createNotification from './Notification.js'; +import createLocation from './Location.js'; +import createLocationHierarchy from './LocationHierarchy.js'; // Batch 1: Organizational Hierarchy & User Management import createRole from './Role.js'; import createPermission from './Permission.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 createZoneManager from './ZoneManager.js'; -import createRegionManager from './RegionManager.js'; -import createAreaManager from './AreaManager.js'; -import createDistrictManager from './DistrictManager.js'; // Batch 2: Opportunity & Application Framework import createOpportunity from './Opportunity.js'; @@ -121,26 +114,19 @@ db.AuditLog = createAuditLog(sequelize); db.FinancePayment = createFinancePayment(sequelize); db.FnF = createFnF(sequelize); db.FnFLineItem = createFnFLineItem(sequelize); -db.Region = createRegion(sequelize); -db.Zone = createZone(sequelize); db.SLAConfiguration = createSLAConfiguration(sequelize); db.SLAReminder = createSLAReminder(sequelize); db.SLAEscalationConfig = createSLAEscalationConfig(sequelize); db.WorkflowStageConfig = createWorkflowStageConfig(sequelize); db.Notification = createNotification(sequelize); +db.Location = createLocation(sequelize); +db.LocationHierarchy = createLocationHierarchy(sequelize); // Batch 1: Organizational Hierarchy & User Management db.Role = createRole(sequelize); db.Permission = createPermission(sequelize); db.RolePermission = createRolePermission(sequelize); -db.State = createState(sequelize); -db.District = createDistrict(sequelize); -db.Area = createArea(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 db.Opportunity = createOpportunity(sequelize); diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index 0d072e6..affe573 100644 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; import bcrypt from 'bcryptjs'; +import { Op } from 'sequelize'; import db from '../../database/models/index.js'; const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db; 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) => { 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 = 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({ + where: whereClause, attributes: { exclude: ['password'] }, include: [ { @@ -134,9 +173,7 @@ export const getAllUsers = async (req: Request, res: Response) => { } ] }, - { model: db.Zone, as: 'zone' }, - { model: db.Region, as: 'region' }, - { model: db.Area, as: 'area' } + { model: db.Location, as: 'location' } ], order: [['createdAt', 'DESC']] }); @@ -152,9 +189,10 @@ export const createUser = async (req: AuthRequest, res: Response) => { const { fullName, email, roleCode, employeeId, mobileNumber, department, designation, - zoneId, regionId, stateId, districtId, areaId + locationId } = req.body; + // Validate required fields if (!fullName || !email || !roleCode) { return res.status(400).json({ @@ -198,11 +236,7 @@ export const createUser = async (req: AuthRequest, res: Response) => { mobileNumber, department, designation, - zoneId, - regionId, - stateId, - districtId, - areaId + locationId }); await AuditLog.create({ @@ -259,7 +293,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => { const { fullName, email, roleCode, status, isActive, employeeId, mobileNumber, department, designation, - zoneId, regionId, stateId, districtId, areaId, + locationId, password // Optional password update } = req.body; @@ -277,11 +311,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => { mobileNumber: mobileNumber || user.mobileNumber, department: department || user.department, designation: designation || user.designation, - zoneId: zoneId !== undefined ? zoneId : user.zoneId, - regionId: regionId !== undefined ? regionId : user.regionId, - stateId: stateId !== undefined ? stateId : user.stateId, - districtId: districtId !== undefined ? districtId : user.districtId, - areaId: areaId !== undefined ? areaId : user.areaId + locationId: (locationId === '' ? null : (locationId !== undefined ? locationId : user.locationId)) }; // 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) => { 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]) // This is a placeholder for the actual business logic diff --git a/src/modules/assessment/assessment.controller.ts b/src/modules/assessment/assessment.controller.ts index 3411125..2a374fd 100644 --- a/src/modules/assessment/assessment.controller.ts +++ b/src/modules/assessment/assessment.controller.ts @@ -587,35 +587,34 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) = }); } - // Always mark interview as completed when a decision is made - // This ensures action buttons hide for the user - await interview.update({ status: 'Completed' }); + // --- Multi-Interviewer Synchronization --- + // Fetch all assigned participants for this interview + const participants = await db.InterviewParticipant.findAll({ + where: { interviewId } + }); - // Update Application Status - if (decision === 'Rejected') { - await db.Application.update({ - overallStatus: 'Rejected', - currentStage: 'Rejected' - }, { where: { id: interview.applicationId } }); + // Fetch all evaluations submitted for this interview + const evaluations = await db.InterviewEvaluation.findAll({ + where: { interviewId } + }); - // Log Status History - await db.ApplicationStatusHistory.create({ - applicationId: interview.applicationId, - previousStatus: 'Interview Pending', - newStatus: 'Rejected', - changedBy: req.user?.id, - reason: remarks || 'Interview Rejected' - }); - } else { - // Determine next status based on current level + const isFullyEvaluated = evaluations.length >= participants.length; + + if (isFullyEvaluated) { + // All interviewers have responded + await interview.update({ status: 'Completed' }); + + // Determine next status based on level const nextStatusMap: any = { 1: 'Level 1 Approved', 2: 'Level 2 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'; - - // Also update currentStage for better tracking const stageMapping: any = { 1: 'Level 1 Approved', 2: 'Level 2 Approved', @@ -633,8 +632,13 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) = previousStatus: 'Interview Pending', newStatus: newStatus, 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({ diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 7fc1974..b05499e 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -9,7 +9,7 @@ import { AuthRequest } from '../../types/express.types.js'; // Register new user export const register = async (req: Request, res: Response) => { try { - const { email, password, fullName, role, phone, region, zone } = req.body; + const { email, password, fullName, role, phone, locationId } = req.body; // Validate input if (!email || !password || !fullName || !role) { @@ -38,8 +38,7 @@ export const register = async (req: Request, res: Response) => { fullName, roleCode: role, mobileNumber: phone, - regionId: region, - zoneId: zone, + locationId, status: 'active' }); @@ -127,8 +126,7 @@ export const login = async (req: Request, res: Response) => { email: user.email, fullName: user.fullName, role: user.roleCode, - region: user.regionId, - zone: user.zoneId + locationId: user.locationId } }); } catch (error) { @@ -163,7 +161,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => { } 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) { @@ -180,8 +178,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => { email: user.email, fullName: user.fullName, role: user.roleCode, - region: user.regionId, - zone: user.zoneId, + locationId: user.locationId, phone: user.mobileNumber, createdAt: (user as any).createdAt } diff --git a/src/modules/dealer/dealer.controller.ts b/src/modules/dealer/dealer.controller.ts index 309c57c..1be25d1 100644 --- a/src/modules/dealer/dealer.controller.ts +++ b/src/modules/dealer/dealer.controller.ts @@ -7,6 +7,7 @@ const { } = db; import { AuthRequest } from '../../types/express.types.js'; import { AUDIT_ACTIONS } from '../../common/config/constants.js'; +import { Op } from 'sequelize'; export const getDealers = async (req: Request, res: Response) => { try { @@ -110,8 +111,7 @@ export const createDealer = async (req: AuthRequest, res: Response) => { status: 'active', isActive: true, isExternal: true, // Dealers are external users - zoneId: application.zoneId, - regionId: application.regionId + locationId: application.locationId }); 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 } }); - // --- Create Primary Outlet for the Dealer --- - // Check if outlet already exists - let outlet = await Outlet.findOne({ where: { dealerId: user.id } }); + // --- Create or Link Outlet for this specific application --- + // We use the application's unique ID to ensure we don't duplicate THE SAME application's outlet, + // 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) { - 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 outletCode = `OUT-${dealerCodeRecord?.dealerCode || Date.now().toString().slice(-6)}`; @@ -161,11 +150,10 @@ export const createDealer = async (req: AuthRequest, res: Response) => { status: 'Active', establishedDate: new Date(), dealerId: user.id, - region: regionName as any, - zone: zoneName + locationId: application.locationId }); - 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({ userId: req.user?.id, diff --git a/src/modules/master/master.controller.ts b/src/modules/master/master.controller.ts index 454edbf..b88f3b5 100644 --- a/src/modules/master/master.controller.ts +++ b/src/modules/master/master.controller.ts @@ -1,60 +1,56 @@ import { Request, Response } from 'express'; 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 --- export const getRegions = async (req: Request, res: Response) => { - try { - 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' }); - } + return getLocationsByType('region', req, res); }; export const createRegion = async (req: Request, res: Response) => { try { - const { zoneId, regionCode, regionName, description, stateIds, regionalManagerId } = req.body; + const { zoneId, regionName } = req.body; - if (!zoneId || !regionName || !regionCode) { - return res.status(400).json({ success: false, message: 'Zone ID, region name and code are required' }); + if (!regionName) { + return res.status(400).json({ success: false, message: 'Region name is required' }); } - const region = await Region.create({ - zoneId, - regionCode, - regionName, - description, - regionalManagerId: regionalManagerId || null + const region = await db.Location.create({ + name: regionName, + type: 'region' }); - // Assign states if provided - if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) { - await State.update( - { regionId: region.id, zoneId }, // Also ensure State belongs to the Zone (hierarchy) - { where: { id: stateIds } } - ); + if (zoneId) { + await db.LocationHierarchy.create({ + locationId: region.id, + parentId: zoneId + }); } 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 --- export const getZones = async (req: Request, res: Response) => { - try { - 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' }); - } + return getLocationsByType('zone', req, res); }; export const createZone = async (req: Request, res: Response) => { try { const { regionId, zoneName } = req.body; - if (!regionId || !zoneName) { - return res.status(400).json({ success: false, message: 'Region ID and zone name are required' }); + if (!zoneName) { + return res.status(400).json({ success: false, message: 'Zone name is required' }); } - const zone = await Zone.create({ - regionId, // Wait, Zone Model doesn't have regionId. It's the other way around? - zoneName + const zone = await db.Location.create({ + name: 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 }); } catch (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 { const { id } = req.params; - const { zoneName, description, isActive, zonalBusinessHeadId, stateIds } = req.body; + const { name, type, parentIds } = req.body; - const zone = await Zone.findByPk(id); - if (!zone) { - return res.status(404).json({ success: false, message: 'Zone not found' }); + const location = await db.Location.findByPk(id); + if (!location) { + return res.status(404).json({ success: false, message: 'Location not found' }); } const updates: any = {}; - if (zoneName) updates.zoneName = zoneName; - if (description !== undefined) updates.description = description; - if (isActive !== undefined) updates.isActive = isActive; - if (zonalBusinessHeadId !== undefined) updates.zonalBusinessHeadId = zonalBusinessHeadId; + if (name) updates.name = name; + if (type) updates.type = type; - await zone.update(updates); + await location.update(updates); - // Handle State assignment - if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) { - // Update all provided states to belong to this zone - // We can't easily "remove" states because zoneId is non-nullable. - // States must be moved TO another zone to be removed from this one. - // 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 } } - ); + if (parentIds && Array.isArray(parentIds)) { + // Re-sync parents + await db.LocationHierarchy.destroy({ where: { locationId: id } }); + for (const pid of parentIds) { + await db.LocationHierarchy.create({ locationId: id, parentId: pid }); + } } - res.json({ success: true, message: 'Zone updated successfully' }); + res.json({ success: true, message: 'Location updated successfully' }); } catch (error) { - console.error('Update zone error:', error); - res.status(500).json({ success: false, message: 'Error updating zone' }); + console.error('Update location error:', error); + res.status(500).json({ success: false, message: 'Error updating location' }); } }; // --- States --- export const getStates = async (req: Request, res: Response) => { - try { - 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' }); - } + return getLocationsByType('state', req, res); }; export const createState = async (req: Request, res: Response) => { try { 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 }); } catch (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 --- export const getDistricts = async (req: Request, res: Response) => { - try { - 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' }); - } + return getLocationsByType('district', req, res); }; export const createDistrict = async (req: Request, res: Response) => { try { 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 }); } catch (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 --- export const getAreas = async (req: Request, res: Response) => { - try { - 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' }); - } + return getLocationsByType('area', req, res); }; export const createArea = async (req: Request, res: Response) => { try { - const { districtId, areaCode, areaName, city, pincode, managerId } = req.body; - if (!districtId || !areaName || !pincode) return res.status(400).json({ success: false, message: 'District ID, area name, and pincode required' }); + const { districtId, areaName } = req.body; + if (!areaName) return res.status(400).json({ success: false, message: 'Area name is required' }); - // Need to fetch regionId from district -> state -> zone -> region? - // 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' } - ] - }] - }); + const area = await db.Location.create({ name: areaName, type: 'area' }); - let regionId = null; - let zoneId = null; - 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 - }); + if (districtId) { + await db.LocationHierarchy.create({ locationId: area.id, parentId: districtId }); } 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 --- -export const getAreaManagers = async (req: Request, res: Response) => { +// --- Managers (Consolidated) --- +export const getManagersByRole = async (req: Request, res: Response) => { try { - // Fetch Users who have active AreaManager assignments - // We use the User model as the primary so we get the User details naturally - const managers = await User.findAll({ - attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId', 'roleCode', 'zoneId', 'regionId'], - 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']] - }); + const { roleCode, locationId } = req.query as any; + const where: any = {}; + if (roleCode) where.roleCode = roleCode; + if (locationId) where.locationId = locationId; - // Transform if necessary to flatten the structure for the frontend - // But the user asked for "straightforward", so a clean nested JSON is usually best - // We can double check if they want a flat list of (User, Area) pairs or User -> [Areas] - // "Arean mangers" implies the People. So User -> [Areas] is the best entity representation. + const managers = await User.findAll({ + where, + attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode', 'locationId'], + include: [{ + model: db.Location, + as: 'location', + attributes: ['id', 'name', 'type'] + }] + }); res.json({ success: true, data: managers }); } catch (error) { - console.error('Get area managers error:', error); - res.status(500).json({ success: false, message: 'Error fetching area managers' }); + console.error('Get managers error:', error); + 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 { const { id } = req.params; - const { areaName, city, pincode, isActive, managerId, districtId, activeFrom, activeTo } = req.body; - const area = await Area.findByPk(id); - if (!area) return res.status(404).json({ success: false, message: 'Area not found' }); - - 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' }); + await db.LocationHierarchy.destroy({ where: { [db.Sequelize.Op.or]: [{ locationId: id }, { parentId: id }] } }); + await db.Location.destroy({ where: { id } }); + res.json({ success: true, message: 'Location deleted' }); } catch (error) { - console.error('Update area error:', error); - res.status(500).json({ success: false, message: 'Error updating area' }); + console.error('Delete location error:', error); + res.status(500).json({ success: false, message: 'Error deleting location' }); } }; diff --git a/src/modules/master/master.routes.ts b/src/modules/master/master.routes.ts index c090e34..c041afd 100644 --- a/src/modules/master/master.routes.ts +++ b/src/modules/master/master.routes.ts @@ -17,25 +17,26 @@ router.use(authenticate as any); // Regions router.get('/regions', masterController.getRegions); 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 router.get('/zones', masterController.getZones); 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) 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) 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 router.get('/areas', masterController.getAreas); 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 router.get('/area-managers', masterController.getAreaManagers); diff --git a/src/modules/onboarding/onboarding.controller.ts b/src/modules/onboarding/onboarding.controller.ts index 92532c5..639810c 100644 --- a/src/modules/onboarding/onboarding.controller.ts +++ b/src/modules/onboarding/onboarding.controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; 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 { v4 as uuidv4 } from 'uuid'; 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 } = req.body; - // Check for duplicate email - const existingApp = await Application.findOne({ where: { email } }); + // Check for duplicate application for SAME location + 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) { - 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()}`; - // Fetch hierarchy from Auto-detected Area - let zoneId, regionId, areaId; + // Fetch hierarchy from Auto-detected Location + let locationId = null; let isOpportunityAvailable = false; - // Auto-detect Area from District + // Auto-detect Location from District if (req.body.district) { const districtName = req.body.district; - // 1. Find District ID by Name - const districtRecord = await District.findOne({ - where: { districtName: { [Op.iLike]: districtName } } - }); - - 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; + // Find Location (type: district) match + const districtRecord = await Location.findOne({ + where: { + name: { [Op.iLike]: districtName }, + type: 'district' } - } - } - - // 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) { - regionId = districtRecord.regionId; - zoneId = districtRecord.zoneId; + locationId = districtRecord.id; + 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, age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType, currentStage: APPLICATION_STAGES.DD, - overallStatus: initialStatus, + overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED, progressPercentage: isOpportunityAvailable ? 10 : 0, - zoneId, - regionId, - areaId // Link to Area + locationId }); // Log Status History await ApplicationStatusHistory.create({ applicationId: application.id, previousStatus: null, - newStatus: initialStatus, + newStatus: application.overallStatus, changedBy: req.user?.id || null, reason: 'Initial Submission' }); diff --git a/src/modules/opportunity/opportunity.controller.ts b/src/modules/opportunity/opportunity.controller.ts index 922f9dc..937f71c 100644 --- a/src/modules/opportunity/opportunity.controller.ts +++ b/src/modules/opportunity/opportunity.controller.ts @@ -7,12 +7,11 @@ import { AUDIT_ACTIONS } from '../../common/config/constants.js'; export const getOpportunities = async (req: Request, res: Response) => { try { - const { status, regionId, zoneId } = req.query as any; + const { status, locationId } = req.query as any; const where: any = {}; if (status) where.status = status; - if (regionId) where.regionId = regionId; - if (zoneId) where.zoneId = zoneId; + if (locationId) where.locationId = locationId; const opportunities = await Opportunity.findAll({ where, @@ -33,7 +32,7 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => { try { const { leadSource, leadName, contactNumber, email, - zoneId, regionId, stateId, districtId, + locationId, opportunityType, priority } = req.body; @@ -42,10 +41,7 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => { leadName, contactNumber, email, - zoneId, - regionId, - stateId, - districtId, + locationId, opportunityType, priority, status: 'New', diff --git a/src/scripts/seedQuestionnaire.ts b/src/scripts/seedQuestionnaire.ts index f334610..8070e3e 100644 --- a/src/scripts/seedQuestionnaire.ts +++ b/src/scripts/seedQuestionnaire.ts @@ -116,6 +116,17 @@ const seedQuestionnaire = async () => { weight: 0, 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) { diff --git a/src/scripts/test_multi_outlet.ts b/src/scripts/test_multi_outlet.ts new file mode 100644 index 0000000..1804322 --- /dev/null +++ b/src/scripts/test_multi_outlet.ts @@ -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(); diff --git a/src/scripts/verify_uuid_fix.ts b/src/scripts/verify_uuid_fix.ts new file mode 100644 index 0000000..48041ab --- /dev/null +++ b/src/scripts/verify_uuid_fix.ts @@ -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(); diff --git a/src/types/auth.types.ts b/src/types/auth.types.ts index eea988a..317184e 100644 --- a/src/types/auth.types.ts +++ b/src/types/auth.types.ts @@ -2,6 +2,5 @@ export interface TokenPayload { userId: string; email: string; role: string; - region: string | null; - zone: string | null; + locationId: string | null; } diff --git a/verify_approval_sync.ts b/verify_approval_sync.ts new file mode 100644 index 0000000..3d23c0e --- /dev/null +++ b/verify_approval_sync.ts @@ -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(); diff --git a/verify_db_logic.ts b/verify_db_logic.ts new file mode 100644 index 0000000..5ed0efa --- /dev/null +++ b/verify_db_logic.ts @@ -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); +});