hirrachy enhanced nd creatded distric as centric and added dealer location stable ui made for the hirarchy
This commit is contained in:
parent
2e1e96cc54
commit
9b645b0480
37
.env.example
Normal file
37
.env.example
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Environment configuration
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=5000
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_WINDOW_MS=900000
|
||||||
|
RATE_LIMIT_MAX_REQUESTS=1000
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
JWT_SECRET=your-secret-key-change-in-production
|
||||||
|
JWT_EXPIRE=7d
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_NAME=royal_enfield_onboarding
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_SSL=false
|
||||||
|
|
||||||
|
# Email Configuration
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
EMAIL_PORT=587
|
||||||
|
EMAIL_SECURE=true
|
||||||
|
EMAIL_USER=your-email@gmail.com
|
||||||
|
EMAIL_PASSWORD=your-app-password
|
||||||
|
EMAIL_FROM="Royal Enfield <noreply@royalenfield.com>"
|
||||||
|
|
||||||
|
# Web Push Notifications (VAPID)
|
||||||
|
VAPID_PUBLIC_KEY=your_vapid_public_key
|
||||||
|
VAPID_PRIVATE_KEY=your_vapid_private_key
|
||||||
|
VAPID_EMAIL=admin@royalenfield.com
|
||||||
|
|
||||||
|
# File Uploads
|
||||||
|
UPLOAD_DIR=./uploads
|
||||||
|
MAX_FILE_SIZE=10485760
|
||||||
32
all_perms.json
Normal file
32
all_perms.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[
|
||||||
|
"action:approve",
|
||||||
|
"action:reject",
|
||||||
|
"action:upload_docs",
|
||||||
|
"action:request_changes",
|
||||||
|
"action:forward",
|
||||||
|
"action:reassign",
|
||||||
|
"action:schedule_interview",
|
||||||
|
"action:add_comments",
|
||||||
|
"action:rank_applicants",
|
||||||
|
"action:final_approval",
|
||||||
|
"view:view_details",
|
||||||
|
"view:view_financial",
|
||||||
|
"view:view_discussions",
|
||||||
|
"view:view_progress",
|
||||||
|
"view:view_audit",
|
||||||
|
"view:view_documents",
|
||||||
|
"view:view_personal",
|
||||||
|
"view:view_business",
|
||||||
|
"view:view_reports",
|
||||||
|
"view:view_history",
|
||||||
|
"stage:initial_review",
|
||||||
|
"stage:field_verification",
|
||||||
|
"stage:level1_interview",
|
||||||
|
"stage:level2_interview",
|
||||||
|
"stage:ranking",
|
||||||
|
"stage:legal_review",
|
||||||
|
"stage:financial_review",
|
||||||
|
"stage:final_approval",
|
||||||
|
"stage:payment",
|
||||||
|
"stage:onboarding"
|
||||||
|
]
|
||||||
32
audit_db.mjs
Normal file
32
audit_db.mjs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import db from './src/database/models/index.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
async function dump() {
|
||||||
|
try {
|
||||||
|
const roles = await db.Role.findAll({
|
||||||
|
include: [{
|
||||||
|
model: db.Permission,
|
||||||
|
as: 'permissions',
|
||||||
|
through: { attributes: [] }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const data = roles.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.roleName,
|
||||||
|
permissions: r.permissions.map(p => p.permissionCode)
|
||||||
|
}));
|
||||||
|
fs.writeFileSync('db_audit.json', JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
const perms = await db.Permission.findAll();
|
||||||
|
fs.writeFileSync('all_perms.json', JSON.stringify(perms.map(p => p.permissionCode), null, 2));
|
||||||
|
|
||||||
|
console.log('Audit complete');
|
||||||
|
} catch (e) {
|
||||||
|
fs.writeFileSync('db_error.txt', e.stack);
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dump();
|
||||||
32
audit_db.ts
Normal file
32
audit_db.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import db from './src/database/models/index.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
async function dump() {
|
||||||
|
try {
|
||||||
|
const roles = await (db as any).Role.findAll({
|
||||||
|
include: [{
|
||||||
|
model: (db as any).Permission,
|
||||||
|
as: 'permissions',
|
||||||
|
through: { attributes: [] }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const data = roles.map((r: any) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.roleName,
|
||||||
|
permissions: r.permissions.map((p: any) => p.permissionCode)
|
||||||
|
}));
|
||||||
|
fs.writeFileSync('db_audit.json', JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
const perms = await (db as any).Permission.findAll();
|
||||||
|
fs.writeFileSync('all_perms.json', JSON.stringify(perms.map((p: any) => p.permissionCode), null, 2));
|
||||||
|
|
||||||
|
console.log('Audit complete');
|
||||||
|
} catch (e: any) {
|
||||||
|
fs.writeFileSync('db_error.txt', e.stack || e.toString());
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dump();
|
||||||
33
check_zms_v2.ts
Normal file
33
check_zms_v2.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import db from './src/database/models/index.js';
|
||||||
|
|
||||||
|
async function checkZMs() {
|
||||||
|
try {
|
||||||
|
const users = await db.User.findAll({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.UserRole,
|
||||||
|
as: 'userRoles',
|
||||||
|
where: { isActive: true },
|
||||||
|
include: [{ model: db.Role, as: 'role' }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('--- Active Zonal Manager Roles ---');
|
||||||
|
users.forEach((u: any) => {
|
||||||
|
const zms = (u.userRoles || []).filter((ur: any) => ['ZM', 'DD-ZM', 'ZBH'].includes(ur.role?.roleCode));
|
||||||
|
if (zms.length > 0) {
|
||||||
|
console.log(`User: ${u.fullName} (ID: ${u.id}, EmployeeID: ${u.employeeId})`);
|
||||||
|
zms.forEach((zm: any) => {
|
||||||
|
console.log(` Role: ${zm.role.roleCode}, managerCode: ${zm.managerCode}, isActive: ${zm.isActive}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error checking ZMs:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkZMs();
|
||||||
67
db_audit.json
Normal file
67
db_audit.json
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "c5a8042f-47f1-4a83-a508-271fdc2774c4",
|
||||||
|
"name": "DD Zonal Manager",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0f4ecb63-49ff-45d9-a66c-a5bd99429aba",
|
||||||
|
"name": "DD Admin",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2c045771-c6a2-41f4-9ca2-29523ae9f03a",
|
||||||
|
"name": "Dealer",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1a740765-0a1f-4f36-bffd-e97a8d2609e2",
|
||||||
|
"name": "Finance",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "da14a401-44f5-471c-a564-224d6c935898",
|
||||||
|
"name": "Regional Business Manager",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "815c1ede-da74-4240-92a9-f5473cf933f7",
|
||||||
|
"name": "DD Head",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "188ce0bc-f845-4f4b-abc5-82ef4be18b68",
|
||||||
|
"name": "Regional Manager",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1cb698cc-3ee2-466b-8e9f-03b71ca4dbc3",
|
||||||
|
"name": "Area Sales Manager",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "9ccfec23-aa11-4031-8cbb-1f4ddf3a91e2",
|
||||||
|
"name": "Legal Admin",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dfb978d4-8556-470f-86e4-2b75044d37f9",
|
||||||
|
"name": "DD Lead",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1a1d91a0-c15e-477d-9381-17131a2a9966",
|
||||||
|
"name": "Super Admin",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "27c9e905-ab9d-42de-8dda-60a1c35d54b7",
|
||||||
|
"name": "Zonal Business Head",
|
||||||
|
"permissions": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ce872857-200f-4a0d-b46a-a46f8022e4dc",
|
||||||
|
"name": "National Business Head",
|
||||||
|
"permissions": []
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -12,10 +12,13 @@
|
|||||||
"migrate": "tsx scripts/migrate.ts",
|
"migrate": "tsx scripts/migrate.ts",
|
||||||
"reset:stable": "tsx scripts/reset_db_stable.ts",
|
"reset:stable": "tsx scripts/reset_db_stable.ts",
|
||||||
"seed": "tsx scripts/seed_normalized_data.ts",
|
"seed": "tsx scripts/seed_normalized_data.ts",
|
||||||
|
"seed:permissions": "tsx scripts/seed-permissions.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:email-templates": "tsx src/scripts/seed-master-emails.ts",
|
||||||
"setup:fresh": "npm run migrate && npm run seed:all",
|
"seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates",
|
||||||
"seed:real-geo": "tsx scripts/seed_real_locations.ts",
|
"setup:fresh": "npm run migrate && npm run seed:all && npm run sync:hierarchy",
|
||||||
|
"seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy",
|
||||||
|
"sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts",
|
||||||
"seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts",
|
"seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
|
|||||||
@ -1,148 +1,159 @@
|
|||||||
import 'dotenv/config';
|
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';
|
||||||
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js';
|
||||||
|
|
||||||
const { Role, Zone, Region, State, Location, User, UserRole } = db;
|
const { Role, Zone, Region, State, District, User, UserRole } = db;
|
||||||
|
|
||||||
async function seed() {
|
async function seed() {
|
||||||
console.log('--- Seeding Normalized Denormalized Data ---');
|
console.log('--- Seeding Normalized Denormalized Data ---');
|
||||||
|
|
||||||
await db.sequelize.authenticate();
|
await db.sequelize.authenticate();
|
||||||
// Use sync with alter false to match main app behavior
|
// Use sync with alter false to match main app behavior
|
||||||
await db.sequelize.sync({ alter: false });
|
await db.sequelize.sync({ alter: false });
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash('Admin@123', 10);
|
const hashedPassword = await bcrypt.hash('Admin@123', 10);
|
||||||
|
|
||||||
// 1. Create Roles
|
// 1. Create Roles
|
||||||
const roles = [
|
const roles = [
|
||||||
{ roleCode: 'NBH', roleName: 'National Business Head', category: 'NATIONAL' },
|
{ roleCode: 'NBH', roleName: 'National Business Head', category: 'NATIONAL' },
|
||||||
{ roleCode: 'DD Head', roleName: 'DD Head', category: 'NATIONAL' },
|
{ roleCode: 'DD Head', roleName: 'DD Head', category: 'NATIONAL' },
|
||||||
{ 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: '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' },
|
||||||
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
|
{ roleCode: 'Finance', roleName: 'Finance', category: 'DEPARTMENT' },
|
||||||
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
|
{ roleCode: 'Dealer', roleName: 'Dealer', category: 'EXTERNAL' },
|
||||||
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
|
{ roleCode: 'DD Admin', roleName: 'DD Admin', category: 'ADMIN' },
|
||||||
{ roleCode: 'Legal Admin', roleName: 'Legal Admin', category: 'DEPARTMENT' }
|
{ roleCode: 'Legal Admin', roleName: 'Legal Admin', category: 'DEPARTMENT' }
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const r of roles) {
|
for (const r of roles) {
|
||||||
await Role.findOrCreate({ where: { roleCode: r.roleCode }, defaults: r });
|
await Role.findOrCreate({ where: { roleCode: r.roleCode }, defaults: r });
|
||||||
}
|
}
|
||||||
console.log('Roles seeded.');
|
console.log('Roles seeded.');
|
||||||
|
|
||||||
// 2. Create Locations (Hierarchy)
|
// 2. Create Districts (Hierarchy)
|
||||||
const [zone1] = await Zone.findOrCreate({
|
const [zone1] = await Zone.findOrCreate({
|
||||||
where: { name: 'North Zone' },
|
where: { name: 'North Zone' },
|
||||||
defaults: { name: 'North Zone', code: 'ZONE-N' }
|
defaults: { name: 'North Zone', code: 'ZONE-N' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [zone2] = await Zone.findOrCreate({
|
const [zone2] = await Zone.findOrCreate({
|
||||||
where: { name: 'South Zone' },
|
where: { name: 'South Zone' },
|
||||||
defaults: { name: 'South Zone', code: 'ZONE-S' }
|
defaults: { name: 'South Zone', code: 'ZONE-S' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [state1] = await State.findOrCreate({
|
const [state1] = await State.findOrCreate({
|
||||||
where: { name: 'Delhi' },
|
where: { name: 'Delhi' },
|
||||||
defaults: { name: 'Delhi', zoneId: zone1.id }
|
defaults: { name: 'Delhi', zoneId: zone1.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [region1] = await Region.findOrCreate({
|
const [region1] = await Region.findOrCreate({
|
||||||
where: { name: 'NCR Region' },
|
where: { name: 'NCR Region' },
|
||||||
defaults: { name: 'NCR Region', zoneId: zone1.id }
|
defaults: { name: 'NCR Region', zoneId: zone1.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [region2] = await Region.findOrCreate({
|
const [region2] = await Region.findOrCreate({
|
||||||
where: { name: 'Bangalore Region' },
|
where: { name: 'Bangalore Region' },
|
||||||
defaults: { name: 'Bangalore Region', zoneId: zone2.id }
|
defaults: { name: 'Bangalore Region', zoneId: zone2.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
const [district1] = await Location.findOrCreate({
|
const [district1] = await District.findOrCreate({
|
||||||
where: { name: 'South Delhi District' },
|
where: { name: 'South Delhi District' },
|
||||||
defaults: { name: 'South Delhi District', stateId: state1.id, regionId: region1.id, zoneId: zone1.id }
|
defaults: { name: 'South Delhi District', stateId: state1.id, regionId: region1.id, zoneId: zone1.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Geographical Hierarchy seeded.');
|
console.log('Geographical Hierarchy seeded.');
|
||||||
|
|
||||||
const mapUserRole = async (userRec: any, roleCode: string, assignment: { zoneId?: string | null, regionId?: string | null, locationId?: string | null } = {}) => {
|
const mapUserRole = async (userRec: any, roleCode: string, assignment: { zoneId?: string | null, regionId?: string | null, districtId?: 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,
|
||||||
...assignment
|
...assignment
|
||||||
},
|
},
|
||||||
|
defaults: {
|
||||||
|
userId: userRec.id,
|
||||||
|
roleId: role.id,
|
||||||
|
...assignment,
|
||||||
|
isActive: true,
|
||||||
|
isPrimary: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Create Users and Map them
|
||||||
|
const nbhResult = await User.findOrCreate({
|
||||||
|
where: { email: 'nbh@example.com' },
|
||||||
|
defaults: { fullName: 'National Head', roleCode: 'NBH', password: hashedPassword }
|
||||||
|
});
|
||||||
|
await mapUserRole(nbhResult[0], 'NBH');
|
||||||
|
|
||||||
|
const zbhResult = await User.findOrCreate({
|
||||||
|
where: { email: 'zbh.north@example.com' },
|
||||||
|
defaults: { fullName: 'North Zonal Head', roleCode: 'ZBH', password: hashedPassword, employeeId: 'ZBH001' }
|
||||||
|
});
|
||||||
|
await mapUserRole(zbhResult[0], 'ZBH', { zoneId: zone1.id });
|
||||||
|
|
||||||
|
const rmResult = await User.findOrCreate({
|
||||||
|
where: { email: 'rbm.delhi@example.com' },
|
||||||
|
defaults: { fullName: 'Delhi Regional Manager', roleCode: 'RM', password: hashedPassword, employeeId: 'RBM001' }
|
||||||
|
});
|
||||||
|
await mapUserRole(rmResult[0], 'RM', { regionId: region1.id });
|
||||||
|
|
||||||
|
const asmResult = await User.findOrCreate({
|
||||||
|
where: { email: 'asm.sdelhi@example.com' },
|
||||||
|
defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM', password: hashedPassword, employeeId: 'ASM001' }
|
||||||
|
});
|
||||||
|
await mapUserRole(asmResult[0], 'ASM', { districtId: district1.id });
|
||||||
|
|
||||||
|
// Mock Users alignment
|
||||||
|
const mockUsers = [
|
||||||
|
{ 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: { districtId: 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({
|
||||||
|
where: { email: m.email },
|
||||||
defaults: {
|
defaults: {
|
||||||
userId: userRec.id,
|
fullName: m.name,
|
||||||
roleId: role.id,
|
roleCode: m.roleCode,
|
||||||
...assignment,
|
password: hashedPassword,
|
||||||
isActive: true,
|
isExternal: m.isExt || false,
|
||||||
isPrimary: true
|
status: 'active'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
await mapUserRole(u, m.roleCode, m.assignment);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// 4. Create Users and Map them
|
console.log('Users and Mappings seeded.');
|
||||||
// Custom Seed Users
|
|
||||||
const nbhResult = await User.findOrCreate({
|
|
||||||
where: { email: 'nbh@example.com' },
|
|
||||||
defaults: { fullName: 'National Head', roleCode: 'NBH', password: hashedPassword }
|
|
||||||
});
|
|
||||||
await mapUserRole(nbhResult[0], 'NBH');
|
|
||||||
|
|
||||||
const zbhResult = await User.findOrCreate({
|
console.log('--- Triggering Hierarchy Synchronization ---');
|
||||||
where: { email: 'zbh.north@example.com' },
|
const districts = await District.findAll({ attributes: ['id'] });
|
||||||
defaults: { fullName: 'North Zonal Head', roleCode: 'ZBH', password: hashedPassword }
|
for (const d of districts) await syncLocationManagers(d.id);
|
||||||
});
|
|
||||||
await mapUserRole(zbhResult[0], 'ZBH', { zoneId: zone1.id });
|
|
||||||
|
|
||||||
const rmResult = await User.findOrCreate({
|
const regions = await Region.findAll({ attributes: ['id'] });
|
||||||
where: { email: 'rbm.delhi@example.com' },
|
for (const r of regions) await syncRegionManager(r.id);
|
||||||
defaults: { fullName: 'Delhi Regional Manager', roleCode: 'RM', password: hashedPassword }
|
|
||||||
});
|
|
||||||
await mapUserRole(rmResult[0], 'RM', { regionId: region1.id });
|
|
||||||
|
|
||||||
const asmResult = await User.findOrCreate({
|
const zones = await Zone.findAll({ attributes: ['id'] });
|
||||||
where: { email: 'asm.sdelhi@example.com' },
|
for (const z of zones) await syncZoneManager(z.id);
|
||||||
defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM', password: hashedPassword }
|
|
||||||
});
|
|
||||||
await mapUserRole(asmResult[0], 'ASM', { locationId: district1.id });
|
|
||||||
|
|
||||||
// Mock Users alignment
|
console.log('--- Seeding & Synchronization Complete ---');
|
||||||
const mockUsers = [
|
|
||||||
{ 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({
|
|
||||||
where: { email: m.email },
|
|
||||||
defaults: {
|
|
||||||
fullName: m.name,
|
|
||||||
roleCode: m.roleCode,
|
|
||||||
password: hashedPassword,
|
|
||||||
isExternal: m.isExt || false,
|
|
||||||
status: 'active'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await mapUserRole(u, m.roleCode, m.assignment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Users and Mappings seeded.');
|
|
||||||
console.log('--- Seeding Complete ---');
|
|
||||||
}
|
|
||||||
|
|
||||||
seed().catch(err => {
|
seed().catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@ -3,14 +3,13 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import db from '../src/database/models/index.js';
|
import db from '../src/database/models/index.js';
|
||||||
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const { Zone, State, Location } = db;
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
console.log('--- Seeding Real Geo Data (Denormalized Model) ---');
|
console.log('--- Seeding Real Geo Data (District -> Area Hierarchy) ---');
|
||||||
try {
|
try {
|
||||||
await db.sequelize.authenticate();
|
await db.sequelize.authenticate();
|
||||||
|
|
||||||
@ -32,7 +31,9 @@ async function run() {
|
|||||||
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} Districts.`);
|
console.log(`Extracted ${ZONES_DATA.length} Zones, ${STATES_DATA.length} States, and ${CITIES_DATA.length} Cities.`);
|
||||||
|
|
||||||
|
const { Zone, State, District, Location, Opportunity } = db;
|
||||||
|
|
||||||
// 1. Seed Zones
|
// 1. Seed Zones
|
||||||
const zoneIdMap = new Map(); // Name -> UUID
|
const zoneIdMap = new Map(); // Name -> UUID
|
||||||
@ -42,15 +43,12 @@ async function run() {
|
|||||||
defaults: { name: z.name, code: z.code }
|
defaults: { name: z.name, code: z.code }
|
||||||
});
|
});
|
||||||
zoneIdMap.set(z.name, zoneRecord.id);
|
zoneIdMap.set(z.name, zoneRecord.id);
|
||||||
// Attach states list for later lookup
|
|
||||||
z._dbId = zoneRecord.id;
|
|
||||||
}
|
}
|
||||||
console.log('Zones seeded.');
|
console.log('Zones seeded.');
|
||||||
|
|
||||||
// 2. Seed States and link to Zones
|
// 2. Seed States and link to Zones
|
||||||
const stateIdMap = new Map(); // Legacy ID -> { id, zoneId }
|
const stateIdMap = new Map(); // Legacy ID -> { id, zoneId }
|
||||||
for (const s of STATES_DATA) {
|
for (const s of STATES_DATA) {
|
||||||
// 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 parentZoneData = ZONES_DATA.find((z: any) => z.states.includes(s.name));
|
||||||
const zoneId = parentZoneData ? zoneIdMap.get(parentZoneData.name) : null;
|
const zoneId = parentZoneData ? zoneIdMap.get(parentZoneData.name) : null;
|
||||||
|
|
||||||
@ -63,12 +61,16 @@ async function run() {
|
|||||||
}
|
}
|
||||||
console.log('States seeded.');
|
console.log('States seeded.');
|
||||||
|
|
||||||
// 3. Seed Districts (Locations)
|
// 3. Seed Districts and Areas (Locations)
|
||||||
let districtCount = 0;
|
let districtCount = 0;
|
||||||
|
let areaCount = 0;
|
||||||
|
let opportunityCount = 0;
|
||||||
|
|
||||||
for (const c of CITIES_DATA) {
|
for (const c of CITIES_DATA) {
|
||||||
const parentStateData = stateIdMap.get(c.state_id);
|
const parentStateData = stateIdMap.get(c.state_id);
|
||||||
if (parentStateData) {
|
if (parentStateData) {
|
||||||
await Location.findOrCreate({
|
// a. Create District (Primary territory entity)
|
||||||
|
const [districtRecord] = await District.findOrCreate({
|
||||||
where: { name: c.name, stateId: parentStateData.id },
|
where: { name: c.name, stateId: parentStateData.id },
|
||||||
defaults: {
|
defaults: {
|
||||||
name: c.name,
|
name: c.name,
|
||||||
@ -77,10 +79,49 @@ async function run() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
districtCount++;
|
districtCount++;
|
||||||
|
|
||||||
|
// b. Create Area (Granular Location record)
|
||||||
|
const [areaRecord] = await Location.findOrCreate({
|
||||||
|
where: { name: c.name, districtId: districtRecord.id },
|
||||||
|
defaults: {
|
||||||
|
name: c.name,
|
||||||
|
city: c.name,
|
||||||
|
districtId: districtRecord.id,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
areaCount++;
|
||||||
|
|
||||||
|
// c. Create associated Opportunity
|
||||||
|
const [oppRecord, created] = await Opportunity.findOrCreate({
|
||||||
|
where: { areaId: areaRecord.id },
|
||||||
|
defaults: {
|
||||||
|
districtId: districtRecord.id,
|
||||||
|
areaId: areaRecord.id, // Linking to Area!
|
||||||
|
city: c.name,
|
||||||
|
status: 'inactive',
|
||||||
|
opportunityType: 'New Dealership',
|
||||||
|
capacity: 'Standard',
|
||||||
|
priority: 'Medium',
|
||||||
|
notes: 'Automatically generated from district seed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (created) opportunityCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log(`✅ Seeded ${districtCount} Districts, ${areaCount} Areas, and ${opportunityCount} Opportunities.`);
|
||||||
|
|
||||||
console.log(`✅ Successfully seeded Real Geo Data! Created/Verified ${districtCount} districts.`);
|
console.log('--- Triggering Hierarchy Synchronization ---');
|
||||||
|
const districts = await District.findAll({ attributes: ['id'] });
|
||||||
|
for (const d of districts) await syncLocationManagers(d.id);
|
||||||
|
|
||||||
|
const regions = await db.Region.findAll({ attributes: ['id'] });
|
||||||
|
for (const r of regions) await syncRegionManager(r.id);
|
||||||
|
|
||||||
|
const zonesArr = await Zone.findAll({ attributes: ['id'] });
|
||||||
|
for (const z of zonesArr) await syncZoneManager(z.id);
|
||||||
|
|
||||||
|
console.log('--- Synchronization Complete ---');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
46
scripts/sync-all-hierarchy.ts
Normal file
46
scripts/sync-all-hierarchy.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import db from '../src/database/models/index.js';
|
||||||
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js';
|
||||||
|
|
||||||
|
async function syncAll() {
|
||||||
|
console.log('--- Starting Master Hierarchy Synchronization ---');
|
||||||
|
try {
|
||||||
|
await db.sequelize.authenticate();
|
||||||
|
console.log('✓ Database connected.');
|
||||||
|
|
||||||
|
// 1. Sync all Zones
|
||||||
|
console.log('Syncing Zones...');
|
||||||
|
const zones = await db.Zone.findAll({ attributes: ['id', 'name'] });
|
||||||
|
for (const zone of zones) {
|
||||||
|
await syncZoneManager(zone.id);
|
||||||
|
console.log(` - Synced Zone: ${zone.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Sync all Regions
|
||||||
|
console.log('Syncing Regions...');
|
||||||
|
const regions = await db.Region.findAll({ attributes: ['id', 'name'] });
|
||||||
|
for (const region of regions) {
|
||||||
|
await syncRegionManager(region.id);
|
||||||
|
console.log(` - Synced Region: ${region.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Sync all Districts
|
||||||
|
console.log('Syncing Districts (This may take a moment)...');
|
||||||
|
const locations = await db.District.findAll({ attributes: ['id', 'name'] });
|
||||||
|
let count = 0;
|
||||||
|
for (const loc of locations) {
|
||||||
|
await syncLocationManagers(loc.id);
|
||||||
|
count++;
|
||||||
|
if (count % 50 === 0) console.log(` - Synced ${count} districts...`);
|
||||||
|
}
|
||||||
|
console.log(`✓ Total Districts Synced: ${count}`);
|
||||||
|
|
||||||
|
console.log('\n--- Synchronization Complete ---');
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Synchronization failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncAll();
|
||||||
@ -10,7 +10,8 @@ export const generateToken = (user: any): string => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.roleCode,
|
role: user.roleCode,
|
||||||
locationId: user.locationId
|
locationId: user.locationId,
|
||||||
|
districtId: user.districtId
|
||||||
};
|
};
|
||||||
|
|
||||||
return jwt.sign(payload, JWT_SECRET, {
|
return jwt.sign(payload, JWT_SECRET, {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export interface ApplicationAttributes {
|
|||||||
architectureAssignedTo: string | null;
|
architectureAssignedTo: string | null;
|
||||||
architectureStatus: string | null;
|
architectureStatus: string | null;
|
||||||
submittedBy: string | null;
|
submittedBy: string | null;
|
||||||
locationId: string | null;
|
districtId: string | null;
|
||||||
architectureAssignedDate: Date | null;
|
architectureAssignedDate: Date | null;
|
||||||
architectureDocumentDate: Date | null;
|
architectureDocumentDate: Date | null;
|
||||||
architectureCompletionDate: Date | null;
|
architectureCompletionDate: Date | null;
|
||||||
@ -200,11 +200,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
locationId: {
|
districtId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'locations',
|
model: 'districts',
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -245,7 +245,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
|
Application.belongsTo(models.User, { foreignKey: 'assignedTo', as: 'assignee' });
|
||||||
Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' });
|
Application.belongsTo(models.User, { foreignKey: 'architectureAssignedTo', as: 'architectureAssignee' });
|
||||||
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
|
Application.belongsTo(models.Opportunity, { foreignKey: 'opportunityId', as: 'opportunity' });
|
||||||
Application.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
|
Application.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
|
||||||
|
|
||||||
Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' });
|
Application.hasMany(models.ApplicationStatusHistory, { foreignKey: 'applicationId', as: 'statusHistory' });
|
||||||
Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' });
|
Application.hasMany(models.ApplicationProgress, { foreignKey: 'applicationId', as: 'progressTracking' });
|
||||||
|
|||||||
128
src/database/models/District.ts
Normal file
128
src/database/models/District.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { Model, DataTypes, Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
export interface DistrictAttributes {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code?: string;
|
||||||
|
stateId?: string | null;
|
||||||
|
regionId?: string | null;
|
||||||
|
zoneId?: string | null;
|
||||||
|
asmId?: string | null;
|
||||||
|
asmCode?: string | null;
|
||||||
|
ddAmId?: string | null;
|
||||||
|
ddAmCode?: string | null;
|
||||||
|
zmId?: string | null;
|
||||||
|
zmCode?: string | null;
|
||||||
|
city?: string | null;
|
||||||
|
isActive?: boolean;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistrictInstance extends Model<DistrictAttributes>, DistrictAttributes { }
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
const District = sequelize.define<DistrictInstance>('District', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
unique: 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' }
|
||||||
|
},
|
||||||
|
asmCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
ddAmId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: { model: 'users', key: 'id' }
|
||||||
|
},
|
||||||
|
ddAmCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
zmId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: { model: 'users', key: 'id' }
|
||||||
|
},
|
||||||
|
zmCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'districts',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{ fields: ['stateId'] },
|
||||||
|
{ fields: ['regionId'] },
|
||||||
|
{ fields: ['zoneId'] },
|
||||||
|
{ unique: true, fields: ['name', 'stateId'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
(District as any).associate = (models: any) => {
|
||||||
|
District.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
|
||||||
|
District.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
||||||
|
District.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
||||||
|
District.belongsTo(models.User, { foreignKey: 'asmId', as: 'asm' });
|
||||||
|
District.belongsTo(models.User, { foreignKey: 'ddAmId', as: 'ddAm' });
|
||||||
|
District.belongsTo(models.User, { foreignKey: 'zmId', as: 'zonalManager' });
|
||||||
|
District.hasMany(models.User, { foreignKey: 'districtId', as: 'users' });
|
||||||
|
District.hasMany(models.UserRole, { foreignKey: 'districtId', as: 'userRoles' });
|
||||||
|
District.hasMany(models.Application, { foreignKey: 'districtId', as: 'applications' });
|
||||||
|
District.hasMany(models.Opportunity, { foreignKey: 'districtId', as: 'opportunities' });
|
||||||
|
District.hasMany(models.Location, { foreignKey: 'districtId', as: 'locations' }); // Will be added soon
|
||||||
|
};
|
||||||
|
|
||||||
|
return District;
|
||||||
|
};
|
||||||
@ -3,12 +3,11 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
|
|||||||
export interface LocationAttributes {
|
export interface LocationAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
code?: string;
|
districtId: string | null;
|
||||||
stateId?: string | null;
|
city?: string | null;
|
||||||
regionId?: string | null;
|
|
||||||
zoneId?: string | null;
|
|
||||||
asmId?: string | null;
|
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
openFrom?: Date | null;
|
||||||
|
openTo?: Date | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,47 +24,30 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
code: {
|
districtId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'districts',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
city: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true
|
||||||
unique: 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: {
|
isActive: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
},
|
},
|
||||||
|
openFrom: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
openTo: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
@ -74,21 +56,16 @@ export default (sequelize: Sequelize) => {
|
|||||||
tableName: 'locations',
|
tableName: 'locations',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{ fields: ['stateId'] },
|
{ fields: ['districtId'] },
|
||||||
{ fields: ['regionId'] },
|
{ unique: true, fields: ['name', 'districtId'] }
|
||||||
{ fields: ['zoneId'] },
|
|
||||||
{ unique: true, fields: ['name', 'stateId'] }
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
(Location as any).associate = (models: any) => {
|
(Location as any).associate = (models: any) => {
|
||||||
Location.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
|
Location.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
|
||||||
Location.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
|
||||||
Location.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
// These can have detailed opportunities if needed in future
|
||||||
Location.belongsTo(models.User, { foreignKey: 'asmId', as: 'asm' });
|
Location.hasMany(models.Opportunity, { foreignKey: 'areaId', as: 'opportunities' });
|
||||||
Location.hasMany(models.User, { foreignKey: 'locationId', as: 'users' });
|
|
||||||
Location.hasMany(models.UserRole, { foreignKey: 'locationId', as: 'userRoles' });
|
|
||||||
Location.hasMany(models.Application, { foreignKey: 'locationId', as: 'applications' });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return Location;
|
return Location;
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
|
|||||||
|
|
||||||
export interface OpportunityAttributes {
|
export interface OpportunityAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
locationId: string;
|
districtId: string;
|
||||||
|
areaId?: string | null;
|
||||||
city: string;
|
city: string;
|
||||||
opportunityType: string;
|
opportunityType: string;
|
||||||
capacity: string;
|
capacity: string;
|
||||||
@ -23,9 +24,17 @@ export default (sequelize: Sequelize) => {
|
|||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
primaryKey: true
|
primaryKey: true
|
||||||
},
|
},
|
||||||
locationId: {
|
districtId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'districts',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
areaId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'locations',
|
model: 'locations',
|
||||||
key: 'id'
|
key: 'id'
|
||||||
@ -77,7 +86,8 @@ export default (sequelize: Sequelize) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
(Opportunity as any).associate = (models: any) => {
|
(Opportunity as any).associate = (models: any) => {
|
||||||
Opportunity.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
|
Opportunity.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
|
||||||
|
Opportunity.belongsTo(models.Location, { foreignKey: 'areaId', as: 'area' });
|
||||||
Opportunity.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' });
|
Opportunity.belongsTo(models.User, { foreignKey: 'createdBy', as: 'creator' });
|
||||||
Opportunity.hasMany(models.Application, { foreignKey: 'opportunityId', as: 'applications' });
|
Opportunity.hasMany(models.Application, { foreignKey: 'opportunityId', as: 'applications' });
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export interface OutletAttributes {
|
|||||||
status: typeof OUTLET_STATUS[keyof typeof OUTLET_STATUS];
|
status: typeof OUTLET_STATUS[keyof typeof OUTLET_STATUS];
|
||||||
establishedDate: string;
|
establishedDate: string;
|
||||||
dealerId: string;
|
dealerId: string;
|
||||||
locationId: string;
|
districtId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OutletInstance extends Model<OutletAttributes>, OutletAttributes { }
|
export interface OutletInstance extends Model<OutletAttributes>, OutletAttributes { }
|
||||||
@ -80,11 +80,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
locationId: {
|
districtId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
references: {
|
references: {
|
||||||
model: 'locations',
|
model: 'districts',
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,7 +96,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
{ fields: ['dealerId'] },
|
{ fields: ['dealerId'] },
|
||||||
{ fields: ['type'] },
|
{ fields: ['type'] },
|
||||||
{ fields: ['status'] },
|
{ fields: ['status'] },
|
||||||
{ fields: ['locationId'] }
|
{ fields: ['districtId'] }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -105,9 +105,9 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'dealerId',
|
foreignKey: 'dealerId',
|
||||||
as: 'dealer'
|
as: 'dealer'
|
||||||
});
|
});
|
||||||
Outlet.belongsTo(models.Location, {
|
Outlet.belongsTo(models.District, {
|
||||||
foreignKey: 'locationId',
|
foreignKey: 'districtId',
|
||||||
as: 'location'
|
as: 'district'
|
||||||
});
|
});
|
||||||
Outlet.hasMany(models.Resignation, {
|
Outlet.hasMany(models.Resignation, {
|
||||||
foreignKey: 'outletId',
|
foreignKey: 'outletId',
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export interface RegionAttributes {
|
|||||||
code: string;
|
code: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
zoneId?: string | null;
|
zoneId?: string | null;
|
||||||
|
rbmId?: string | null; // Regional Business Manager
|
||||||
|
rbmCode?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegionInstance extends Model<RegionAttributes>, RegionAttributes { }
|
export interface RegionInstance extends Model<RegionAttributes>, RegionAttributes { }
|
||||||
@ -38,6 +40,18 @@ export default (sequelize: Sequelize) => {
|
|||||||
model: 'zones',
|
model: 'zones',
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
rbmId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rbmCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'regions',
|
tableName: 'regions',
|
||||||
@ -46,7 +60,8 @@ export default (sequelize: Sequelize) => {
|
|||||||
|
|
||||||
(Region as any).associate = (models: any) => {
|
(Region as any).associate = (models: any) => {
|
||||||
Region.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
Region.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
||||||
Region.hasMany(models.Location, { foreignKey: 'regionId', as: 'districts' });
|
Region.belongsTo(models.User, { foreignKey: 'rbmId', as: 'regionalManager' });
|
||||||
|
Region.hasMany(models.District, { foreignKey: 'regionId', as: 'districts' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return Region;
|
return Region;
|
||||||
|
|||||||
@ -11,7 +11,10 @@ export interface UserAttributes {
|
|||||||
department: string | null;
|
department: string | null;
|
||||||
designation: string | null;
|
designation: string | null;
|
||||||
roleCode: string | null;
|
roleCode: string | null;
|
||||||
locationId: string | null;
|
districtId: string | null;
|
||||||
|
zoneId: string | null;
|
||||||
|
regionId: string | null;
|
||||||
|
stateId: string | null;
|
||||||
dealerId: string | null;
|
dealerId: string | null;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isExternal: boolean;
|
isExternal: boolean;
|
||||||
@ -44,7 +47,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true // SSO might not need passwords
|
allowNull: true
|
||||||
},
|
},
|
||||||
fullName: {
|
fullName: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
@ -66,16 +69,28 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
locationId: {
|
districtId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: { model: 'districts', key: 'id' }
|
||||||
model: 'locations',
|
},
|
||||||
key: 'id'
|
zoneId: {
|
||||||
}
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: { model: 'zones', key: 'id' }
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: { model: 'regions', key: 'id' }
|
||||||
|
},
|
||||||
|
stateId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: { model: 'states', key: 'id' }
|
||||||
},
|
},
|
||||||
dealerId: {
|
dealerId: {
|
||||||
type: DataTypes.UUID, // Link to Dealer entity if applicable
|
type: DataTypes.UUID,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
@ -116,7 +131,18 @@ export default (sequelize: Sequelize) => {
|
|||||||
});
|
});
|
||||||
User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' });
|
User.hasMany(models.UserRole, { foreignKey: 'userId', as: 'userRoles' });
|
||||||
User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' });
|
User.hasMany(models.UserRole, { foreignKey: 'assignedBy', as: 'assignedRoles' });
|
||||||
User.belongsTo(models.Location, { foreignKey: 'locationId', as: 'location' });
|
|
||||||
|
// Link to District (Parent Territory)
|
||||||
|
User.belongsTo(models.District, { foreignKey: 'districtId', as: 'district' });
|
||||||
|
|
||||||
|
// Role-specific managed districts (pointing to District model now)
|
||||||
|
User.hasMany(models.District, { foreignKey: 'asmId', as: 'managedAsmDistricts' });
|
||||||
|
User.hasMany(models.District, { foreignKey: 'ddAmId', as: 'managedAreaDistricts' });
|
||||||
|
User.hasMany(models.District, { foreignKey: 'zmId', as: 'managedZmDistricts' });
|
||||||
|
|
||||||
|
User.belongsTo(models.Zone, { foreignKey: 'zoneId', as: 'zone' });
|
||||||
|
User.belongsTo(models.Region, { foreignKey: 'regionId', as: 'region' });
|
||||||
|
User.belongsTo(models.State, { foreignKey: 'stateId', as: 'state' });
|
||||||
|
|
||||||
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
|
User.hasMany(models.AuditLog, { foreignKey: 'userId', as: 'auditLogs' });
|
||||||
User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' });
|
User.belongsTo(models.Dealer, { foreignKey: 'dealerId', as: 'dealerProfile' });
|
||||||
|
|||||||
@ -4,9 +4,10 @@ export interface UserRoleAttributes {
|
|||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
roleId: string;
|
roleId: string;
|
||||||
locationId: string | null; // District
|
districtId: string | null;
|
||||||
zoneId: string | null;
|
zoneId: string | null;
|
||||||
regionId: string | null;
|
regionId: string | null;
|
||||||
|
stateId?: string | null;
|
||||||
managerCode: string | null;
|
managerCode: string | null;
|
||||||
isPrimary: boolean;
|
isPrimary: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@ -41,11 +42,11 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
locationId: {
|
districtId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'locations',
|
model: 'districts',
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -65,6 +66,14 @@ export default (sequelize: Sequelize) => {
|
|||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
stateId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'states',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
managerCode: {
|
managerCode: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
@ -99,8 +108,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'user_roles',
|
tableName: 'user_roles',
|
||||||
timestamps: true,
|
timestamps: true
|
||||||
updatedAt: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
(UserRole as any).associate = (models: any) => {
|
(UserRole as any).associate = (models: any) => {
|
||||||
@ -112,9 +120,9 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'roleId',
|
foreignKey: 'roleId',
|
||||||
as: 'role'
|
as: 'role'
|
||||||
});
|
});
|
||||||
UserRole.belongsTo(models.Location, {
|
UserRole.belongsTo(models.District, {
|
||||||
foreignKey: 'locationId',
|
foreignKey: 'districtId',
|
||||||
as: 'location'
|
as: 'district'
|
||||||
});
|
});
|
||||||
UserRole.belongsTo(models.Zone, {
|
UserRole.belongsTo(models.Zone, {
|
||||||
foreignKey: 'zoneId',
|
foreignKey: 'zoneId',
|
||||||
@ -124,6 +132,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
foreignKey: 'regionId',
|
foreignKey: 'regionId',
|
||||||
as: 'region'
|
as: 'region'
|
||||||
});
|
});
|
||||||
|
UserRole.belongsTo(models.State, {
|
||||||
|
foreignKey: 'stateId',
|
||||||
|
as: 'state'
|
||||||
|
});
|
||||||
UserRole.belongsTo(models.User, {
|
UserRole.belongsTo(models.User, {
|
||||||
foreignKey: 'assignedBy',
|
foreignKey: 'assignedBy',
|
||||||
as: 'assigner'
|
as: 'assigner'
|
||||||
|
|||||||
@ -5,6 +5,8 @@ export interface ZoneAttributes {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
zbhId?: string | null; // Zonal Business Head
|
||||||
|
zbhCode?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZoneInstance extends Model<ZoneAttributes>, ZoneAttributes { }
|
export interface ZoneInstance extends Model<ZoneAttributes>, ZoneAttributes { }
|
||||||
@ -29,6 +31,18 @@ export default (sequelize: Sequelize) => {
|
|||||||
description: {
|
description: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
|
},
|
||||||
|
zbhId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zbhCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'zones',
|
tableName: 'zones',
|
||||||
@ -38,7 +52,8 @@ export default (sequelize: Sequelize) => {
|
|||||||
(Zone as any).associate = (models: any) => {
|
(Zone as any).associate = (models: any) => {
|
||||||
Zone.hasMany(models.Region, { foreignKey: 'zoneId', as: 'regions' });
|
Zone.hasMany(models.Region, { foreignKey: 'zoneId', as: 'regions' });
|
||||||
Zone.hasMany(models.State, { foreignKey: 'zoneId', as: 'states' });
|
Zone.hasMany(models.State, { foreignKey: 'zoneId', as: 'states' });
|
||||||
Zone.hasMany(models.Location, { foreignKey: 'zoneId', as: 'districts' });
|
Zone.hasMany(models.District, { foreignKey: 'zoneId', as: 'districts' });
|
||||||
|
Zone.belongsTo(models.User, { foreignKey: 'zbhId', as: 'zonalBusinessHead' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return Zone;
|
return Zone;
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import createSLAReminder from './SLAReminder.js';
|
|||||||
import createSLAEscalationConfig from './SLAEscalationConfig.js';
|
import createSLAEscalationConfig from './SLAEscalationConfig.js';
|
||||||
import createWorkflowStageConfig from './WorkflowStageConfig.js';
|
import createWorkflowStageConfig from './WorkflowStageConfig.js';
|
||||||
import createNotification from './Notification.js';
|
import createNotification from './Notification.js';
|
||||||
|
import createDistrict from './District.js';
|
||||||
import createLocation from './Location.js';
|
import createLocation from './Location.js';
|
||||||
import createZone from './Zone.js';
|
import createZone from './Zone.js';
|
||||||
import createRegion from './Region.js';
|
import createRegion from './Region.js';
|
||||||
@ -123,6 +124,7 @@ db.SLAReminder = createSLAReminder(sequelize);
|
|||||||
db.SLAEscalationConfig = createSLAEscalationConfig(sequelize);
|
db.SLAEscalationConfig = createSLAEscalationConfig(sequelize);
|
||||||
db.WorkflowStageConfig = createWorkflowStageConfig(sequelize);
|
db.WorkflowStageConfig = createWorkflowStageConfig(sequelize);
|
||||||
db.Notification = createNotification(sequelize);
|
db.Notification = createNotification(sequelize);
|
||||||
|
db.District = createDistrict(sequelize);
|
||||||
db.Location = createLocation(sequelize);
|
db.Location = createLocation(sequelize);
|
||||||
db.Zone = createZone(sequelize);
|
db.Zone = createZone(sequelize);
|
||||||
db.Region = createRegion(sequelize);
|
db.Region = createRegion(sequelize);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import db from '../../database/models/index.js';
|
|||||||
const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
|
const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db;
|
||||||
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../master/syncHierarchy.service.js';
|
||||||
|
|
||||||
const upsertUserAssignments = async (
|
const upsertUserAssignments = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
@ -23,10 +24,12 @@ const upsertUserAssignments = async (
|
|||||||
const role = await Role.findOne({ where: { roleCode } });
|
const role = await Role.findOne({ where: { roleCode } });
|
||||||
if (!role) continue;
|
if (!role) continue;
|
||||||
|
|
||||||
await db.UserRole.create({
|
const createdRole = await db.UserRole.create({
|
||||||
userId,
|
userId,
|
||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
locationId: assignment.locationId || null,
|
districtId: assignment.locationId || assignment.districtId || null,
|
||||||
|
zoneId: assignment.zoneId || null,
|
||||||
|
regionId: assignment.regionId || null,
|
||||||
managerCode: assignment.managerCode || assignment.asmCode || null,
|
managerCode: assignment.managerCode || assignment.asmCode || null,
|
||||||
isPrimary: assignment.isPrimary !== undefined ? Boolean(assignment.isPrimary) : i === 0,
|
isPrimary: assignment.isPrimary !== undefined ? Boolean(assignment.isPrimary) : i === 0,
|
||||||
isActive: assignment.isActive !== undefined ? Boolean(assignment.isActive) : true,
|
isActive: assignment.isActive !== undefined ? Boolean(assignment.isActive) : true,
|
||||||
@ -34,6 +37,12 @@ const upsertUserAssignments = async (
|
|||||||
effectiveTo: assignment.effectiveTo || null,
|
effectiveTo: assignment.effectiveTo || null,
|
||||||
assignedBy: actorUserId || null
|
assignedBy: actorUserId || null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Trigger Sync
|
||||||
|
const targetId = assignment.locationId || assignment.districtId;
|
||||||
|
if (targetId) await syncLocationManagers(targetId);
|
||||||
|
if (assignment.regionId) await syncRegionManager(assignment.regionId);
|
||||||
|
if (assignment.zoneId) await syncZoneManager(assignment.zoneId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -103,20 +112,32 @@ export const createRole = async (req: AuthRequest, res: Response) => {
|
|||||||
export const updateRole = async (req: AuthRequest, res: Response) => {
|
export const updateRole = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { roleName, description, permissionIds, isActive } = req.body;
|
const { roleName, description, permissionIds, permissions, isActive } = req.body;
|
||||||
|
const permsToUpdate = permissionIds || permissions;
|
||||||
|
|
||||||
const role = await Role.findByPk(id);
|
const role = await Role.findByPk(id);
|
||||||
if (!role) return res.status(404).json({ success: false, message: 'Role not found' });
|
if (!role) return res.status(404).json({ success: false, message: 'Role not found' });
|
||||||
|
|
||||||
await role.update({ roleName, description, isActive });
|
await role.update({ roleName, description, isActive });
|
||||||
|
|
||||||
if (permissionIds) {
|
if (permsToUpdate && Array.isArray(permsToUpdate)) {
|
||||||
|
// Resolve IDs if they are passed as codes
|
||||||
|
const resolvedIds: string[] = [];
|
||||||
|
for (const pid of permsToUpdate) {
|
||||||
|
if (pid.includes(':') || /^[A-Z_]+$/.test(pid)) {
|
||||||
|
const perm = await Permission.findOne({ where: { permissionCode: pid } });
|
||||||
|
if (perm) resolvedIds.push(perm.id);
|
||||||
|
} else {
|
||||||
|
resolvedIds.push(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove existing permissions and re-add new ones
|
// Remove existing permissions and re-add new ones
|
||||||
await RolePermission.destroy({ where: { roleId: id } });
|
await RolePermission.destroy({ where: { roleId: id } });
|
||||||
for (const pid of permissionIds) {
|
for (const resolvedId of resolvedIds) {
|
||||||
await RolePermission.create({
|
await RolePermission.create({
|
||||||
roleId: id,
|
roleId: id,
|
||||||
permissionId: pid
|
permissionId: resolvedId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,25 +190,16 @@ 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) {
|
||||||
const district: any = await db.Location.findByPk(locationId as string, {
|
const district: any = await db.District.findByPk(locationId as string, {
|
||||||
attributes: ['id', 'zoneId', 'regionId', 'stateId']
|
attributes: ['id', 'zoneId', 'regionId', 'stateId']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (district) {
|
if (district) {
|
||||||
const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean);
|
const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean);
|
||||||
whereClause.locationId = { [Op.in]: relevantIds };
|
whereClause.districtId = { [Op.in]: relevantIds };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all locations to build a map if needed
|
|
||||||
const allLocations = await db.Location.findAll({
|
|
||||||
include: [
|
|
||||||
{ model: db.Zone, as: 'zone', attributes: ['name'] },
|
|
||||||
{ model: db.Region, as: 'region', attributes: ['name'] },
|
|
||||||
{ model: db.State, as: 'state', attributes: ['name'] }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const users = await User.findAll({
|
const users = await User.findAll({
|
||||||
where: whereClause,
|
where: whereClause,
|
||||||
attributes: { exclude: ['password'] },
|
attributes: { exclude: ['password'] },
|
||||||
@ -203,60 +215,49 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ model: db.Location, as: 'location' },
|
{ model: db.District, as: 'district' },
|
||||||
{
|
{
|
||||||
model: db.UserRole,
|
model: db.UserRole,
|
||||||
as: 'userRoles',
|
as: 'userRoles',
|
||||||
include: [
|
include: [
|
||||||
{ model: db.Role, as: 'role', attributes: ['id', 'roleCode', 'roleName'] },
|
{
|
||||||
{ model: db.Location, as: 'location', attributes: ['id', 'name', 'type'] }
|
model: db.District,
|
||||||
|
as: 'district',
|
||||||
|
attributes: ['id', 'name', 'stateId', 'regionId', 'zoneId'],
|
||||||
|
include: [
|
||||||
|
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
|
||||||
|
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
|
||||||
|
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] },
|
||||||
|
{ model: db.Region, as: 'region', attributes: ['id', 'name'] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
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 result = users.map((u: any) => {
|
||||||
const userJson = u.toJSON();
|
const userJson = u.toJSON();
|
||||||
const assignments = userJson.userRoles || [];
|
const assignments = userJson.userRoles || [];
|
||||||
|
|
||||||
// Consolidate roles and territories with DEEP resolution
|
// territories mapping — provide fallbacks to the nested location fields
|
||||||
const territories = assignments.map((a: any) => {
|
const territories = assignments.map((a: any) => ({
|
||||||
const zone = findAncestor(a.locationId, 'zone');
|
role: a.role?.roleName,
|
||||||
const region = findAncestor(a.locationId, 'region');
|
roleCode: a.role?.roleCode,
|
||||||
const state = findAncestor(a.locationId, 'state');
|
districtId: a.districtId,
|
||||||
|
districtName: a.district?.name,
|
||||||
return {
|
locationType: 'district',
|
||||||
role: a.role?.roleName,
|
managerCode: a.managerCode,
|
||||||
roleCode: a.role?.roleCode,
|
zoneId: a.zoneId || a.district?.zoneId,
|
||||||
locationId: a.locationId,
|
zone: a.zone?.name || a.district?.zone?.name,
|
||||||
locationName: a.location?.name,
|
regionId: a.regionId || a.district?.regionId,
|
||||||
locationType: a.location?.type,
|
region: a.region?.name || a.district?.region?.name,
|
||||||
managerCode: a.managerCode,
|
stateId: a.district?.state?.id || a.district?.stateId,
|
||||||
zone: zone?.name,
|
state: a.district?.state?.name,
|
||||||
zoneId: zone?.id,
|
isActive: a.isActive
|
||||||
region: region?.name,
|
}));
|
||||||
regionId: region?.id,
|
|
||||||
state: state?.name,
|
|
||||||
stateId: state?.id,
|
|
||||||
isActive: a.isActive
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
userJson.territoryProfile = territories;
|
userJson.territoryProfile = territories;
|
||||||
userJson.allRoles = Array.from(new Set([
|
userJson.allRoles = Array.from(new Set([
|
||||||
@ -264,8 +265,12 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
|||||||
u.role?.roleName,
|
u.role?.roleName,
|
||||||
...assignments.flatMap((a: any) => [a.role?.roleCode, a.role?.roleName])
|
...assignments.flatMap((a: any) => [a.role?.roleCode, a.role?.roleName])
|
||||||
].filter(Boolean)));
|
].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.allZones = Array.from(new Set(
|
||||||
userJson.allRegions = Array.from(new Set([u.location?.region?.name, ...territories.map((t: any) => t.region)].filter(Boolean).map(r => r.toUpperCase())));
|
territories.map((t: any) => t.zone).filter(Boolean).map((z: string) => z.toUpperCase())
|
||||||
|
));
|
||||||
|
userJson.allRegions = Array.from(new Set(
|
||||||
|
territories.map((t: any) => t.region).filter(Boolean).map((r: string) => r.toUpperCase())
|
||||||
|
));
|
||||||
|
|
||||||
return userJson;
|
return userJson;
|
||||||
});
|
});
|
||||||
@ -283,7 +288,10 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
fullName, email, roleCode,
|
fullName, email, roleCode,
|
||||||
employeeId, mobileNumber, department, designation,
|
employeeId, mobileNumber, department, designation,
|
||||||
locationId,
|
locationId,
|
||||||
assignments
|
assignments,
|
||||||
|
districts, // New: ASM managed areas
|
||||||
|
asmCode, // New: ASM code
|
||||||
|
zmCode // New: ZM code
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
|
||||||
@ -330,11 +338,41 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
mobileNumber,
|
mobileNumber,
|
||||||
department,
|
department,
|
||||||
designation,
|
designation,
|
||||||
locationId
|
districtId: locationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Array.isArray(assignments) && assignments.length > 0) {
|
if (Array.isArray(assignments) && assignments.length > 0) {
|
||||||
await upsertUserAssignments(user.id, assignments, req.user?.id);
|
await upsertUserAssignments(user.id, assignments, req.user?.id);
|
||||||
|
} else if (districts && Array.isArray(districts) && (roleCode === 'ASM' || roleCode === 'ZM')) {
|
||||||
|
const targetRole = await Role.findOne({ where: { roleCode: roleCode } });
|
||||||
|
if (targetRole) {
|
||||||
|
// Resolve Zone and Region from the districts
|
||||||
|
let targetZoneId = null;
|
||||||
|
let targetRegionId = null;
|
||||||
|
if (districts.length > 0) {
|
||||||
|
const sampleDistrict = await db.District.findByPk(districts[0]);
|
||||||
|
if (sampleDistrict) {
|
||||||
|
targetZoneId = sampleDistrict.zoneId;
|
||||||
|
targetRegionId = sampleDistrict.regionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const distId of districts) {
|
||||||
|
await db.UserRole.create({
|
||||||
|
userId: user.id,
|
||||||
|
roleId: targetRole.id,
|
||||||
|
districtId: distId,
|
||||||
|
zoneId: targetZoneId,
|
||||||
|
regionId: targetRegionId,
|
||||||
|
managerCode: asmCode || zmCode || null,
|
||||||
|
isPrimary: false,
|
||||||
|
isActive: true,
|
||||||
|
assignedBy: req.user?.id || null
|
||||||
|
});
|
||||||
|
// Atomic Sync
|
||||||
|
await syncLocationManagers(distId);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (roleCode) {
|
} else if (roleCode) {
|
||||||
const role = await Role.findOne({ where: { roleCode } });
|
const role = await Role.findOne({ where: { roleCode } });
|
||||||
if (role) {
|
if (role) {
|
||||||
@ -382,6 +420,11 @@ export const updateUserStatus = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
await user.update({ status, isActive });
|
await user.update({ status, isActive });
|
||||||
|
|
||||||
|
// If user is deactivated, clear their ASM assignments in District table
|
||||||
|
if (isActive === false) {
|
||||||
|
await db.District.update({ asmId: null }, { where: { asmId: id } });
|
||||||
|
}
|
||||||
|
|
||||||
await AuditLog.create({
|
await AuditLog.create({
|
||||||
userId: req.user?.id,
|
userId: req.user?.id,
|
||||||
action: AUDIT_ACTIONS.UPDATED,
|
action: AUDIT_ACTIONS.UPDATED,
|
||||||
@ -407,6 +450,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
assignments,
|
assignments,
|
||||||
districts, // New: ASM managed areas/districts
|
districts, // New: ASM managed areas/districts
|
||||||
asmCode, // New: ASM code to store in managerCode
|
asmCode, // New: ASM code to store in managerCode
|
||||||
|
zmCode, // New: ZM code
|
||||||
password // Optional password update
|
password // Optional password update
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
@ -423,10 +467,19 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
employeeId: employeeId || user.employeeId,
|
employeeId: employeeId || user.employeeId,
|
||||||
mobileNumber: mobileNumber || user.mobileNumber,
|
mobileNumber: mobileNumber || user.mobileNumber,
|
||||||
department: department || user.department,
|
department: department || user.department,
|
||||||
designation: designation || user.designation,
|
designation: designation || user.designation
|
||||||
locationId: (locationId === '' ? null : (locationId !== undefined ? locationId : user.locationId))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NEW: Validate locationId if provided (must exist in districts table, otherwise set to null on User record)
|
||||||
|
if (locationId !== undefined) {
|
||||||
|
if (locationId === '' || locationId === null) {
|
||||||
|
updates.districtId = null;
|
||||||
|
} else {
|
||||||
|
const districtExists = await db.District.findByPk(locationId);
|
||||||
|
updates.districtId = districtExists ? locationId : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If password is provided, hash it and update
|
// If password is provided, hash it and update
|
||||||
if (password && password.trim() !== '') {
|
if (password && password.trim() !== '') {
|
||||||
updates.password = await bcrypt.hash(password, 10);
|
updates.password = await bcrypt.hash(password, 10);
|
||||||
@ -445,7 +498,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
const duplicate = await db.UserRole.findOne({
|
const duplicate = await db.UserRole.findOne({
|
||||||
where: {
|
where: {
|
||||||
roleId: targetRole.id,
|
roleId: targetRole.id,
|
||||||
locationId: { [Op.in]: districts },
|
districtId: { [Op.in]: districts },
|
||||||
userId: { [Op.ne]: id },
|
userId: { [Op.ne]: id },
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
@ -453,7 +506,7 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
const location = await db.Location.findByPk(duplicate.locationId);
|
const location = await db.District.findByPk(duplicate.districtId);
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: `Territory "${location?.name}" is already assigned to ${duplicate.user?.fullName}. Duplicate assignments for ${roleCode} are restricted.`
|
message: `Territory "${location?.name}" is already assigned to ${duplicate.user?.fullName}. Duplicate assignments for ${roleCode} are restricted.`
|
||||||
@ -463,16 +516,49 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
// 2. Transactional Update: Clear old assignments for this role and add new ones
|
// 2. Transactional Update: Clear old assignments for this role and add new ones
|
||||||
await db.UserRole.destroy({ where: { userId: id, roleId: targetRole.id } });
|
await db.UserRole.destroy({ where: { userId: id, roleId: targetRole.id } });
|
||||||
|
|
||||||
|
// Clear old asmId/managerId assignments in District table for this specific user
|
||||||
|
// (The sync service will handle the new ones)
|
||||||
|
if (roleCode === 'ASM') await db.District.update({ asmId: null, asmCode: null }, { where: { asmId: id } });
|
||||||
|
if (roleCode === 'ZM') await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: id } });
|
||||||
|
|
||||||
|
// 3. TRANSFER LOGIC: If any of these districts are currently assigned to ANOTHER manager for this role,
|
||||||
|
// deactivate those assignments to prevent duplication.
|
||||||
|
await db.UserRole.update({ isActive: false }, {
|
||||||
|
where: {
|
||||||
|
roleId: targetRole.id,
|
||||||
|
districtId: { [Op.in]: districts },
|
||||||
|
userId: { [Op.ne]: id },
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Resolve Zone and Region from the districts
|
||||||
|
let targetZoneId = null;
|
||||||
|
let targetRegionId = null;
|
||||||
|
if (districts.length > 0) {
|
||||||
|
const sampleDistrict = await db.District.findByPk(districts[0]);
|
||||||
|
if (sampleDistrict) {
|
||||||
|
targetZoneId = sampleDistrict.zoneId;
|
||||||
|
targetRegionId = sampleDistrict.regionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const distId of districts) {
|
for (const distId of districts) {
|
||||||
|
// Update UserRole table
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: id,
|
userId: id,
|
||||||
roleId: targetRole.id,
|
roleId: targetRole.id,
|
||||||
locationId: distId,
|
districtId: distId,
|
||||||
managerCode: asmCode || (req.body as any).zmCode || null,
|
zoneId: targetZoneId,
|
||||||
|
regionId: targetRegionId,
|
||||||
|
managerCode: asmCode || zmCode || null,
|
||||||
isPrimary: false,
|
isPrimary: false,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
assignedBy: req.user?.id || null
|
assignedBy: req.user?.id || null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Atomic Sync (handles Location table asmId / asmCode / etc)
|
||||||
|
await syncLocationManagers(distId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (roleCode !== undefined || locationId !== undefined) {
|
else if (roleCode !== undefined || locationId !== undefined) {
|
||||||
@ -481,14 +567,18 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
const role = await Role.findOne({ where: { roleCode: primaryRoleCode } });
|
const role = await Role.findOne({ where: { roleCode: primaryRoleCode } });
|
||||||
if (role) {
|
if (role) {
|
||||||
await db.UserRole.destroy({ where: { userId: id, isPrimary: true } });
|
await db.UserRole.destroy({ where: { userId: id, isPrimary: true } });
|
||||||
await db.UserRole.create({
|
const created = await db.UserRole.create({
|
||||||
userId: id,
|
userId: id,
|
||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
locationId: updates.locationId ?? user.locationId ?? null,
|
districtId: updates.districtId ?? user.districtId ?? null,
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
isActive: updates.isActive,
|
isActive: updates.isActive,
|
||||||
assignedBy: req.user?.id || null
|
assignedBy: req.user?.id || null
|
||||||
});
|
});
|
||||||
|
// Sync primary location if exists
|
||||||
|
if (created.districtId) await syncLocationManagers(created.districtId);
|
||||||
|
if (created.regionId) await syncRegionManager(created.regionId);
|
||||||
|
if (created.zoneId) await syncZoneManager(created.zoneId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,36 +2,16 @@ import { Request, Response } from 'express';
|
|||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const {
|
const {
|
||||||
Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireScore,
|
Questionnaire, QuestionnaireQuestion, QuestionnaireResponse, QuestionnaireScore,
|
||||||
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User, RequestParticipant, Role, LocationHierarchy, StageApprovalPolicy, StageApprovalAction
|
Interview, InterviewEvaluation, InterviewParticipant, AiSummary, User, RequestParticipant, Role, District, StageApprovalPolicy, StageApprovalAction
|
||||||
} = db;
|
} = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import * as EmailService from '../../common/utils/email.service.js';
|
import * as EmailService from '../../common/utils/email.service.js';
|
||||||
|
|
||||||
const getLocationAncestors = async (locationId: string): Promise<string[]> => {
|
const getLocationAncestors = async (locationId: string): Promise<string[]> => {
|
||||||
const ancestors: string[] = [];
|
const district: any = await District.findByPk(locationId);
|
||||||
const visited = new Set<string>();
|
if (!district) return [locationId];
|
||||||
const queue: string[] = [locationId];
|
return [district.id, district.stateId, district.regionId, district.zoneId].filter(Boolean);
|
||||||
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const currentId = queue.shift() as string;
|
|
||||||
if (visited.has(currentId)) continue;
|
|
||||||
visited.add(currentId);
|
|
||||||
ancestors.push(currentId);
|
|
||||||
|
|
||||||
const parentLinks = await LocationHierarchy.findAll({
|
|
||||||
where: { locationId: currentId },
|
|
||||||
attributes: ['parentId']
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const link of parentLinks as any[]) {
|
|
||||||
if (link.parentId && !visited.has(link.parentId)) {
|
|
||||||
queue.push(link.parentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ancestors;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const interviewStageCode = (level: number) => `INTERVIEW_LEVEL_${level}`;
|
const interviewStageCode = (level: number) => `INTERVIEW_LEVEL_${level}`;
|
||||||
@ -312,8 +292,8 @@ export const scheduleInterview = async (req: AuthRequest, res: Response) => {
|
|||||||
let participantIds: string[] = Array.isArray(participants) ? participants : [];
|
let participantIds: string[] = Array.isArray(participants) ? participants : [];
|
||||||
|
|
||||||
// Auto-include relevant ZBH by location hierarchy when interviewer list is omitted.
|
// Auto-include relevant ZBH by location hierarchy when interviewer list is omitted.
|
||||||
if (participantIds.length === 0 && application?.locationId) {
|
if (participantIds.length === 0 && (application?.districtId || application?.locationId)) {
|
||||||
const ancestorLocationIds = await getLocationAncestors(application.locationId);
|
const ancestorLocationIds = await getLocationAncestors(application.districtId || application.locationId);
|
||||||
const zonalHeads = await User.findAll({
|
const zonalHeads = await User.findAll({
|
||||||
where: {
|
where: {
|
||||||
roleCode: 'ZBH',
|
roleCode: 'ZBH',
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export const register = async (req: Request, res: Response) => {
|
|||||||
fullName,
|
fullName,
|
||||||
roleCode: role,
|
roleCode: role,
|
||||||
mobileNumber: phone,
|
mobileNumber: phone,
|
||||||
locationId,
|
districtId: locationId,
|
||||||
status: 'active'
|
status: 'active'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ export const login = async (req: Request, res: Response) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
fullName: user.fullName,
|
fullName: user.fullName,
|
||||||
role: user.roleCode,
|
role: user.roleCode,
|
||||||
locationId: user.locationId
|
districtId: user.districtId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -161,7 +161,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findByPk(req.user.id, {
|
const user = await User.findByPk(req.user.id, {
|
||||||
attributes: ['id', 'email', 'fullName', 'roleCode', 'locationId', 'mobileNumber', 'createdAt']
|
attributes: ['id', 'email', 'fullName', 'roleCode', 'districtId', 'mobileNumber', 'createdAt']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -178,7 +178,7 @@ export const getProfile = async (req: AuthRequest, res: Response) => {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
fullName: user.fullName,
|
fullName: user.fullName,
|
||||||
role: user.roleCode,
|
role: user.roleCode,
|
||||||
locationId: user.locationId,
|
districtId: user.districtId,
|
||||||
phone: user.mobileNumber,
|
phone: user.mobileNumber,
|
||||||
createdAt: (user as any).createdAt
|
createdAt: (user as any).createdAt
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,7 +111,7 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isExternal: true, // Dealers are external users
|
isExternal: true, // Dealers are external users
|
||||||
locationId: application.locationId
|
districtId: application.districtId || application.locationId
|
||||||
});
|
});
|
||||||
console.log(`[Dealer Onboarding] Created new Dealer user account for ${user.email}.`);
|
console.log(`[Dealer Onboarding] Created new Dealer user account for ${user.email}.`);
|
||||||
}
|
}
|
||||||
@ -150,7 +150,7 @@ export const createDealer = async (req: AuthRequest, res: Response) => {
|
|||||||
status: 'Active',
|
status: 'Active',
|
||||||
establishedDate: new Date(),
|
establishedDate: new Date(),
|
||||||
dealerId: user.id,
|
dealerId: user.id,
|
||||||
locationId: application.locationId
|
districtId: application.districtId || application.locationId
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[Dealer Onboarding] Created outlet ${outlet.code} for application ${application.applicationId} linked to user ${user.email}.`);
|
console.log(`[Dealer Onboarding] Created outlet ${outlet.code} for application ${application.applicationId} linked to user ${user.email}.`);
|
||||||
|
|||||||
@ -1,18 +1,118 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { User } = db;
|
const { User } = db;
|
||||||
|
|
||||||
// --- Districts (Locations) ---
|
// --- Areas (Granular Locations) ---
|
||||||
|
export const getAreas = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
let search = req.query.search as string;
|
||||||
|
let page = (req.query.page || 1) as any;
|
||||||
|
let limit = (req.query.limit || 10) as any;
|
||||||
|
|
||||||
|
const checkNested = (obj: any) => {
|
||||||
|
if (!obj || typeof obj !== 'object') return;
|
||||||
|
if (!search && obj.search) search = obj.search;
|
||||||
|
if (page === 1 && obj.page) page = obj.page;
|
||||||
|
if (limit === 10 && obj.limit) limit = obj.limit;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkNested(req.query.params);
|
||||||
|
|
||||||
|
const isAll = limit === 'all' || limit === -1 || limit === '-1';
|
||||||
|
|
||||||
|
page = Number(page || 1);
|
||||||
|
limit = isAll ? null : Number(limit || 10);
|
||||||
|
const offset = isAll ? null : (page - 1) * limit;
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (search) {
|
||||||
|
where[Op.or] = [
|
||||||
|
{ name: { [Op.iLike]: `%${search}%` } },
|
||||||
|
{ city: { [Op.iLike]: `%${search}%` } },
|
||||||
|
{ '$district.name$': { [Op.iLike]: `%${search}%` } }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { count, rows: areas } = await db.Location.findAndCountAll({
|
||||||
|
where,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.District,
|
||||||
|
as: 'district',
|
||||||
|
include: [
|
||||||
|
{ model: db.Zone, as: 'zone', attributes: ['name'] },
|
||||||
|
{ model: db.Region, as: 'region', attributes: ['name'] },
|
||||||
|
{ model: db.State, as: 'state', attributes: ['name'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [['name', 'ASC']],
|
||||||
|
limit: limit === null ? undefined : Number(limit),
|
||||||
|
offset: offset === null ? undefined : Number(offset),
|
||||||
|
distinct: true,
|
||||||
|
subQuery: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = areas.map((a: any) => {
|
||||||
|
const d = a.district || {};
|
||||||
|
return {
|
||||||
|
...a.toJSON(),
|
||||||
|
districtName: d.name || 'N/A',
|
||||||
|
zoneName: d.zone?.name || 'UNKNOWN',
|
||||||
|
regionName: d.region?.name || 'UNKNOWN',
|
||||||
|
stateName: d.state?.name || 'UNKNOWN'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
pagination: {
|
||||||
|
total: count,
|
||||||
|
page: Number(page),
|
||||||
|
limit: isAll ? count : Number(limit),
|
||||||
|
totalPages: isAll ? 1 : Math.ceil(count / Number(limit))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get areas error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error fetching areas' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Districts (Territory Entities) ---
|
||||||
export const getDistricts = async (req: Request, res: Response) => {
|
export const getDistricts = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const districts = await db.Location.findAll({
|
let search = req.query.search as string;
|
||||||
|
let limit = (req.query.limit || 10) as any;
|
||||||
|
const stateId = req.query.stateId as string;
|
||||||
|
const zoneId = req.query.zoneId as string;
|
||||||
|
const regionId = req.query.regionId as string;
|
||||||
|
|
||||||
|
const isAll = limit === 'all' || limit === -1 || limit === '-1';
|
||||||
|
|
||||||
|
const where: any = {};
|
||||||
|
if (search) {
|
||||||
|
where.name = { [Op.iLike]: `%${search}%` };
|
||||||
|
}
|
||||||
|
if (stateId) where.stateId = stateId;
|
||||||
|
if (zoneId) where.zoneId = zoneId;
|
||||||
|
if (regionId) where.regionId = regionId;
|
||||||
|
|
||||||
|
const { count, rows: districts } = await db.District.findAndCountAll({
|
||||||
|
where,
|
||||||
include: [
|
include: [
|
||||||
{ model: db.Zone, as: 'zone', attributes: ['name'] },
|
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] },
|
||||||
{ model: db.Region, as: 'region', attributes: ['name'] },
|
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
|
||||||
{ model: db.State, as: 'state', attributes: ['name'] },
|
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
|
||||||
{ model: db.User, as: 'asm', attributes: ['id', 'fullName', 'email'] }
|
{ model: db.User, as: 'asm', attributes: ['id', 'fullName', 'email', 'employeeId'] },
|
||||||
|
{ model: db.User, as: 'zonalManager', attributes: ['id', 'fullName', 'email', 'employeeId'] }
|
||||||
],
|
],
|
||||||
order: [['name', 'ASC']]
|
order: [['name', 'ASC']],
|
||||||
|
limit: isAll ? undefined : Number(limit),
|
||||||
|
distinct: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = districts.map((d: any) => ({
|
const result = districts.map((d: any) => ({
|
||||||
@ -20,35 +120,67 @@ export const getDistricts = async (req: Request, res: Response) => {
|
|||||||
zoneName: d.zone?.name || 'UNKNOWN',
|
zoneName: d.zone?.name || 'UNKNOWN',
|
||||||
regionName: d.region?.name || 'UNKNOWN',
|
regionName: d.region?.name || 'UNKNOWN',
|
||||||
stateName: d.state?.name || 'UNKNOWN',
|
stateName: d.state?.name || 'UNKNOWN',
|
||||||
asmName: d.asm?.fullName || 'UNASSIGNED'
|
asmName: d.asm?.fullName || 'UNASSIGNED',
|
||||||
|
zmName: d.zonalManager?.fullName || 'UNASSIGNED'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({ success: true, data: result });
|
res.json({ success: true, data: result, total: count });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get districts error:', error);
|
console.error('Get districts error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error fetching districts' });
|
res.status(500).json({ success: false, message: 'Error fetching districts' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const createDistrict = async (req: Request, res: Response) => {
|
export const createDistrict = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { name, code, stateId, regionId, zoneId, asmId, description } = req.body;
|
const { name, code, stateName, city, openFrom, openTo, status, isActive, description } = req.body;
|
||||||
if (!name) 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({
|
// Find or Create state if stateName provided
|
||||||
|
let stateId = req.body.stateId;
|
||||||
|
if (stateName && !stateId) {
|
||||||
|
const [state] = await db.State.findOrCreate({
|
||||||
|
where: { name: stateName },
|
||||||
|
defaults: { name: stateName }
|
||||||
|
});
|
||||||
|
stateId = state.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const district = await db.District.create({
|
||||||
name,
|
name,
|
||||||
code,
|
code,
|
||||||
stateId,
|
stateId,
|
||||||
regionId,
|
isActive: isActive !== undefined ? isActive : true
|
||||||
zoneId,
|
|
||||||
asmId,
|
|
||||||
description
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json({ success: true, data: district });
|
const area = await db.Location.create({
|
||||||
|
name,
|
||||||
|
districtId: district.id,
|
||||||
|
city: city || name,
|
||||||
|
isActive: true,
|
||||||
|
openFrom: openFrom || null,
|
||||||
|
openTo: openTo || null,
|
||||||
|
description: description || null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create associated Opportunity for "Active Period" and "City"
|
||||||
|
await db.Opportunity.create({
|
||||||
|
districtId: district.id,
|
||||||
|
areaId: area.id,
|
||||||
|
city: city || name,
|
||||||
|
openFrom: openFrom || null,
|
||||||
|
openTo: openTo || null,
|
||||||
|
status: status || 'inactive',
|
||||||
|
opportunityType: 'New Dealership',
|
||||||
|
capacity: 'Standard',
|
||||||
|
priority: 'Medium'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({ success: true, data: area });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create district error:', error);
|
console.error('Create area error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error creating district' });
|
res.status(500).json({ success: false, message: 'Error creating area' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,7 +190,12 @@ export const getRegions = async (req: Request, res: Response) => {
|
|||||||
const regions = await db.Region.findAll({
|
const regions = await db.Region.findAll({
|
||||||
include: [
|
include: [
|
||||||
{ model: db.Zone, as: 'zone', attributes: ['name'] },
|
{ model: db.Zone, as: 'zone', attributes: ['name'] },
|
||||||
{ model: db.Location, as: 'districts', attributes: ['id', 'name'] }
|
{
|
||||||
|
model: db.District,
|
||||||
|
as: 'districts',
|
||||||
|
attributes: ['id', 'name', 'stateId'],
|
||||||
|
include: [{ model: db.State, as: 'state', attributes: ['id', 'name'] }]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
order: [['name', 'ASC']]
|
order: [['name', 'ASC']]
|
||||||
});
|
});
|
||||||
@ -78,7 +215,7 @@ export const getRegions = async (req: Request, res: Response) => {
|
|||||||
const districtIds = (region.districts || []).map((d: any) => d.id);
|
const districtIds = (region.districts || []).map((d: any) => d.id);
|
||||||
|
|
||||||
const [asmCount, rmCount, rmAssignment] = await Promise.all([
|
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]: asmRoleIds }, districtId: { [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.count({ where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true }, distinct: true, col: 'userId' }),
|
||||||
db.UserRole.findOne({
|
db.UserRole.findOne({
|
||||||
where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true },
|
where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true },
|
||||||
@ -89,7 +226,22 @@ export const getRegions = async (req: Request, res: Response) => {
|
|||||||
regionJson.asmCount = asmCount;
|
regionJson.asmCount = asmCount;
|
||||||
regionJson.regionalOfficerCount = rmCount;
|
regionJson.regionalOfficerCount = rmCount;
|
||||||
regionJson.regionalManager = rmAssignment?.user || null;
|
regionJson.regionalManager = rmAssignment?.user || null;
|
||||||
regionJson.districts = (region.districts || []).map((d: any) => ({ id: d.id, name: d.name.toUpperCase() }));
|
regionJson.rbmCode = region.rbmCode || 'N/A';
|
||||||
|
|
||||||
|
// Extract unique states for this region
|
||||||
|
const statesMap = new Map();
|
||||||
|
(region.districts || []).forEach((d: any) => {
|
||||||
|
if (d.state) {
|
||||||
|
statesMap.set(d.state.id, d.state.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
regionJson.states = Array.from(statesMap.values());
|
||||||
|
|
||||||
|
regionJson.districts = (region.districts || []).map((d: any) => ({
|
||||||
|
id: d.id,
|
||||||
|
name: d.name.toUpperCase(),
|
||||||
|
stateId: d.stateId
|
||||||
|
}));
|
||||||
return regionJson;
|
return regionJson;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -124,18 +276,38 @@ export const createRegion = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Assign Districts
|
// 2. Assign Districts (with conflict check)
|
||||||
const targetDistrictIds = districts || districtIds;
|
const targetDistrictIds = districts || districtIds;
|
||||||
if (Array.isArray(targetDistrictIds) && targetDistrictIds.length > 0) {
|
if (Array.isArray(targetDistrictIds) && targetDistrictIds.length > 0) {
|
||||||
await db.Location.update(
|
const conflicts = await db.District.findAll({
|
||||||
|
where: {
|
||||||
|
id: { [db.Sequelize.Op.in]: targetDistrictIds },
|
||||||
|
regionId: { [db.Sequelize.Op.and]: [{ [db.Sequelize.Op.ne]: null }, { [db.Sequelize.Op.ne]: region.id }] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
message: `Districts already assigned to another region: ${conflicts.map((c: any) => c.name).join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await db.District.update(
|
||||||
{ regionId: region.id, zoneId: targetZoneId },
|
{ regionId: region.id, zoneId: targetZoneId },
|
||||||
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
|
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, message: 'Region created', data: region });
|
res.status(201).json({ success: true, message: 'Region created', data: region });
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Create region error:', error);
|
console.error('Create region error:', error);
|
||||||
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
|
const field = error.errors?.[0]?.path || 'field';
|
||||||
|
const value = error.errors?.[0]?.value || '';
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
message: `A region with this ${field} "${value}" already exists. Please use a different name.`
|
||||||
|
});
|
||||||
|
}
|
||||||
res.status(500).json({ success: false, message: 'Error creating region' });
|
res.status(500).json({ success: false, message: 'Error creating region' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -175,12 +347,26 @@ export const updateRegion = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Update Districts
|
// 2. Update Districts (with conflict check)
|
||||||
const targetDistrictIds = districts || districtIds;
|
const targetDistrictIds = districts || districtIds;
|
||||||
if (Array.isArray(targetDistrictIds)) {
|
if (Array.isArray(targetDistrictIds)) {
|
||||||
await db.Location.update({ regionId: null }, { where: { regionId: id } });
|
|
||||||
if (targetDistrictIds.length > 0) {
|
if (targetDistrictIds.length > 0) {
|
||||||
await db.Location.update(
|
const conflicts = await db.District.findAll({
|
||||||
|
where: {
|
||||||
|
id: { [db.Sequelize.Op.in]: targetDistrictIds },
|
||||||
|
regionId: { [db.Sequelize.Op.and]: [{ [db.Sequelize.Op.ne]: null }, { [db.Sequelize.Op.ne]: id }] }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
message: `Districts already assigned to another region: ${conflicts.map((c: any) => c.name).join(', ')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.District.update({ regionId: null }, { where: { regionId: id } });
|
||||||
|
if (targetDistrictIds.length > 0) {
|
||||||
|
await db.District.update(
|
||||||
{ regionId: id, zoneId: region.zoneId },
|
{ regionId: id, zoneId: region.zoneId },
|
||||||
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
|
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
|
||||||
);
|
);
|
||||||
@ -188,8 +374,16 @@ export const updateRegion = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, message: 'Region updated' });
|
res.json({ success: true, message: 'Region updated' });
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Update region error:', error);
|
console.error('Update region error:', error);
|
||||||
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
|
const field = error.errors?.[0]?.path || 'field';
|
||||||
|
const value = error.errors?.[0]?.value || '';
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
message: `A region with this ${field} "${value}" already exists. Please use a different name.`
|
||||||
|
});
|
||||||
|
}
|
||||||
res.status(500).json({ success: false, message: 'Error updating region' });
|
res.status(500).json({ success: false, message: 'Error updating region' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -201,7 +395,7 @@ export const getZones = async (req: Request, res: Response) => {
|
|||||||
include: [
|
include: [
|
||||||
{ model: db.Region, as: 'regions', attributes: ['id', 'name'] },
|
{ model: db.Region, as: 'regions', attributes: ['id', 'name'] },
|
||||||
{ model: db.State, as: 'states', attributes: ['id', 'name'] },
|
{ model: db.State, as: 'states', attributes: ['id', 'name'] },
|
||||||
{ model: db.Location, as: 'districts', attributes: ['id'] }
|
{ model: db.District, as: 'districts', attributes: ['id'] }
|
||||||
],
|
],
|
||||||
order: [['name', 'ASC']]
|
order: [['name', 'ASC']]
|
||||||
});
|
});
|
||||||
@ -228,8 +422,8 @@ export const getZones = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// For each ZM, fetch their assigned districts in this zone
|
// For each ZM, fetch their assigned districts in this zone
|
||||||
const zonalManagers = await Promise.all(zms.map(async (zmRole: any) => {
|
const zonalManagers = await Promise.all(zms.map(async (zmRole: any) => {
|
||||||
const districts = await db.Location.findAll({
|
const districts = await db.District.findAll({
|
||||||
where: { zoneId: zone.id, regionId: zmRole.regionId || null }, // ZMs usually managed regions or specific district sets
|
where: { zmId: zmRole.user.id, zoneId: zone.id }, // ZMs usually managed regions or specific district sets
|
||||||
attributes: ['name']
|
attributes: ['name']
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -237,6 +431,7 @@ export const getZones = async (req: Request, res: Response) => {
|
|||||||
name: zmRole.user.fullName || zmRole.user.name,
|
name: zmRole.user.fullName || zmRole.user.name,
|
||||||
email: zmRole.user.email,
|
email: zmRole.user.email,
|
||||||
phone: zmRole.user.mobileNumber || 'N/A',
|
phone: zmRole.user.mobileNumber || 'N/A',
|
||||||
|
code: zmRole.managerCode || zmRole.user.employeeId || 'N/A',
|
||||||
districts: districts.map((d: any) => d.name)
|
districts: districts.map((d: any) => d.name)
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
@ -249,7 +444,8 @@ export const getZones = async (req: Request, res: Response) => {
|
|||||||
id: zbhAssignment.user.id,
|
id: zbhAssignment.user.id,
|
||||||
name: zbhAssignment.user.fullName || zbhAssignment.user.name,
|
name: zbhAssignment.user.fullName || zbhAssignment.user.name,
|
||||||
email: zbhAssignment.user.email,
|
email: zbhAssignment.user.email,
|
||||||
phone: zbhAssignment.user.mobileNumber || 'N/A'
|
phone: zbhAssignment.user.mobileNumber || 'N/A',
|
||||||
|
code: zone.zbhCode || 'N/A'
|
||||||
} : null;
|
} : null;
|
||||||
zoneJson.zonalManagers = zonalManagers;
|
zoneJson.zonalManagers = zonalManagers;
|
||||||
return zoneJson;
|
return zoneJson;
|
||||||
@ -285,7 +481,7 @@ export const createZone = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (Array.isArray(stateIds) && stateIds.length > 0) {
|
if (Array.isArray(stateIds) && stateIds.length > 0) {
|
||||||
await db.State.update({ zoneId: zone.id }, { where: { id: { [db.Sequelize.Op.in]: stateIds } } });
|
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 } } });
|
await db.District.update({ zoneId: zone.id }, { where: { stateId: { [db.Sequelize.Op.in]: stateIds } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, message: 'Zone created', data: zone });
|
res.status(201).json({ success: true, message: 'Zone created', data: zone });
|
||||||
@ -325,11 +521,11 @@ export const updateZone = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (Array.isArray(stateIds)) {
|
if (Array.isArray(stateIds)) {
|
||||||
await db.State.update({ zoneId: null }, { where: { zoneId: id } });
|
await db.State.update({ zoneId: null }, { where: { zoneId: id } });
|
||||||
await db.Location.update({ zoneId: null }, { where: { zoneId: id } });
|
await db.District.update({ zoneId: null }, { where: { zoneId: id } });
|
||||||
|
|
||||||
if (stateIds.length > 0) {
|
if (stateIds.length > 0) {
|
||||||
await db.State.update({ zoneId: id }, { where: { id: { [db.Sequelize.Op.in]: stateIds } } });
|
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 } } });
|
await db.District.update({ zoneId: id }, { where: { stateId: { [db.Sequelize.Op.in]: stateIds } } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,7 +557,7 @@ export const createState = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const state = await db.State.create({ name, zoneId });
|
const state = await db.State.create({ name, zoneId });
|
||||||
if (zoneId) {
|
if (zoneId) {
|
||||||
await db.Location.update({ zoneId }, { where: { stateId: state.id } });
|
await db.District.update({ zoneId }, { where: { stateId: state.id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json({ success: true, data: state });
|
res.status(201).json({ success: true, data: state });
|
||||||
@ -388,7 +584,7 @@ export const getManagersByRole = async (req: Request, res: Response) => {
|
|||||||
const filteredManagers = managers.filter((m: any) => {
|
const filteredManagers = managers.filter((m: any) => {
|
||||||
const hasRole = !roleCode || m.roleCode === roleCode || (m.userRoles || []).some((a: any) => a.role?.roleCode === roleCode);
|
const hasRole = !roleCode || m.roleCode === roleCode || (m.userRoles || []).some((a: any) => a.role?.roleCode === roleCode);
|
||||||
const hasLocation = !locationId || (m.userRoles || []).some((a: any) =>
|
const hasLocation = !locationId || (m.userRoles || []).some((a: any) =>
|
||||||
a.locationId === locationId ||
|
a.districtId === locationId ||
|
||||||
a.zoneId === locationId ||
|
a.zoneId === locationId ||
|
||||||
a.regionId === locationId
|
a.regionId === locationId
|
||||||
);
|
);
|
||||||
@ -406,45 +602,307 @@ export const getAreaManagers = async (req: Request, res: Response) => {
|
|||||||
return getManagersByRole(req, res);
|
return getManagersByRole(req, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Delete ---
|
// --- Delete Area (Location) ---
|
||||||
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.Location.destroy({ where: { id } });
|
const area = await db.Location.findByPk(id);
|
||||||
res.json({ success: true, message: 'District deleted' });
|
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
|
||||||
|
|
||||||
|
// Delete associated opportunities if they belong to this granular location
|
||||||
|
await db.Opportunity.destroy({ where: { areaId: id } });
|
||||||
|
|
||||||
|
// Delete the location itself
|
||||||
|
await area.destroy();
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Area deleted successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete district error:', error);
|
console.error('Delete area error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error deleting district' });
|
res.status(500).json({ success: false, message: 'Error deleting area' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateLocation = async (req: Request, res: Response) => {
|
export const updateLocation = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params; // This is the Area ID
|
||||||
const { name, code, stateId, regionId, zoneId, asmId, isActive, description } = req.body;
|
const { name, code, stateName, city, openFrom, openTo, status, 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({
|
const area = await db.Location.findByPk(id, {
|
||||||
name,
|
include: [{ model: db.District, as: 'district' }]
|
||||||
code,
|
});
|
||||||
stateId,
|
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
|
||||||
regionId,
|
|
||||||
zoneId,
|
const district = area.district;
|
||||||
asmId,
|
|
||||||
isActive,
|
// 1. Update District
|
||||||
description
|
if (district) {
|
||||||
|
let stateId = req.body.stateId;
|
||||||
|
if (stateName && !stateId) {
|
||||||
|
const [state] = await db.State.findOrCreate({
|
||||||
|
where: { name: stateName },
|
||||||
|
defaults: { name: stateName }
|
||||||
|
});
|
||||||
|
stateId = state.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await district.update({
|
||||||
|
name: name || district.name,
|
||||||
|
code: code || district.code,
|
||||||
|
stateId: stateId || district.stateId,
|
||||||
|
isActive: isActive !== undefined ? isActive : district.isActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update Area
|
||||||
|
await area.update({
|
||||||
|
name: name || area.name,
|
||||||
|
city: city || area.city,
|
||||||
|
isActive: isActive !== undefined ? isActive : area.isActive,
|
||||||
|
openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom,
|
||||||
|
openTo: openTo !== undefined ? (openTo || null) : area.openTo,
|
||||||
|
description: description || area.description
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, message: 'District updated' });
|
// 3. Update or Create associated Opportunity
|
||||||
|
const [opportunity] = await db.Opportunity.findOrBuild({
|
||||||
|
where: { areaId: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
opportunity.set({
|
||||||
|
districtId: district?.id || opportunity.districtId,
|
||||||
|
city: city || opportunity.city || name || area.name,
|
||||||
|
openFrom: openFrom !== undefined ? (openFrom || null) : opportunity.openFrom,
|
||||||
|
openTo: openTo !== undefined ? (openTo || null) : opportunity.openTo,
|
||||||
|
status: status || opportunity.status || 'inactive'
|
||||||
|
});
|
||||||
|
await opportunity.save();
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Area updated' });
|
||||||
|
|
||||||
|
if (district && typeof district.id === 'string') {
|
||||||
|
await syncLocationManagers(district.id);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update district error:', error);
|
console.error('Update area error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error updating district' });
|
res.status(500).json({ success: false, message: 'Error updating area' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Managers ---
|
||||||
|
export const getASMs = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const asms = await db.User.findAll({
|
||||||
|
where: {
|
||||||
|
roleCode: { [db.Sequelize.Op.in]: ['ASM', 'AREA SALES MANAGER'] },
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.UserRole,
|
||||||
|
as: 'userRoles',
|
||||||
|
where: { isActive: true },
|
||||||
|
required: false,
|
||||||
|
include: [{ model: db.Role, as: 'role', where: { roleCode: 'ASM' } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: db.District,
|
||||||
|
as: 'managedAsmDistricts',
|
||||||
|
include: [
|
||||||
|
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
|
||||||
|
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
|
||||||
|
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [['fullName', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = (asms || []).map((u: any) => {
|
||||||
|
const districts = u.managedAsmDistricts || [];
|
||||||
|
const asmRoleAssignment = (u.userRoles || []).find((r: any) => r.role?.roleCode === 'ASM');
|
||||||
|
const asmCode = asmRoleAssignment?.managerCode || u.employeeId;
|
||||||
|
|
||||||
|
const zoneSet = new Set();
|
||||||
|
const regionSet = new Set();
|
||||||
|
const stateSet = new Set();
|
||||||
|
|
||||||
|
const territoryInfo = districts.map((d: any) => {
|
||||||
|
if (d.zone) zoneSet.add(JSON.stringify({ id: d.zone.id, name: d.zone.name }));
|
||||||
|
if (d.region) regionSet.add(JSON.stringify({ id: d.region.id, name: d.region.name }));
|
||||||
|
if (d.state) stateSet.add(d.state.name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
stateId: d.stateId,
|
||||||
|
regionId: d.regionId,
|
||||||
|
zoneId: d.zoneId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const zones = Array.from(zoneSet).map((s: any) => JSON.parse(s));
|
||||||
|
const regions = Array.from(regionSet).map((s: any) => JSON.parse(s));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: u.id,
|
||||||
|
name: u.fullName,
|
||||||
|
email: u.email,
|
||||||
|
phone: u.mobileNumber,
|
||||||
|
employeeId: u.employeeId,
|
||||||
|
asmCode: asmCode || 'N/A',
|
||||||
|
status: u.status,
|
||||||
|
zoneId: zones[0]?.id || '',
|
||||||
|
zoneName: zones[0]?.name || 'Unassigned',
|
||||||
|
regionId: regions[0]?.id || '',
|
||||||
|
regionName: regions[0]?.name || 'Unassigned',
|
||||||
|
areasManaged: territoryInfo,
|
||||||
|
stateNames: Array.from(stateSet),
|
||||||
|
totalDistricts: territoryInfo.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get ASMs error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error fetching ASMs' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getZonalManagers = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const zms = await db.User.findAll({
|
||||||
|
attributes: ['id', 'fullName', 'email', 'employeeId', 'status'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.UserRole,
|
||||||
|
as: 'userRoles',
|
||||||
|
where: { isActive: true },
|
||||||
|
required: true,
|
||||||
|
include: [{
|
||||||
|
model: db.Role,
|
||||||
|
as: 'role',
|
||||||
|
where: { roleCode: { [Op.in]: ['ZM', 'DD-ZM', 'ZBH'] } }
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: db.District,
|
||||||
|
as: 'managedZmDistricts',
|
||||||
|
include: [
|
||||||
|
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] },
|
||||||
|
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
|
||||||
|
{ model: db.State, as: 'state', attributes: ['id', 'name'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: [['fullName', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = (zms || []).map((u: any) => {
|
||||||
|
const rolePriority = ['DD-ZM', 'ZM', 'ZBH'];
|
||||||
|
const roleAssignment = (u.userRoles || []).sort((a: any, b: any) => {
|
||||||
|
const aIndex = rolePriority.indexOf(a.role?.roleCode || '');
|
||||||
|
const bIndex = rolePriority.indexOf(b.role?.roleCode || '');
|
||||||
|
if (aIndex !== bIndex) return aIndex - bIndex;
|
||||||
|
// If same role type, prefer the one with a code
|
||||||
|
if (a.managerCode && !b.managerCode) return -1;
|
||||||
|
if (!a.managerCode && b.managerCode) return 1;
|
||||||
|
return 0;
|
||||||
|
})[0];
|
||||||
|
const zmCode = roleAssignment?.managerCode || u.employeeId || 'N/A';
|
||||||
|
|
||||||
|
// Collect unique zones and states
|
||||||
|
const zoneSet = new Set<string>();
|
||||||
|
const stateSet = new Set<string>();
|
||||||
|
let inferredZoneId = roleAssignment?.zoneId || null;
|
||||||
|
|
||||||
|
(u.managedZmDistricts || []).forEach((d: any) => {
|
||||||
|
if (d.zone) {
|
||||||
|
zoneSet.add(d.zone.name);
|
||||||
|
if (!inferredZoneId) inferredZoneId = d.zone.id; // Fallback to first district's zone if role zone is missing
|
||||||
|
}
|
||||||
|
if (d.state) stateSet.add(d.state.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: u.id,
|
||||||
|
name: u.fullName,
|
||||||
|
email: u.email,
|
||||||
|
employeeId: u.employeeId,
|
||||||
|
zmCode: zmCode,
|
||||||
|
status: u.status,
|
||||||
|
zoneId: inferredZoneId,
|
||||||
|
zones: Array.from(zoneSet).length > 0 ? Array.from(zoneSet) : ["Assigned Zone"],
|
||||||
|
stateNames: Array.from(stateSet),
|
||||||
|
districts: (u.managedZmDistricts || []).map((d: any) => ({
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
state: d.state?.name
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get ZMs error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error fetching Zonal Managers' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveZM = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { userId, zmCode, zoneId, districts, status } = req.body;
|
||||||
|
if (!userId) return res.status(400).json({ success: false, message: 'userId is required' });
|
||||||
|
|
||||||
|
// Find the ZM role (DD-ZM)
|
||||||
|
const zmRole = await db.Role.findOne({ where: { roleCode: 'DD-ZM' } });
|
||||||
|
if (!zmRole) return res.status(404).json({ success: false, message: 'ZM role (DD-ZM) not found in roles table' });
|
||||||
|
|
||||||
|
// Update User status if provided
|
||||||
|
if (status) {
|
||||||
|
await db.User.update({ status }, { where: { id: userId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deactivate existing ZM role assignments for this user
|
||||||
|
await db.UserRole.update({ isActive: false }, {
|
||||||
|
where: { userId, roleId: zmRole.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create new active UserRole with managerCode = zmCode
|
||||||
|
await db.UserRole.create({
|
||||||
|
userId,
|
||||||
|
roleId: zmRole.id,
|
||||||
|
zoneId: zoneId || null,
|
||||||
|
managerCode: zmCode || null,
|
||||||
|
isActive: true,
|
||||||
|
isPrimary: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign districts to this user if provided
|
||||||
|
// First, clear this ZM from any other districts they might have had
|
||||||
|
await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: userId } });
|
||||||
|
|
||||||
|
if (Array.isArray(districts) && districts.length > 0) {
|
||||||
|
// Then assign new ones
|
||||||
|
const updateProps: any = {
|
||||||
|
zmId: userId,
|
||||||
|
zmCode: zmCode || null
|
||||||
|
};
|
||||||
|
if (zoneId) updateProps.zoneId = zoneId;
|
||||||
|
|
||||||
|
await db.District.update(
|
||||||
|
updateProps,
|
||||||
|
{ where: { id: { [db.Sequelize.Op.in]: districts } } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Zonal Manager saved successfully' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save ZM error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error saving Zonal Manager' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Semantic Aliases for Backward Compatibility ---
|
|
||||||
export const getAreas = getDistricts;
|
|
||||||
export const createArea = createDistrict;
|
export const createArea = createDistrict;
|
||||||
export const deleteArea = deleteLocation;
|
export const deleteArea = deleteLocation;
|
||||||
export const createDistrictLegacy = createDistrict; // Just in case
|
export const createDistrictLegacy = createDistrict;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import {
|
||||||
|
// Districts
|
||||||
|
getDistricts,
|
||||||
|
getAreas,
|
||||||
|
createDistrict,
|
||||||
|
updateLocation,
|
||||||
|
deleteLocation,
|
||||||
|
// Regions
|
||||||
|
getRegions,
|
||||||
|
createRegion,
|
||||||
|
updateRegion,
|
||||||
|
// Zones
|
||||||
|
getZones,
|
||||||
|
createZone,
|
||||||
|
updateZone,
|
||||||
|
// States
|
||||||
|
getStates,
|
||||||
|
createState,
|
||||||
|
// Managers
|
||||||
|
getManagersByRole,
|
||||||
|
getAreaManagers,
|
||||||
|
getASMs,
|
||||||
|
getZonalManagers,
|
||||||
|
saveZM
|
||||||
|
} from './master.controller.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// --- Districts ---
|
||||||
|
router.get('/districts', getDistricts);
|
||||||
|
router.post('/districts', createDistrict);
|
||||||
|
router.put('/districts/:id', updateLocation);
|
||||||
|
router.delete('/districts/:id', deleteLocation);
|
||||||
|
|
||||||
|
// --- Areas ---
|
||||||
|
router.get('/areas', getAreas);
|
||||||
|
router.post('/areas', createDistrict);
|
||||||
|
router.put('/areas/:id', updateLocation);
|
||||||
|
router.delete('/areas/:id', deleteLocation);
|
||||||
|
|
||||||
|
// --- Regions ---
|
||||||
|
router.get('/regions', getRegions);
|
||||||
|
router.post('/regions', createRegion);
|
||||||
|
router.put('/regions/:id', updateRegion);
|
||||||
|
|
||||||
|
// --- Zones ---
|
||||||
|
router.get('/zones', getZones);
|
||||||
|
router.post('/zones', createZone);
|
||||||
|
router.put('/zones/:id', updateZone);
|
||||||
|
|
||||||
|
// --- States ---
|
||||||
|
router.get('/states', getStates);
|
||||||
|
router.post('/states', createState);
|
||||||
|
|
||||||
|
// --- Managers ---
|
||||||
|
router.get('/managers', getManagersByRole);
|
||||||
|
router.get('/area-managers', getAreaManagers);
|
||||||
|
router.get('/asms', getASMs);
|
||||||
|
router.get('/zonal-managers', getZonalManagers);
|
||||||
|
router.post('/zonal-managers', saveZM);
|
||||||
|
|
||||||
|
export default router;
|
||||||
105
src/modules/master/syncHierarchy.service.ts
Normal file
105
src/modules/master/syncHierarchy.service.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import db from '../../database/models/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes the Location (District) table's manager IDs with the UserRole table.
|
||||||
|
* This ensures the Location table always reflects the "Active" managers for each role type.
|
||||||
|
*/
|
||||||
|
export const syncLocationManagers = async (districtId: string) => {
|
||||||
|
try {
|
||||||
|
const UserRole = db.UserRole;
|
||||||
|
const Role = db.Role;
|
||||||
|
const Op = db.Sequelize.Op;
|
||||||
|
|
||||||
|
// Fetch active assignments for this district
|
||||||
|
const activeAssignments = await UserRole.findAll({
|
||||||
|
where: { districtId, isActive: true },
|
||||||
|
include: [
|
||||||
|
{ model: Role, as: 'role', attributes: ['roleCode'] },
|
||||||
|
{ model: db.User, as: 'user', attributes: ['employeeId'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find primary/last assigned manager for each type
|
||||||
|
const asm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'ASM' || (a.role as any)?.roleCode === 'AREA SALES MANAGER');
|
||||||
|
const ddAm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'DD-AM' || (a.role as any)?.roleCode === 'AREA MANAGER');
|
||||||
|
const zm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER');
|
||||||
|
|
||||||
|
// Update District table with IDs and Codes
|
||||||
|
await db.District.update({
|
||||||
|
asmId: asm?.userId || null,
|
||||||
|
asmCode: asm?.managerCode || asm?.user?.employeeId || null,
|
||||||
|
ddAmId: ddAm?.userId || null,
|
||||||
|
ddAmCode: ddAm?.managerCode || ddAm?.user?.employeeId || null,
|
||||||
|
zmId: zm?.userId || null,
|
||||||
|
zmCode: zm?.managerCode || zm?.user?.employeeId || null
|
||||||
|
}, {
|
||||||
|
where: { id: districtId }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Sync] District ${districtId} synchronized successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Sync] Error synchronizing District ${districtId}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes the Region table's manager ID and Code.
|
||||||
|
*/
|
||||||
|
export const syncRegionManager = async (regionId: string) => {
|
||||||
|
try {
|
||||||
|
const activeAssignment = await db.UserRole.findOne({
|
||||||
|
where: { regionId, isActive: true },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.Role,
|
||||||
|
as: 'role',
|
||||||
|
where: { roleCode: { [db.Sequelize.Op.in]: ['RM', 'RBM', 'REGIONAL MANAGER'] } }
|
||||||
|
},
|
||||||
|
{ model: db.User, as: 'user', attributes: ['employeeId'] }
|
||||||
|
],
|
||||||
|
order: [['assignedAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.Region.update({
|
||||||
|
rbmId: activeAssignment?.userId || null,
|
||||||
|
rbmCode: activeAssignment?.managerCode || activeAssignment?.user?.employeeId || null
|
||||||
|
}, {
|
||||||
|
where: { id: regionId }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Sync] Region ${regionId} synchronized successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Sync] Error synchronizing Region ${regionId}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes the Zone table's manager ID and Code.
|
||||||
|
*/
|
||||||
|
export const syncZoneManager = async (zoneId: string) => {
|
||||||
|
try {
|
||||||
|
const activeAssignment = await db.UserRole.findOne({
|
||||||
|
where: { zoneId, isActive: true },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: db.Role,
|
||||||
|
as: 'role',
|
||||||
|
where: { roleCode: { [db.Sequelize.Op.in]: ['ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'] } }
|
||||||
|
},
|
||||||
|
{ model: db.User, as: 'user', attributes: ['employeeId'] }
|
||||||
|
],
|
||||||
|
order: [['assignedAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.Zone.update({
|
||||||
|
zbhId: activeAssignment?.userId || null,
|
||||||
|
zbhCode: activeAssignment?.managerCode || activeAssignment?.user?.employeeId || null
|
||||||
|
}, {
|
||||||
|
where: { id: zoneId }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Sync] Zone ${zoneId} synchronized successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Sync] Error synchronizing Zone ${zoneId}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, Location, LocationHierarchy } = db;
|
const { Application, Opportunity, ApplicationStatusHistory, ApplicationProgress, AuditLog, District, State, Region } = db;
|
||||||
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS, APPLICATION_STAGES, APPLICATION_STATUS } from '../../common/config/constants.js';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
@ -48,77 +48,39 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
let isOpportunityAvailable = false;
|
let isOpportunityAvailable = false;
|
||||||
const normalizedType = normalizeLocationType(locationType);
|
const normalizedType = normalizeLocationType(locationType);
|
||||||
|
|
||||||
if (req.body.locationId && normalizedType) {
|
if (req.body.locationId && normalizedType === 'district') {
|
||||||
const selectedLocation = await Location.findOne({
|
const selectedDistrict = await District.findByPk(req.body.locationId);
|
||||||
where: {
|
if (selectedDistrict) {
|
||||||
id: req.body.locationId,
|
locationId = selectedDistrict.id;
|
||||||
type: normalizedType
|
isOpportunityAvailable = true;
|
||||||
}
|
}
|
||||||
});
|
} else if (req.body.locationId && normalizedType === 'state') {
|
||||||
if (selectedLocation) {
|
const selectedState = await State.findByPk(req.body.locationId);
|
||||||
locationId = selectedLocation.id;
|
if (selectedState) {
|
||||||
|
locationId = selectedState.id;
|
||||||
isOpportunityAvailable = true;
|
isOpportunityAvailable = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward-compatible fallback path for older payloads that send only names.
|
// Backward-compatible fallback path for older payloads that send only names.
|
||||||
if (!locationId && req.body.district) {
|
if (!locationId && req.body.district) {
|
||||||
const districtName = req.body.district;
|
const districtRecord: any = await District.findOne({
|
||||||
const stateName = req.body.state;
|
where: { name: { [Op.iLike]: req.body.district } },
|
||||||
|
include: req.body.state ? [{
|
||||||
// If state is available, disambiguate district by hierarchy parent.
|
model: State,
|
||||||
let districtRecord: any = null;
|
as: 'state',
|
||||||
if (stateName) {
|
where: { name: { [Op.iLike]: req.body.state } }
|
||||||
const matchedStates = await Location.findAll({
|
}] : []
|
||||||
where: {
|
});
|
||||||
name: { [Op.iLike]: stateName },
|
|
||||||
type: 'state'
|
|
||||||
},
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchedStates.length > 0) {
|
|
||||||
const stateIds = matchedStates.map((s: any) => s.id);
|
|
||||||
const districtLinks = await LocationHierarchy.findAll({
|
|
||||||
where: { parentId: { [Op.in]: stateIds } },
|
|
||||||
attributes: ['locationId']
|
|
||||||
});
|
|
||||||
const districtIds = districtLinks.map((link: any) => link.locationId);
|
|
||||||
if (districtIds.length > 0) {
|
|
||||||
districtRecord = await Location.findOne({
|
|
||||||
where: {
|
|
||||||
id: { [Op.in]: districtIds },
|
|
||||||
name: { [Op.iLike]: districtName },
|
|
||||||
type: 'district'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final fallback to old behavior if state context was unavailable or unresolved.
|
|
||||||
if (!districtRecord) {
|
|
||||||
districtRecord = await Location.findOne({
|
|
||||||
where: {
|
|
||||||
name: { [Op.iLike]: districtName },
|
|
||||||
type: 'district'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (districtRecord) {
|
if (districtRecord) {
|
||||||
locationId = districtRecord.id;
|
locationId = districtRecord.id;
|
||||||
isOpportunityAvailable = true;
|
isOpportunityAvailable = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last fallback: allow state-level canonical submissions.
|
if (!locationId && req.body.state) {
|
||||||
if (!locationId && normalizedType === 'state' && req.body.state) {
|
const stateRecord = await State.findOne({
|
||||||
const stateRecord = await Location.findOne({
|
where: { name: { [Op.iLike]: req.body.state } }
|
||||||
where: {
|
|
||||||
name: { [Op.iLike]: req.body.state },
|
|
||||||
type: 'state'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (stateRecord) {
|
if (stateRecord) {
|
||||||
locationId = stateRecord.id;
|
locationId = stateRecord.id;
|
||||||
@ -142,7 +104,8 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
currentStage: APPLICATION_STAGES.DD,
|
currentStage: APPLICATION_STAGES.DD,
|
||||||
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
|
overallStatus: isOpportunityAvailable ? APPLICATION_STATUS.QUESTIONNAIRE_PENDING : APPLICATION_STATUS.SUBMITTED,
|
||||||
progressPercentage: isOpportunityAvailable ? 10 : 0,
|
progressPercentage: isOpportunityAvailable ? 10 : 0,
|
||||||
locationId
|
locationId,
|
||||||
|
districtId: locationId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log Status History
|
// Log Status History
|
||||||
|
|||||||
@ -7,11 +7,12 @@ import { AUDIT_ACTIONS } from '../../common/config/constants.js';
|
|||||||
|
|
||||||
export const getOpportunities = async (req: Request, res: Response) => {
|
export const getOpportunities = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { status, locationId } = req.query as any;
|
const { status, locationId, districtId } = req.query as any;
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
|
|
||||||
if (status) where.status = status;
|
if (status) where.status = status;
|
||||||
if (locationId) where.locationId = locationId;
|
const targetDistrictId = districtId || locationId;
|
||||||
|
if (targetDistrictId) where.districtId = targetDistrictId;
|
||||||
|
|
||||||
const opportunities = await Opportunity.findAll({
|
const opportunities = await Opportunity.findAll({
|
||||||
where,
|
where,
|
||||||
@ -32,8 +33,8 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
leadSource, leadName, contactNumber, email,
|
leadSource, leadName, contactNumber, email,
|
||||||
locationId,
|
locationId, districtId, city,
|
||||||
opportunityType, priority
|
opportunityType, priority, capacity, notes
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const opportunity = await Opportunity.create({
|
const opportunity = await Opportunity.create({
|
||||||
@ -41,9 +42,12 @@ export const createOpportunity = async (req: AuthRequest, res: Response) => {
|
|||||||
leadName,
|
leadName,
|
||||||
contactNumber,
|
contactNumber,
|
||||||
email,
|
email,
|
||||||
locationId,
|
districtId: districtId || locationId,
|
||||||
|
city: city || 'Unknown', // Fallback
|
||||||
opportunityType,
|
opportunityType,
|
||||||
priority,
|
priority,
|
||||||
|
capacity: capacity || '1', // Fallback
|
||||||
|
notes,
|
||||||
status: 'New',
|
status: 'New',
|
||||||
assignedTo: req.user?.id
|
assignedTo: req.user?.id
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,4 +3,5 @@ export interface TokenPayload {
|
|||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
locationId: string | null;
|
locationId: string | null;
|
||||||
|
districtId: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user