hirarchchy changed
This commit is contained in:
parent
6dc506077f
commit
d20e573d69
16
check_db_roles.ts
Normal file
16
check_db_roles.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import db from './src/database/models/index';
|
||||||
|
const { Role } = db;
|
||||||
|
|
||||||
|
async function check() {
|
||||||
|
try {
|
||||||
|
const roles = await Role.findAll();
|
||||||
|
console.log('--- Database Roles Check ---');
|
||||||
|
console.log('Roles Data:', JSON.stringify(roles, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Check failed:', error);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
25
check_db_users.ts
Normal file
25
check_db_users.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import db from './src/database/models/index';
|
||||||
|
const { User } = db;
|
||||||
|
|
||||||
|
async function check() {
|
||||||
|
try {
|
||||||
|
const users = await User.findAll({
|
||||||
|
attributes: ['id', 'roleCode', 'zoneId', 'regionId', 'areaId']
|
||||||
|
});
|
||||||
|
console.log('--- Database User Check ---');
|
||||||
|
console.log('Users Data:', JSON.stringify(users, null, 2));
|
||||||
|
|
||||||
|
const nbhUsers = users.filter((u: any) => u.roleCode === 'NBH');
|
||||||
|
console.log('NBH Users count:', nbhUsers.length);
|
||||||
|
|
||||||
|
const zmUsers = users.filter((u: any) => u.roleCode === 'DD-ZM');
|
||||||
|
console.log('DD-ZM Users count:', zmUsers.length);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Check failed:', error);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
39
check_questions.ts
Normal file
39
check_questions.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import db from './src/database/models/index.js';
|
||||||
|
|
||||||
|
async function checkQuestions() {
|
||||||
|
try {
|
||||||
|
const questionnaire = await db.Questionnaire.findOne({
|
||||||
|
where: { isActive: true },
|
||||||
|
include: [{
|
||||||
|
model: db.QuestionnaireQuestion,
|
||||||
|
as: 'questions',
|
||||||
|
include: [{ model: db.QuestionnaireOption, as: 'questionOptions' }]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!questionnaire) {
|
||||||
|
console.log('No active questionnaire found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Active Questionnaire: ${questionnaire.version} (${questionnaire.id})`);
|
||||||
|
|
||||||
|
questionnaire.questions.forEach((q: any) => {
|
||||||
|
console.log(`- [${q.order}] ${q.questionText} (Weight: ${q.weight}, Type: ${q.inputType})`);
|
||||||
|
if (q.questionOptions && q.questionOptions.length > 0) {
|
||||||
|
q.questionOptions.forEach((opt: any) => {
|
||||||
|
console.log(` * ${opt.optionText} (Score: ${opt.score})`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(` (No options)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkQuestions();
|
||||||
@ -11,6 +11,7 @@
|
|||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"migrate": "tsx scripts/migrate.ts",
|
"migrate": "tsx scripts/migrate.ts",
|
||||||
"seed": "tsx scripts/seed-geo.ts",
|
"seed": "tsx scripts/seed-geo.ts",
|
||||||
|
"seed-normalized": "tsx scripts/seed_normalized_data.ts",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"clear-logs": "rm -rf logs/*.log"
|
"clear-logs": "rm -rf logs/*.log"
|
||||||
|
|||||||
@ -15,6 +15,7 @@ const rolesToSeed = [
|
|||||||
{ roleCode: ROLES.FINANCE, roleName: 'Finance', category: 'DEPARTMENT', description: 'Finance Department' },
|
{ roleCode: ROLES.FINANCE, roleName: 'Finance', category: 'DEPARTMENT', description: 'Finance Department' },
|
||||||
{ roleCode: ROLES.LEGAL_ADMIN, roleName: 'Legal Admin', category: 'DEPARTMENT', description: 'Legal Department' },
|
{ roleCode: ROLES.LEGAL_ADMIN, roleName: 'Legal Admin', category: 'DEPARTMENT', description: 'Legal Department' },
|
||||||
{ roleCode: ROLES.NBH, roleName: 'NBH', category: 'NATIONAL', description: 'National Business Head' },
|
{ roleCode: ROLES.NBH, roleName: 'NBH', category: 'NATIONAL', description: 'National Business Head' },
|
||||||
|
{ roleCode: ROLES.ASM, roleName: 'ASM', category: 'SALES', description: 'Area Sales Manager' },
|
||||||
{ roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' }
|
{ roleCode: ROLES.DEALER, roleName: 'Dealer', category: 'EXTERNAL', description: 'Dealer Principal' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
97
scripts/seed_normalized_data.ts
Normal file
97
scripts/seed_normalized_data.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
|
||||||
|
import db from '../src/database/models/index.js';
|
||||||
|
const { Role, Location, LocationHierarchy, User, UserRole, Permission } = db;
|
||||||
|
|
||||||
|
async function seed() {
|
||||||
|
console.log('--- Seeding Normalized Graph Data ---');
|
||||||
|
|
||||||
|
// 1. Create Roles
|
||||||
|
const roles = [
|
||||||
|
{ roleCode: 'NBH', roleName: 'National Business Head', category: 'NATIONAL' },
|
||||||
|
{ roleCode: 'DD Head', roleName: 'DD Head', category: 'NATIONAL' },
|
||||||
|
{ roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' },
|
||||||
|
{ roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' },
|
||||||
|
{ roleCode: 'RBM', roleName: 'Regional Business Manager', category: 'REGIONAL' },
|
||||||
|
{ roleCode: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' },
|
||||||
|
{ roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' },
|
||||||
|
{ roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const r of roles) {
|
||||||
|
await Role.findOrCreate({ where: { roleCode: r.roleCode }, defaults: r });
|
||||||
|
}
|
||||||
|
console.log('Roles seeded.');
|
||||||
|
|
||||||
|
// 2. Create Locations
|
||||||
|
const zone1 = await Location.create({ name: 'North Zone', type: 'zone' });
|
||||||
|
const region1 = await Location.create({ name: 'Delhi Region', type: 'region' });
|
||||||
|
const area1 = await Location.create({ name: 'South Delhi Area', type: 'area' });
|
||||||
|
|
||||||
|
const zone2 = await Location.create({ name: 'South Zone', type: 'zone' });
|
||||||
|
const region2 = await Location.create({ name: 'Bangalore Region', type: 'region' });
|
||||||
|
|
||||||
|
console.log('Locations created.');
|
||||||
|
|
||||||
|
// 3. Create Hierarchies (Bridge Table)
|
||||||
|
await LocationHierarchy.create({ locationId: region1.id, parentId: zone1.id });
|
||||||
|
await LocationHierarchy.create({ locationId: area1.id, parentId: region1.id });
|
||||||
|
|
||||||
|
// Example of multiple parents if needed
|
||||||
|
// await LocationHierarchy.create({ locationId: area1.id, parentId: someOtherParent.id });
|
||||||
|
|
||||||
|
await LocationHierarchy.create({ locationId: region2.id, parentId: zone2.id });
|
||||||
|
|
||||||
|
console.log('Hierarchies seeded.');
|
||||||
|
|
||||||
|
// 4. Create Users and Map them
|
||||||
|
// NBH (Global)
|
||||||
|
const nbhUser = await User.findOrCreate({
|
||||||
|
where: { email: 'nbh@example.com' },
|
||||||
|
defaults: { fullName: 'National Head', roleCode: 'NBH' }
|
||||||
|
});
|
||||||
|
await UserRole.create({ userId: nbhUser[0].id, roleId: (await Role.findOne({ where: { roleCode: 'NBH' } })).id });
|
||||||
|
|
||||||
|
// ZBH (North Zone)
|
||||||
|
const zbhUser = await User.findOrCreate({
|
||||||
|
where: { email: 'zbh.north@example.com' },
|
||||||
|
defaults: { fullName: 'North Zonal Head', roleCode: 'ZBH' }
|
||||||
|
});
|
||||||
|
const zbhRole = await Role.findOne({ where: { roleCode: 'ZBH' } });
|
||||||
|
await UserRole.create({
|
||||||
|
userId: zbhUser[0].id,
|
||||||
|
roleId: zbhRole.id,
|
||||||
|
locationId: zone1.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// RBM (Delhi Region)
|
||||||
|
const rbmUser = await User.findOrCreate({
|
||||||
|
where: { email: 'rbm.delhi@example.com' },
|
||||||
|
defaults: { fullName: 'Delhi Regional Manager', roleCode: 'RBM' }
|
||||||
|
});
|
||||||
|
const rbmRole = await Role.findOne({ where: { roleCode: 'RBM' } });
|
||||||
|
await UserRole.create({
|
||||||
|
userId: rbmUser[0].id,
|
||||||
|
roleId: rbmRole.id,
|
||||||
|
locationId: region1.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASM (South Delhi Area)
|
||||||
|
const asmUser = await User.findOrCreate({
|
||||||
|
where: { email: 'asm.sdelhi@example.com' },
|
||||||
|
defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM' }
|
||||||
|
});
|
||||||
|
const asmRole = await Role.findOne({ where: { roleCode: 'ASM' } });
|
||||||
|
await UserRole.create({
|
||||||
|
userId: asmUser[0].id,
|
||||||
|
roleId: asmRole.id,
|
||||||
|
locationId: area1.id
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Users and Mappings seeded.');
|
||||||
|
console.log('--- Seeding Complete ---');
|
||||||
|
}
|
||||||
|
|
||||||
|
seed().catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}).then(() => process.exit(0));
|
||||||
@ -10,8 +10,7 @@ export const generateToken = (user: any): string => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.roleCode,
|
role: user.roleCode,
|
||||||
region: user.regionId,
|
locationId: user.locationId
|
||||||
zone: user.zoneId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return jwt.sign(payload, JWT_SECRET, {
|
return jwt.sign(payload, JWT_SECRET, {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export const ROLES = {
|
|||||||
LEGAL_ADMIN: 'Legal Admin',
|
LEGAL_ADMIN: 'Legal Admin',
|
||||||
SUPER_ADMIN: 'Super Admin',
|
SUPER_ADMIN: 'Super Admin',
|
||||||
DD_AM: 'DD AM',
|
DD_AM: 'DD AM',
|
||||||
|
ASM: 'ASM',
|
||||||
FINANCE: 'Finance',
|
FINANCE: 'Finance',
|
||||||
DEALER: 'Dealer'
|
DEALER: 'Dealer'
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -34,9 +34,7 @@ export interface ApplicationAttributes {
|
|||||||
architectureAssignedTo: string | null;
|
architectureAssignedTo: string | null;
|
||||||
architectureStatus: string | null;
|
architectureStatus: string | null;
|
||||||
submittedBy: string | null;
|
submittedBy: string | null;
|
||||||
zoneId: string | null;
|
locationId: string | null;
|
||||||
regionId: string | null;
|
|
||||||
areaId: string | null;
|
|
||||||
architectureAssignedDate: Date | null;
|
architectureAssignedDate: Date | null;
|
||||||
architectureDocumentDate: Date | null;
|
architectureDocumentDate: Date | null;
|
||||||
architectureCompletionDate: Date | null;
|
architectureCompletionDate: Date | null;
|
||||||
@ -202,27 +200,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zoneId: {
|
locationId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'zones',
|
model: 'locations',
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
areaId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'areas',
|
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -263,9 +245,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
|
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
|
||||||
Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' });
|
Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' });
|
||||||
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
|
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
|
||||||
Application.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
Application.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
|
||||||
Application.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
|
||||||
Application.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' });
|
|
||||||
|
|
||||||
Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' });
|
Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' });
|
||||||
Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' });
|
Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' });
|
||||||
|
|||||||
@ -1,141 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface AreaAttributes {
|
|
||||||
id: string;
|
|
||||||
regionId: string;
|
|
||||||
stateId: string;
|
|
||||||
zoneId: string;
|
|
||||||
districtId: string;
|
|
||||||
managerId: string | null;
|
|
||||||
areaCode: string;
|
|
||||||
areaName: string;
|
|
||||||
city: string | null;
|
|
||||||
pincode: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
activeFrom?: string | null;
|
|
||||||
activeTo?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AreaInstance extends Model<AreaAttributes>, AreaAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const Area = sequelize.define<AreaInstance>('Area', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stateId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'states',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
zoneId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'zones',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
districtId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'districts',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
managerId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
areaCode: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
unique: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
areaName: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
city: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
pincode: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
},
|
|
||||||
activeFrom: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
activeTo: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'areas',
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
(Area as any).associate = (models: any) => {
|
|
||||||
Area.belongsTo(models.Region, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'region'
|
|
||||||
});
|
|
||||||
Area.belongsTo(models.State, {
|
|
||||||
foreignKey: 'stateId',
|
|
||||||
as: 'state'
|
|
||||||
});
|
|
||||||
Area.belongsTo(models.Zone, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'zone'
|
|
||||||
});
|
|
||||||
Area.belongsTo(models.District, {
|
|
||||||
foreignKey: 'districtId',
|
|
||||||
as: 'district'
|
|
||||||
});
|
|
||||||
Area.belongsTo(models.User, {
|
|
||||||
foreignKey: 'managerId',
|
|
||||||
as: 'manager'
|
|
||||||
});
|
|
||||||
Area.hasMany(models.Application, {
|
|
||||||
foreignKey: 'areaId',
|
|
||||||
as: 'applications'
|
|
||||||
});
|
|
||||||
// Dedicated Manager Table Associations
|
|
||||||
Area.hasMany(models.AreaManager, {
|
|
||||||
foreignKey: 'areaId',
|
|
||||||
as: 'areaManagers'
|
|
||||||
});
|
|
||||||
Area.belongsToMany(models.User, {
|
|
||||||
through: models.AreaManager,
|
|
||||||
foreignKey: 'areaId',
|
|
||||||
otherKey: 'userId',
|
|
||||||
as: 'assignedManagers'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return Area;
|
|
||||||
};
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface AreaManagerAttributes {
|
|
||||||
id: string;
|
|
||||||
areaId: string;
|
|
||||||
userId: string;
|
|
||||||
managerType: string;
|
|
||||||
asmCode?: string;
|
|
||||||
isActive: boolean;
|
|
||||||
assignedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AreaManagerInstance extends Model<AreaManagerAttributes>, AreaManagerAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const AreaManager = sequelize.define<AreaManagerInstance>('AreaManager', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
areaId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'areas',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
managerType: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
asmCode: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
},
|
|
||||||
assignedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'area_managers',
|
|
||||||
timestamps: true,
|
|
||||||
updatedAt: false
|
|
||||||
});
|
|
||||||
|
|
||||||
(AreaManager as any).associate = (models: any) => {
|
|
||||||
AreaManager.belongsTo(models.Area, {
|
|
||||||
foreignKey: 'areaId',
|
|
||||||
as: 'area'
|
|
||||||
});
|
|
||||||
AreaManager.belongsTo(models.User, {
|
|
||||||
foreignKey: 'userId',
|
|
||||||
as: 'user'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return AreaManager;
|
|
||||||
};
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface DistrictAttributes {
|
|
||||||
id: string;
|
|
||||||
stateId: string;
|
|
||||||
zoneId: string;
|
|
||||||
regionId: string;
|
|
||||||
districtName: string;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DistrictInstance extends Model<DistrictAttributes>, DistrictAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const District = sequelize.define<DistrictInstance>('District', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
stateId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'states',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
zoneId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'zones',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
districtName: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'districts',
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
(District as any).associate = (models: any) => {
|
|
||||||
District.belongsTo(models.State, {
|
|
||||||
foreignKey: 'stateId',
|
|
||||||
as: 'state'
|
|
||||||
});
|
|
||||||
District.belongsTo(models.Zone, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'zone'
|
|
||||||
});
|
|
||||||
District.belongsTo(models.Region, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'region'
|
|
||||||
});
|
|
||||||
District.hasMany(models.Area, {
|
|
||||||
foreignKey: 'districtId',
|
|
||||||
as: 'areas'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return District;
|
|
||||||
};
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface DistrictManagerAttributes {
|
|
||||||
id: string;
|
|
||||||
districtId: string;
|
|
||||||
userId: string;
|
|
||||||
managerType: string;
|
|
||||||
isActive: boolean;
|
|
||||||
assignedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DistrictManagerInstance extends Model<DistrictManagerAttributes>, DistrictManagerAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const DistrictManager = sequelize.define<DistrictManagerInstance>('DistrictManager', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
districtId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'districts',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
managerType: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
},
|
|
||||||
assignedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'district_managers',
|
|
||||||
timestamps: true,
|
|
||||||
updatedAt: false
|
|
||||||
});
|
|
||||||
|
|
||||||
(DistrictManager as any).associate = (models: any) => {
|
|
||||||
DistrictManager.belongsTo(models.District, {
|
|
||||||
foreignKey: 'districtId',
|
|
||||||
as: 'district'
|
|
||||||
});
|
|
||||||
DistrictManager.belongsTo(models.User, {
|
|
||||||
foreignKey: 'userId',
|
|
||||||
as: 'user'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return DistrictManager;
|
|
||||||
};
|
|
||||||
51
src/database/models/Location.ts
Normal file
51
src/database/models/Location.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
export interface LocationAttributes {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: 'zone' | 'region' | 'area' | 'state' | 'district';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationInstance extends Model<LocationAttributes>, LocationAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const Location = sequelize.define<LocationInstance>('Location', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: DataTypes.ENUM('zone', 'region', 'area', 'state', 'district'),
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'locations',
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
(Location as any).associate = (models: any) => {
|
||||||
|
// Many-to-Many hierarchy via LocationHierarchy bridge table
|
||||||
|
Location.belongsToMany(models.Location, {
|
||||||
|
through: models.LocationHierarchy,
|
||||||
|
as: 'parents',
|
||||||
|
foreignKey: 'locationId',
|
||||||
|
otherKey: 'parentId'
|
||||||
|
});
|
||||||
|
Location.belongsToMany(models.Location, {
|
||||||
|
through: models.LocationHierarchy,
|
||||||
|
as: 'children',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
otherKey: 'locationId'
|
||||||
|
});
|
||||||
|
|
||||||
|
Location.hasMany(models.UserRole, { foreignKey: 'locationId', as: 'userRoles' });
|
||||||
|
Location.hasMany(models.Application, { foreignKey: 'locationId', as: 'applications' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return Location;
|
||||||
|
};
|
||||||
51
src/database/models/LocationHierarchy.ts
Normal file
51
src/database/models/LocationHierarchy.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
export interface LocationHierarchyAttributes {
|
||||||
|
id: string;
|
||||||
|
locationId: string;
|
||||||
|
parentId: string;
|
||||||
|
relationshipType: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationHierarchyInstance extends Model<LocationHierarchyAttributes>, LocationHierarchyAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const LocationHierarchy = sequelize.define<LocationHierarchyInstance>('LocationHierarchy', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
locationId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'locations',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parentId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'locations',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
relationshipType: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 'direct'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'location_hierarchies',
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
(LocationHierarchy as any).associate = (models: any) => {
|
||||||
|
LocationHierarchy.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
|
||||||
|
LocationHierarchy.belongsTo(models.Location, { foreignKey: 'parentId', as: 'parent' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return LocationHierarchy;
|
||||||
|
};
|
||||||
@ -2,10 +2,7 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
|
|||||||
|
|
||||||
export interface OpportunityAttributes {
|
export interface OpportunityAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
zoneId: string;
|
locationId: string;
|
||||||
regionId: string;
|
|
||||||
stateId: string | null;
|
|
||||||
districtId: string | null;
|
|
||||||
city: string;
|
city: string;
|
||||||
opportunityType: string;
|
opportunityType: string;
|
||||||
capacity: string;
|
capacity: string;
|
||||||
@ -26,35 +23,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
primaryKey: true
|
primaryKey: true
|
||||||
},
|
},
|
||||||
zoneId: {
|
locationId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: 'zones',
|
model: 'locations',
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stateId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'states',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
districtId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'districts',
|
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -104,10 +77,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
(Opportunity as any).associate = (models: any) => {
|
(Opportunity as any).associate = (models: any) => {
|
||||||
Opportunity.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
Opportunity.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
|
||||||
Opportunity.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
|
||||||
Opportunity.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
|
|
||||||
Opportunity.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
|
|
||||||
Opportunity.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' });
|
Opportunity.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' });
|
||||||
Opportunity.hasMany(models.Application, { foreignKey: 'opportunityId', as: 'applications' });
|
Opportunity.hasMany(models.Application, { foreignKey: 'opportunityId', as: 'applications' });
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,8 +15,7 @@ export interface OutletAttributes {
|
|||||||
status: typeof OUTLET_STATUS[keyof typeof OUTLET_STATUS];
|
status: typeof OUTLET_STATUS[keyof typeof OUTLET_STATUS];
|
||||||
establishedDate: string;
|
establishedDate: string;
|
||||||
dealerId: string;
|
dealerId: string;
|
||||||
region: typeof REGIONS[keyof typeof REGIONS];
|
locationId: string;
|
||||||
zone: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OutletInstance extends Model<OutletAttributes>, OutletAttributes { }
|
export interface OutletInstance extends Model<OutletAttributes>, OutletAttributes { }
|
||||||
@ -81,13 +80,13 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
region: {
|
locationId: {
|
||||||
type: DataTypes.ENUM(...Object.values(REGIONS)),
|
type: DataTypes.UUID,
|
||||||
allowNull: false
|
allowNull: false,
|
||||||
},
|
references: {
|
||||||
zone: {
|
model: 'locations',
|
||||||
type: DataTypes.STRING,
|
key: 'id'
|
||||||
allowNull: false
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'outlets',
|
tableName: 'outlets',
|
||||||
@ -97,8 +96,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
{ fields: ['dealerId'] },
|
{ fields: ['dealerId'] },
|
||||||
{ fields: ['type'] },
|
{ fields: ['type'] },
|
||||||
{ fields: ['status'] },
|
{ fields: ['status'] },
|
||||||
{ fields: ['region'] },
|
{ fields: ['locationId'] }
|
||||||
{ fields: ['zone'] }
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -107,6 +105,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'dealerId',
|
foreignKey: 'dealerId',
|
||||||
as: 'dealer'
|
as: 'dealer'
|
||||||
});
|
});
|
||||||
|
Outlet.belongsTo(models.Location, {
|
||||||
|
foreignKey: 'locationId',
|
||||||
|
as: 'location'
|
||||||
|
});
|
||||||
Outlet.hasMany(models.Resignation, {
|
Outlet.hasMany(models.Resignation, {
|
||||||
foreignKey: 'outletId',
|
foreignKey: 'outletId',
|
||||||
as: 'resignations'
|
as: 'resignations'
|
||||||
|
|||||||
@ -1,101 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface RegionAttributes {
|
|
||||||
id: string;
|
|
||||||
zoneId: string;
|
|
||||||
// stateId: string | null; // Removed as Region covers multiple states
|
|
||||||
regionalManagerId: string | null;
|
|
||||||
regionCode: string;
|
|
||||||
regionName: string;
|
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegionInstance extends Model<RegionAttributes>, RegionAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const Region = sequelize.define<RegionInstance>('Region', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
zoneId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'zones',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// stateId: {
|
|
||||||
// type: DataTypes.UUID,
|
|
||||||
// allowNull: true,
|
|
||||||
// references: {
|
|
||||||
// model: 'states',
|
|
||||||
// key: 'id'
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
regionalManagerId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
regionCode: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
unique: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
regionName: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'regions',
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
(Region as any).associate = (models: any) => {
|
|
||||||
Region.belongsTo(models.Zone, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'zone'
|
|
||||||
});
|
|
||||||
// Region.belongsTo(models.State, {
|
|
||||||
// foreignKey: 'stateId',
|
|
||||||
// as: 'state'
|
|
||||||
// });
|
|
||||||
Region.hasMany(models.State, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'states'
|
|
||||||
});
|
|
||||||
Region.hasMany(models.Area, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'areas'
|
|
||||||
});
|
|
||||||
Region.hasMany(models.RegionManager, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'managers'
|
|
||||||
});
|
|
||||||
Region.hasMany(models.Application, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'applications'
|
|
||||||
});
|
|
||||||
Region.belongsTo(models.User, {
|
|
||||||
foreignKey: 'regionalManagerId',
|
|
||||||
as: 'regionalManager'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return Region;
|
|
||||||
};
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface RegionManagerAttributes {
|
|
||||||
id: string;
|
|
||||||
regionId: string;
|
|
||||||
userId: string;
|
|
||||||
managerType: string;
|
|
||||||
isActive: boolean;
|
|
||||||
assignedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegionManagerInstance extends Model<RegionManagerAttributes>, RegionManagerAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const RegionManager = sequelize.define<RegionManagerInstance>('RegionManager', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
managerType: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
},
|
|
||||||
assignedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'region_managers',
|
|
||||||
timestamps: true,
|
|
||||||
updatedAt: false
|
|
||||||
});
|
|
||||||
|
|
||||||
(RegionManager as any).associate = (models: any) => {
|
|
||||||
RegionManager.belongsTo(models.Region, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'region'
|
|
||||||
});
|
|
||||||
RegionManager.belongsTo(models.User, {
|
|
||||||
foreignKey: 'userId',
|
|
||||||
as: 'user'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return RegionManager;
|
|
||||||
};
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface StateAttributes {
|
|
||||||
id: string;
|
|
||||||
stateName: string;
|
|
||||||
zoneId: string;
|
|
||||||
regionId: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StateInstance extends Model<StateAttributes>, StateAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const State = sequelize.define<StateInstance>('State', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
stateName: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
unique: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
zoneId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'zones',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'states',
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
(State as any).associate = (models: any) => {
|
|
||||||
State.belongsTo(models.Zone, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'zone'
|
|
||||||
});
|
|
||||||
State.belongsTo(models.Region, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'region'
|
|
||||||
});
|
|
||||||
State.hasMany(models.District, {
|
|
||||||
foreignKey: 'stateId',
|
|
||||||
as: 'districts'
|
|
||||||
});
|
|
||||||
State.hasMany(models.Region, {
|
|
||||||
foreignKey: 'stateId',
|
|
||||||
as: 'regions'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return State;
|
|
||||||
};
|
|
||||||
@ -11,11 +11,7 @@ export interface UserAttributes {
|
|||||||
department: string | null;
|
department: string | null;
|
||||||
designation: string | null;
|
designation: string | null;
|
||||||
roleCode: string | null;
|
roleCode: string | null;
|
||||||
zoneId: string | null;
|
locationId: string | null;
|
||||||
regionId: string | null;
|
|
||||||
stateId: string | null;
|
|
||||||
districtId: string | null;
|
|
||||||
areaId: string | null;
|
|
||||||
dealerId: string | null;
|
dealerId: string | null;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isExternal: boolean;
|
isExternal: boolean;
|
||||||
@ -70,43 +66,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
zoneId: {
|
locationId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'zones',
|
model: 'locations',
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stateId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'states',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
districtId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'districts',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
areaId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'areas',
|
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -152,14 +116,9 @@ export default (sequelize: Sequelize) => {
|
|||||||
});
|
});
|
||||||
User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' });
|
User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' });
|
||||||
User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' });
|
User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' });
|
||||||
User.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
User.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
|
||||||
User.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
|
||||||
User.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
|
|
||||||
User.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
|
|
||||||
User.belongsTo(models.Area, { foreignKey: 'areaId', as: 'area' });
|
|
||||||
|
|
||||||
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
|
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
|
||||||
User.hasMany(models.AreaManager, { foreignKey: 'userId', as: 'areaManagers' });
|
|
||||||
User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' });
|
User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,7 @@ export interface UserRoleAttributes {
|
|||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
roleId: string;
|
roleId: string;
|
||||||
zoneId: string | null;
|
locationId: string | null;
|
||||||
regionId: string | null;
|
|
||||||
areaId: string | null;
|
|
||||||
assignedAt: Date;
|
assignedAt: Date;
|
||||||
assignedBy: string | null;
|
assignedBy: string | null;
|
||||||
}
|
}
|
||||||
@ -36,27 +34,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
zoneId: {
|
locationId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'zones',
|
model: 'locations',
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
regionId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'regions',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
areaId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'areas',
|
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -87,17 +69,9 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'roleId',
|
foreignKey: 'roleId',
|
||||||
as: 'role'
|
as: 'role'
|
||||||
});
|
});
|
||||||
UserRole.belongsTo(models.Zone, {
|
UserRole.belongsTo(models.Location, {
|
||||||
foreignKey: 'zoneId',
|
foreignKey: 'locationId',
|
||||||
as: 'zone'
|
as: 'location'
|
||||||
});
|
|
||||||
UserRole.belongsTo(models.Region, {
|
|
||||||
foreignKey: 'regionId',
|
|
||||||
as: 'region'
|
|
||||||
});
|
|
||||||
UserRole.belongsTo(models.Area, {
|
|
||||||
foreignKey: 'areaId',
|
|
||||||
as: 'area'
|
|
||||||
});
|
});
|
||||||
UserRole.belongsTo(models.User, {
|
UserRole.belongsTo(models.User, {
|
||||||
foreignKey: 'assignedBy',
|
foreignKey: 'assignedBy',
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface ZoneAttributes {
|
|
||||||
id: string;
|
|
||||||
zoneCode: string;
|
|
||||||
zoneName: string;
|
|
||||||
description: string | null;
|
|
||||||
isActive: boolean;
|
|
||||||
zonalBusinessHeadId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ZoneInstance extends Model<ZoneAttributes>, ZoneAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const Zone = sequelize.define<ZoneInstance>('Zone', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
zoneCode: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
unique: true,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
zoneName: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: true
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
},
|
|
||||||
zonalBusinessHeadId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: true,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'zones',
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
(Zone as any).associate = (models: any) => {
|
|
||||||
Zone.belongsTo(models.User, {
|
|
||||||
foreignKey: 'zonalBusinessHeadId',
|
|
||||||
as: 'zonalBusinessHead'
|
|
||||||
});
|
|
||||||
Zone.hasMany(models.Region, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'regions'
|
|
||||||
});
|
|
||||||
Zone.hasMany(models.State, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'states'
|
|
||||||
});
|
|
||||||
Zone.hasMany(models.ZoneManager, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'managers'
|
|
||||||
});
|
|
||||||
Zone.hasMany(models.Application, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'applications'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return Zone;
|
|
||||||
};
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { Model, DataTypes, Sequelize } from 'sequelize';
|
|
||||||
|
|
||||||
export interface ZoneManagerAttributes {
|
|
||||||
id: string;
|
|
||||||
zoneId: string;
|
|
||||||
userId: string;
|
|
||||||
managerType: string;
|
|
||||||
isActive: boolean;
|
|
||||||
assignedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ZoneManagerInstance extends Model<ZoneManagerAttributes>, ZoneManagerAttributes { }
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
const ZoneManager = sequelize.define<ZoneManagerInstance>('ZoneManager', {
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
defaultValue: DataTypes.UUIDV4,
|
|
||||||
primaryKey: true
|
|
||||||
},
|
|
||||||
zoneId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'zones',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userId: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
references: {
|
|
||||||
model: 'users',
|
|
||||||
key: 'id'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
managerType: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false
|
|
||||||
},
|
|
||||||
isActive: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: true
|
|
||||||
},
|
|
||||||
assignedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
tableName: 'zone_managers',
|
|
||||||
timestamps: true,
|
|
||||||
updatedAt: false
|
|
||||||
});
|
|
||||||
|
|
||||||
(ZoneManager as any).associate = (models: any) => {
|
|
||||||
ZoneManager.belongsTo(models.Zone, {
|
|
||||||
foreignKey: 'zoneId',
|
|
||||||
as: 'zone'
|
|
||||||
});
|
|
||||||
ZoneManager.belongsTo(models.User, {
|
|
||||||
foreignKey: 'userId',
|
|
||||||
as: 'user'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return ZoneManager;
|
|
||||||
};
|
|
||||||
@ -14,26 +14,19 @@ import createAuditLog from './AuditLog.js';
|
|||||||
import createFinancePayment from './FinancePayment.js';
|
import createFinancePayment from './FinancePayment.js';
|
||||||
import createFnF from './FnF.js';
|
import createFnF from './FnF.js';
|
||||||
import createFnFLineItem from './FnFLineItem.js';
|
import createFnFLineItem from './FnFLineItem.js';
|
||||||
import createRegion from './Region.js';
|
|
||||||
import createZone from './Zone.js';
|
|
||||||
import createSLAConfiguration from './SLAConfiguration.js';
|
import createSLAConfiguration from './SLAConfiguration.js';
|
||||||
import createSLAReminder from './SLAReminder.js';
|
import createSLAReminder from './SLAReminder.js';
|
||||||
import createSLAEscalationConfig from './SLAEscalationConfig.js';
|
import createSLAEscalationConfig from './SLAEscalationConfig.js';
|
||||||
import createWorkflowStageConfig from './WorkflowStageConfig.js';
|
import createWorkflowStageConfig from './WorkflowStageConfig.js';
|
||||||
import createNotification from './Notification.js';
|
import createNotification from './Notification.js';
|
||||||
|
import createLocation from './Location.js';
|
||||||
|
import createLocationHierarchy from './LocationHierarchy.js';
|
||||||
|
|
||||||
// Batch 1: Organizational Hierarchy & User Management
|
// Batch 1: Organizational Hierarchy & User Management
|
||||||
import createRole from './Role.js';
|
import createRole from './Role.js';
|
||||||
import createPermission from './Permission.js';
|
import createPermission from './Permission.js';
|
||||||
import createRolePermission from './RolePermission.js';
|
import createRolePermission from './RolePermission.js';
|
||||||
import createState from './State.js';
|
|
||||||
import createDistrict from './District.js';
|
|
||||||
import createArea from './Area.js';
|
|
||||||
import createUserRole from './UserRole.js';
|
import createUserRole from './UserRole.js';
|
||||||
import createZoneManager from './ZoneManager.js';
|
|
||||||
import createRegionManager from './RegionManager.js';
|
|
||||||
import createAreaManager from './AreaManager.js';
|
|
||||||
import createDistrictManager from './DistrictManager.js';
|
|
||||||
|
|
||||||
// Batch 2: Opportunity & Application Framework
|
// Batch 2: Opportunity & Application Framework
|
||||||
import createOpportunity from './Opportunity.js';
|
import createOpportunity from './Opportunity.js';
|
||||||
@ -121,26 +114,19 @@ db.AuditLog = createAuditLog(sequelize);
|
|||||||
db.FinancePayment = createFinancePayment(sequelize);
|
db.FinancePayment = createFinancePayment(sequelize);
|
||||||
db.FnF = createFnF(sequelize);
|
db.FnF = createFnF(sequelize);
|
||||||
db.FnFLineItem = createFnFLineItem(sequelize);
|
db.FnFLineItem = createFnFLineItem(sequelize);
|
||||||
db.Region = createRegion(sequelize);
|
|
||||||
db.Zone = createZone(sequelize);
|
|
||||||
db.SLAConfiguration = createSLAConfiguration(sequelize);
|
db.SLAConfiguration = createSLAConfiguration(sequelize);
|
||||||
db.SLAReminder = createSLAReminder(sequelize);
|
db.SLAReminder = createSLAReminder(sequelize);
|
||||||
db.SLAEscalationConfig = createSLAEscalationConfig(sequelize);
|
db.SLAEscalationConfig = createSLAEscalationConfig(sequelize);
|
||||||
db.WorkflowStageConfig = createWorkflowStageConfig(sequelize);
|
db.WorkflowStageConfig = createWorkflowStageConfig(sequelize);
|
||||||
db.Notification = createNotification(sequelize);
|
db.Notification = createNotification(sequelize);
|
||||||
|
db.Location = createLocation(sequelize);
|
||||||
|
db.LocationHierarchy = createLocationHierarchy(sequelize);
|
||||||
|
|
||||||
// Batch 1: Organizational Hierarchy & User Management
|
// Batch 1: Organizational Hierarchy & User Management
|
||||||
db.Role = createRole(sequelize);
|
db.Role = createRole(sequelize);
|
||||||
db.Permission = createPermission(sequelize);
|
db.Permission = createPermission(sequelize);
|
||||||
db.RolePermission = createRolePermission(sequelize);
|
db.RolePermission = createRolePermission(sequelize);
|
||||||
db.State = createState(sequelize);
|
|
||||||
db.District = createDistrict(sequelize);
|
|
||||||
db.Area = createArea(sequelize);
|
|
||||||
db.UserRole = createUserRole(sequelize);
|
db.UserRole = createUserRole(sequelize);
|
||||||
db.ZoneManager = createZoneManager(sequelize);
|
|
||||||
db.RegionManager = createRegionManager(sequelize);
|
|
||||||
db.AreaManager = createAreaManager(sequelize);
|
|
||||||
db.DistrictManager = createDistrictManager(sequelize);
|
|
||||||
|
|
||||||
// Batch 2: Opportunity & Application Framework
|
// Batch 2: Opportunity & Application Framework
|
||||||
db.Opportunity = createOpportunity(sequelize);
|
db.Opportunity = createOpportunity(sequelize);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
|
const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
|
||||||
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
||||||
@ -120,7 +121,45 @@ export const getPermissions = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const getAllUsers = async (req: Request, res: Response) => {
|
export const getAllUsers = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
const { roleCode, locationId } = req.query;
|
||||||
|
const whereClause: any = {};
|
||||||
|
|
||||||
|
if (roleCode) {
|
||||||
|
// Handle both single string and array of role codes (if passed as multiple params)
|
||||||
|
if (Array.isArray(roleCode)) {
|
||||||
|
whereClause.roleCode = { [Op.in]: roleCode };
|
||||||
|
} else {
|
||||||
|
whereClause.roleCode = roleCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationalRoles = ['NBH', 'DD Head', 'Super Admin'];
|
||||||
|
const isNationalRole = (typeof roleCode === 'string' && nationalRoles.includes(roleCode)) ||
|
||||||
|
(Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r)));
|
||||||
|
|
||||||
|
if (!isNationalRole && locationId) {
|
||||||
|
// Find all ancestors of the given location (BFS for many-to-many support)
|
||||||
|
let ancestorIds: Set<string> = new Set([locationId as string]);
|
||||||
|
let queue: string[] = [locationId as string];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const currentId = queue.shift()!;
|
||||||
|
const hierarchies = await db.LocationHierarchy.findAll({
|
||||||
|
where: { locationId: currentId }
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const h of hierarchies) {
|
||||||
|
if (!ancestorIds.has(h.parentId)) {
|
||||||
|
ancestorIds.add(h.parentId);
|
||||||
|
queue.push(h.parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
whereClause.locationId = { [Op.in]: Array.from(ancestorIds) };
|
||||||
|
}
|
||||||
|
|
||||||
const users = await User.findAll({
|
const users = await User.findAll({
|
||||||
|
where: whereClause,
|
||||||
attributes: { exclude: ['password'] },
|
attributes: { exclude: ['password'] },
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
@ -134,9 +173,7 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ model: db.Zone, as: 'zone' },
|
{ model: db.Location, as: 'location' }
|
||||||
{ model: db.Region, as: 'region' },
|
|
||||||
{ model: db.Area, as: 'area' }
|
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
@ -152,9 +189,10 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
const {
|
const {
|
||||||
fullName, email, roleCode,
|
fullName, email, roleCode,
|
||||||
employeeId, mobileNumber, department, designation,
|
employeeId, mobileNumber, department, designation,
|
||||||
zoneId, regionId, stateId, districtId, areaId
|
locationId
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!fullName || !email || !roleCode) {
|
if (!fullName || !email || !roleCode) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@ -198,11 +236,7 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
mobileNumber,
|
mobileNumber,
|
||||||
department,
|
department,
|
||||||
designation,
|
designation,
|
||||||
zoneId,
|
locationId
|
||||||
regionId,
|
|
||||||
stateId,
|
|
||||||
districtId,
|
|
||||||
areaId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await AuditLog.create({
|
await AuditLog.create({
|
||||||
@ -259,7 +293,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
const {
|
const {
|
||||||
fullName, email, roleCode, status, isActive, employeeId,
|
fullName, email, roleCode, status, isActive, employeeId,
|
||||||
mobileNumber, department, designation,
|
mobileNumber, department, designation,
|
||||||
zoneId, regionId, stateId, districtId, areaId,
|
locationId,
|
||||||
password // Optional password update
|
password // Optional password update
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
@ -277,11 +311,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
mobileNumber: mobileNumber || user.mobileNumber,
|
mobileNumber: mobileNumber || user.mobileNumber,
|
||||||
department: department || user.department,
|
department: department || user.department,
|
||||||
designation: designation || user.designation,
|
designation: designation || user.designation,
|
||||||
zoneId: zoneId !== undefined ? zoneId : user.zoneId,
|
locationId: (locationId === '' ? null : (locationId !== undefined ? locationId : user.locationId))
|
||||||
regionId: regionId !== undefined ? regionId : user.regionId,
|
|
||||||
stateId: stateId !== undefined ? stateId : user.stateId,
|
|
||||||
districtId: districtId !== undefined ? districtId : user.districtId,
|
|
||||||
areaId: areaId !== undefined ? areaId : user.areaId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// If password is provided, hash it and update
|
// If password is provided, hash it and update
|
||||||
@ -319,7 +349,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
export const generateDealerCode = async (req: AuthRequest, res: Response) => {
|
export const generateDealerCode = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { regionId, stateId, channel } = req.body;
|
const { locationId, channel } = req.body;
|
||||||
// Logic to generate unique code based on format (e.g., RE-[Region]-[State]-[Seq])
|
// Logic to generate unique code based on format (e.g., RE-[Region]-[State]-[Seq])
|
||||||
// This is a placeholder for the actual business logic
|
// This is a placeholder for the actual business logic
|
||||||
|
|
||||||
|
|||||||
@ -587,35 +587,34 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) =
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always mark interview as completed when a decision is made
|
// --- Multi-Interviewer Synchronization ---
|
||||||
// This ensures action buttons hide for the user
|
// Fetch all assigned participants for this interview
|
||||||
|
const participants = await db.InterviewParticipant.findAll({
|
||||||
|
where: { interviewId }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch all evaluations submitted for this interview
|
||||||
|
const evaluations = await db.InterviewEvaluation.findAll({
|
||||||
|
where: { interviewId }
|
||||||
|
});
|
||||||
|
|
||||||
|
const isFullyEvaluated = evaluations.length >= participants.length;
|
||||||
|
|
||||||
|
if (isFullyEvaluated) {
|
||||||
|
// All interviewers have responded
|
||||||
await interview.update({ status: 'Completed' });
|
await interview.update({ status: 'Completed' });
|
||||||
|
|
||||||
// Update Application Status
|
// Determine next status based on level
|
||||||
if (decision === 'Rejected') {
|
|
||||||
await db.Application.update({
|
|
||||||
overallStatus: 'Rejected',
|
|
||||||
currentStage: 'Rejected'
|
|
||||||
}, { where: { id: interview.applicationId } });
|
|
||||||
|
|
||||||
// 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 nextStatusMap: any = {
|
const nextStatusMap: any = {
|
||||||
1: 'Level 1 Approved',
|
1: 'Level 1 Approved',
|
||||||
2: 'Level 2 Approved',
|
2: 'Level 2 Approved',
|
||||||
3: 'Level 3 Approved'
|
3: 'Level 3 Approved'
|
||||||
};
|
};
|
||||||
const newStatus = nextStatusMap[interview.level] || 'Approved';
|
|
||||||
|
|
||||||
// Also update currentStage for better tracking
|
// Check if any interviewer rejected (for logging/metadata, though we still move forward as requested)
|
||||||
|
const hasRejection = evaluations.some((e: any) => e.decision === 'Rejected');
|
||||||
|
|
||||||
|
const newStatus = nextStatusMap[interview.level] || 'Approved';
|
||||||
const stageMapping: any = {
|
const stageMapping: any = {
|
||||||
1: 'Level 1 Approved',
|
1: 'Level 1 Approved',
|
||||||
2: 'Level 2 Approved',
|
2: 'Level 2 Approved',
|
||||||
@ -633,8 +632,13 @@ export const updateInterviewDecision = async (req: AuthRequest, res: Response) =
|
|||||||
previousStatus: 'Interview Pending',
|
previousStatus: 'Interview Pending',
|
||||||
newStatus: newStatus,
|
newStatus: newStatus,
|
||||||
changedBy: req.user?.id,
|
changedBy: req.user?.id,
|
||||||
reason: remarks || 'Interview Approved'
|
reason: hasRejection ? 'Interview completed with mixed recommendations' : 'Interview Approved by all'
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Still waiting for other interviewers
|
||||||
|
// We can mark the status as 'In Progress' or keep it as 'Scheduled'
|
||||||
|
// But we do NOT update the Application status yet.
|
||||||
|
console.log(`Interview ${interviewId}: Waiting for more evaluations. (${evaluations.length}/${participants.length})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.AuditLog.create({
|
await db.AuditLog.create({
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { AuthRequest } from '../../types/express.types.js';
|
|||||||
// Register new user
|
// Register new user
|
||||||
export const register = async (req: Request, res: Response) => {
|
export const register = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { email, password, fullName, role, phone, region, zone } = req.body;
|
const { email, password, fullName, role, phone, locationId } = req.body;
|
||||||
|
|
||||||
// Validate input
|
// Validate input
|
||||||
if (!email || !password || !fullName || !role) {
|
if (!email || !password || !fullName || !role) {
|
||||||
@ -38,8 +38,7 @@ export const register = async (req: Request, res: Response) => {
|
|||||||
fullName,
|
fullName,
|
||||||
roleCode: role,
|
roleCode: role,
|
||||||
mobileNumber: phone,
|
mobileNumber: phone,
|
||||||
regionId: region,
|
locationId,
|
||||||
zoneId: zone,
|
|
||||||
status: 'active'
|
status: 'active'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,8 +126,7 @@ export const login = async (req: Request, res: Response) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
fullName: user.fullName,
|
fullName: user.fullName,
|
||||||
role: user.roleCode,
|
role: user.roleCode,
|
||||||
region: user.regionId,
|
locationId: user.locationId
|
||||||
zone: user.zoneId
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -163,7 +161,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findByPk(req.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
attributes: ['id', 'email', 'fullName', 'roleCode', 'regionId', 'zoneId', 'mobileNumber', 'createdAt']
|
attributes: ['id', 'email', 'fullName', 'roleCode', 'locationId', 'mobileNumber', 'createdAt']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -180,8 +178,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
fullName: user.fullName,
|
fullName: user.fullName,
|
||||||
role: user.roleCode,
|
role: user.roleCode,
|
||||||
region: user.regionId,
|
locationId: user.locationId,
|
||||||
zone: user.zoneId,
|
|
||||||
phone: user.mobileNumber,
|
phone: user.mobileNumber,
|
||||||
createdAt: (user as any).createdAt
|
createdAt: (user as any).createdAt
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const {
|
|||||||
} = db;
|
} = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
export const getDealers = async (req: Request, res: Response) => {
|
export const getDealers = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@ -110,8 +111,7 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isExternal: true, // Dealers are external users
|
isExternal: true, // Dealers are external users
|
||||||
zoneId: application.zoneId,
|
locationId: application.locationId
|
||||||
regionId: application.regionId
|
|
||||||
});
|
});
|
||||||
console.log(`[Dealer Onboarding] Created new Dealer user account for ${user.email}.`);
|
console.log(`[Dealer Onboarding] Created new Dealer user account for ${user.email}.`);
|
||||||
}
|
}
|
||||||
@ -124,29 +124,18 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
|
|||||||
newData: { roleCode: 'Dealer', dealerId: dealer.id }
|
newData: { roleCode: 'Dealer', dealerId: dealer.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Create Primary Outlet for the Dealer ---
|
// --- Create or Link Outlet for this specific application ---
|
||||||
// Check if outlet already exists
|
// We use the application's unique ID to ensure we don't duplicate THE SAME application's outlet,
|
||||||
let outlet = await Outlet.findOne({ where: { dealerId: user.id } });
|
// but we allow multiple outlets for the same User ID.
|
||||||
|
let outlet = await Outlet.findOne({
|
||||||
|
where: {
|
||||||
|
dealerId: user.id,
|
||||||
|
name: { [Op.like]: `%${application.city || 'Primary'}%` }, // Rough check for same location
|
||||||
|
createdAt: { [Op.gte]: new Date(new Date().getTime() - 60000) } // Prevent double-clicks in same minute
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!outlet) {
|
if (!outlet) {
|
||||||
let regionName = 'Central';
|
|
||||||
let zoneName = 'National';
|
|
||||||
|
|
||||||
if (application.regionId) {
|
|
||||||
const reg = await Region.findByPk(application.regionId);
|
|
||||||
if (reg) regionName = reg.regionName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (application.zoneId) {
|
|
||||||
const zon = await Zone.findByPk(application.zoneId);
|
|
||||||
if (zon) zoneName = zon.zoneName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map region name to valid ENUM values if necessary
|
|
||||||
const validRegions = ['East', 'West', 'North', 'South', 'Central'];
|
|
||||||
if (!validRegions.includes(regionName)) {
|
|
||||||
regionName = 'Central'; // Fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId: application.id } });
|
const dealerCodeRecord = await DealerCode.findOne({ where: { applicationId: application.id } });
|
||||||
const outletCode = `OUT-${dealerCodeRecord?.dealerCode || Date.now().toString().slice(-6)}`;
|
const outletCode = `OUT-${dealerCodeRecord?.dealerCode || Date.now().toString().slice(-6)}`;
|
||||||
|
|
||||||
@ -161,11 +150,10 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
|
|||||||
status: 'Active',
|
status: 'Active',
|
||||||
establishedDate: new Date(),
|
establishedDate: new Date(),
|
||||||
dealerId: user.id,
|
dealerId: user.id,
|
||||||
region: regionName as any,
|
locationId: application.locationId
|
||||||
zone: zoneName
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[Dealer Onboarding] Created primary outlet ${outlet.code} for dealer ${user.email}.`);
|
console.log(`[Dealer Onboarding] Created outlet ${outlet.code} for application ${application.applicationId} linked to user ${user.email}.`);
|
||||||
|
|
||||||
await AuditLog.create({
|
await AuditLog.create({
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
|
|||||||
@ -1,60 +1,56 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Region, Zone, State, District, Area, User, AreaManager } = db;
|
const { User } = db;
|
||||||
|
|
||||||
|
// --- Generic Location Fetching ---
|
||||||
|
const getLocationsByType = async (type: string, req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const locations = await db.Location.findAll({
|
||||||
|
where: { type },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.Location,
|
||||||
|
as: 'parents',
|
||||||
|
through: { attributes: [] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: db.Location,
|
||||||
|
as: 'children',
|
||||||
|
through: { attributes: [] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [['name', 'ASC']]
|
||||||
|
});
|
||||||
|
res.json({ success: true, data: locations });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Get ${type} list error:`, error);
|
||||||
|
res.status(500).json({ success: false, message: `Error fetching ${type} list` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- Regions ---
|
// --- Regions ---
|
||||||
export const getRegions = async (req: Request, res: Response) => {
|
export const getRegions = async (req: Request, res: Response) => {
|
||||||
try {
|
return getLocationsByType('region', req, res);
|
||||||
const regions = await Region.findAll({
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: State,
|
|
||||||
as: 'states',
|
|
||||||
attributes: ['id', 'stateName']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Zone,
|
|
||||||
as: 'zone',
|
|
||||||
attributes: ['id', 'zoneName']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: 'regionalManager',
|
|
||||||
attributes: ['id', 'fullName', 'email', 'mobileNumber']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: [['regionName', 'ASC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ success: true, data: regions });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get regions error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error fetching regions' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createRegion = async (req: Request, res: Response) => {
|
export const createRegion = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { zoneId, regionCode, regionName, description, stateIds, regionalManagerId } = req.body;
|
const { zoneId, regionName } = req.body;
|
||||||
|
|
||||||
if (!zoneId || !regionName || !regionCode) {
|
if (!regionName) {
|
||||||
return res.status(400).json({ success: false, message: 'Zone ID, region name and code are required' });
|
return res.status(400).json({ success: false, message: 'Region name is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const region = await Region.create({
|
const region = await db.Location.create({
|
||||||
zoneId,
|
name: regionName,
|
||||||
regionCode,
|
type: 'region'
|
||||||
regionName,
|
|
||||||
description,
|
|
||||||
regionalManagerId: regionalManagerId || null
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assign states if provided
|
if (zoneId) {
|
||||||
if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) {
|
await db.LocationHierarchy.create({
|
||||||
await State.update(
|
locationId: region.id,
|
||||||
{ regionId: region.id, zoneId }, // Also ensure State belongs to the Zone (hierarchy)
|
parentId: zoneId
|
||||||
{ where: { id: stateIds } }
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, message: 'Region created successfully', data: region });
|
res.status(201).json({ success: true, message: 'Region created successfully', data: region });
|
||||||
@ -64,122 +60,31 @@ export const createRegion = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateRegion = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { zoneId, regionCode, regionName, description, isActive, stateIds, regionalManagerId } = req.body;
|
|
||||||
|
|
||||||
const region = await Region.findByPk(id);
|
|
||||||
if (!region) {
|
|
||||||
return res.status(404).json({ success: false, message: 'Region not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const updates: any = {};
|
|
||||||
if (zoneId) updates.zoneId = zoneId;
|
|
||||||
if (regionCode) updates.regionCode = regionCode;
|
|
||||||
if (regionName) updates.regionName = regionName;
|
|
||||||
if (description !== undefined) updates.description = description;
|
|
||||||
if (isActive !== undefined) updates.isActive = isActive;
|
|
||||||
if (regionalManagerId !== undefined) updates.regionalManagerId = regionalManagerId;
|
|
||||||
|
|
||||||
await region.update(updates);
|
|
||||||
|
|
||||||
// Handle State reassignment
|
|
||||||
if (stateIds && Array.isArray(stateIds)) {
|
|
||||||
// 1. Unassign states currently assigned to this region but NOT in the new list?
|
|
||||||
// Or just simpler: Assign the new ones. Old ones stay?
|
|
||||||
// Standard behavior for "List of items in a container": Sync list.
|
|
||||||
// We should set regionId=null for states previously in this region but not in stateIds.
|
|
||||||
// But let's check safety. If I uncheck a state, I want it removed from the region.
|
|
||||||
|
|
||||||
// First, find states currently in this region
|
|
||||||
// Actually, simplest 'Reset and Set' approach:
|
|
||||||
// 1. Set regionId=null for all states where regionId = this.id
|
|
||||||
// 2. Set regionId=this.id for states in stateIds.
|
|
||||||
|
|
||||||
// Note: We should probably also enforce zoneId match?
|
|
||||||
// If a user moves a state to this Region, the State must conceptually belong to the Region's Zone.
|
|
||||||
// So we update both regionId and zoneId for the target states.
|
|
||||||
|
|
||||||
// Step 1: Remove States from this Region (if they are NOT in the new list)
|
|
||||||
// We can do this by:
|
|
||||||
// await State.update({ regionId: null }, { where: { regionId: id } });
|
|
||||||
// But wait, if I am only ADDING, I don't want to nuke everything.
|
|
||||||
// But "update" implies "this is the new state of the world".
|
|
||||||
// Assuming frontend sends the FULL list of selected states.
|
|
||||||
|
|
||||||
await State.update({ regionId: null }, { where: { regionId: id } });
|
|
||||||
|
|
||||||
if (stateIds.length > 0) {
|
|
||||||
await State.update(
|
|
||||||
{
|
|
||||||
regionId: id,
|
|
||||||
zoneId: zoneId || region.zoneId // Ensure state moves to the region's zone
|
|
||||||
},
|
|
||||||
{ where: { id: stateIds } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, message: 'Region updated successfully' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update region error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error updating region' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Zones ---
|
// --- Zones ---
|
||||||
export const getZones = async (req: Request, res: Response) => {
|
export const getZones = async (req: Request, res: Response) => {
|
||||||
try {
|
return getLocationsByType('zone', req, res);
|
||||||
const { regionId } = req.query as { regionId?: string };
|
|
||||||
|
|
||||||
const where: any = {};
|
|
||||||
if (regionId) {
|
|
||||||
where.regionId = regionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const zones = await Zone.findAll({
|
|
||||||
where,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Region,
|
|
||||||
as: 'regions',
|
|
||||||
attributes: ['regionName']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: 'zonalBusinessHead',
|
|
||||||
attributes: ['fullName', 'email', 'mobileNumber']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: State,
|
|
||||||
as: 'states',
|
|
||||||
attributes: ['stateName']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: [['zoneName', 'ASC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ success: true, data: zones });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get zones error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error fetching zones' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createZone = async (req: Request, res: Response) => {
|
export const createZone = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { regionId, zoneName } = req.body;
|
const { regionId, zoneName } = req.body;
|
||||||
|
|
||||||
if (!regionId || !zoneName) {
|
if (!zoneName) {
|
||||||
return res.status(400).json({ success: false, message: 'Region ID and zone name are required' });
|
return res.status(400).json({ success: false, message: 'Zone name is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const zone = await Zone.create({
|
const zone = await db.Location.create({
|
||||||
regionId, // Wait, Zone Model doesn't have regionId. It's the other way around?
|
name: zoneName,
|
||||||
zoneName
|
type: 'zone'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (regionId) {
|
||||||
|
await db.LocationHierarchy.create({
|
||||||
|
locationId: zone.id,
|
||||||
|
parentId: regionId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, message: 'Zone created successfully', data: zone });
|
res.status(201).json({ success: true, message: 'Zone created successfully', data: zone });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create zone error:', error);
|
console.error('Create zone error:', error);
|
||||||
@ -187,69 +92,53 @@ export const createZone = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateZone = async (req: Request, res: Response) => {
|
export const updateLocation = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { zoneName, description, isActive, zonalBusinessHeadId, stateIds } = req.body;
|
const { name, type, parentIds } = req.body;
|
||||||
|
|
||||||
const zone = await Zone.findByPk(id);
|
const location = await db.Location.findByPk(id);
|
||||||
if (!zone) {
|
if (!location) {
|
||||||
return res.status(404).json({ success: false, message: 'Zone not found' });
|
return res.status(404).json({ success: false, message: 'Location not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates: any = {};
|
const updates: any = {};
|
||||||
if (zoneName) updates.zoneName = zoneName;
|
if (name) updates.name = name;
|
||||||
if (description !== undefined) updates.description = description;
|
if (type) updates.type = type;
|
||||||
if (isActive !== undefined) updates.isActive = isActive;
|
|
||||||
if (zonalBusinessHeadId !== undefined) updates.zonalBusinessHeadId = zonalBusinessHeadId;
|
|
||||||
|
|
||||||
await zone.update(updates);
|
await location.update(updates);
|
||||||
|
|
||||||
// Handle State assignment
|
if (parentIds && Array.isArray(parentIds)) {
|
||||||
if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) {
|
// Re-sync parents
|
||||||
// Update all provided states to belong to this zone
|
await db.LocationHierarchy.destroy({ where: { locationId: id } });
|
||||||
// We can't easily "remove" states because zoneId is non-nullable.
|
for (const pid of parentIds) {
|
||||||
// States must be moved TO another zone to be removed from this one.
|
await db.LocationHierarchy.create({ locationId: id, parentId: pid });
|
||||||
// So we primarily handle "bringing states into this zone".
|
}
|
||||||
// However, we should check if they exist first.
|
|
||||||
await State.update(
|
|
||||||
{ zoneId: zone.id },
|
|
||||||
{ where: { id: stateIds } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, message: 'Zone updated successfully' });
|
res.json({ success: true, message: 'Location updated successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update zone error:', error);
|
console.error('Update location error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error updating zone' });
|
res.status(500).json({ success: false, message: 'Error updating location' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- States ---
|
// --- States ---
|
||||||
export const getStates = async (req: Request, res: Response) => {
|
export const getStates = async (req: Request, res: Response) => {
|
||||||
try {
|
return getLocationsByType('state', req, res);
|
||||||
const { zoneId } = req.query as { zoneId?: string };
|
|
||||||
const where: any = {};
|
|
||||||
if (zoneId) where.zoneId = zoneId;
|
|
||||||
|
|
||||||
const states = await State.findAll({
|
|
||||||
where,
|
|
||||||
include: [{ model: Zone, as: 'zone', attributes: ['zoneName'] }],
|
|
||||||
order: [['stateName', 'ASC']]
|
|
||||||
});
|
|
||||||
res.json({ success: true, states });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get states error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error fetching states' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createState = async (req: Request, res: Response) => {
|
export const createState = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { zoneId, stateName } = req.body;
|
const { zoneId, stateName } = req.body;
|
||||||
if (!zoneId || !stateName) return res.status(400).json({ success: false, message: 'Zone ID and state name required' });
|
if (!stateName) return res.status(400).json({ success: false, message: 'State name is required' });
|
||||||
|
|
||||||
|
const state = await db.Location.create({ name: stateName, type: 'state' });
|
||||||
|
|
||||||
|
if (zoneId) {
|
||||||
|
await db.LocationHierarchy.create({ locationId: state.id, parentId: zoneId });
|
||||||
|
}
|
||||||
|
|
||||||
const state = await State.create({ zoneId, stateName });
|
|
||||||
res.status(201).json({ success: true, message: 'State created', data: state });
|
res.status(201).json({ success: true, message: 'State created', data: state });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create state error:', error);
|
console.error('Create state error:', error);
|
||||||
@ -257,46 +146,22 @@ export const createState = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateState = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { stateName, isActive } = req.body;
|
|
||||||
const state = await State.findByPk(id);
|
|
||||||
if (!state) return res.status(404).json({ success: false, message: 'State not found' });
|
|
||||||
|
|
||||||
await state.update({ stateName, isActive });
|
|
||||||
res.json({ success: true, message: 'State updated' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update state error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error updating state' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Districts ---
|
// --- Districts ---
|
||||||
export const getDistricts = async (req: Request, res: Response) => {
|
export const getDistricts = async (req: Request, res: Response) => {
|
||||||
try {
|
return getLocationsByType('district', req, res);
|
||||||
const { stateId } = req.query as { stateId?: string };
|
|
||||||
const where: any = {};
|
|
||||||
if (stateId) where.stateId = stateId;
|
|
||||||
|
|
||||||
const districts = await District.findAll({
|
|
||||||
where,
|
|
||||||
include: [{ model: State, as: 'state', attributes: ['stateName'] }],
|
|
||||||
order: [['districtName', 'ASC']]
|
|
||||||
});
|
|
||||||
res.json({ success: true, districts });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get districts error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error fetching districts' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDistrict = async (req: Request, res: Response) => {
|
export const createDistrict = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { stateId, districtName } = req.body;
|
const { stateId, districtName } = req.body;
|
||||||
if (!stateId || !districtName) return res.status(400).json({ success: false, message: 'State ID and district name required' });
|
if (!districtName) return res.status(400).json({ success: false, message: 'District name is required' });
|
||||||
|
|
||||||
|
const district = await db.Location.create({ name: districtName, type: 'district' });
|
||||||
|
|
||||||
|
if (stateId) {
|
||||||
|
await db.LocationHierarchy.create({ locationId: district.id, parentId: stateId });
|
||||||
|
}
|
||||||
|
|
||||||
const district = await District.create({ stateId, districtName });
|
|
||||||
res.status(201).json({ success: true, message: 'District created', data: district });
|
res.status(201).json({ success: true, message: 'District created', data: district });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create district error:', error);
|
console.error('Create district error:', error);
|
||||||
@ -304,121 +169,20 @@ export const createDistrict = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDistrict = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { districtName, isActive } = req.body;
|
|
||||||
const district = await District.findByPk(id);
|
|
||||||
if (!district) return res.status(404).json({ success: false, message: 'District not found' });
|
|
||||||
|
|
||||||
await district.update({ districtName, isActive });
|
|
||||||
res.json({ success: true, message: 'District updated' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Update district error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error updating district' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Areas ---
|
// --- Areas ---
|
||||||
export const getAreas = async (req: Request, res: Response) => {
|
export const getAreas = async (req: Request, res: Response) => {
|
||||||
try {
|
return getLocationsByType('area', req, res);
|
||||||
const { districtId } = req.query as { districtId?: string };
|
|
||||||
const where: any = {};
|
|
||||||
if (districtId) where.districtId = districtId;
|
|
||||||
|
|
||||||
const areas = await Area.findAll({
|
|
||||||
where,
|
|
||||||
include: [
|
|
||||||
{ model: District, as: 'district', attributes: ['districtName'] },
|
|
||||||
{ model: State, as: 'state', attributes: ['stateName'] },
|
|
||||||
{ model: Region, as: 'region', attributes: ['regionName'] },
|
|
||||||
{ model: Zone, as: 'zone', attributes: ['zoneName'] },
|
|
||||||
// Include explicit manager column (legacy/fallback)
|
|
||||||
{ model: User, as: 'manager', attributes: ['id', 'fullName', 'email', 'mobileNumber'] },
|
|
||||||
// Include active managers from dedicated table
|
|
||||||
{
|
|
||||||
model: AreaManager,
|
|
||||||
as: 'areaManagers',
|
|
||||||
where: { isActive: true },
|
|
||||||
required: false, // Left join, so we get areas even without managers
|
|
||||||
include: [{
|
|
||||||
model: User,
|
|
||||||
as: 'user',
|
|
||||||
attributes: ['id', 'fullName', 'email', 'mobileNumber']
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
order: [['areaName', 'ASC']]
|
|
||||||
});
|
|
||||||
res.json({ success: true, areas });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get areas error:', error);
|
|
||||||
res.status(500).json({ success: false, message: 'Error fetching areas' });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createArea = async (req: Request, res: Response) => {
|
export const createArea = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { districtId, areaCode, areaName, city, pincode, managerId } = req.body;
|
const { districtId, areaName } = req.body;
|
||||||
if (!districtId || !areaName || !pincode) return res.status(400).json({ success: false, message: 'District ID, area name, and pincode required' });
|
if (!areaName) return res.status(400).json({ success: false, message: 'Area name is required' });
|
||||||
|
|
||||||
// Need to fetch regionId from district -> state -> zone -> region?
|
const area = await db.Location.create({ name: areaName, type: 'area' });
|
||||||
// Or user provides it?
|
|
||||||
// The Area model has regionId, districtId.
|
|
||||||
// It's safer to fetch relationships.
|
|
||||||
const district = await District.findByPk(districtId, {
|
|
||||||
include: [{
|
|
||||||
model: State,
|
|
||||||
as: 'state',
|
|
||||||
include: [
|
|
||||||
{ model: Zone, as: 'zone' },
|
|
||||||
{ model: Region, as: 'region' }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
let regionId = null;
|
if (districtId) {
|
||||||
let zoneId = null;
|
await db.LocationHierarchy.create({ locationId: area.id, parentId: districtId });
|
||||||
let stateId = null;
|
|
||||||
|
|
||||||
if (district) {
|
|
||||||
stateId = district.stateId;
|
|
||||||
// Access associations using the logical structure (District -> State -> Zone/Region)
|
|
||||||
if (district.state) {
|
|
||||||
if (district.state.zone) {
|
|
||||||
zoneId = district.state.zone.id;
|
|
||||||
}
|
|
||||||
if (district.state.region) {
|
|
||||||
regionId = district.state.region.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const area = await Area.create({
|
|
||||||
districtId,
|
|
||||||
stateId,
|
|
||||||
zoneId,
|
|
||||||
regionId,
|
|
||||||
areaCode,
|
|
||||||
areaName,
|
|
||||||
city,
|
|
||||||
pincode,
|
|
||||||
managerId: managerId || null, // Legacy support
|
|
||||||
isActive: req.body.isActive ?? true,
|
|
||||||
activeFrom: req.body.activeFrom || null,
|
|
||||||
activeTo: req.body.activeTo || null
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create AreaManager record if manager assigned
|
|
||||||
if (managerId) {
|
|
||||||
await AreaManager.create({
|
|
||||||
areaId: area.id,
|
|
||||||
userId: managerId,
|
|
||||||
managerType: 'ASM',
|
|
||||||
isActive: true,
|
|
||||||
assignedAt: new Date(),
|
|
||||||
asmCode: req.body.asmCode || null
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, message: 'Area created', data: area });
|
res.status(201).json({ success: true, message: 'Area created', data: area });
|
||||||
@ -428,144 +192,44 @@ export const createArea = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Area Managers ---
|
// --- Managers (Consolidated) ---
|
||||||
export const getAreaManagers = async (req: Request, res: Response) => {
|
export const getManagersByRole = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Fetch Users who have active AreaManager assignments
|
const { roleCode, locationId } = req.query as any;
|
||||||
// We use the User model as the primary so we get the User details naturally
|
const where: any = {};
|
||||||
|
if (roleCode) where.roleCode = roleCode;
|
||||||
|
if (locationId) where.locationId = locationId;
|
||||||
|
|
||||||
const managers = await User.findAll({
|
const managers = await User.findAll({
|
||||||
attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId', 'roleCode', 'zoneId', 'regionId'],
|
where,
|
||||||
include: [
|
attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode', 'locationId'],
|
||||||
{
|
|
||||||
model: AreaManager,
|
|
||||||
as: 'areaManagers',
|
|
||||||
where: { isActive: true },
|
|
||||||
required: true, // Only return users who ARE active managers
|
|
||||||
attributes: ['asmCode'],
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Area,
|
|
||||||
as: 'area',
|
|
||||||
attributes: ['id', 'areaName', 'areaCode'],
|
|
||||||
include: [
|
|
||||||
{ model: District, as: 'district', attributes: ['districtName'] },
|
|
||||||
{ model: State, as: 'state', attributes: ['stateName'] },
|
|
||||||
{ model: Region, as: 'region', attributes: ['id', 'regionName'] },
|
|
||||||
{ model: Zone, as: 'zone', attributes: ['id', 'zoneName'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ model: Zone, as: 'zone', attributes: ['id', 'zoneName'] },
|
|
||||||
{ model: Region, as: 'region', attributes: ['id', 'regionName'] }
|
|
||||||
],
|
|
||||||
order: [['fullName', 'ASC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transform if necessary to flatten the structure for the frontend
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
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' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateArea = 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: [{
|
include: [{
|
||||||
model: State,
|
model: db.Location,
|
||||||
as: 'state',
|
as: 'location',
|
||||||
include: [
|
attributes: ['id', 'name', 'type']
|
||||||
{ model: Zone, as: 'zone' },
|
|
||||||
{ model: Region, as: 'region' }
|
|
||||||
]
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (district) {
|
res.json({ success: true, data: managers });
|
||||||
updates.stateId = district.stateId;
|
|
||||||
if (district.state) {
|
|
||||||
if (district.state.zone) {
|
|
||||||
updates.zoneId = district.state.zone.id;
|
|
||||||
}
|
|
||||||
if (district.state.region) {
|
|
||||||
updates.regionId = district.state.region.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await area.update(updates);
|
|
||||||
|
|
||||||
// Handle AreaManager Table Update
|
|
||||||
if (managerId !== undefined) {
|
|
||||||
const asmCode = req.body.asmCode;
|
|
||||||
|
|
||||||
// 1. Find currently active manager for this area
|
|
||||||
const currentActiveManager = await AreaManager.findOne({
|
|
||||||
where: {
|
|
||||||
areaId: id,
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there is an active manager
|
|
||||||
if (currentActiveManager) {
|
|
||||||
// If the new managerId is different (or null, meaning unassign), deactivate the old one
|
|
||||||
if (currentActiveManager.userId !== managerId) {
|
|
||||||
await currentActiveManager.update({ isActive: false });
|
|
||||||
} else {
|
|
||||||
// If SAME user, update asmCode if provided
|
|
||||||
if (asmCode !== undefined) {
|
|
||||||
await currentActiveManager.update({ asmCode });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. If a new manager is being assigned (and it's not null)
|
|
||||||
if (managerId) {
|
|
||||||
// Check if this specific user is already active (to avoid duplicates if logic above missed it)
|
|
||||||
const isAlreadyActive = currentActiveManager && currentActiveManager.userId === managerId;
|
|
||||||
|
|
||||||
if (!isAlreadyActive) {
|
|
||||||
await AreaManager.create({
|
|
||||||
areaId: id,
|
|
||||||
userId: managerId,
|
|
||||||
managerType: 'ASM', // Default type
|
|
||||||
isActive: true,
|
|
||||||
assignedAt: new Date(),
|
|
||||||
asmCode: asmCode || null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true, message: 'Area updated successfully' });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update area error:', error);
|
console.error('Get managers error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error updating area' });
|
res.status(500).json({ success: false, message: 'Error fetching managers' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
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('Delete location error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error deleting location' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,25 +17,26 @@ router.use(authenticate as any);
|
|||||||
// Regions
|
// Regions
|
||||||
router.get('/regions', masterController.getRegions);
|
router.get('/regions', masterController.getRegions);
|
||||||
router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createRegion);
|
router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createRegion);
|
||||||
router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateRegion);
|
router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
|
||||||
|
|
||||||
// Zones
|
// Zones
|
||||||
router.get('/zones', masterController.getZones);
|
router.get('/zones', masterController.getZones);
|
||||||
router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createZone);
|
router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createZone);
|
||||||
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateZone);
|
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
|
||||||
|
|
||||||
// States (Update only)
|
// States (Update only)
|
||||||
router.post('/states', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createState);
|
router.post('/states', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createState);
|
||||||
router.put('/states/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateState);
|
router.put('/states/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
|
||||||
|
|
||||||
// Districts (Update only)
|
// Districts (Update only)
|
||||||
router.post('/districts', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createDistrict);
|
router.post('/districts', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createDistrict);
|
||||||
router.put('/districts/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateDistrict);
|
router.put('/districts/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
|
||||||
|
|
||||||
// Areas
|
// Areas
|
||||||
router.get('/areas', masterController.getAreas);
|
router.get('/areas', masterController.getAreas);
|
||||||
router.post('/areas', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createArea);
|
router.post('/areas', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createArea);
|
||||||
router.put('/areas/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateArea);
|
router.put('/areas/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
|
||||||
|
router.delete('/locations/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.deleteLocation);
|
||||||
|
|
||||||
// Area Managers
|
// Area Managers
|
||||||
router.get('/area-managers', masterController.getAreaManagers);
|
router.get('/area-managers', masterController.getAreaManagers);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, Region, Zone, Area } = db;
|
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, Location } = db;
|
||||||
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
@ -17,72 +17,44 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode
|
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Check for duplicate email
|
// Check for duplicate application for SAME location
|
||||||
const existingApp = await Application.findOne({ where: { email } });
|
const existingApp = await Application.findOne({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
city: city || null,
|
||||||
|
preferredLocation: preferredLocation || null,
|
||||||
|
overallStatus: { [Op.ne]: 'Rejected' } // Don't block if previous was rejected
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (existingApp) {
|
if (existingApp) {
|
||||||
return res.status(400).json({ success: false, message: 'Application with this email already exists' });
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'An active application for this location already exists with this email address.'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
|
const applicationId = `APP-${new Date().getFullYear()}-${uuidv4().substring(0, 6).toUpperCase()}`;
|
||||||
|
|
||||||
// Fetch hierarchy from Auto-detected Area
|
// Fetch hierarchy from Auto-detected Location
|
||||||
let zoneId, regionId, areaId;
|
let locationId = null;
|
||||||
let isOpportunityAvailable = false;
|
let isOpportunityAvailable = false;
|
||||||
|
|
||||||
// Auto-detect Area from District
|
// Auto-detect Location from District
|
||||||
if (req.body.district) {
|
if (req.body.district) {
|
||||||
const districtName = req.body.district;
|
const districtName = req.body.district;
|
||||||
|
|
||||||
// 1. Find District ID by Name
|
// Find Location (type: district) match
|
||||||
const districtRecord = await District.findOne({
|
const districtRecord = await Location.findOne({
|
||||||
where: { districtName: { [Op.iLike]: districtName } }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (districtRecord) {
|
|
||||||
// 2. Find Active Area for this District
|
|
||||||
const today = new Date();
|
|
||||||
const validArea = await Area.findOne({
|
|
||||||
where: {
|
where: {
|
||||||
districtId: districtRecord.id,
|
name: { [Op.iLike]: districtName },
|
||||||
isActive: true,
|
type: 'district'
|
||||||
[Op.and]: [
|
|
||||||
{
|
|
||||||
[Op.or]: [
|
|
||||||
{ activeFrom: { [Op.eq]: null } },
|
|
||||||
{ activeFrom: { [Op.lte]: today } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[Op.or]: [
|
|
||||||
{ activeTo: { [Op.eq]: null } },
|
|
||||||
{ activeTo: { [Op.gte]: today } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (validArea) {
|
|
||||||
areaId = validArea.id;
|
|
||||||
zoneId = validArea.zoneId;
|
|
||||||
regionId = validArea.regionId;
|
|
||||||
isOpportunityAvailable = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine Initial Status
|
|
||||||
let initialStatus = isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED;
|
|
||||||
|
|
||||||
// Auto-assign Zone/Region from District if still null (even if no opportunity found)
|
|
||||||
if (!zoneId && req.body.district) {
|
|
||||||
const districtRecord = await District.findOne({
|
|
||||||
where: { districtName: { [Op.iLike]: req.body.district } },
|
|
||||||
include: [{ model: Region, as: 'region' }, { model: Zone, as: 'zone' }]
|
|
||||||
});
|
|
||||||
if (districtRecord) {
|
if (districtRecord) {
|
||||||
regionId = districtRecord.regionId;
|
locationId = districtRecord.id;
|
||||||
zoneId = districtRecord.zoneId;
|
isOpportunityAvailable = true; // For now, assume if district exists, it's an opportunity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,18 +72,16 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
investmentCapacity,
|
investmentCapacity,
|
||||||
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
|
age, education, companyName, source, existingDealer, ownRoyalEnfield, royalEnfieldModel, description, address, pincode, locationType,
|
||||||
currentStage: APPLICATION_STAGES.DD,
|
currentStage: APPLICATION_STAGES.DD,
|
||||||
overallStatus: initialStatus,
|
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
|
||||||
progressPercentage: isOpportunityAvailable ? 10 : 0,
|
progressPercentage: isOpportunityAvailable ? 10 : 0,
|
||||||
zoneId,
|
locationId
|
||||||
regionId,
|
|
||||||
areaId // Link to Area
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log Status History
|
// Log Status History
|
||||||
await ApplicationStatusHistory.create({
|
await ApplicationStatusHistory.create({
|
||||||
applicationId: application.id,
|
applicationId: application.id,
|
||||||
previousStatus: null,
|
previousStatus: null,
|
||||||
newStatus: initialStatus,
|
newStatus: application.overallStatus,
|
||||||
changedBy: req.user?.id || null,
|
changedBy: req.user?.id || null,
|
||||||
reason: 'Initial Submission'
|
reason: 'Initial Submission'
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,12 +7,11 @@ import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
|||||||
|
|
||||||
export const getOpportunities = async (req: Request, res: Response) => {
|
export const getOpportunities = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { status, regionId, zoneId } = req.query as any;
|
const { status, locationId } = req.query as any;
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
|
|
||||||
if (status) where.status = status;
|
if (status) where.status = status;
|
||||||
if (regionId) where.regionId = regionId;
|
if (locationId) where.locationId = locationId;
|
||||||
if (zoneId) where.zoneId = zoneId;
|
|
||||||
|
|
||||||
const opportunities = await Opportunity.findAll({
|
const opportunities = await Opportunity.findAll({
|
||||||
where,
|
where,
|
||||||
@ -33,7 +32,7 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
leadSource, leadName, contactNumber, email,
|
leadSource, leadName, contactNumber, email,
|
||||||
zoneId, regionId, stateId, districtId,
|
locationId,
|
||||||
opportunityType, priority
|
opportunityType, priority
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
@ -42,10 +41,7 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => {
|
|||||||
leadName,
|
leadName,
|
||||||
contactNumber,
|
contactNumber,
|
||||||
email,
|
email,
|
||||||
zoneId,
|
locationId,
|
||||||
regionId,
|
|
||||||
stateId,
|
|
||||||
districtId,
|
|
||||||
opportunityType,
|
opportunityType,
|
||||||
priority,
|
priority,
|
||||||
status: 'New',
|
status: 'New',
|
||||||
|
|||||||
@ -116,6 +116,17 @@ const seedQuestionnaire = async () => {
|
|||||||
weight: 0,
|
weight: 0,
|
||||||
order: 7
|
order: 7
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "Are you an existing dealer/vendor of Royal Enfield?",
|
||||||
|
type: "radio",
|
||||||
|
section: "Basic Information",
|
||||||
|
options: [
|
||||||
|
{ text: "Yes", score: 0 },
|
||||||
|
{ text: "No", score: 0 }
|
||||||
|
],
|
||||||
|
weight: 0,
|
||||||
|
order: 8
|
||||||
|
},
|
||||||
|
|
||||||
// Section 2: Profile & Background (Scoring Starts)
|
// Section 2: Profile & Background (Scoring Starts)
|
||||||
{
|
{
|
||||||
|
|||||||
169
src/scripts/test_multi_outlet.ts
Normal file
169
src/scripts/test_multi_outlet.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import db from '../database/models/index.js';
|
||||||
|
const { Application, User, Outlet, Dealer, DealerCode } = db;
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
async function test() {
|
||||||
|
console.log('--- Starting Multi-Outlet Support Test ---');
|
||||||
|
|
||||||
|
const email = `test_dealer_multi_${Date.now()}@example.com`;
|
||||||
|
const phone = '9876543210';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Initial Application for City X
|
||||||
|
console.log('1. Submitting first application for City X...');
|
||||||
|
const app1 = await Application.create({
|
||||||
|
applicationId: `APP-X-${Date.now()}`,
|
||||||
|
applicantName: 'Test Dealer',
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
businessType: 'Dealership',
|
||||||
|
city: 'City X',
|
||||||
|
preferredLocation: 'Location X',
|
||||||
|
overallStatus: 'Pending',
|
||||||
|
currentStage: 'DD',
|
||||||
|
zoneId: null,
|
||||||
|
regionId: null
|
||||||
|
});
|
||||||
|
console.log('Success: Application 1 created.');
|
||||||
|
|
||||||
|
// 2. Duplicate Application for SAME Location
|
||||||
|
console.log('2. Attempting duplicate application for City X...');
|
||||||
|
const existingApp = await Application.findOne({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
city: 'City X',
|
||||||
|
preferredLocation: 'Location X',
|
||||||
|
overallStatus: { [Op.ne]: 'Rejected' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingApp) {
|
||||||
|
console.log('Correct: Detected duplicate application for same location.');
|
||||||
|
} else {
|
||||||
|
console.error('Error: Failed to detect duplicate location!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. New Application for DIFFERENT Location (City Y)
|
||||||
|
console.log('3. Submitting application for City Y...');
|
||||||
|
const app2 = await Application.create({
|
||||||
|
applicationId: `APP-Y-${Date.now()}`,
|
||||||
|
applicantName: 'Test Dealer',
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
businessType: 'Dealership',
|
||||||
|
city: 'City Y',
|
||||||
|
preferredLocation: 'Location Y',
|
||||||
|
overallStatus: 'Pending',
|
||||||
|
currentStage: 'DD',
|
||||||
|
zoneId: null,
|
||||||
|
regionId: null
|
||||||
|
});
|
||||||
|
console.log('Success: Application 2 created for different location.');
|
||||||
|
|
||||||
|
// 4. Onboard Application 1 (Simulated step)
|
||||||
|
console.log('4. Onboarding Application 1...');
|
||||||
|
const dCode1 = await DealerCode.create({
|
||||||
|
dealerCode: `CODE-X-${Date.now()}`,
|
||||||
|
applicationId: app1.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const dealer1 = await Dealer.create({
|
||||||
|
applicationId: app1.id,
|
||||||
|
dealerCodeId: dCode1.id,
|
||||||
|
legalName: 'Test Dealer X Ltd',
|
||||||
|
businessName: 'Test Dealer - X',
|
||||||
|
constitutionType: 'Partnership',
|
||||||
|
status: 'Active'
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
fullName: 'Test Dealer',
|
||||||
|
email,
|
||||||
|
password: 'hashed_password',
|
||||||
|
roleCode: 'Dealer',
|
||||||
|
dealerId: dealer1.id,
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Current Logic in dealer.controller.ts for outlet 1
|
||||||
|
const outlet1 = await Outlet.create({
|
||||||
|
code: `OUT-X-${Date.now()}`,
|
||||||
|
name: `${app1.applicantName} - ${app1.city}`,
|
||||||
|
type: 'Dealership',
|
||||||
|
address: 'Address X',
|
||||||
|
city: 'City X',
|
||||||
|
state: 'State X',
|
||||||
|
pincode: '123456',
|
||||||
|
status: 'Active',
|
||||||
|
establishedDate: new Date(),
|
||||||
|
dealerId: user.id,
|
||||||
|
region: 'Central',
|
||||||
|
zone: 'National'
|
||||||
|
});
|
||||||
|
console.log('Success: Application 1 onboarded.');
|
||||||
|
|
||||||
|
// 5. Onboard Application 2 (Simulation of our fix)
|
||||||
|
console.log('5. Onboarding Application 2 (sharing same user)...');
|
||||||
|
const dCode2 = await DealerCode.create({
|
||||||
|
dealerCode: `CODE-Y-${Date.now()}`,
|
||||||
|
applicationId: app2.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const dealer2 = await Dealer.create({
|
||||||
|
applicationId: app2.id,
|
||||||
|
dealerCodeId: dCode2.id,
|
||||||
|
legalName: 'Test Dealer Y Ltd',
|
||||||
|
businessName: 'Test Dealer - Y',
|
||||||
|
constitutionType: 'Partnership',
|
||||||
|
status: 'Active'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update existing user with new dealerId (Latest)
|
||||||
|
await user.update({ dealerId: dealer2.id });
|
||||||
|
|
||||||
|
// Our Fix in dealer.controller.ts: Check for outlet for THIS application/city
|
||||||
|
let existingOutletForThisApp = await Outlet.findOne({
|
||||||
|
where: {
|
||||||
|
dealerId: user.id,
|
||||||
|
name: { [Op.like]: `%${app2.city}%` }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingOutletForThisApp) {
|
||||||
|
const outlet2 = await Outlet.create({
|
||||||
|
code: `OUT-Y-${Date.now()}`,
|
||||||
|
name: `${app2.applicantName} - ${app2.city}`,
|
||||||
|
type: 'Dealership',
|
||||||
|
address: 'Address Y',
|
||||||
|
city: 'City Y',
|
||||||
|
state: 'State Y',
|
||||||
|
pincode: '654321',
|
||||||
|
status: 'Active',
|
||||||
|
establishedDate: new Date(),
|
||||||
|
dealerId: user.id,
|
||||||
|
region: 'Central',
|
||||||
|
zone: 'National'
|
||||||
|
});
|
||||||
|
console.log('Success: Outlet 2 created for City Y.');
|
||||||
|
} else {
|
||||||
|
console.error('Error: Outlet for City Y already exists wrongly!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Final Check
|
||||||
|
const outletCount = await Outlet.count({ where: { dealerId: user.id } });
|
||||||
|
console.log(`Final Verification: User ${email} has ${outletCount} outlets total.`);
|
||||||
|
|
||||||
|
if (outletCount === 2) {
|
||||||
|
console.log('TEST PASSED: Multi-outlet support verified.');
|
||||||
|
} else {
|
||||||
|
console.error(`TEST FAILED: Expected 2 outlets, found ${outletCount}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test error:', error);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test();
|
||||||
74
src/scripts/verify_uuid_fix.ts
Normal file
74
src/scripts/verify_uuid_fix.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import db from '../database/models/index.js';
|
||||||
|
const { User, Region, Zone } = db;
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
async function verifyFix() {
|
||||||
|
console.log('--- Starting UUID Sanitization Verification ---');
|
||||||
|
|
||||||
|
const testEmail = `test_uuid_fix_${Date.now()}@example.com`;
|
||||||
|
const password = await bcrypt.hash('Test@123', 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Attempt to create a user with empty string for regionId
|
||||||
|
// This simulates what the createUser controller now does after sanitization
|
||||||
|
console.log('1. Testing User creation with empty-string sanitized to null...');
|
||||||
|
|
||||||
|
const userData = {
|
||||||
|
fullName: 'Test UUID User',
|
||||||
|
email: testEmail,
|
||||||
|
password: password,
|
||||||
|
roleCode: 'ZBH',
|
||||||
|
status: 'active',
|
||||||
|
isActive: true,
|
||||||
|
employeeId: `EMP-${Date.now()}`,
|
||||||
|
mobileNumber: '1234567890',
|
||||||
|
zoneId: null, // If frontend sends "", my controller now converts to null
|
||||||
|
regionId: null,
|
||||||
|
stateId: null,
|
||||||
|
districtId: null,
|
||||||
|
areaId: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = await User.create(userData);
|
||||||
|
console.log('Success: User created with null UUID fields.');
|
||||||
|
|
||||||
|
// 2. Simulate the Circular Dependency Workflow
|
||||||
|
console.log('2. Testing Circular Dependency Workflow...');
|
||||||
|
|
||||||
|
// a. Create a Zone first (needed for Region)
|
||||||
|
const zone = await Zone.create({
|
||||||
|
zoneCode: `ZNE-${Date.now().toString().slice(-4)}`,
|
||||||
|
zoneName: `Test Zone ${Date.now()}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// b. Create a Region and assign the user as manager
|
||||||
|
console.log('Creating Region with User as Manager...');
|
||||||
|
const region = await Region.create({
|
||||||
|
zoneId: zone.id,
|
||||||
|
regionCode: `REG-${Date.now().toString().slice(-4)}`,
|
||||||
|
regionName: 'Test Region',
|
||||||
|
regionalManagerId: user.id
|
||||||
|
});
|
||||||
|
console.log('Success: Region created.');
|
||||||
|
|
||||||
|
// c. Link User back to the Region
|
||||||
|
console.log('Linking User back to the new Region...');
|
||||||
|
await user.update({ regionId: region.id });
|
||||||
|
console.log('Success: User updated with regionId.');
|
||||||
|
|
||||||
|
// 3. Verify final state
|
||||||
|
const updatedUser = await User.findByPk(user.id);
|
||||||
|
if (updatedUser?.regionId === region.id) {
|
||||||
|
console.log('VERIFICATION PASSED: User-Region cycle completed successfully.');
|
||||||
|
} else {
|
||||||
|
console.error('VERIFICATION FAILED: user.regionId was not updated correctly.');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('VERIFICATION FAILED with error:', error);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyFix();
|
||||||
@ -2,6 +2,5 @@ export interface TokenPayload {
|
|||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
region: string | null;
|
locationId: string | null;
|
||||||
zone: string | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
112
verify_approval_sync.ts
Normal file
112
verify_approval_sync.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import db from './src/database/models/index';
|
||||||
|
const { Application, Interview, InterviewParticipant, InterviewEvaluation } = db;
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
async function testApprovalSync() {
|
||||||
|
console.log('--- Testing Synchronized Approval Logic (Direct DB Simulation) ---');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Setup IDs for test (must exist in DB)
|
||||||
|
const user1Id = 'fb9ba702-233b-4b3f-ba21-33bedec6209a'; // DD-ZM
|
||||||
|
const user2Id = 'c3602348-c9bd-4938-83b2-acc3c62791aa'; // RBM
|
||||||
|
|
||||||
|
// 1. Setup a test application
|
||||||
|
console.log('\n1. Creating test application...');
|
||||||
|
const app = await Application.create({
|
||||||
|
applicationId: 'APP-' + Date.now(),
|
||||||
|
registrationNumber: 'TEST-' + Date.now(),
|
||||||
|
fullName: 'Test Applicant',
|
||||||
|
applicantName: 'Test Applicant',
|
||||||
|
email: 'test' + Date.now() + '@example.com',
|
||||||
|
phone: '1234567890',
|
||||||
|
businessType: 'Dealership',
|
||||||
|
overallStatus: 'Level 1 Interview Pending',
|
||||||
|
currentStage: 'Level 1 Approved', // Mocking stage
|
||||||
|
zoneId: 'aedf5f64-3f95-4fa7-ac64-52c97cb330e7',
|
||||||
|
regionId: 'a5ea0aa2-c26c-49a9-95fe-0dbd7283c125'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Create an interview with 2 participants
|
||||||
|
console.log('2. Creating interview for Level 1...');
|
||||||
|
const interview = await Interview.create({
|
||||||
|
applicationId: app.id,
|
||||||
|
level: 1,
|
||||||
|
status: 'Scheduled',
|
||||||
|
interviewType: 'Level 1 Interview'
|
||||||
|
});
|
||||||
|
|
||||||
|
const participant1 = await InterviewParticipant.create({
|
||||||
|
interviewId: interview.id,
|
||||||
|
userId: user1Id,
|
||||||
|
roleInPanel: 'Interviewer'
|
||||||
|
});
|
||||||
|
|
||||||
|
const participant2 = await InterviewParticipant.create({
|
||||||
|
interviewId: interview.id,
|
||||||
|
userId: user2Id,
|
||||||
|
roleInPanel: 'Interviewer'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Interview created with ID: ${interview.id}`);
|
||||||
|
|
||||||
|
// Decision logic
|
||||||
|
async function submitDecision(userId: string, decision: string) {
|
||||||
|
console.log(`\nSubmitting ${decision} for User: ${userId}...`);
|
||||||
|
|
||||||
|
await InterviewEvaluation.create({
|
||||||
|
interviewId: interview.id,
|
||||||
|
evaluatorId: userId,
|
||||||
|
recommendation: decision,
|
||||||
|
decision: decision,
|
||||||
|
remarks: 'Testing'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replicate the sync check from controller
|
||||||
|
const participants = await InterviewParticipant.findAll({ where: { interviewId: interview.id } });
|
||||||
|
const evaluations = await InterviewEvaluation.findAll({ where: { interviewId: interview.id } });
|
||||||
|
|
||||||
|
const isFullyEvaluated = evaluations.length >= participants.length;
|
||||||
|
console.log(`Evaluations: ${evaluations.length}/${participants.length}. Fully Evaluated: ${isFullyEvaluated}`);
|
||||||
|
|
||||||
|
if (isFullyEvaluated) {
|
||||||
|
console.log('Finalizing interview and updating application status...');
|
||||||
|
await interview.update({ status: 'Completed' });
|
||||||
|
|
||||||
|
const nextStatus = 'Level 1 Approved';
|
||||||
|
await Application.update({
|
||||||
|
overallStatus: nextStatus,
|
||||||
|
currentStage: 'Level 1 Approved'
|
||||||
|
}, { where: { id: app.id } });
|
||||||
|
console.log(`Application status updated to: ${nextStatus}`);
|
||||||
|
} else {
|
||||||
|
console.log('Waiting for more evaluations. Application status remains unchanged.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. First participant rejects
|
||||||
|
await submitDecision(user1Id, 'Rejected');
|
||||||
|
let currentApp = await Application.findByPk(app.id);
|
||||||
|
console.log(`Current App Overall Status: ${currentApp.overallStatus}`);
|
||||||
|
|
||||||
|
// 4. Second participant approves
|
||||||
|
await submitDecision(user2Id, 'Approved');
|
||||||
|
currentApp = await Application.findByPk(app.id);
|
||||||
|
console.log(`Final App Overall Status: ${currentApp.overallStatus}`);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
console.log('\nCleaning up...');
|
||||||
|
await InterviewEvaluation.destroy({ where: { interviewId: interview.id } });
|
||||||
|
await InterviewParticipant.destroy({ where: { interviewId: interview.id } });
|
||||||
|
await Interview.destroy({ where: { id: interview.id } });
|
||||||
|
await Application.destroy({ where: { id: app.id } });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
} finally {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testApprovalSync();
|
||||||
74
verify_db_logic.ts
Normal file
74
verify_db_logic.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
import 'dotenv/config';
|
||||||
|
import db from './src/database/models/index';
|
||||||
|
const { User, Role } = db;
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
|
||||||
|
async function testFiltering() {
|
||||||
|
console.log('--- Testing Backend Filtering Logic (Direct DB) ---');
|
||||||
|
|
||||||
|
const testCasesMap = [
|
||||||
|
{
|
||||||
|
name: 'National Role (NBH) - Should ignore location',
|
||||||
|
params: { roleCode: 'NBH', zoneId: 'some-random-id' },
|
||||||
|
expectedRoles: ['NBH']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'National Role (DD Head) - Should ignore location',
|
||||||
|
params: { roleCode: 'DD Head', zoneId: 'some-random-id' },
|
||||||
|
expectedRoles: ['DD Head']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Zonal Role (RBM) - Should respect location (Matching)',
|
||||||
|
params: { roleCode: 'RBM', zoneId: 'aedf5f64-3f95-4fa7-ac64-52c97cb330e7' }, // Known RBM zone from check_db_users
|
||||||
|
expectedCount: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Zonal Role (RBM) - Should respect location (Mismatch)',
|
||||||
|
params: { roleCode: 'RBM', zoneId: '741e0e70-32d3-40d6-987f-5d2ffd54f152' }, // ZBH zone
|
||||||
|
expectedCount: 0
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCasesMap) {
|
||||||
|
console.log(`\nTesting: ${testCase.name}`);
|
||||||
|
const { roleCode, zoneId, regionId, areaId } = testCase.params;
|
||||||
|
const whereClause: any = {};
|
||||||
|
|
||||||
|
// Replicating logic from admin.controller.ts
|
||||||
|
if (roleCode) {
|
||||||
|
if (Array.isArray(roleCode)) {
|
||||||
|
whereClause.roleCode = { [Op.in]: roleCode };
|
||||||
|
} else {
|
||||||
|
whereClause.roleCode = roleCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationalRoles = ['NBH', 'DD Head'];
|
||||||
|
const isNationalRole = (typeof roleCode === 'string' && nationalRoles.includes(roleCode)) ||
|
||||||
|
(Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r)));
|
||||||
|
|
||||||
|
if (!isNationalRole) {
|
||||||
|
if (zoneId) whereClause.zoneId = zoneId;
|
||||||
|
if (regionId) whereClause.regionId = regionId;
|
||||||
|
if (areaId) whereClause.areaId = areaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await User.findAll({
|
||||||
|
where: whereClause,
|
||||||
|
attributes: ['id', 'roleCode', 'zoneId']
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Result: ${users.length} users found.`);
|
||||||
|
if (users.length > 0) {
|
||||||
|
console.log('Found Roles:', Array.from(new Set(users.map(u => u.roleCode))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
testFiltering().catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user