diff --git a/package.json b/package.json index a637c81..64037ae 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "tsc", "type-check": "tsc --noEmit", "migrate": "tsx scripts/migrate.ts", + "reset:stable": "tsx scripts/reset_db_stable.ts", "seed": "tsx scripts/seed_normalized_data.ts", "seed:approval-policies": "tsx scripts/seed-approval-policies.ts", "seed:all": "npm run seed && npm run seed:approval-policies && npm run seed:questionnaire", diff --git a/scripts/reset_db_stable.ts b/scripts/reset_db_stable.ts new file mode 100644 index 0000000..33d8aa7 --- /dev/null +++ b/scripts/reset_db_stable.ts @@ -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(); diff --git a/scripts/seed_normalized_data.ts b/scripts/seed_normalized_data.ts index 644eafe..0e6bc72 100644 --- a/scripts/seed_normalized_data.ts +++ b/scripts/seed_normalized_data.ts @@ -2,17 +2,15 @@ import 'dotenv/config'; import db from '../src/database/models/index.js'; import bcrypt from 'bcryptjs'; -const { Role, Location, LocationHierarchy, User, UserRole } = db; +const { Role, Zone, Region, State, Location, User, UserRole } = db; 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(); + // Use sync with alter false to match main app behavior await db.sequelize.sync({ alter: false }); - // Hash default password for test users const hashedPassword = await bcrypt.hash('Admin@123', 10); // 1. Create Roles @@ -22,6 +20,7 @@ async function seed() { { roleCode: 'ZBH', roleName: 'Zonal Business Head', category: 'ZONAL' }, { roleCode: 'DD Lead', roleName: 'DD Lead', category: 'ZONAL' }, { roleCode: 'RBM', roleName: 'Regional Business Manager', category: 'REGIONAL' }, + { roleCode: 'RM', roleName: 'Regional Manager', category: 'REGIONAL' }, { roleCode: 'DD-ZM', roleName: 'DD Zonal Manager', category: 'ZONAL' }, { roleCode: 'ASM', roleName: 'Area Sales Manager', category: 'AREA' }, { roleCode: 'Super Admin', roleName: 'Super Admin', category: 'NATIONAL' }, @@ -36,76 +35,54 @@ async function seed() { } console.log('Roles seeded.'); - // 2. Create Locations - const existingZones = await Location.findAll({ - where: { type: 'zone' }, - order: [['createdAt', 'ASC']] + // 2. Create Locations (Hierarchy) + const [zone1] = await Zone.findOrCreate({ + where: { name: 'North Zone' }, + defaults: { name: 'North Zone', code: 'ZONE-N' } }); - let zone1: any = existingZones[0]; - let zone2: any = existingZones[1]; - - 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 [zone2] = await Zone.findOrCreate({ + where: { name: 'South Zone' }, + defaults: { name: 'South Zone', code: 'ZONE-S' } }); - const [region2] = await Location.findOrCreate({ - where: { name: 'Bangalore Region', type: 'region' }, - defaults: { name: 'Bangalore Region', type: 'region' } + const [state1] = await State.findOrCreate({ + where: { name: 'Delhi' }, + defaults: { name: 'Delhi', zoneId: zone1.id } }); - console.log('Locations created.'); - - // 3. Create Hierarchies (Bridge Table) - 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 } + const [region1] = await Region.findOrCreate({ + where: { name: 'NCR Region' }, + defaults: { name: 'NCR Region', zoneId: zone1.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 } }); if (role) { await UserRole.findOrCreate({ where: { userId: userRec.id, roleId: role.id, - locationId: locationId || null + ...assignment }, defaults: { userId: userRec.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 // Custom Seed Users - const nbhUser = await User.findOrCreate({ + const nbhResult = await User.findOrCreate({ where: { email: 'nbh@example.com' }, 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' }, 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' }, - 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' }, 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 = [ - { email: 'ddlead@royalenfield.com', name: 'Meera Iyer', roleCode: 'DD Lead', location: zone1.id }, - { email: 'finance@royalenfield.com', name: 'Rahul Verma', roleCode: 'Finance', location: null }, - { email: 'dealer@royalenfield.com', name: 'Amit Sharma', roleCode: 'Dealer', location: area1.id, isExt: true }, - { email: 'admin@royalenfield.com', name: 'Laxman H', roleCode: 'DD Lead', location: zone2.id }, - { email: 'yashwin@gmail.com', name: 'Yashwin', roleCode: 'ZBH', location: zone1.id }, - { email: 'kenil@gmail.com', name: 'Kenil', roleCode: 'DD Lead', location: zone1.id }, - { email: 'lince@gmail.com', name: 'Lince', roleCode: 'DD Admin', location: null } + { email: 'ddlead@royalenfield.com', name: 'Meera Iyer', roleCode: 'DD Lead', assignment: { zoneId: zone1.id } }, + { email: 'finance@royalenfield.com', name: 'Rahul Verma', roleCode: 'Finance', assignment: {} }, + { 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', assignment: { zoneId: zone2.id } }, + { email: 'yashwin@gmail.com', name: 'Yashwin', roleCode: 'ZBH', assignment: { zoneId: zone1.id } }, + { email: 'kenil@gmail.com', name: 'Kenil', roleCode: 'DD Lead', assignment: { zoneId: zone1.id } }, + { email: 'lince@gmail.com', name: 'Lince', roleCode: 'DD Admin', assignment: {} } ]; for (const m of mockUsers) { - const u = await User.findOrCreate({ + const [u] = await User.findOrCreate({ where: { email: m.email }, defaults: { fullName: m.name, @@ -159,7 +136,7 @@ async function seed() { status: 'active' } }); - await mapUserRole(u[0], m.roleCode, m.location); + await mapUserRole(u, m.roleCode, m.assignment); } console.log('Users and Mappings seeded.'); diff --git a/scripts/seed_real_locations.ts b/scripts/seed_real_locations.ts index 7343bb4..32931b0 100644 --- a/scripts/seed_real_locations.ts +++ b/scripts/seed_real_locations.ts @@ -7,78 +7,80 @@ import db from '../src/database/models/index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const { Location, LocationHierarchy } = db; +const { Zone, State, Location } = db; async function run() { - console.log('--- Migrating Real Geo Data to Normalized Location Models ---'); + console.log('--- Seeding Real Geo Data (Denormalized Model) ---'); 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 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 statesMatch = content.match(/const STATES_DATA = \[([\s\S]*?)\];/); const citiesMatch = content.match(/const CITIES_DATA = \[([\s\S]*?)\];/); 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 STATES_DATA = eval(`[${statesMatch[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 - const zoneIdMap = new Map(); + // 1. Seed Zones + const zoneIdMap = new Map(); // Name -> UUID for (const z of ZONES_DATA) { - const [loc] = await Location.findOrCreate({ - where: { name: z.name, type: 'zone' }, - defaults: { name: z.name, type: 'zone' } + const [zoneRecord] = await Zone.findOrCreate({ + where: { name: z.name }, + defaults: { name: z.name, code: z.code } }); - zoneIdMap.set(z.code, loc.id); - z._dbId = loc.id; + zoneIdMap.set(z.name, zoneRecord.id); + // Attach states list for later lookup + z._dbId = zoneRecord.id; } + console.log('Zones seeded.'); - // 2. Insert States and link to Zones - const stateIdMap = new Map(); + // 2. Seed States and link to Zones + const stateIdMap = new Map(); // Legacy ID -> { id, zoneId } for (const s of STATES_DATA) { - const [loc] = await Location.findOrCreate({ - where: { name: s.name, type: 'state' }, - defaults: { name: s.name, type: 'state' } + // Find parent zone by checking which zone's states array contains this state name + const parentZoneData = ZONES_DATA.find((z: any) => z.states.includes(s.name)); + 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 - 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 } - }); - } + + stateIdMap.set(s.id, { id: stateRecord.id, zoneId: zoneId }); } + console.log('States seeded.'); - // 3. Insert Cities (Districts) and link to States - let cityCount = 0; + // 3. Seed Districts (Locations) + let districtCount = 0; for (const c of CITIES_DATA) { - const stateDbId = stateIdMap.get(c.state_id); - if (stateDbId) { - const [loc] = await Location.findOrCreate({ - where: { name: c.name, type: 'district' }, - defaults: { name: c.name, type: 'district' } + const parentStateData = stateIdMap.get(c.state_id); + if (parentStateData) { + await Location.findOrCreate({ + where: { name: c.name, stateId: parentStateData.id }, + defaults: { + name: c.name, + stateId: parentStateData.id, + zoneId: parentStateData.zoneId + } }); - await LocationHierarchy.findOrCreate({ - where: { locationId: loc.id, parentId: stateDbId }, - defaults: { locationId: loc.id, parentId: stateDbId } - }); - cityCount++; + districtCount++; } } - 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); } catch (e: any) { diff --git a/src/database/models/Location.ts b/src/database/models/Location.ts index 15a6425..bb2e2c9 100644 --- a/src/database/models/Location.ts +++ b/src/database/models/Location.ts @@ -3,12 +3,13 @@ import { Model, DataTypes, Sequelize } from 'sequelize'; export interface LocationAttributes { id: string; name: string; - type: 'zone' | 'region' | 'area' | 'state' | 'district'; code?: string; - pincode?: string; + stateId?: string | null; + regionId?: string | null; + zoneId?: string | null; + asmId?: string | null; isActive?: boolean; - activeFrom?: string | Date | null; - activeTo?: string | Date | null; + description?: string | null; } export interface LocationInstance extends Model, LocationAttributes { } @@ -24,50 +25,68 @@ export default (sequelize: Sequelize) => { type: DataTypes.STRING, allowNull: false }, - type: { - type: DataTypes.ENUM('zone', 'region', 'area', 'state', 'district'), - allowNull: false - }, code: { type: DataTypes.STRING, - allowNull: true + allowNull: true, + unique: true }, - pincode: { - type: DataTypes.STRING, - allowNull: true + stateId: { + type: DataTypes.UUID, + 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: { type: DataTypes.BOOLEAN, defaultValue: true }, - activeFrom: { - type: DataTypes.DATE, - allowNull: true - }, - activeTo: { - type: DataTypes.DATE, + description: { + type: DataTypes.TEXT, allowNull: true } }, { tableName: 'locations', - timestamps: true + timestamps: true, + indexes: [ + { fields: ['stateId'] }, + { fields: ['regionId'] }, + { fields: ['zoneId'] }, + { unique: true, fields: ['name', 'stateId'] } + ] }); (Location as any).associate = (models: any) => { - // Many-to-Many hierarchy via LocationHierarchy bridge table - Location.belongsToMany(models.Location, { - through: models.LocationHierarchy, - as: 'parents', - foreignKey: 'locationId', - otherKey: 'parentId' - }); - Location.belongsToMany(models.Location, { - through: models.LocationHierarchy, - as: 'children', - foreignKey: 'parentId', - otherKey: 'locationId' - }); - + Location.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' }); + Location.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' }); + Location.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' }); + Location.belongsTo(models.User, { foreignKey: 'asmId', as: 'asm' }); + Location.hasMany(models.User, { foreignKey: 'locationId', as: 'users' }); Location.hasMany(models.UserRole, { foreignKey: 'locationId', as: 'userRoles' }); Location.hasMany(models.Application, { foreignKey: 'locationId', as: 'applications' }); }; diff --git a/src/database/models/Region.ts b/src/database/models/Region.ts new file mode 100644 index 0000000..323dc80 --- /dev/null +++ b/src/database/models/Region.ts @@ -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 { } + +export default (sequelize: Sequelize) => { + const Region = sequelize.define('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; +}; diff --git a/src/database/models/State.ts b/src/database/models/State.ts new file mode 100644 index 0000000..b6da721 --- /dev/null +++ b/src/database/models/State.ts @@ -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 { } + +export default (sequelize: Sequelize) => { + const State = sequelize.define('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; +}; diff --git a/src/database/models/UserRole.ts b/src/database/models/UserRole.ts index 32e7e21..81e237e 100644 --- a/src/database/models/UserRole.ts +++ b/src/database/models/UserRole.ts @@ -4,7 +4,9 @@ export interface UserRoleAttributes { id: string; userId: string; roleId: string; - locationId: string | null; + locationId: string | null; // District + zoneId: string | null; + regionId: string | null; managerCode: string | null; isPrimary: boolean; isActive: boolean; @@ -47,6 +49,22 @@ export default (sequelize: Sequelize) => { 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: { type: DataTypes.STRING, allowNull: true @@ -98,6 +116,14 @@ export default (sequelize: Sequelize) => { foreignKey: 'locationId', as: 'location' }); + UserRole.belongsTo(models.Zone, { + foreignKey: 'zoneId', + as: 'zone' + }); + UserRole.belongsTo(models.Region, { + foreignKey: 'regionId', + as: 'region' + }); UserRole.belongsTo(models.User, { foreignKey: 'assignedBy', as: 'assigner' diff --git a/src/database/models/Zone.ts b/src/database/models/Zone.ts new file mode 100644 index 0000000..b130ba6 --- /dev/null +++ b/src/database/models/Zone.ts @@ -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 { } + +export default (sequelize: Sequelize) => { + const Zone = sequelize.define('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; +}; diff --git a/src/database/models/index.ts b/src/database/models/index.ts index 3ade843..fc50cf8 100644 --- a/src/database/models/index.ts +++ b/src/database/models/index.ts @@ -20,7 +20,9 @@ import createSLAEscalationConfig from './SLAEscalationConfig.js'; import createWorkflowStageConfig from './WorkflowStageConfig.js'; import createNotification from './Notification.js'; import createLocation from './Location.js'; -import createLocationHierarchy from './LocationHierarchy.js'; +import createZone from './Zone.js'; +import createRegion from './Region.js'; +import createState from './State.js'; // Batch 1: Organizational Hierarchy & User Management import createRole from './Role.js'; @@ -122,7 +124,9 @@ db.SLAEscalationConfig = createSLAEscalationConfig(sequelize); db.WorkflowStageConfig = createWorkflowStageConfig(sequelize); db.Notification = createNotification(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 db.Role = createRole(sequelize); diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index bdef007..a8b595d 100644 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -169,26 +169,25 @@ export const getAllUsers = async (req: Request, res: Response) => { (Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && nationalRoles.includes(r))); if (!isNationalRole && locationId) { - // Find all ancestors of the given location (BFS for many-to-many support) - let ancestorIds: Set = new Set([locationId as string]); - let queue: string[] = [locationId as string]; - - while (queue.length > 0) { - const currentId = queue.shift()!; - const hierarchies = await db.LocationHierarchy.findAll({ - where: { locationId: currentId } - }); - - for (const h of hierarchies) { - if (!ancestorIds.has(h.parentId)) { - ancestorIds.add(h.parentId); - queue.push(h.parentId); - } - } + const district: any = await db.Location.findByPk(locationId as string, { + attributes: ['id', 'zoneId', 'regionId', 'stateId'] + }); + + if (district) { + const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean); + whereClause.locationId = { [Op.in]: relevantIds }; } - 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({ where: whereClause, attributes: { exclude: ['password'] }, @@ -216,7 +215,62 @@ export const getAllUsers = async (req: Request, res: Response) => { ], 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) { console.error('Get users error:', error); 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, locationId, assignments, + districts, // New: ASM managed areas/districts + asmCode, // New: ASM code to store in managerCode password // Optional password update } = req.body; @@ -380,7 +436,46 @@ export const updateUser = async (req: AuthRequest, res: Response) => { if (Array.isArray(assignments)) { 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; if (primaryRoleCode) { const role = await Role.findOne({ where: { roleCode: primaryRoleCode } }); diff --git a/src/modules/master/master.controller.ts b/src/modules/master/master.controller.ts index 8c3d586..026e982 100644 --- a/src/modules/master/master.controller.ts +++ b/src/modules/master/master.controller.ts @@ -2,352 +2,398 @@ import { Request, Response } from 'express'; import db from '../../database/models/index.js'; const { User } = db; -// --- Generic Location Fetching --- -const getLocationsByType = async (type: string, req: Request, res: Response) => { +// --- Districts (Locations) --- +export const getDistricts = async (req: Request, res: Response) => { try { - const locations = await db.Location.findAll({ - where: { type }, + const districts = await db.Location.findAll({ include: [ - { - model: db.Location, - as: 'parents', - through: { attributes: [] } - }, - { - model: db.Location, - as: 'children', - through: { attributes: [] } - } + { model: db.Zone, as: 'zone', attributes: ['name'] }, + { model: db.Region, as: 'region', attributes: ['name'] }, + { model: db.State, as: 'state', attributes: ['name'] }, + { model: db.User, as: 'asm', attributes: ['id', 'fullName', 'email'] } ], order: [['name', 'ASC']] }); - res.json({ success: true, data: locations }); - } catch (error) { - console.error(`Get ${type} list error:`, error); - res.status(500).json({ success: false, message: `Error fetching ${type} list` }); - } -}; - -// --- Regions --- -export const getRegions = async (req: Request, res: Response) => { - 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 }); - } + 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' + })); - 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' }); + res.json({ success: true, data: result }); } catch (error) { - console.error('Update location error:', error); - res.status(500).json({ success: false, message: 'Error updating location' }); + console.error('Get districts error:', error); + res.status(500).json({ success: false, message: 'Error fetching districts' }); } }; -// --- 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) => { try { - const { stateId, districtName } = req.body; - if (!districtName) return res.status(400).json({ success: false, message: 'District name is required' }); - - const district = await db.Location.create({ name: districtName, type: 'district' }); + const { name, code, stateId, regionId, zoneId, asmId, description } = req.body; + if (!name) return res.status(400).json({ success: false, message: 'District name is required' }); - if (stateId) { - await db.LocationHierarchy.create({ locationId: district.id, parentId: stateId }); - } + const district = await db.Location.create({ + name, + code, + stateId, + regionId, + zoneId, + asmId, + description + }); - res.status(201).json({ success: true, message: 'District created', data: district }); + res.status(201).json({ success: true, data: district }); } catch (error) { console.error('Create district error:', error); res.status(500).json({ success: false, message: 'Error creating district' }); } }; -// --- Areas --- -export const getAreas = async (req: Request, res: Response) => { - return getLocationsByType('area', req, res); -}; - -export const createArea = async (req: Request, res: Response) => { +// --- Regions --- +export const getRegions = async (req: Request, res: Response) => { try { - // Intercept all legacy property keys matching the MasterPage payload. - const { districtId, areaName, city, pincode, areaCode, isActive, activeFrom, activeTo } = req.body; - if (!areaName) return res.status(400).json({ success: false, message: 'Area name is required' }); - - const area = await db.Location.create({ - name: areaName, - type: 'area', - code: areaCode, - pincode: pincode, - isActive: isActive !== undefined ? isActive : true, - activeFrom: activeFrom || null, - activeTo: activeTo || null + const regions = await db.Region.findAll({ + include: [ + { model: db.Zone, as: 'zone', attributes: ['name'] }, + { model: db.Location, as: 'districts', attributes: ['id', 'name'] } + ], + order: [['name', 'ASC']] }); - if (districtId) { - await db.LocationHierarchy.create({ locationId: area.id, parentId: districtId }); - } + const roles = await db.Role.findAll({ + 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) { - console.error('Create area error:', error); - res.status(500).json({ success: false, message: 'Error creating area' }); + console.error('Get regions error:', error); + 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) => { try { const { roleCode, locationId } = req.query as any; const managers = await User.findAll({ - attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode', 'locationId'], - include: [{ - model: db.Location, - as: 'location', - 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'], - 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 + attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode'], + include: [ + { + model: db.UserRole, + as: 'userRoles', + include: [{ model: db.Role, as: 'role' }] } - })); - 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 }); } catch (error) { console.error('Get managers error:', error); @@ -360,14 +406,45 @@ export const getAreaManagers = async (req: Request, res: Response) => { return getManagersByRole(req, res); }; +// --- Delete --- export const deleteLocation = async (req: Request, res: Response) => { try { const { id } = req.params; - await db.LocationHierarchy.destroy({ where: { [db.Sequelize.Op.or]: [{ locationId: id }, { parentId: id }] } }); await db.Location.destroy({ where: { id } }); - res.json({ success: true, message: 'Location deleted' }); + res.json({ success: true, message: 'District deleted' }); } catch (error) { - console.error('Delete location error:', error); - res.status(500).json({ success: false, message: 'Error deleting location' }); + console.error('Delete district error:', error); + 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 diff --git a/src/modules/master/master.routes.ts b/src/modules/master/master.routes.ts index c041afd..e69de29 100644 --- a/src/modules/master/master.routes.ts +++ b/src/modules/master/master.routes.ts @@ -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;