differnt apprach aded for hirarchy
This commit is contained in:
parent
6edb5da84f
commit
2e1e96cc54
@ -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",
|
||||||
|
|||||||
90
scripts/reset_db_stable.ts
Normal file
90
scripts/reset_db_stable.ts
Normal 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();
|
||||||
@ -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.');
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
|
||||||
stateIdMap.set(s.id, loc.id);
|
|
||||||
|
|
||||||
// Find which zone string array it belongs to
|
const [stateRecord] = await State.findOrCreate({
|
||||||
const parentZone = ZONES_DATA.find((z: any) => z.states.includes(s.name));
|
where: { name: s.name },
|
||||||
if (parentZone) {
|
defaults: { name: s.name, zoneId: zoneId }
|
||||||
await LocationHierarchy.findOrCreate({
|
|
||||||
where: { locationId: loc.id, parentId: parentZone._dbId },
|
|
||||||
defaults: { locationId: loc.id, parentId: parentZone._dbId }
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Insert Cities (Districts) and link to States
|
stateIdMap.set(s.id, { id: stateRecord.id, zoneId: zoneId });
|
||||||
let cityCount = 0;
|
}
|
||||||
|
console.log('States seeded.');
|
||||||
|
|
||||||
|
// 3. Seed Districts (Locations)
|
||||||
|
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) {
|
||||||
|
|||||||
@ -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' });
|
||||||
};
|
};
|
||||||
|
|||||||
53
src/database/models/Region.ts
Normal file
53
src/database/models/Region.ts
Normal 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;
|
||||||
|
};
|
||||||
42
src/database/models/State.ts
Normal file
42
src/database/models/State.ts
Normal 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;
|
||||||
|
};
|
||||||
@ -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'
|
||||||
|
|||||||
45
src/database/models/Zone.ts
Normal file
45
src/database/models/Zone.ts
Normal 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;
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
|||||||
@ -169,25 +169,24 @@ 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) {
|
|
||||||
const currentId = queue.shift()!;
|
|
||||||
const hierarchies = await db.LocationHierarchy.findAll({
|
|
||||||
where: { locationId: currentId }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const h of hierarchies) {
|
if (district) {
|
||||||
if (!ancestorIds.has(h.parentId)) {
|
const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean);
|
||||||
ancestorIds.add(h.parentId);
|
whereClause.locationId = { [Op.in]: relevantIds };
|
||||||
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,
|
||||||
@ -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 } });
|
||||||
|
|||||||
@ -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: [{
|
|
||||||
model: db.Location,
|
|
||||||
as: 'location',
|
|
||||||
attributes: ['id', 'name', 'type'],
|
|
||||||
include: [
|
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'],
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: db.Location,
|
|
||||||
as: 'parents',
|
|
||||||
through: { attributes: [] },
|
|
||||||
attributes: ['id', 'name', 'type']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
model: db.UserRole,
|
model: db.UserRole,
|
||||||
as: 'userRoles',
|
as: 'userRoles',
|
||||||
attributes: ['id', 'locationId', 'managerCode', 'isPrimary', 'isActive'],
|
include: [{ model: db.Role, as: 'role' }]
|
||||||
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 filteredManagers = managers.filter((m: any) => {
|
||||||
const assignments = Array.isArray(m.userRoles) ? m.userRoles : [];
|
const hasRole = !roleCode || m.roleCode === roleCode || (m.userRoles || []).some((a: any) => a.role?.roleCode === roleCode);
|
||||||
const hasRole = !roleCode || m.roleCode === roleCode || assignments.some((a: any) => a.role?.roleCode === roleCode);
|
const hasLocation = !locationId || (m.userRoles || []).some((a: any) =>
|
||||||
const hasLocation = !locationId || m.locationId === locationId || assignments.some((a: any) => a.locationId === locationId);
|
a.locationId === locationId ||
|
||||||
return hasRole && hasLocation;
|
a.zoneId === locationId ||
|
||||||
}).map((m: any) => {
|
a.regionId === locationId
|
||||||
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) =>
|
return hasRole && hasLocation;
|
||||||
(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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@ -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;
|
|
||||||
Loading…
Reference in New Issue
Block a user