differnt apprach aded for hirarchy

This commit is contained in:
laxman h 2026-03-27 23:04:15 +05:30
parent 6edb5da84f
commit 2e1e96cc54
13 changed files with 917 additions and 537 deletions

View File

@ -10,6 +10,7 @@
"build": "tsc", "build": "tsc",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"migrate": "tsx scripts/migrate.ts", "migrate": "tsx scripts/migrate.ts",
"reset:stable": "tsx scripts/reset_db_stable.ts",
"seed": "tsx scripts/seed_normalized_data.ts", "seed": "tsx scripts/seed_normalized_data.ts",
"seed:approval-policies": "tsx scripts/seed-approval-policies.ts", "seed:approval-policies": "tsx scripts/seed-approval-policies.ts",
"seed:all": "npm run seed && npm run seed:approval-policies && npm run seed:questionnaire", "seed:all": "npm run seed && npm run seed:approval-policies && npm run seed:questionnaire",

View File

@ -0,0 +1,90 @@
import 'dotenv/config';
import db from '../src/database/models/index.js';
import bcrypt from 'bcryptjs';
const { Role, Zone, Region, State, Location, User, UserRole } = db;
async function resetAndSeed() {
console.log('--- RESETTING DATABASE TO DENORMALIZED DISTRICT MODEL ---');
try {
await db.sequelize.authenticate();
console.log('Database connected.');
// 1. Force Sync
await db.sequelize.sync({ force: true });
console.log('Database schema reset (force synced).');
const hashedPassword = await bcrypt.hash('Admin@123', 10);
// 2. Seed 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-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' },
{ roleCode: 'RM', roleName: 'Regional Manager', category: 'REGIONAL' },
{ roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' },
{ roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' },
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' }
];
for (const r of roles) await Role.create(r);
console.log('Roles seeded.');
// 3. Seed Hierarchy (Zone -> State & Region -> District)
// Zone
const northZone = await Zone.create({ name: 'North Zone', code: 'ZONE-N' });
const southZone = await Zone.create({ name: 'South Zone', code: 'ZONE-S' });
// State
const delhiState = await State.create({ name: 'Delhi', zoneId: northZone.id });
const haryanaState = await State.create({ name: 'Haryana', zoneId: northZone.id });
const karnatakaState = await State.create({ name: 'Karnataka', zoneId: southZone.id });
// Region
const ncrRegion = await Region.create({ name: 'NCR Region', zoneId: northZone.id });
const bangaloreRegion = await Region.create({ name: 'Bangalore Region', zoneId: southZone.id });
// District (Location)
await Location.create({ name: 'Central Delhi', stateId: delhiState.id, regionId: ncrRegion.id, zoneId: northZone.id });
await Location.create({ name: 'Gurgaon', stateId: haryanaState.id, regionId: ncrRegion.id, zoneId: northZone.id });
await Location.create({ name: 'Bangalore Urban', stateId: karnatakaState.id, regionId: bangaloreRegion.id, zoneId: southZone.id });
console.log('Denormalized Hierarchy seeded.');
// 4. Seed Admin Users
const adminUser = await User.create({
fullName: 'Super Admin',
email: 'admin@royalenfield.com',
roleCode: 'Super Admin',
password: hashedPassword,
status: 'active'
});
const superAdminRole = await Role.findOne({ where: { roleCode: 'Super Admin' } });
if (superAdminRole) {
await UserRole.create({ userId: adminUser.id, roleId: superAdminRole.id, isActive: true, isPrimary: true });
}
const zbhUser = await User.create({
fullName: 'Yashwin (ZBH North)',
email: 'yashwin@gmail.com',
roleCode: 'ZBH',
password: hashedPassword,
status: 'active'
});
const zbhRole = await Role.findOne({ where: { roleCode: 'ZBH' } });
if (zbhRole) {
await UserRole.create({ userId: zbhUser.id, roleId: zbhRole.id, zoneId: northZone.id, isActive: true, isPrimary: true });
}
console.log('Admin Users seeded.');
console.log('--- DATABASE RESET & SEEDING COMPLETE ---');
process.exit(0);
} catch (error) {
console.error('Error during database reset/seed:', error);
process.exit(1);
}
}
resetAndSeed();

View File

@ -2,17 +2,15 @@ import 'dotenv/config';
import db from '../src/database/models/index.js'; import db from '../src/database/models/index.js';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
const { Role, Location, LocationHierarchy, User, UserRole } = db; const { Role, Zone, Region, State, Location, User, UserRole } = db;
async function seed() { async function seed() {
console.log('--- Seeding Normalized Graph Data ---'); console.log('--- Seeding Normalized Denormalized Data ---');
// Ensure schema exists when seed is run on a fresh/empty database.
// This is non-destructive (does not drop data).
await db.sequelize.authenticate(); await db.sequelize.authenticate();
// Use sync with alter false to match main app behavior
await db.sequelize.sync({ alter: false }); await db.sequelize.sync({ alter: false });
// Hash default password for test users
const hashedPassword = await bcrypt.hash('Admin@123', 10); const hashedPassword = await bcrypt.hash('Admin@123', 10);
// 1. Create Roles // 1. Create Roles
@ -22,6 +20,7 @@ async function seed() {
{ roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' }, { roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' },
{ roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' }, { roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' },
{ roleCode: 'RBM', roleName: 'Regional Business Manager', category: 'REGIONAL' }, { roleCode: 'RBM', roleName: 'Regional Business Manager', category: 'REGIONAL' },
{ roleCode: 'RM', roleName: 'Regional Manager', category: 'REGIONAL' },
{ roleCode: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' }, { roleCode: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' },
{ roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' }, { roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' },
{ roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' }, { roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' },
@ -36,76 +35,54 @@ async function seed() {
} }
console.log('Roles seeded.'); console.log('Roles seeded.');
// 2. Create Locations // 2. Create Locations (Hierarchy)
const existingZones = await Location.findAll({ const [zone1] = await Zone.findOrCreate({
where: { type: 'zone' }, where: { name: 'North Zone' },
order: [['createdAt', 'ASC']] defaults: { name: 'North Zone', code: 'ZONE-N' }
}); });
let zone1: any = existingZones[0]; const [zone2] = await Zone.findOrCreate({
let zone2: any = existingZones[1]; where: { name: 'South Zone' },
defaults: { name: 'South Zone', code: 'ZONE-S' }
if (!zone1) {
const [createdZone1] = await Location.findOrCreate({
where: { name: 'North Zone', type: 'zone' },
defaults: { name: 'North Zone', type: 'zone' }
});
zone1 = createdZone1;
}
if (!zone2) {
const [createdZone2] = await Location.findOrCreate({
where: { name: 'South Zone', type: 'zone' },
defaults: { name: 'South Zone', type: 'zone' }
});
zone2 = createdZone2;
}
const [region1] = await Location.findOrCreate({
where: { name: 'Delhi Region', type: 'region' },
defaults: { name: 'Delhi Region', type: 'region' }
});
const [area1] = await Location.findOrCreate({
where: { name: 'South Delhi Area', type: 'area' },
defaults: { name: 'South Delhi Area', type: 'area' }
}); });
const [region2] = await Location.findOrCreate({ const [state1] = await State.findOrCreate({
where: { name: 'Bangalore Region', type: 'region' }, where: { name: 'Delhi' },
defaults: { name: 'Bangalore Region', type: 'region' } defaults: { name: 'Delhi', zoneId: zone1.id }
}); });
console.log('Locations created.'); const [region1] = await Region.findOrCreate({
where: { name: 'NCR Region' },
// 3. Create Hierarchies (Bridge Table) defaults: { name: 'NCR Region', zoneId: zone1.id }
await LocationHierarchy.findOrCreate({
where: { locationId: region1.id, parentId: zone1.id },
defaults: { locationId: region1.id, parentId: zone1.id }
});
await LocationHierarchy.findOrCreate({
where: { locationId: area1.id, parentId: region1.id },
defaults: { locationId: area1.id, parentId: region1.id }
});
await LocationHierarchy.findOrCreate({
where: { locationId: region2.id, parentId: zone2.id },
defaults: { locationId: region2.id, parentId: zone2.id }
}); });
console.log('Hierarchies seeded.'); const [region2] = await Region.findOrCreate({
where: { name: 'Bangalore Region' },
defaults: { name: 'Bangalore Region', zoneId: zone2.id }
});
const mapUserRole = async (userRec: any, roleCode: string, locationId?: string) => { const [district1] = await Location.findOrCreate({
where: { name: 'South Delhi District' },
defaults: { name: 'South Delhi District', stateId: state1.id, regionId: region1.id, zoneId: zone1.id }
});
console.log('Geographical Hierarchy seeded.');
const mapUserRole = async (userRec: any, roleCode: string, assignment: { zoneId?: string | null, regionId?: string | null, locationId?: string | null } = {}) => {
const role = await Role.findOne({ where: { roleCode } }); const role = await Role.findOne({ where: { roleCode } });
if (role) { if (role) {
await UserRole.findOrCreate({ await UserRole.findOrCreate({
where: { where: {
userId: userRec.id, userId: userRec.id,
roleId: role.id, roleId: role.id,
locationId: locationId || null ...assignment
}, },
defaults: { defaults: {
userId: userRec.id, userId: userRec.id,
roleId: role.id, roleId: role.id,
locationId: locationId || null ...assignment,
isActive: true,
isPrimary: true
} }
}); });
} }
@ -113,43 +90,43 @@ async function seed() {
// 4. Create Users and Map them // 4. Create Users and Map them
// Custom Seed Users // Custom Seed Users
const nbhUser = await User.findOrCreate({ const nbhResult = await User.findOrCreate({
where: { email: 'nbh@example.com' }, where: { email: 'nbh@example.com' },
defaults: { fullName: 'National Head', roleCode: 'NBH', password: hashedPassword } defaults: { fullName: 'National Head', roleCode: 'NBH', password: hashedPassword }
}); });
await mapUserRole(nbhUser[0], 'NBH'); await mapUserRole(nbhResult[0], 'NBH');
const zbhUser = await User.findOrCreate({ const zbhResult = await User.findOrCreate({
where: { email: 'zbh.north@example.com' }, where: { email: 'zbh.north@example.com' },
defaults: { fullName: 'North Zonal Head', roleCode: 'ZBH', password: hashedPassword } defaults: { fullName: 'North Zonal Head', roleCode: 'ZBH', password: hashedPassword }
}); });
await mapUserRole(zbhUser[0], 'ZBH', zone1.id); await mapUserRole(zbhResult[0], 'ZBH', { zoneId: zone1.id });
const rbmUser = await User.findOrCreate({ const rmResult = await User.findOrCreate({
where: { email: 'rbm.delhi@example.com' }, where: { email: 'rbm.delhi@example.com' },
defaults: { fullName: 'Delhi Regional Manager', roleCode: 'RBM', password: hashedPassword } defaults: { fullName: 'Delhi Regional Manager', roleCode: 'RM', password: hashedPassword }
}); });
await mapUserRole(rbmUser[0], 'RBM', region1.id); await mapUserRole(rmResult[0], 'RM', { regionId: region1.id });
const asmUser = await User.findOrCreate({ const asmResult = await User.findOrCreate({
where: { email: 'asm.sdelhi@example.com' }, where: { email: 'asm.sdelhi@example.com' },
defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM', password: hashedPassword } defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM', password: hashedPassword }
}); });
await mapUserRole(asmUser[0], 'ASM', area1.id); await mapUserRole(asmResult[0], 'ASM', { locationId: district1.id });
// Requested Mock Users // Mock Users alignment
const mockUsers = [ const mockUsers = [
{ email: 'ddlead@royalenfield.com', name: 'Meera Iyer', roleCode: 'DD Lead', location: zone1.id }, { email: 'ddlead@royalenfield.com', name: 'Meera Iyer', roleCode: 'DD Lead', assignment: { zoneId: zone1.id } },
{ email: 'finance@royalenfield.com', name: 'Rahul Verma', roleCode: 'Finance', location: null }, { email: 'finance@royalenfield.com', name: 'Rahul Verma', roleCode: 'Finance', assignment: {} },
{ email: 'dealer@royalenfield.com', name: 'Amit Sharma', roleCode: 'Dealer', location: area1.id, isExt: true }, { email: 'dealer@royalenfield.com', name: 'Amit Sharma', roleCode: 'Dealer', assignment: { locationId: district1.id }, isExt: true },
{ email: 'admin@royalenfield.com', name: 'Laxman H', roleCode: 'DD Lead', location: zone2.id }, { email: 'admin@royalenfield.com', name: 'Laxman H', roleCode: 'DD Lead', assignment: { zoneId: zone2.id } },
{ email: 'yashwin@gmail.com', name: 'Yashwin', roleCode: 'ZBH', location: zone1.id }, { email: 'yashwin@gmail.com', name: 'Yashwin', roleCode: 'ZBH', assignment: { zoneId: zone1.id } },
{ email: 'kenil@gmail.com', name: 'Kenil', roleCode: 'DD Lead', location: zone1.id }, { email: 'kenil@gmail.com', name: 'Kenil', roleCode: 'DD Lead', assignment: { zoneId: zone1.id } },
{ email: 'lince@gmail.com', name: 'Lince', roleCode: 'DD Admin', location: null } { email: 'lince@gmail.com', name: 'Lince', roleCode: 'DD Admin', assignment: {} }
]; ];
for (const m of mockUsers) { for (const m of mockUsers) {
const u = await User.findOrCreate({ const [u] = await User.findOrCreate({
where: { email: m.email }, where: { email: m.email },
defaults: { defaults: {
fullName: m.name, fullName: m.name,
@ -159,7 +136,7 @@ async function seed() {
status: 'active' status: 'active'
} }
}); });
await mapUserRole(u[0], m.roleCode, m.location); await mapUserRole(u, m.roleCode, m.assignment);
} }
console.log('Users and Mappings seeded.'); console.log('Users and Mappings seeded.');

View File

@ -7,78 +7,80 @@ import db from '../src/database/models/index.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const { Location, LocationHierarchy } = db; const { Zone, State, Location } = db;
async function run() { async function run() {
console.log('--- Migrating Real Geo Data to Normalized Location Models ---'); console.log('--- Seeding Real Geo Data (Denormalized Model) ---');
try { try {
// Read the original seeder file as text so we don't have to duplicate the 350 items await db.sequelize.authenticate();
// Read the source seeder file
const seederPath = path.join(__dirname, '../seeders/20240127-seed-geo-data.js'); const seederPath = path.join(__dirname, '../seeders/20240127-seed-geo-data.js');
const content = fs.readFileSync(seederPath, 'utf8'); const content = fs.readFileSync(seederPath, 'utf8');
// Extract the arrays using eval since it's a known static JS file // Extract arrays using regex
const zonesMatch = content.match(/const ZONES_DATA = \[([\s\S]*?)\];/); const zonesMatch = content.match(/const ZONES_DATA = \[([\s\S]*?)\];/);
const statesMatch = content.match(/const STATES_DATA = \[([\s\S]*?)\];/); const statesMatch = content.match(/const STATES_DATA = \[([\s\S]*?)\];/);
const citiesMatch = content.match(/const CITIES_DATA = \[([\s\S]*?)\];/); const citiesMatch = content.match(/const CITIES_DATA = \[([\s\S]*?)\];/);
if (!zonesMatch || !statesMatch || !citiesMatch) { if (!zonesMatch || !statesMatch || !citiesMatch) {
throw new Error('Could not parse geo data arrays!'); throw new Error('Could not parse geo data arrays from seeder file!');
} }
// Eval helper (data is trusted since it's our own seeder)
const ZONES_DATA = eval(`[${zonesMatch[1]}]`); const ZONES_DATA = eval(`[${zonesMatch[1]}]`);
const STATES_DATA = eval(`[${statesMatch[1]}]`); const STATES_DATA = eval(`[${statesMatch[1]}]`);
const CITIES_DATA = eval(`[${citiesMatch[1]}]`); const CITIES_DATA = eval(`[${citiesMatch[1]}]`);
console.log(`Extracted ${ZONES_DATA.length} Zones, ${STATES_DATA.length} States, and ${CITIES_DATA.length} Cities.`); console.log(`Extracted ${ZONES_DATA.length} Zones, ${STATES_DATA.length} States, and ${CITIES_DATA.length} Districts.`);
// 1. Insert Zones // 1. Seed Zones
const zoneIdMap = new Map(); const zoneIdMap = new Map(); // Name -> UUID
for (const z of ZONES_DATA) { for (const z of ZONES_DATA) {
const [loc] = await Location.findOrCreate({ const [zoneRecord] = await Zone.findOrCreate({
where: { name: z.name, type: 'zone' }, where: { name: z.name },
defaults: { name: z.name, type: 'zone' } defaults: { name: z.name, code: z.code }
}); });
zoneIdMap.set(z.code, loc.id); zoneIdMap.set(z.name, zoneRecord.id);
z._dbId = loc.id; // Attach states list for later lookup
z._dbId = zoneRecord.id;
} }
console.log('Zones seeded.');
// 2. Insert States and link to Zones // 2. Seed States and link to Zones
const stateIdMap = new Map(); const stateIdMap = new Map(); // Legacy ID -> { id, zoneId }
for (const s of STATES_DATA) { for (const s of STATES_DATA) {
const [loc] = await Location.findOrCreate({ // Find parent zone by checking which zone's states array contains this state name
where: { name: s.name, type: 'state' }, const parentZoneData = ZONES_DATA.find((z: any) => z.states.includes(s.name));
defaults: { name: s.name, type: 'state' } const zoneId = parentZoneData ? zoneIdMap.get(parentZoneData.name) : null;
const [stateRecord] = await State.findOrCreate({
where: { name: s.name },
defaults: { name: s.name, zoneId: zoneId }
}); });
stateIdMap.set(s.id, loc.id);
// Find which zone string array it belongs to stateIdMap.set(s.id, { id: stateRecord.id, zoneId: zoneId });
const parentZone = ZONES_DATA.find((z: any) => z.states.includes(s.name));
if (parentZone) {
await LocationHierarchy.findOrCreate({
where: { locationId: loc.id, parentId: parentZone._dbId },
defaults: { locationId: loc.id, parentId: parentZone._dbId }
});
}
} }
console.log('States seeded.');
// 3. Insert Cities (Districts) and link to States // 3. Seed Districts (Locations)
let cityCount = 0; let districtCount = 0;
for (const c of CITIES_DATA) { for (const c of CITIES_DATA) {
const stateDbId = stateIdMap.get(c.state_id); const parentStateData = stateIdMap.get(c.state_id);
if (stateDbId) { if (parentStateData) {
const [loc] = await Location.findOrCreate({ await Location.findOrCreate({
where: { name: c.name, type: 'district' }, where: { name: c.name, stateId: parentStateData.id },
defaults: { name: c.name, type: 'district' } defaults: {
name: c.name,
stateId: parentStateData.id,
zoneId: parentStateData.zoneId
}
}); });
await LocationHierarchy.findOrCreate({ districtCount++;
where: { locationId: loc.id, parentId: stateDbId },
defaults: { locationId: loc.id, parentId: stateDbId }
});
cityCount++;
} }
} }
console.log(`✅ Successfully seeded Real Geo Data! Created ${cityCount} districts tied to their respective states and zones.`); console.log(`✅ Successfully seeded Real Geo Data! Created/Verified ${districtCount} districts.`);
process.exit(0); process.exit(0);
} catch (e: any) { } catch (e: any) {

View File

@ -3,12 +3,13 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
export interface LocationAttributes { export interface LocationAttributes {
id: string; id: string;
name: string; name: string;
type: 'zone' | 'region' | 'area' | 'state' | 'district';
code?: string; code?: string;
pincode?: string; stateId?: string | null;
regionId?: string | null;
zoneId?: string | null;
asmId?: string | null;
isActive?: boolean; isActive?: boolean;
activeFrom?: string | Date | null; description?: string | null;
activeTo?: string | Date | null;
} }
export interface LocationInstance extends Model<LocationAttributes>, LocationAttributes { } export interface LocationInstance extends Model<LocationAttributes>, LocationAttributes { }
@ -24,50 +25,68 @@ export default (sequelize: Sequelize) => {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false
}, },
type: {
type: DataTypes.ENUM('zone', 'region', 'area', 'state', 'district'),
allowNull: false
},
code: { code: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
unique: true
}, },
pincode: { stateId: {
type: DataTypes.STRING, type: DataTypes.UUID,
allowNull: true allowNull: true,
references: {
model: 'states',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
},
asmId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'users',
key: 'id'
}
}, },
isActive: { isActive: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
defaultValue: true defaultValue: true
}, },
activeFrom: { description: {
type: DataTypes.DATE, type: DataTypes.TEXT,
allowNull: true
},
activeTo: {
type: DataTypes.DATE,
allowNull: true allowNull: true
} }
}, { }, {
tableName: 'locations', tableName: 'locations',
timestamps: true timestamps: true,
indexes: [
{ fields: ['stateId'] },
{ fields: ['regionId'] },
{ fields: ['zoneId'] },
{ unique: true, fields: ['name', 'stateId'] }
]
}); });
(Location as any).associate = (models: any) => { (Location as any).associate = (models: any) => {
// Many-to-Many hierarchy via LocationHierarchy bridge table Location.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
Location.belongsToMany(models.Location, { Location.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
through: models.LocationHierarchy, Location.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
as: 'parents', Location.belongsTo(models.User, { foreignKey: 'asmId', as: 'asm' });
foreignKey: 'locationId', Location.hasMany(models.User, { foreignKey: 'locationId', as: 'users' });
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.UserRole, { foreignKey: 'locationId', as: 'userRoles' });
Location.hasMany(models.Application, { foreignKey: 'locationId', as: 'applications' }); Location.hasMany(models.Application, { foreignKey: 'locationId', as: 'applications' });
}; };

View File

@ -0,0 +1,53 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface RegionAttributes {
id: string;
name: string;
code: string;
description?: string | null;
zoneId?: string | null;
}
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
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
code: {
type: DataTypes.STRING,
allowNull: true,
unique: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
}
}, {
tableName: 'regions',
timestamps: true
});
(Region as any).associate = (models: any) => {
Region.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
Region.hasMany(models.Location, { foreignKey: 'regionId', as: 'districts' });
};
return Region;
};

View File

@ -0,0 +1,42 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface StateAttributes {
id: string;
name: string;
zoneId?: string | null;
}
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
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
}
}, {
tableName: 'states',
timestamps: true
});
(State as any).associate = (models: any) => {
State.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
State.hasMany(models.Location, { foreignKey: 'stateId', as: 'districts' });
};
return State;
};

View File

@ -4,7 +4,9 @@ export interface UserRoleAttributes {
id: string; id: string;
userId: string; userId: string;
roleId: string; roleId: string;
locationId: string | null; locationId: string | null; // District
zoneId: string | null;
regionId: string | null;
managerCode: string | null; managerCode: string | null;
isPrimary: boolean; isPrimary: boolean;
isActive: boolean; isActive: boolean;
@ -47,6 +49,22 @@ export default (sequelize: Sequelize) => {
key: 'id' key: 'id'
} }
}, },
zoneId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'zones',
key: 'id'
}
},
regionId: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'regions',
key: 'id'
}
},
managerCode: { managerCode: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true
@ -98,6 +116,14 @@ export default (sequelize: Sequelize) => {
foreignKey: 'locationId', foreignKey: 'locationId',
as: 'location' as: 'location'
}); });
UserRole.belongsTo(models.Zone, {
foreignKey: 'zoneId',
as: 'zone'
});
UserRole.belongsTo(models.Region, {
foreignKey: 'regionId',
as: 'region'
});
UserRole.belongsTo(models.User, { UserRole.belongsTo(models.User, {
foreignKey: 'assignedBy', foreignKey: 'assignedBy',
as: 'assigner' as: 'assigner'

View File

@ -0,0 +1,45 @@
import { Model, DataTypes, Sequelize } from 'sequelize';
export interface ZoneAttributes {
id: string;
name: string;
code: string;
description?: 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
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
code: {
type: DataTypes.STRING,
allowNull: true,
unique: true
},
description: {
type: DataTypes.TEXT,
allowNull: true
}
}, {
tableName: 'zones',
timestamps: true
});
(Zone as any).associate = (models: any) => {
Zone.hasMany(models.Region, { foreignKey: 'zoneId', as: 'regions' });
Zone.hasMany(models.State, { foreignKey: 'zoneId', as: 'states' });
Zone.hasMany(models.Location, { foreignKey: 'zoneId', as: 'districts' });
};
return Zone;
};

View File

@ -20,7 +20,9 @@ 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 createLocation from './Location.js';
import createLocationHierarchy from './LocationHierarchy.js'; import createZone from './Zone.js';
import createRegion from './Region.js';
import createState from './State.js';
// Batch 1: Organizational Hierarchy & User Management // Batch 1: Organizational Hierarchy & User Management
import createRole from './Role.js'; import createRole from './Role.js';
@ -122,7 +124,9 @@ 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.Location = createLocation(sequelize);
db.LocationHierarchy = createLocationHierarchy(sequelize); db.Zone = createZone(sequelize);
db.Region = createRegion(sequelize);
db.State = createState(sequelize);
// Batch 1: Organizational Hierarchy & User Management // Batch 1: Organizational Hierarchy & User Management
db.Role = createRole(sequelize); db.Role = createRole(sequelize);

View File

@ -169,26 +169,25 @@ export const getAllUsers = async (req: Request, res: Response) => {
(Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r))); (Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r)));
if (!isNationalRole && locationId) { if (!isNationalRole && locationId) {
// Find all ancestors of the given location (BFS for many-to-many support) const district: any = await db.Location.findByPk(locationId as string, {
let ancestorIds: Set<string> = new Set([locationId as string]); attributes: ['id', 'zoneId', 'regionId', 'stateId']
let queue: string[] = [locationId as string]; });
while (queue.length > 0) { if (district) {
const currentId = queue.shift()!; const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean);
const hierarchies = await db.LocationHierarchy.findAll({ whereClause.locationId = { [Op.in]: relevantIds };
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) };
} }
// Fetch all locations to build a map if needed
const allLocations = await db.Location.findAll({
include: [
{ model: db.Zone, as: 'zone', attributes: ['name'] },
{ model: db.Region, as: 'region', attributes: ['name'] },
{ model: db.State, as: 'state', attributes: ['name'] }
]
});
const users = await User.findAll({ const users = await User.findAll({
where: whereClause, where: whereClause,
attributes: { exclude: ['password'] }, attributes: { exclude: ['password'] },
@ -216,7 +215,62 @@ export const getAllUsers = async (req: Request, res: Response) => {
], ],
order: [['createdAt', 'DESC']] order: [['createdAt', 'DESC']]
}); });
res.json({ success: true, data: users });
const findAncestor = (locId: string, targetType: string): any => {
const queue = [locId];
const visited = new Set();
while (queue.length > 0) {
const id = queue.shift();
if (visited.has(id)) continue;
visited.add(id);
const loc = allLocations.find((l: any) => l.id === id);
if (!loc) continue;
if (loc.type.toLowerCase() === targetType.toLowerCase()) return loc;
if (loc.parents) queue.push(...loc.parents.map((p: any) => p.id));
}
return null;
};
const result = users.map((u: any) => {
const userJson = u.toJSON();
const assignments = userJson.userRoles || [];
// Consolidate roles and territories with DEEP resolution
const territories = assignments.map((a: any) => {
const zone = findAncestor(a.locationId, 'zone');
const region = findAncestor(a.locationId, 'region');
const state = findAncestor(a.locationId, 'state');
return {
role: a.role?.roleName,
roleCode: a.role?.roleCode,
locationId: a.locationId,
locationName: a.location?.name,
locationType: a.location?.type,
managerCode: a.managerCode,
zone: zone?.name,
zoneId: zone?.id,
region: region?.name,
regionId: region?.id,
state: state?.name,
stateId: state?.id,
isActive: a.isActive
};
});
userJson.territoryProfile = territories;
userJson.allRoles = Array.from(new Set([
u.role?.roleCode,
u.role?.roleName,
...assignments.flatMap((a: any) => [a.role?.roleCode, a.role?.roleName])
].filter(Boolean)));
userJson.allZones = Array.from(new Set([u.location?.zone?.name, ...territories.map((t: any) => t.zone)].filter(Boolean).map(z => z.toUpperCase())));
userJson.allRegions = Array.from(new Set([u.location?.region?.name, ...territories.map((t: any) => t.region)].filter(Boolean).map(r => r.toUpperCase())));
return userJson;
});
res.json({ success: true, data: result });
} catch (error) { } catch (error) {
console.error('Get users error:', error); console.error('Get users error:', error);
res.status(500).json({ success: false, message: 'Error fetching users' }); res.status(500).json({ success: false, message: 'Error fetching users' });
@ -351,6 +405,8 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
mobileNumber, department, designation, mobileNumber, department, designation,
locationId, locationId,
assignments, assignments,
districts, // New: ASM managed areas/districts
asmCode, // New: ASM code to store in managerCode
password // Optional password update password // Optional password update
} = req.body; } = req.body;
@ -380,7 +436,46 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
if (Array.isArray(assignments)) { if (Array.isArray(assignments)) {
await upsertUserAssignments(id as string, assignments, req.user?.id); await upsertUserAssignments(id as string, assignments, req.user?.id);
} else if (roleCode !== undefined || locationId !== undefined) { } else if (districts && Array.isArray(districts) && (roleCode === 'ASM' || roleCode === 'ZM')) {
// Specialized logic for Manager level (ASM/ZM) territory management
const targetRole = await Role.findOne({ where: { roleCode: roleCode } });
if (!targetRole) throw new Error(`${roleCode} role not found`);
// 1. DUPLICATION CHECK: Ensure these districts aren't assigned to another active manager of the same role
const duplicate = await db.UserRole.findOne({
where: {
roleId: targetRole.id,
locationId: { [Op.in]: districts },
userId: { [Op.ne]: id },
isActive: true
},
include: [{ model: db.User, as: 'user', attributes: ['fullName'] }]
});
if (duplicate) {
const location = await db.Location.findByPk(duplicate.locationId);
return res.status(400).json({
success: false,
message: `Territory "${location?.name}" is already assigned to ${duplicate.user?.fullName}. Duplicate assignments for ${roleCode} are restricted.`
});
}
// 2. Transactional Update: Clear old assignments for this role and add new ones
await db.UserRole.destroy({ where: { userId: id, roleId: targetRole.id } });
for (const distId of districts) {
await db.UserRole.create({
userId: id,
roleId: targetRole.id,
locationId: distId,
managerCode: asmCode || (req.body as any).zmCode || null,
isPrimary: false,
isActive: true,
assignedBy: req.user?.id || null
});
}
}
else if (roleCode !== undefined || locationId !== undefined) {
const primaryRoleCode = roleCode || user.roleCode; const primaryRoleCode = roleCode || user.roleCode;
if (primaryRoleCode) { if (primaryRoleCode) {
const role = await Role.findOne({ where: { roleCode: primaryRoleCode } }); const role = await Role.findOne({ where: { roleCode: primaryRoleCode } });

View File

@ -2,352 +2,398 @@ import { Request, Response } from 'express';
import db from '../../database/models/index.js'; import db from '../../database/models/index.js';
const { User } = db; const { User } = db;
// --- Generic Location Fetching --- // --- Districts (Locations) ---
const getLocationsByType = async (type: string, req: Request, res: Response) => { export const getDistricts = async (req: Request, res: Response) => {
try { try {
const locations = await db.Location.findAll({ const districts = await db.Location.findAll({
where: { type },
include: [ include: [
{ { model: db.Zone, as: 'zone', attributes: ['name'] },
model: db.Location, { model: db.Region, as: 'region', attributes: ['name'] },
as: 'parents', { model: db.State, as: 'state', attributes: ['name'] },
through: { attributes: [] } { model: db.User, as: 'asm', attributes: ['id', 'fullName', 'email'] }
},
{
model: db.Location,
as: 'children',
through: { attributes: [] }
}
], ],
order: [['name', 'ASC']] order: [['name', 'ASC']]
}); });
res.json({ success: true, data: locations });
const result = districts.map((d: any) => ({
...d.toJSON(),
zoneName: d.zone?.name || 'UNKNOWN',
regionName: d.region?.name || 'UNKNOWN',
stateName: d.state?.name || 'UNKNOWN',
asmName: d.asm?.fullName || 'UNASSIGNED'
}));
res.json({ success: true, data: result });
} catch (error) { } catch (error) {
console.error(`Get ${type} list error:`, error); console.error('Get districts error:', error);
res.status(500).json({ success: false, message: `Error fetching ${type} list` }); res.status(500).json({ success: false, message: 'Error fetching districts' });
} }
}; };
// --- Regions ---
export const getRegions = async (req: Request, res: Response) => {
return getLocationsByType('region', req, res);
};
export const createRegion = async (req: Request, res: Response) => {
try {
const { parentIds, name, childrenIds, regionalManagerId } = req.body;
if (!name) {
return res.status(400).json({ success: false, message: 'Region name is required' });
}
const region = await db.Location.create({
name: name,
type: 'region'
});
if (parentIds && Array.isArray(parentIds)) {
for (const pid of parentIds) {
await db.LocationHierarchy.create({ locationId: region.id, parentId: pid });
}
}
if (childrenIds && Array.isArray(childrenIds)) {
for (const cid of childrenIds) {
await db.LocationHierarchy.create({ locationId: cid, parentId: region.id });
}
}
if (regionalManagerId) {
await db.User.update({ locationId: region.id }, { where: { id: regionalManagerId } });
}
res.status(201).json({ success: true, message: 'Region created successfully', data: region });
} catch (error) {
console.error('Create region error:', error);
res.status(500).json({ success: false, message: 'Error creating region' });
}
};
// --- Zones ---
export const getZones = async (req: Request, res: Response) => {
return getLocationsByType('zone', req, res);
};
export const createZone = async (req: Request, res: Response) => {
try {
const { name, childrenIds } = req.body;
if (!name) {
return res.status(400).json({ success: false, message: 'Zone name is required' });
}
const zone = await db.Location.create({
name: name,
type: 'zone'
});
if (childrenIds && Array.isArray(childrenIds)) {
for (const childId of childrenIds) {
await db.LocationHierarchy.create({
locationId: childId,
parentId: zone.id
});
}
}
if (req.body.zonalBusinessHeadId) {
await db.User.update({ locationId: zone.id }, { where: { id: req.body.zonalBusinessHeadId } });
}
res.status(201).json({ success: true, message: 'Zone created successfully', data: zone });
} catch (error) {
console.error('Create zone error:', error);
res.status(500).json({ success: false, message: 'Error creating zone' });
}
};
export const updateLocation = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { name, type, parentIds, childrenIds, zonalBusinessHeadId, regionalManagerId, areaName, pincode, isActive, activeFrom, activeTo, districtId } = req.body;
const location = await db.Location.findByPk(id);
if (!location) {
return res.status(404).json({ success: false, message: 'Location not found' });
}
const updates: any = {};
if (name) updates.name = name;
if (areaName) updates.name = areaName; // Fallback mapping for Area dialog payloads
if (type) updates.type = type;
if (pincode !== undefined) updates.pincode = pincode;
if (isActive !== undefined) updates.isActive = isActive;
if (activeFrom !== undefined) updates.activeFrom = activeFrom;
if (activeTo !== undefined) updates.activeTo = activeTo;
await location.update(updates);
if (parentIds && Array.isArray(parentIds)) {
// Re-sync parents (Where this location is the child)
await db.LocationHierarchy.destroy({ where: { locationId: id } });
for (const pid of parentIds) {
await db.LocationHierarchy.create({ locationId: id, parentId: pid });
}
}
if (childrenIds && Array.isArray(childrenIds)) {
// Re-sync children (Where this location is the parent)
await db.LocationHierarchy.destroy({ where: { parentId: id } });
for (const cid of childrenIds) {
await db.LocationHierarchy.create({ locationId: cid, parentId: id });
}
}
// Handling Area Dialog parentId mapping which passes exclusively districtId instead of parentIds array
if (districtId) {
await db.LocationHierarchy.destroy({ where: { locationId: id } });
await db.LocationHierarchy.create({ locationId: id, parentId: districtId });
}
if (zonalBusinessHeadId !== undefined) {
const roleCodes = ['ZBH', 'Zonal Business Head'];
await db.User.update({ locationId: null }, { where: { locationId: id, roleCode: { [db.Sequelize.Op.in]: roleCodes } } });
if (zonalBusinessHeadId !== null) {
await db.User.update({ locationId: id }, { where: { id: zonalBusinessHeadId } });
}
}
if (regionalManagerId !== undefined) {
const roleCodes = ['RM', 'Regional Manager'];
await db.User.update({ locationId: null }, { where: { locationId: id, roleCode: { [db.Sequelize.Op.in]: roleCodes } } });
if (regionalManagerId !== null) {
await db.User.update({ locationId: id }, { where: { id: regionalManagerId } });
}
}
res.json({ success: true, message: 'Location updated successfully' });
} catch (error) {
console.error('Update location error:', error);
res.status(500).json({ success: false, message: 'Error updating location' });
}
};
// --- States ---
export const getStates = async (req: Request, res: Response) => {
return getLocationsByType('state', req, res);
};
export const createState = async (req: Request, res: Response) => {
try {
const { zoneId, stateName } = req.body;
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 });
}
res.status(201).json({ success: true, message: 'State created', data: state });
} catch (error) {
console.error('Create state error:', error);
res.status(500).json({ success: false, message: 'Error creating state' });
}
};
// --- Districts ---
export const getDistricts = async (req: Request, res: Response) => {
return getLocationsByType('district', req, res);
};
export const createDistrict = async (req: Request, res: Response) => { export const createDistrict = async (req: Request, res: Response) => {
try { try {
const { stateId, districtName } = req.body; const { name, code, stateId, regionId, zoneId, asmId, description } = req.body;
if (!districtName) return res.status(400).json({ success: false, message: 'District name is required' }); if (!name) return res.status(400).json({ success: false, message: 'District name is required' });
const district = await db.Location.create({ name: districtName, type: 'district' }); const district = await db.Location.create({
name,
code,
stateId,
regionId,
zoneId,
asmId,
description
});
if (stateId) { res.status(201).json({ success: true, data: district });
await db.LocationHierarchy.create({ locationId: district.id, parentId: stateId });
}
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);
res.status(500).json({ success: false, message: 'Error creating district' }); res.status(500).json({ success: false, message: 'Error creating district' });
} }
}; };
// --- Areas --- // --- Regions ---
export const getAreas = async (req: Request, res: Response) => { export const getRegions = async (req: Request, res: Response) => {
return getLocationsByType('area', req, res);
};
export const createArea = async (req: Request, res: Response) => {
try { try {
// Intercept all legacy property keys matching the MasterPage payload. const regions = await db.Region.findAll({
const { districtId, areaName, city, pincode, areaCode, isActive, activeFrom, activeTo } = req.body; include: [
if (!areaName) return res.status(400).json({ success: false, message: 'Area name is required' }); { model: db.Zone, as: 'zone', attributes: ['name'] },
{ model: db.Location, as: 'districts', attributes: ['id', 'name'] }
const area = await db.Location.create({ ],
name: areaName, order: [['name', 'ASC']]
type: 'area',
code: areaCode,
pincode: pincode,
isActive: isActive !== undefined ? isActive : true,
activeFrom: activeFrom || null,
activeTo: activeTo || null
}); });
if (districtId) { const roles = await db.Role.findAll({
await db.LocationHierarchy.create({ locationId: area.id, parentId: districtId }); where: {
} roleCode: { [db.Sequelize.Op.in]: ['ASM', 'RM', 'RBM'] }
}
});
const asmRoleIds = roles.filter((r: any) => r.roleCode === 'ASM').map((r: any) => r.id);
const rmRoleIds = roles.filter((r: any) => ['RM', 'RBM'].includes(r.roleCode)).map((r: any) => r.id);
res.status(201).json({ success: true, message: 'Area created', data: area }); const result = await Promise.all(regions.map(async (region: any) => {
const regionJson = region.toJSON();
regionJson.zoneName = region.zone?.name || 'UNKNOWN';
const districtIds = (region.districts || []).map((d: any) => d.id);
const [asmCount, rmCount, rmAssignment] = await Promise.all([
db.UserRole.count({ where: { roleId: { [db.Sequelize.Op.in]: asmRoleIds }, locationId: { [db.Sequelize.Op.in]: districtIds }, isActive: true }, distinct: true, col: 'userId' }),
db.UserRole.count({ where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true }, distinct: true, col: 'userId' }),
db.UserRole.findOne({
where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true },
include: [{ model: db.User, as: 'user' }]
})
]);
regionJson.asmCount = asmCount;
regionJson.regionalOfficerCount = rmCount;
regionJson.regionalManager = rmAssignment?.user || null;
regionJson.districts = (region.districts || []).map((d: any) => ({ id: d.id, name: d.name.toUpperCase() }));
return regionJson;
}));
res.json({ success: true, data: result });
} catch (error) { } catch (error) {
console.error('Create area error:', error); console.error('Get regions error:', error);
res.status(500).json({ success: false, message: 'Error creating area' }); res.status(500).json({ success: false, message: 'Error fetching regions' });
} }
}; };
// --- Managers (Consolidated) --- export const createRegion = async (req: Request, res: Response) => {
try {
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body;
const targetZoneId = zoneId || parentId;
if (!name) return res.status(400).json({ success: false, message: 'Region name is required' });
const region = await db.Region.create({ name, code, zoneId: targetZoneId });
// 1. Assign Manager
if (managerId) {
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } });
if (rmRole) {
await db.UserRole.update({ isActive: false }, { where: { regionId: region.id, roleId: rmRole.id } });
await db.UserRole.create({
userId: managerId,
roleId: rmRole.id,
regionId: region.id,
zoneId: targetZoneId,
isActive: true,
isPrimary: true
});
}
}
// 2. Assign Districts
const targetDistrictIds = districts || districtIds;
if (Array.isArray(targetDistrictIds) && targetDistrictIds.length > 0) {
await db.Location.update(
{ regionId: region.id, zoneId: targetZoneId },
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
);
}
res.status(201).json({ success: true, message: 'Region created', data: region });
} catch (error) {
console.error('Create region error:', error);
res.status(500).json({ success: false, message: 'Error creating region' });
}
};
export const updateRegion = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body;
const targetZoneId = zoneId || parentId;
const region = await db.Region.findByPk(id);
if (!region) return res.status(404).json({ success: false, message: 'Region not found' });
await region.update({
name,
code,
zoneId: targetZoneId || region.zoneId
});
// 1. Update Manager
if (managerId) {
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } });
if (rmRole) {
// Deactivate old RMs for this region
await db.UserRole.update({ isActive: false }, {
where: { regionId: id, roleId: rmRole.id }
});
// Assign new RM
await db.UserRole.create({
userId: managerId,
roleId: rmRole.id,
regionId: id,
zoneId: region.zoneId,
isActive: true,
isPrimary: true
});
}
}
// 2. Update Districts
const targetDistrictIds = districts || districtIds;
if (Array.isArray(targetDistrictIds)) {
await db.Location.update({ regionId: null }, { where: { regionId: id } });
if (targetDistrictIds.length > 0) {
await db.Location.update(
{ regionId: id, zoneId: region.zoneId },
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
);
}
}
res.json({ success: true, message: 'Region updated' });
} catch (error) {
console.error('Update region error:', error);
res.status(500).json({ success: false, message: 'Error updating region' });
}
};
// --- Zones ---
export const getZones = async (req: Request, res: Response) => {
try {
const zones = await db.Zone.findAll({
include: [
{ model: db.Region, as: 'regions', attributes: ['id', 'name'] },
{ model: db.State, as: 'states', attributes: ['id', 'name'] },
{ model: db.Location, as: 'districts', attributes: ['id'] }
],
order: [['name', 'ASC']]
});
const roles = await db.Role.findAll({
where: { roleCode: { [db.Sequelize.Op.in]: ['ZBH', 'DD-ZM'] } }
});
const zbhRoleIds = roles.filter((r: any) => r.roleCode === 'ZBH').map((r: any) => r.id);
const zmRoleIds = roles.filter((r: any) => r.roleCode === 'DD-ZM').map((r: any) => r.id);
const result = await Promise.all(zones.map(async (zone: any) => {
const zoneJson = zone.toJSON();
const [zbhAssignment, zms] = await Promise.all([
db.UserRole.findOne({
where: { roleId: { [db.Sequelize.Op.in]: zbhRoleIds }, zoneId: zone.id, isPrimary: true, isActive: true },
include: [{ model: db.User, as: 'user' }]
}),
db.UserRole.findAll({
where: { roleId: { [db.Sequelize.Op.in]: zmRoleIds }, zoneId: zone.id, isActive: true },
include: [{ model: db.User, as: 'user' }]
})
]);
// For each ZM, fetch their assigned districts in this zone
const zonalManagers = await Promise.all(zms.map(async (zmRole: any) => {
const districts = await db.Location.findAll({
where: { zoneId: zone.id, regionId: zmRole.regionId || null }, // ZMs usually managed regions or specific district sets
attributes: ['name']
});
return {
id: zmRole.user.id,
name: zmRole.user.fullName || zmRole.user.name,
email: zmRole.user.email,
phone: zmRole.user.mobileNumber || 'N/A',
districts: districts.map((d: any) => d.name)
};
}));
zoneJson.regionCount = (zone.regions || []).length;
zoneJson.districtCount = (zone.districts || []).length;
zoneJson.states = (zone.states || []).map((s: any) => s.name.toUpperCase());
zoneJson.zmCount = zms.length;
zoneJson.zonalBusinessHead = zbhAssignment ? {
id: zbhAssignment.user.id,
name: zbhAssignment.user.fullName || zbhAssignment.user.name,
email: zbhAssignment.user.email,
phone: zbhAssignment.user.mobileNumber || 'N/A'
} : null;
zoneJson.zonalManagers = zonalManagers;
return zoneJson;
}));
res.json({ success: true, data: result });
} 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) => {
try {
const { name, code, stateIds, managerId } = req.body;
if (!name) return res.status(400).json({ success: false, message: 'Zone name is required' });
const zone = await db.Zone.create({ name, code });
// 1. Assign ZBH
if (managerId) {
const zbhRole = await db.Role.findOne({ where: { roleCode: 'ZBH' } });
if (zbhRole) {
await db.UserRole.update({ isActive: false }, { where: { zoneId: zone.id, roleId: zbhRole.id } });
await db.UserRole.create({
userId: managerId,
roleId: zbhRole.id,
zoneId: zone.id,
isActive: true,
isPrimary: true
});
}
}
if (Array.isArray(stateIds) && stateIds.length > 0) {
await db.State.update({ zoneId: zone.id }, { where: { id: { [db.Sequelize.Op.in]: stateIds } } });
await db.Location.update({ zoneId: zone.id }, { where: { stateId: { [db.Sequelize.Op.in]: stateIds } } });
}
res.status(201).json({ success: true, message: 'Zone created', data: zone });
} catch (error) {
console.error('Create zone error:', error);
res.status(500).json({ success: false, message: 'Error creating zone' });
}
};
export const updateZone = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { name, code, stateIds, managerId } = req.body;
const zone = await db.Zone.findByPk(id);
if (!zone) return res.status(404).json({ success: false, message: 'Zone not found' });
await zone.update({ name, code });
// 1. Update ZBH
if (managerId) {
const zbhRole = await db.Role.findOne({ where: { roleCode: 'ZBH' } });
if (zbhRole) {
// Deactivate old ZBHs for this zone
await db.UserRole.update({ isActive: false }, {
where: { zoneId: id, roleId: zbhRole.id }
});
// Assign new ZBH
await db.UserRole.create({
userId: managerId,
roleId: zbhRole.id,
zoneId: id,
isActive: true,
isPrimary: true
});
}
}
if (Array.isArray(stateIds)) {
await db.State.update({ zoneId: null }, { where: { zoneId: id } });
await db.Location.update({ zoneId: null }, { where: { zoneId: id } });
if (stateIds.length > 0) {
await db.State.update({ zoneId: id }, { where: { id: { [db.Sequelize.Op.in]: stateIds } } });
await db.Location.update({ zoneId: id }, { where: { stateId: { [db.Sequelize.Op.in]: stateIds } } });
}
}
res.json({ success: true, message: 'Zone updated' });
} catch (error) {
console.error('Update zone error:', error);
res.status(500).json({ success: false, message: 'Error updating zone' });
}
};
// --- States ---
export const getStates = async (req: Request, res: Response) => {
try {
const states = await db.State.findAll({
include: [{ model: db.Zone, as: 'zone', attributes: ['name'] }],
order: [['name', 'ASC']]
});
res.json({ success: true, data: 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) => {
try {
const { name, zoneId } = req.body;
if (!name) return res.status(400).json({ success: false, message: 'State name is required' });
const state = await db.State.create({ name, zoneId });
if (zoneId) {
await db.Location.update({ zoneId }, { where: { stateId: state.id } });
}
res.status(201).json({ success: true, data: state });
} catch (error) {
console.error('Create state error:', error);
res.status(500).json({ success: false, message: 'Error creating state' });
}
};
// --- Managers ---
export const getManagersByRole = async (req: Request, res: Response) => { export const getManagersByRole = async (req: Request, res: Response) => {
try { try {
const { roleCode, locationId } = req.query as any; const { roleCode, locationId } = req.query as any;
const managers = await User.findAll({ const managers = await User.findAll({
attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode', 'locationId'], attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode'],
include: [{ include: [
model: db.Location, {
as: 'location', model: db.UserRole,
attributes: ['id', 'name', 'type'], as: 'userRoles',
include: [ include: [{ model: db.Role, as: 'role' }]
{
model: db.Location,
as: 'parents',
through: { attributes: [] },
attributes: ['id', 'name', 'type'],
include: [
{
model: db.Location,
as: 'parents',
through: { attributes: [] },
attributes: ['id', 'name', 'type'],
include: [
{
model: db.Location,
as: 'parents',
through: { attributes: [] },
attributes: ['id', 'name', 'type'],
include: [
{
model: db.Location,
as: 'parents',
through: { attributes: [] },
attributes: ['id', 'name', 'type']
}
]
}
]
}
]
}
]
},
{
model: db.UserRole,
as: 'userRoles',
attributes: ['id', 'locationId', 'managerCode', 'isPrimary', 'isActive'],
include: [
{
model: db.Role,
as: 'role',
attributes: ['id', 'roleCode', 'roleName']
},
{
model: db.Location,
as: 'location',
attributes: ['id', 'name', 'type'],
include: [
{
model: db.Location,
as: 'parents',
through: { attributes: [] },
attributes: ['id', 'name', 'type']
}
]
}
]
}]
});
const filteredManagers = managers.filter((m: any) => {
const assignments = Array.isArray(m.userRoles) ? m.userRoles : [];
const hasRole = !roleCode || m.roleCode === roleCode || assignments.some((a: any) => a.role?.roleCode === roleCode);
const hasLocation = !locationId || m.locationId === locationId || assignments.some((a: any) => a.locationId === locationId);
return hasRole && hasLocation;
}).map((m: any) => {
const assignments = Array.isArray(m.userRoles) ? m.userRoles : [];
const asmAssignments = assignments.filter((a: any) =>
(a.role?.roleCode === 'ASM' || m.roleCode === 'ASM') && a.location?.type === 'area'
);
const asmCode = assignments.find((a: any) =>
(a.role?.roleCode === 'ASM' || m.roleCode === 'ASM') && a.managerCode
)?.managerCode || null;
const result = m.toJSON();
result.asmCode = asmCode;
result.areaManagers = asmAssignments.map((a: any) => ({
area: {
id: a.location.id,
areaName: a.location.name,
district: (a.location.parents || []).find((p: any) => p.type === 'district') || null,
state: (a.location.parents || []).find((p: any) => p.type === 'state') || null
} }
})); ]
return result; });
const filteredManagers = managers.filter((m: any) => {
const hasRole = !roleCode || m.roleCode === roleCode || (m.userRoles || []).some((a: any) => a.role?.roleCode === roleCode);
const hasLocation = !locationId || (m.userRoles || []).some((a: any) =>
a.locationId === locationId ||
a.zoneId === locationId ||
a.regionId === locationId
);
return hasRole && hasLocation;
}); });
res.json({ success: true, data: filteredManagers }); res.json({ success: true, data: filteredManagers });
} catch (error) { } catch (error) {
console.error('Get managers error:', error); console.error('Get managers error:', error);
@ -360,14 +406,45 @@ export const getAreaManagers = async (req: Request, res: Response) => {
return getManagersByRole(req, res); return getManagersByRole(req, res);
}; };
// --- Delete ---
export const deleteLocation = async (req: Request, res: Response) => { export const deleteLocation = async (req: Request, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
await db.LocationHierarchy.destroy({ where: { [db.Sequelize.Op.or]: [{ locationId: id }, { parentId: id }] } });
await db.Location.destroy({ where: { id } }); await db.Location.destroy({ where: { id } });
res.json({ success: true, message: 'Location deleted' }); res.json({ success: true, message: 'District deleted' });
} catch (error) { } catch (error) {
console.error('Delete location error:', error); console.error('Delete district error:', error);
res.status(500).json({ success: false, message: 'Error deleting location' }); res.status(500).json({ success: false, message: 'Error deleting district' });
} }
}; };
export const updateLocation = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { name, code, stateId, regionId, zoneId, asmId, isActive, description } = req.body;
const district = await db.Location.findByPk(id);
if (!district) return res.status(404).json({ success: false, message: 'District not found' });
await district.update({
name,
code,
stateId,
regionId,
zoneId,
asmId,
isActive,
description
});
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' });
}
};
// --- Semantic Aliases for Backward Compatibility ---
export const getAreas = getDistricts;
export const createArea = createDistrict;
export const deleteArea = deleteLocation;
export const createDistrictLegacy = createDistrict; // Just in case

View File

@ -1,51 +0,0 @@
import express from 'express';
const router = express.Router();
import * as masterController from './master.controller.js';
import * as outletController from './outlet.controller.js';
import { authenticate } from '../../common/middleware/auth.js';
import { checkRole } from '../../common/middleware/roleCheck.js';
import { ROLES } from '../../common/config/constants.js';
// States
router.get('/states', masterController.getStates);
// Districts
router.get('/districts', masterController.getDistricts);
// All routes require authentication
router.use(authenticate as any);
// Regions
router.get('/regions', masterController.getRegions);
router.post('/regions', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createRegion);
router.put('/regions/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// Zones
router.get('/zones', masterController.getZones);
router.post('/zones', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN, ROLES.DD_LEAD]) as any, masterController.createZone);
router.put('/zones/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// States (Update only)
router.post('/states', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createState);
router.put('/states/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// Districts (Update only)
router.post('/districts', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createDistrict);
router.put('/districts/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
// Areas
router.get('/areas', masterController.getAreas);
router.post('/areas', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.createArea);
router.put('/areas/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.updateLocation);
router.delete('/locations/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, masterController.deleteLocation);
// Area Managers
router.get('/area-managers', masterController.getAreaManagers);
// Outlets
router.get('/outlets', outletController.getOutlets);
router.get('/outlets/code/:code', outletController.getOutletByCode);
router.get('/outlets/:id', outletController.getOutletById);
router.post('/outlets', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, outletController.createOutlet);
router.put('/outlets/:id', checkRole([ROLES.SUPER_ADMIN, ROLES.DD_ADMIN]) as any, outletController.updateOutlet);
export default router;