master ata modification done removed unecesary input fileds in modal popup like emp id added request id resolver for readable id resolve
This commit is contained in:
parent
86f2323641
commit
96edda54d9
@ -12,6 +12,7 @@
|
|||||||
"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:roles": "tsx scripts/seed-roles.ts",
|
||||||
"seed:permissions": "tsx scripts/seed-permissions.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:email-templates": "tsx src/scripts/seed-master-emails.ts",
|
"seed:email-templates": "tsx src/scripts/seed-master-emails.ts",
|
||||||
@ -20,6 +21,9 @@
|
|||||||
"seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates && npm run seed:configs && npm run seed:document-configs",
|
"seed:all": "npm run seed:permissions && npm run seed && npm run seed:approval-policies && npm run seed:questionnaire && npm run seed:email-templates && npm run seed:configs && npm run seed:document-configs",
|
||||||
"setup:fresh": "npm run migrate && npm run seed:real-geo && npm run seed:all && npm run sync:hierarchy",
|
"setup:fresh": "npm run migrate && npm run seed:real-geo && npm run seed:all && npm run sync:hierarchy",
|
||||||
"seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy",
|
"seed:real-geo": "tsx scripts/seed_real_locations.ts && npm run sync:hierarchy",
|
||||||
|
"seed:state-district": "tsx scripts/seed-state-district-only.ts",
|
||||||
|
"seed:minimal-admin": "tsx scripts/seed-minimal-admin.ts",
|
||||||
|
"setup:fresh:minimal": "npm run migrate && npm run seed:roles && npm run seed:state-district && npm run seed:minimal-admin",
|
||||||
"sync:hierarchy": "tsx scripts/sync-all-hierarchy.ts",
|
"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",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ 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 { ROLES, APPLICATION_STAGES, APPLICATION_STATUS } from '../src/common/config/constants.js';
|
import { ROLES, APPLICATION_STAGES, APPLICATION_STATUS } from '../src/common/config/constants.js';
|
||||||
|
import { resolveManagerCode } from '../src/services/userRoleCode.service.js';
|
||||||
|
|
||||||
const { Role, User, UserRole, Zone, State, Region, Location } = db;
|
const { Role, User, UserRole, Zone, State, Region, Location } = db;
|
||||||
|
|
||||||
@ -84,6 +85,7 @@ async function masterReset() {
|
|||||||
// Map assignments based on role category (Regional vs Granular)
|
// Map assignments based on role category (Regional vs Granular)
|
||||||
const isRegionalRole = [ROLES.RBM, ROLES.DD_ZM, ROLES.ZBH].includes(u.roleCode as any);
|
const isRegionalRole = [ROLES.RBM, ROLES.DD_ZM, ROLES.ZBH].includes(u.roleCode as any);
|
||||||
const isGranularRole = [ROLES.ASM].includes(u.roleCode as any);
|
const isGranularRole = [ROLES.ASM].includes(u.roleCode as any);
|
||||||
|
const managerCode = await resolveManagerCode(role.id, u.roleCode as string, null);
|
||||||
|
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -92,7 +94,8 @@ async function masterReset() {
|
|||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
zoneId: isRegionalRole || isGranularRole ? zone.id : null,
|
zoneId: isRegionalRole || isGranularRole ? zone.id : null,
|
||||||
regionId: isRegionalRole ? region.id : null,
|
regionId: isRegionalRole ? region.id : null,
|
||||||
districtId: isGranularRole ? district.id : null
|
districtId: isGranularRole ? district.id : null,
|
||||||
|
managerCode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
scripts/seed-minimal-admin.ts
Normal file
85
scripts/seed-minimal-admin.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import db from '../src/database/models/index.js';
|
||||||
|
import { ROLES } from '../src/common/config/constants.js';
|
||||||
|
|
||||||
|
const ADMIN_EMAIL = 'admin@gmail.com';
|
||||||
|
const ADMIN_PASSWORD = 'Admin@123';
|
||||||
|
const ADMIN_NAME = 'System Admin';
|
||||||
|
|
||||||
|
async function seedMinimalAdmin() {
|
||||||
|
console.log('--- Seeding minimal admin-only data ---');
|
||||||
|
try {
|
||||||
|
await db.sequelize.authenticate();
|
||||||
|
|
||||||
|
const stateCount = await db.State.count();
|
||||||
|
const districtCount = await db.District.count();
|
||||||
|
const regionCount = await db.Region.count();
|
||||||
|
const zoneCount = await db.Zone.count();
|
||||||
|
|
||||||
|
if (!stateCount || !districtCount) {
|
||||||
|
throw new Error(
|
||||||
|
'Geo master data is incomplete. Run "npm run seed:state-district" before "seed:minimal-admin".'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!regionCount || !zoneCount) {
|
||||||
|
console.log('ℹ️ Region/Zone not seeded (expected for minimal setup). You can add them manually later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure only one user remains for this minimal environment.
|
||||||
|
await db.UserRole.destroy({ where: {} });
|
||||||
|
await db.User.destroy({ where: {} });
|
||||||
|
|
||||||
|
// Ensure required roles exist.
|
||||||
|
await db.Role.findOrCreate({
|
||||||
|
where: { roleCode: ROLES.SUPER_ADMIN },
|
||||||
|
defaults: {
|
||||||
|
roleCode: ROLES.SUPER_ADMIN,
|
||||||
|
roleName: 'Super Admin',
|
||||||
|
category: 'ADMIN',
|
||||||
|
description: 'Full system access'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.Role.findOrCreate({
|
||||||
|
where: { roleCode: ROLES.DD_ADMIN },
|
||||||
|
defaults: {
|
||||||
|
roleCode: ROLES.DD_ADMIN,
|
||||||
|
roleName: 'DD Admin',
|
||||||
|
category: 'ADMIN',
|
||||||
|
description: 'Dealer Development Admin'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
||||||
|
const adminUser = await db.User.create({
|
||||||
|
email: ADMIN_EMAIL,
|
||||||
|
fullName: ADMIN_NAME,
|
||||||
|
password: hashedPassword,
|
||||||
|
roleCode: ROLES.SUPER_ADMIN,
|
||||||
|
status: 'active',
|
||||||
|
isActive: true,
|
||||||
|
isExternal: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = await db.Role.findOne({ where: { roleCode: ROLES.SUPER_ADMIN } });
|
||||||
|
if (role) {
|
||||||
|
await db.UserRole.create({
|
||||||
|
userId: adminUser.id,
|
||||||
|
roleId: role.id,
|
||||||
|
isPrimary: true,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Minimal admin seed completed.');
|
||||||
|
console.log(`✅ Login: ${ADMIN_EMAIL}`);
|
||||||
|
console.log(`✅ Password: ${ADMIN_PASSWORD}`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Minimal admin seed failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedMinimalAdmin();
|
||||||
70
scripts/seed-state-district-only.ts
Normal file
70
scripts/seed-state-district-only.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import db from '../src/database/models/index.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
async function seedStateDistrictOnly() {
|
||||||
|
console.log('--- Seeding State + District only (no Zone/Region) ---');
|
||||||
|
try {
|
||||||
|
await db.sequelize.authenticate();
|
||||||
|
|
||||||
|
const seederPath = path.join(__dirname, '../seeders/20240127-seed-geo-data.js');
|
||||||
|
const content = fs.readFileSync(seederPath, 'utf8');
|
||||||
|
|
||||||
|
const statesMatch = content.match(/const STATES_DATA = \[([\s\S]*?)\];/);
|
||||||
|
const citiesMatch = content.match(/const CITIES_DATA = \[([\s\S]*?)\];/);
|
||||||
|
|
||||||
|
if (!statesMatch || !citiesMatch) {
|
||||||
|
throw new Error('Could not parse STATES_DATA / CITIES_DATA from geo seeder.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data source is internal seeder, safe to eval.
|
||||||
|
const STATES_DATA = eval(`[${statesMatch[1]}]`);
|
||||||
|
const CITIES_DATA = eval(`[${citiesMatch[1]}]`);
|
||||||
|
|
||||||
|
const { State, District } = db;
|
||||||
|
const stateIdMap = new Map<number, string>();
|
||||||
|
|
||||||
|
for (const s of STATES_DATA) {
|
||||||
|
const [stateRecord] = await State.findOrCreate({
|
||||||
|
where: { name: s.name },
|
||||||
|
defaults: {
|
||||||
|
name: s.name,
|
||||||
|
zoneId: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stateIdMap.set(s.id, stateRecord.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let districtCount = 0;
|
||||||
|
for (const c of CITIES_DATA) {
|
||||||
|
const stateId = stateIdMap.get(c.state_id);
|
||||||
|
if (!stateId) continue;
|
||||||
|
|
||||||
|
await District.findOrCreate({
|
||||||
|
where: { name: c.name, stateId },
|
||||||
|
defaults: {
|
||||||
|
name: c.name,
|
||||||
|
stateId,
|
||||||
|
regionId: null,
|
||||||
|
zoneId: null,
|
||||||
|
city: c.name,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
districtCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Seeded ${stateIdMap.size} states and ${districtCount} districts.`);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ State/District seed failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seedStateDistrictOnly();
|
||||||
@ -3,6 +3,7 @@ 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';
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.js';
|
||||||
import { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } from '../src/common/config/constants.js';
|
import { APPLICATION_STAGES, APPLICATION_STATUS, BUSINESS_TYPES } from '../src/common/config/constants.js';
|
||||||
|
import { resolveManagerCode } from '../src/services/userRoleCode.service.js';
|
||||||
|
|
||||||
const { Role, Zone, Region, State, District, User, UserRole } = db;
|
const { Role, Zone, Region, State, District, User, UserRole } = db;
|
||||||
|
|
||||||
@ -41,9 +42,17 @@ async function seed() {
|
|||||||
const mapUserRole = async (userRec: any, roleCode: string, assignment: any = {}) => {
|
const mapUserRole = async (userRec: any, roleCode: string, assignment: any = {}) => {
|
||||||
const role = await Role.findOne({ where: { roleCode } });
|
const role = await Role.findOne({ where: { roleCode } });
|
||||||
if (role) {
|
if (role) {
|
||||||
|
const managerCode = await resolveManagerCode(role.id, roleCode, null);
|
||||||
await UserRole.findOrCreate({
|
await UserRole.findOrCreate({
|
||||||
where: { userId: userRec.id, roleId: role.id, ...assignment },
|
where: { userId: userRec.id, roleId: role.id, ...assignment },
|
||||||
defaults: { userId: userRec.id, roleId: role.id, ...assignment, isActive: true, isPrimary: true }
|
defaults: {
|
||||||
|
userId: userRec.id,
|
||||||
|
roleId: role.id,
|
||||||
|
...assignment,
|
||||||
|
managerCode,
|
||||||
|
isActive: true,
|
||||||
|
isPrimary: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -62,18 +71,21 @@ async function seed() {
|
|||||||
|
|
||||||
// Regions
|
// Regions
|
||||||
const regions = [
|
const regions = [
|
||||||
{ name: 'NCR Region', zoneName: 'North Zone' },
|
{ name: 'NCR Region', zoneName: 'North Zone', code: 'NZ-R1' },
|
||||||
{ name: 'Punjab Region', zoneName: 'North Zone' },
|
{ name: 'Punjab Region', zoneName: 'North Zone', code: 'NZ-R2' },
|
||||||
{ name: 'Karnataka Region', zoneName: 'South Zone' },
|
{ name: 'Karnataka Region', zoneName: 'South Zone', code: 'SZ-R1' },
|
||||||
{ name: 'Tamil Nadu Region', zoneName: 'South Zone' }
|
{ name: 'Tamil Nadu Region', zoneName: 'South Zone', code: 'SZ-R2' }
|
||||||
];
|
];
|
||||||
const regionMap: Record<string, any> = {};
|
const regionMap: Record<string, any> = {};
|
||||||
for (const r of regions) {
|
for (const r of regions) {
|
||||||
const zone = zoneMap[r.zoneName];
|
const zone = zoneMap[r.zoneName];
|
||||||
const [region] = await Region.findOrCreate({
|
const [region] = await Region.findOrCreate({
|
||||||
where: { name: r.name },
|
where: { name: r.name },
|
||||||
defaults: { name: r.name, zoneId: zone.id }
|
defaults: { name: r.name, code: r.code, zoneId: zone.id }
|
||||||
});
|
});
|
||||||
|
if (!region.code) {
|
||||||
|
await region.update({ code: r.code });
|
||||||
|
}
|
||||||
regionMap[r.name] = region;
|
regionMap[r.name] = region;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
src/common/utils/requestResolver.ts
Normal file
67
src/common/utils/requestResolver.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Op } from 'sequelize';
|
||||||
|
import { REQUEST_TYPES } from '../config/constants.js';
|
||||||
|
|
||||||
|
type DbLike = Record<string, any>;
|
||||||
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
|
const TYPE_ALIASES: Record<string, string> = {
|
||||||
|
application: 'application',
|
||||||
|
onboarding: 'application',
|
||||||
|
resignation: REQUEST_TYPES.RESIGNATION,
|
||||||
|
relocation: REQUEST_TYPES.RELOCATION,
|
||||||
|
relocation_request: REQUEST_TYPES.RELOCATION,
|
||||||
|
constitutional: REQUEST_TYPES.CONSTITUTIONAL,
|
||||||
|
constitutional_change: REQUEST_TYPES.CONSTITUTIONAL,
|
||||||
|
'constitutional-change': REQUEST_TYPES.CONSTITUTIONAL,
|
||||||
|
termination: REQUEST_TYPES.TERMINATION,
|
||||||
|
fnf: REQUEST_TYPES.FNF
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOOKUP_CONFIG: Record<string, { model: string; codeField: string }> = {
|
||||||
|
application: { model: 'Application', codeField: 'applicationId' },
|
||||||
|
resignation: { model: 'Resignation', codeField: 'resignationId' },
|
||||||
|
relocation: { model: 'RelocationRequest', codeField: 'requestId' },
|
||||||
|
constitutional: { model: 'ConstitutionalChange', codeField: 'requestId' },
|
||||||
|
termination: { model: 'TerminationRequest', codeField: 'requestId' },
|
||||||
|
fnf: { model: 'FnF', codeField: 'settlementId' }
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeRequestType(rawType: string | undefined | null): string {
|
||||||
|
const type = String(rawType || 'application').trim().toLowerCase();
|
||||||
|
return TYPE_ALIASES[type] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestTypeQueryVariants(normalizedType: string): string[] {
|
||||||
|
if (normalizedType === REQUEST_TYPES.CONSTITUTIONAL) {
|
||||||
|
return [REQUEST_TYPES.CONSTITUTIONAL, 'constitutional-change'];
|
||||||
|
}
|
||||||
|
return [normalizedType];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveEntityUuidByType(
|
||||||
|
db: DbLike,
|
||||||
|
rawId: string | undefined | null,
|
||||||
|
rawType: string | undefined | null
|
||||||
|
): Promise<{ resolvedId: string; normalizedType: string }> {
|
||||||
|
const id = String(rawId || '').trim();
|
||||||
|
const normalizedType = normalizeRequestType(rawType);
|
||||||
|
if (!id) return { resolvedId: id, normalizedType };
|
||||||
|
|
||||||
|
const cfg = LOOKUP_CONFIG[normalizedType];
|
||||||
|
if (!cfg || !db?.[cfg.model]) return { resolvedId: id, normalizedType };
|
||||||
|
|
||||||
|
const isUuid = UUID_REGEX.test(id);
|
||||||
|
const where = isUuid
|
||||||
|
? { [Op.or]: [{ id }, { [cfg.codeField]: id }] }
|
||||||
|
: { [cfg.codeField]: id };
|
||||||
|
|
||||||
|
const row = await db[cfg.model].findOne({
|
||||||
|
where,
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
resolvedId: row?.id || id,
|
||||||
|
normalizedType
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ 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';
|
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../master/syncHierarchy.service.js';
|
||||||
|
import { resolveManagerCode } from '../../services/userRoleCode.service.js';
|
||||||
|
|
||||||
const upsertUserAssignments = async (
|
const upsertUserAssignments = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
@ -23,6 +24,7 @@ const upsertUserAssignments = async (
|
|||||||
|
|
||||||
const role = await Role.findOne({ where: { roleCode } });
|
const role = await Role.findOne({ where: { roleCode } });
|
||||||
if (!role) continue;
|
if (!role) continue;
|
||||||
|
const managerCode = await resolveManagerCode(role.id, roleCode, null);
|
||||||
|
|
||||||
const createdRole = await db.UserRole.create({
|
const createdRole = await db.UserRole.create({
|
||||||
userId,
|
userId,
|
||||||
@ -30,7 +32,7 @@ const upsertUserAssignments = async (
|
|||||||
districtId: assignment.locationId || assignment.districtId || null,
|
districtId: assignment.locationId || assignment.districtId || null,
|
||||||
zoneId: assignment.zoneId || null,
|
zoneId: assignment.zoneId || null,
|
||||||
regionId: assignment.regionId || null,
|
regionId: assignment.regionId || null,
|
||||||
managerCode: assignment.managerCode || assignment.asmCode || null,
|
managerCode,
|
||||||
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,
|
||||||
effectiveFrom: assignment.effectiveFrom || null,
|
effectiveFrom: assignment.effectiveFrom || null,
|
||||||
@ -353,6 +355,13 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
// Hash default password
|
// Hash default password
|
||||||
const hashedPassword = await bcrypt.hash('Admin@123', 10);
|
const hashedPassword = await bcrypt.hash('Admin@123', 10);
|
||||||
|
|
||||||
|
// Location is optional. Only persist to user.districtId if the value is a valid district UUID.
|
||||||
|
let safeDistrictId: string | null = null;
|
||||||
|
if (locationId) {
|
||||||
|
const districtExists = await db.District.findByPk(locationId);
|
||||||
|
safeDistrictId = districtExists ? locationId : null;
|
||||||
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
fullName,
|
fullName,
|
||||||
@ -365,7 +374,7 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
mobileNumber,
|
mobileNumber,
|
||||||
department,
|
department,
|
||||||
designation,
|
designation,
|
||||||
districtId: locationId
|
districtId: safeDistrictId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Array.isArray(assignments) && assignments.length > 0) {
|
if (Array.isArray(assignments) && assignments.length > 0) {
|
||||||
@ -375,13 +384,14 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
if (targetRole) {
|
if (targetRole) {
|
||||||
for (const distId of districts) {
|
for (const distId of districts) {
|
||||||
const sampleDistrict = await db.District.findByPk(distId);
|
const sampleDistrict = await db.District.findByPk(distId);
|
||||||
|
const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null);
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
roleId: targetRole.id,
|
roleId: targetRole.id,
|
||||||
districtId: distId,
|
districtId: distId,
|
||||||
zoneId: sampleDistrict?.zoneId || null,
|
zoneId: sampleDistrict?.zoneId || null,
|
||||||
regionId: sampleDistrict?.regionId || null,
|
regionId: sampleDistrict?.regionId || null,
|
||||||
managerCode: asmCode || null,
|
managerCode,
|
||||||
isPrimary: false,
|
isPrimary: false,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
assignedBy: req.user?.id || null
|
assignedBy: req.user?.id || null
|
||||||
@ -401,12 +411,13 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
for (const regId of finalRegionIds) {
|
for (const regId of finalRegionIds) {
|
||||||
const region = await db.Region.findByPk(regId);
|
const region = await db.Region.findByPk(regId);
|
||||||
|
const managerCode = await resolveManagerCode(targetRole.id, roleCode, null);
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
roleId: targetRole.id,
|
roleId: targetRole.id,
|
||||||
regionId: regId,
|
regionId: regId,
|
||||||
zoneId: region?.zoneId || null,
|
zoneId: region?.zoneId || null,
|
||||||
managerCode: zmCode || null,
|
managerCode,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
assignedBy: req.user?.id || null
|
assignedBy: req.user?.id || null
|
||||||
});
|
});
|
||||||
@ -419,10 +430,12 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
|||||||
} else if (roleCode) {
|
} else if (roleCode) {
|
||||||
const role = await Role.findOne({ where: { roleCode } });
|
const role = await Role.findOne({ where: { roleCode } });
|
||||||
if (role) {
|
if (role) {
|
||||||
|
const managerCode = await resolveManagerCode(role.id, roleCode, null);
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
locationId: locationId || null,
|
districtId: safeDistrictId,
|
||||||
|
managerCode,
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
assignedBy: req.user?.id || null
|
assignedBy: req.user?.id || null
|
||||||
@ -542,13 +555,14 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
for (const distId of districts) {
|
for (const distId of districts) {
|
||||||
const sampleDistrict = await db.District.findByPk(distId);
|
const sampleDistrict = await db.District.findByPk(distId);
|
||||||
|
const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null);
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: id,
|
userId: id,
|
||||||
roleId: targetRole.id,
|
roleId: targetRole.id,
|
||||||
districtId: distId,
|
districtId: distId,
|
||||||
zoneId: sampleDistrict?.zoneId || null,
|
zoneId: sampleDistrict?.zoneId || null,
|
||||||
regionId: sampleDistrict?.regionId || null,
|
regionId: sampleDistrict?.regionId || null,
|
||||||
managerCode: asmCode || null,
|
managerCode,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
assignedBy: req.user?.id || null
|
assignedBy: req.user?.id || null
|
||||||
});
|
});
|
||||||
@ -569,12 +583,13 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
for (const regId of finalRegionIds) {
|
for (const regId of finalRegionIds) {
|
||||||
const region = await db.Region.findByPk(regId);
|
const region = await db.Region.findByPk(regId);
|
||||||
|
const managerCode = await resolveManagerCode(targetRole.id, roleCode, null);
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: id,
|
userId: id,
|
||||||
roleId: targetRole.id,
|
roleId: targetRole.id,
|
||||||
regionId: regId,
|
regionId: regId,
|
||||||
zoneId: region?.zoneId || null,
|
zoneId: region?.zoneId || null,
|
||||||
managerCode: zmCode || null,
|
managerCode,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
assignedBy: req.user?.id || null
|
assignedBy: req.user?.id || null
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
|||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { AuditLog, User } = db;
|
const { AuditLog, User } = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import { Op } from 'sequelize';
|
import { resolveEntityUuidByType, normalizeRequestType } from '../../common/utils/requestResolver.js';
|
||||||
|
|
||||||
// Human-readable descriptions for audit actions
|
// Human-readable descriptions for audit actions
|
||||||
const ACTION_DESCRIPTIONS: Record<string, string> = {
|
const ACTION_DESCRIPTIONS: Record<string, string> = {
|
||||||
@ -216,16 +216,12 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
// Dynamic Table Switching based on Module
|
// Dynamic Table Switching based on Module
|
||||||
// Case-insensitive entity type routing
|
// Case-insensitive entity type routing
|
||||||
const type = (entityType as string).toLowerCase();
|
const type = normalizeRequestType(entityType as string);
|
||||||
|
const { resolvedId } = await resolveEntityUuidByType(db as any, entityId as string, type);
|
||||||
|
|
||||||
if (type === 'resignation') {
|
if (type === 'resignation') {
|
||||||
const resignation = await db.Resignation.findOne({
|
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedResignationId = resignation?.id || (entityId as string);
|
|
||||||
const result = await db.ResignationAudit.findAndCountAll({
|
const result = await db.ResignationAudit.findAndCountAll({
|
||||||
where: { resignationId: resolvedResignationId },
|
where: { resignationId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -233,13 +229,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
count = result.count;
|
count = result.count;
|
||||||
logs = result.rows;
|
logs = result.rows;
|
||||||
} else if (type === 'termination') {
|
} else if (type === 'termination') {
|
||||||
const termination = await db.TerminationRequest.findOne({
|
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedTerminationId = termination?.id || (entityId as string);
|
|
||||||
const result = await db.TerminationAudit.findAndCountAll({
|
const result = await db.TerminationAudit.findAndCountAll({
|
||||||
where: { terminationRequestId: resolvedTerminationId },
|
where: { terminationRequestId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -247,27 +238,17 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
count = result.count;
|
count = result.count;
|
||||||
logs = result.rows;
|
logs = result.rows;
|
||||||
} else if (type === 'fnf') {
|
} else if (type === 'fnf') {
|
||||||
const fnf = await db.FnF.findOne({
|
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedFnfId = fnf?.id || (entityId as string);
|
|
||||||
const result = await db.FnFAudit.findAndCountAll({
|
const result = await db.FnFAudit.findAndCountAll({
|
||||||
where: { fnfId: resolvedFnfId },
|
where: { fnfId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
});
|
});
|
||||||
count = result.count;
|
count = result.count;
|
||||||
logs = result.rows;
|
logs = result.rows;
|
||||||
} else if (type === 'constitutional_change') {
|
} else if (type === 'constitutional') {
|
||||||
const constitutional = await db.ConstitutionalChange.findOne({
|
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
|
|
||||||
const result = await db.ConstitutionalAudit.findAndCountAll({
|
const result = await db.ConstitutionalAudit.findAndCountAll({
|
||||||
where: { constitutionalChangeId: resolvedConstitutionalId },
|
where: { constitutionalChangeId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -275,13 +256,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
count = result.count;
|
count = result.count;
|
||||||
logs = result.rows;
|
logs = result.rows;
|
||||||
} else if (type === 'relocation') {
|
} else if (type === 'relocation') {
|
||||||
const relocation = await db.RelocationRequest.findOne({
|
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedRelocationId = relocation?.id || (entityId as string);
|
|
||||||
const result = await db.RelocationAudit.findAndCountAll({
|
const result = await db.RelocationAudit.findAndCountAll({
|
||||||
where: { relocationRequestId: resolvedRelocationId },
|
where: { relocationRequestId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -291,7 +267,7 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`[AuditController] Falling back to global AuditLog for type: ${type}`);
|
console.log(`[AuditController] Falling back to global AuditLog for type: ${type}`);
|
||||||
const result = await db.AuditLog.findAndCountAll({
|
const result = await db.AuditLog.findAndCountAll({
|
||||||
where: { entityType: entityType as string, entityId: entityId as string },
|
where: { entityType: entityType as string, entityId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||||
order: [['createdAt', 'DESC']],
|
order: [['createdAt', 'DESC']],
|
||||||
limit: limitNum, offset
|
limit: limitNum, offset
|
||||||
@ -343,73 +319,49 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
let totalLogs = 0;
|
let totalLogs = 0;
|
||||||
let latestLog: any = null;
|
let latestLog: any = null;
|
||||||
const type = (entityType as string).toLowerCase();
|
const type = normalizeRequestType(entityType as string);
|
||||||
|
const { resolvedId } = await resolveEntityUuidByType(db as any, entityId as string, type);
|
||||||
|
|
||||||
// Dynamic Table Switching
|
// Dynamic Table Switching
|
||||||
if (type === 'resignation') {
|
if (type === 'resignation') {
|
||||||
const resignation = await db.Resignation.findOne({
|
totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedId } });
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { resignationId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedResignationId = resignation?.id || (entityId as string);
|
|
||||||
totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedResignationId } });
|
|
||||||
latestLog = await db.ResignationAudit.findOne({
|
latestLog = await db.ResignationAudit.findOne({
|
||||||
where: { resignationId: resolvedResignationId },
|
where: { resignationId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else if (type === 'termination') {
|
} else if (type === 'termination') {
|
||||||
const termination = await db.TerminationRequest.findOne({
|
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedId } });
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedTerminationId = termination?.id || (entityId as string);
|
|
||||||
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedTerminationId } });
|
|
||||||
latestLog = await db.TerminationAudit.findOne({
|
latestLog = await db.TerminationAudit.findOne({
|
||||||
where: { terminationRequestId: resolvedTerminationId },
|
where: { terminationRequestId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else if (type === 'fnf') {
|
} else if (type === 'fnf') {
|
||||||
const fnf = await db.FnF.findOne({
|
totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedId } });
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { settlementId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedFnfId = fnf?.id || (entityId as string);
|
|
||||||
totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedFnfId } });
|
|
||||||
latestLog = await db.FnFAudit.findOne({
|
latestLog = await db.FnFAudit.findOne({
|
||||||
where: { fnfId: resolvedFnfId },
|
where: { fnfId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else if (type === 'constitutional' || type === 'constitutional_change') {
|
} else if (type === 'constitutional') {
|
||||||
const constitutional = await db.ConstitutionalChange.findOne({
|
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedId } });
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedConstitutionalId = constitutional?.id || (entityId as string);
|
|
||||||
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedConstitutionalId } });
|
|
||||||
latestLog = await db.ConstitutionalAudit.findOne({
|
latestLog = await db.ConstitutionalAudit.findOne({
|
||||||
where: { constitutionalChangeId: resolvedConstitutionalId },
|
where: { constitutionalChangeId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else if (type === 'relocation' || type === 'relocation_request') {
|
} else if (type === 'relocation') {
|
||||||
const relocation = await db.RelocationRequest.findOne({
|
totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedId } });
|
||||||
where: { [Op.or]: [{ id: entityId as string }, { requestId: entityId as string }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
const resolvedRelocationId = relocation?.id || (entityId as string);
|
|
||||||
totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedRelocationId } });
|
|
||||||
latestLog = await db.RelocationAudit.findOne({
|
latestLog = await db.RelocationAudit.findOne({
|
||||||
where: { relocationRequestId: resolvedRelocationId },
|
where: { relocationRequestId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
totalLogs = await db.AuditLog.count({ where: { entityType: entityType as string, entityId: entityId as string } });
|
totalLogs = await db.AuditLog.count({ where: { entityType: entityType as string, entityId: resolvedId } });
|
||||||
latestLog = await db.AuditLog.findOne({
|
latestLog = await db.AuditLog.findOne({
|
||||||
where: { entityType: entityType as string, entityId: entityId as string },
|
where: { entityType: entityType as string, entityId: resolvedId },
|
||||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||||
order: [['createdAt', 'DESC']]
|
order: [['createdAt', 'DESC']]
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Op } from 'sequelize';
|
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const {
|
const {
|
||||||
Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog,
|
Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog,
|
||||||
@ -7,6 +6,7 @@ const {
|
|||||||
} = db;
|
} = db;
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import { AUDIT_ACTIONS, REQUEST_TYPES } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS, REQUEST_TYPES } from '../../common/config/constants.js';
|
||||||
|
import { resolveEntityUuidByType, requestTypeQueryVariants } from '../../common/utils/requestResolver.js';
|
||||||
import * as EmailService from '../../common/utils/email.service.js';
|
import * as EmailService from '../../common/utils/email.service.js';
|
||||||
import { getIO } from '../../common/utils/socket.js';
|
import { getIO } from '../../common/utils/socket.js';
|
||||||
import * as NotificationService from '../../common/utils/notification.service.js';
|
import * as NotificationService from '../../common/utils/notification.service.js';
|
||||||
@ -66,38 +66,12 @@ const stitchWorknoteAttachments = async (worknotes: any[]) => {
|
|||||||
return Promise.all(notePromises);
|
return Promise.all(notePromises);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Resolve REQ-… vs UUID and align constitutional aliases with `REQUEST_TYPES.CONSTITUTIONAL`. */
|
function worknoteListWhere(rawId: string, resolvedId: string, normalizedType: string) {
|
||||||
async function resolveWorknoteRequestKeys(rawId: string, rawType: string) {
|
const idVariants = Array.from(new Set([String(rawId || '').trim(), String(resolvedId || '').trim()].filter(Boolean)));
|
||||||
const id = String(rawId || '');
|
const variants = requestTypeQueryVariants(normalizedType);
|
||||||
let t = String(rawType || 'application').toLowerCase();
|
const requestIdWhere = idVariants.length > 1 ? { [db.Sequelize.Op.in]: idVariants } : idVariants[0];
|
||||||
if (t === 'constitutional_change') t = 'constitutional-change';
|
if (variants.length > 1) return { requestId: requestIdWhere, requestType: { [db.Sequelize.Op.in]: variants } };
|
||||||
let resolvedId = id;
|
return { requestId: requestIdWhere, requestType: variants[0] };
|
||||||
|
|
||||||
if (id && (t === 'constitutional' || t === 'constitutional-change')) {
|
|
||||||
const row = await db.ConstitutionalChange.findOne({
|
|
||||||
where: { [Op.or]: [{ id }, { requestId: id }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
if (row) resolvedId = (row as any).id;
|
|
||||||
t = REQUEST_TYPES.CONSTITUTIONAL;
|
|
||||||
} else if (id && t === 'relocation') {
|
|
||||||
const row = await db.RelocationRequest.findOne({
|
|
||||||
where: { [Op.or]: [{ id }, { requestId: id }] },
|
|
||||||
attributes: ['id']
|
|
||||||
});
|
|
||||||
if (row) resolvedId = (row as any).id;
|
|
||||||
}
|
|
||||||
return { resolvedId, normalizedType: t };
|
|
||||||
}
|
|
||||||
|
|
||||||
function worknoteListWhere(resolvedId: string, normalizedType: string) {
|
|
||||||
if (normalizedType === REQUEST_TYPES.CONSTITUTIONAL) {
|
|
||||||
return {
|
|
||||||
requestId: resolvedId,
|
|
||||||
requestType: { [Op.in]: [REQUEST_TYPES.CONSTITUTIONAL, 'constitutional-change'] }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { requestId: resolvedId, requestType: normalizedType };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Worknotes ---
|
// --- Worknotes ---
|
||||||
@ -105,7 +79,7 @@ function worknoteListWhere(resolvedId: string, normalizedType: string) {
|
|||||||
export const addWorknote = async (req: AuthRequest, res: Response) => {
|
export const addWorknote = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { requestId, requestType, noteText, noteType, tags, attachmentDocIds } = req.body;
|
const { requestId, requestType, noteText, noteType, tags, attachmentDocIds } = req.body;
|
||||||
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType);
|
const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
|
||||||
logger.info(`Adding worknote for ${normalizedType} ${resolvedId}. Body:`, { noteText, tags, attachmentDocIds });
|
logger.info(`Adding worknote for ${normalizedType} ${resolvedId}. Body:`, { noteText, tags, attachmentDocIds });
|
||||||
|
|
||||||
// Debug: Log participants
|
// Debug: Log participants
|
||||||
@ -240,8 +214,8 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
|
|||||||
export const getWorknotes = async (req: AuthRequest, res: Response) => {
|
export const getWorknotes = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { requestId, requestType } = req.query as any;
|
const { requestId, requestType } = req.query as any;
|
||||||
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType);
|
const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
|
||||||
const where = worknoteListWhere(resolvedId, normalizedType);
|
const where = worknoteListWhere(String(requestId || ''), resolvedId, normalizedType);
|
||||||
|
|
||||||
const worknotes = await Worknote.findAll({
|
const worknotes = await Worknote.findAll({
|
||||||
where,
|
where,
|
||||||
@ -264,7 +238,7 @@ export const uploadWorknoteAttachment = async (req: any, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
const { requestId, requestType } = req.body;
|
const { requestId, requestType } = req.body;
|
||||||
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType);
|
const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||||
|
|||||||
@ -3,8 +3,50 @@ import { Op } from 'sequelize';
|
|||||||
import { syncDistrictsByRegion, syncDistrictsByZone, syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js';
|
import { syncDistrictsByRegion, syncDistrictsByZone, syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
import { ROLES } from '../../common/config/constants.js';
|
import { ROLES } from '../../common/config/constants.js';
|
||||||
|
import { resolveManagerCode } from '../../services/userRoleCode.service.js';
|
||||||
const { User } = db;
|
const { User } = db;
|
||||||
|
|
||||||
|
const deriveZonePrefix = (zone: any): string => {
|
||||||
|
const rawCode = String(zone?.code || '').trim().toUpperCase();
|
||||||
|
if (/^[A-Z]{2,4}$/.test(rawCode)) return rawCode;
|
||||||
|
|
||||||
|
const words = String(zone?.name || '')
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.split(/[\s_-]+/)
|
||||||
|
.filter(Boolean);
|
||||||
|
if (words.length >= 2) return `${words[0][0]}${words[1][0]}`;
|
||||||
|
if (words.length === 1) return words[0].slice(0, 2) || 'RG';
|
||||||
|
return 'RG';
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextRegionCode = async (zoneId: string, excludeRegionId?: string): Promise<string> => {
|
||||||
|
const zone = await db.Zone.findByPk(zoneId, { attributes: ['id', 'name', 'code'] });
|
||||||
|
if (!zone) throw new Error('Zone not found');
|
||||||
|
|
||||||
|
const prefix = deriveZonePrefix(zone);
|
||||||
|
const where: any = {
|
||||||
|
zoneId,
|
||||||
|
code: { [Op.iLike]: `${prefix}-R%` }
|
||||||
|
};
|
||||||
|
if (excludeRegionId) where.id = { [Op.ne]: excludeRegionId };
|
||||||
|
|
||||||
|
const existing = await db.Region.findAll({
|
||||||
|
where,
|
||||||
|
attributes: ['code']
|
||||||
|
});
|
||||||
|
|
||||||
|
let maxSeq = 0;
|
||||||
|
for (const row of existing as any[]) {
|
||||||
|
const match = String(row.code || '').match(new RegExp(`^${prefix}-R(\\d+)$`, 'i'));
|
||||||
|
if (!match) continue;
|
||||||
|
const seq = Number(match[1]);
|
||||||
|
if (Number.isFinite(seq) && seq > maxSeq) maxSeq = seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix}-R${maxSeq + 1}`;
|
||||||
|
};
|
||||||
|
|
||||||
// --- Areas (Granular Locations) ---
|
// --- Areas (Granular Locations) ---
|
||||||
export const getAreas = async (req: Request, res: Response) => {
|
export const getAreas = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
@ -135,7 +177,41 @@ export const getDistricts = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const createDistrict = async (req: Request, res: Response) => {
|
export const createDistrict = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { name, code, stateName, city, openFrom, openTo, status, isActive, description } = req.body;
|
const { name, code, stateName, city, openFrom, openTo, status, isActive, description, districtId } = req.body;
|
||||||
|
|
||||||
|
// Preferred path: create location against an existing district
|
||||||
|
if (districtId) {
|
||||||
|
const district = await db.District.findByPk(districtId);
|
||||||
|
if (!district) {
|
||||||
|
return res.status(404).json({ success: false, message: 'District not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const areaName = name || city || district.name;
|
||||||
|
const area = await db.Location.create({
|
||||||
|
name: areaName,
|
||||||
|
districtId: district.id,
|
||||||
|
city: city || areaName,
|
||||||
|
isActive: isActive !== undefined ? isActive : true,
|
||||||
|
openFrom: openFrom || null,
|
||||||
|
openTo: openTo || null,
|
||||||
|
description: description || null
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.Opportunity.create({
|
||||||
|
districtId: district.id,
|
||||||
|
areaId: area.id,
|
||||||
|
city: city || areaName,
|
||||||
|
openFrom: openFrom || null,
|
||||||
|
openTo: openTo || null,
|
||||||
|
status: status || 'inactive',
|
||||||
|
opportunityType: 'New Dealership',
|
||||||
|
capacity: 'Standard',
|
||||||
|
priority: 'Medium'
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(201).json({ success: true, data: area });
|
||||||
|
}
|
||||||
|
|
||||||
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' });
|
||||||
|
|
||||||
// Find or Create state if stateName provided
|
// Find or Create state if stateName provided
|
||||||
@ -255,22 +331,30 @@ export const getRegions = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const createRegion = async (req: Request, res: Response) => {
|
export const createRegion = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body;
|
const { name, parentId, zoneId, managerId, districts, districtIds } = req.body;
|
||||||
const targetZoneId = zoneId || parentId;
|
const targetZoneId = zoneId || parentId;
|
||||||
if (!name) return res.status(400).json({ success: false, message: 'Region name is required' });
|
if (!name) return res.status(400).json({ success: false, message: 'Region name is required' });
|
||||||
|
if (!targetZoneId) return res.status(400).json({ success: false, message: 'Zone is required for region creation' });
|
||||||
|
|
||||||
|
const generatedCode = await nextRegionCode(targetZoneId);
|
||||||
|
const region = await db.Region.create({ name, code: generatedCode, zoneId: targetZoneId });
|
||||||
|
|
||||||
const region = await db.Region.create({ name, code, zoneId: targetZoneId });
|
// 1. Assign Manager (RBM is the regional manager role; keep RM as legacy fallback)
|
||||||
|
|
||||||
// 1. Assign Manager
|
|
||||||
if (managerId) {
|
if (managerId) {
|
||||||
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } });
|
const rmRole = await db.Role.findOne({
|
||||||
|
where: {
|
||||||
|
roleCode: { [db.Sequelize.Op.in]: [ROLES.RBM, 'RM'] }
|
||||||
|
}
|
||||||
|
});
|
||||||
if (rmRole) {
|
if (rmRole) {
|
||||||
await db.UserRole.update({ isActive: false }, { where: { regionId: region.id, roleId: rmRole.id } });
|
await db.UserRole.update({ isActive: false }, { where: { regionId: region.id, roleId: rmRole.id } });
|
||||||
|
const managerCode = await resolveManagerCode(rmRole.id, rmRole.roleCode, null);
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: managerId,
|
userId: managerId,
|
||||||
roleId: rmRole.id,
|
roleId: rmRole.id,
|
||||||
regionId: region.id,
|
regionId: region.id,
|
||||||
zoneId: targetZoneId,
|
zoneId: targetZoneId,
|
||||||
|
managerCode,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isPrimary: true
|
isPrimary: true
|
||||||
});
|
});
|
||||||
@ -321,32 +405,45 @@ export const createRegion = async (req: Request, res: Response) => {
|
|||||||
export const updateRegion = async (req: Request, res: Response) => {
|
export const updateRegion = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body;
|
const { name, parentId, zoneId, managerId, districts, districtIds } = req.body;
|
||||||
const targetZoneId = zoneId || parentId;
|
const targetZoneId = zoneId || parentId;
|
||||||
|
|
||||||
const region = await db.Region.findByPk(id);
|
const region = await db.Region.findByPk(id);
|
||||||
if (!region) return res.status(404).json({ success: false, message: 'Region not found' });
|
if (!region) return res.status(404).json({ success: false, message: 'Region not found' });
|
||||||
|
|
||||||
await region.update({
|
const nextZoneId = targetZoneId || region.zoneId;
|
||||||
name,
|
const zoneChanged = Boolean(targetZoneId && targetZoneId !== region.zoneId);
|
||||||
code,
|
const generatedCode =
|
||||||
zoneId: targetZoneId || region.zoneId
|
zoneChanged || !region.code
|
||||||
|
? await nextRegionCode(nextZoneId, id as string)
|
||||||
|
: region.code;
|
||||||
|
|
||||||
|
await region.update({
|
||||||
|
name,
|
||||||
|
code: generatedCode,
|
||||||
|
zoneId: nextZoneId
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. Update Manager
|
// 1. Update Manager (RBM is the regional manager role; keep RM as legacy fallback)
|
||||||
if (managerId) {
|
if (managerId) {
|
||||||
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } });
|
const rmRole = await db.Role.findOne({
|
||||||
|
where: {
|
||||||
|
roleCode: { [db.Sequelize.Op.in]: [ROLES.RBM, 'RM'] }
|
||||||
|
}
|
||||||
|
});
|
||||||
if (rmRole) {
|
if (rmRole) {
|
||||||
// Deactivate old RMs for this region
|
// Deactivate old RMs for this region
|
||||||
await db.UserRole.update({ isActive: false }, {
|
await db.UserRole.update({ isActive: false }, {
|
||||||
where: { regionId: id, roleId: rmRole.id }
|
where: { regionId: id, roleId: rmRole.id }
|
||||||
});
|
});
|
||||||
// Assign new RM
|
// Assign new RM
|
||||||
|
const managerCode = await resolveManagerCode(rmRole.id, rmRole.roleCode, null);
|
||||||
await db.UserRole.create({
|
await db.UserRole.create({
|
||||||
userId: managerId,
|
userId: managerId,
|
||||||
roleId: rmRole.id,
|
roleId: rmRole.id,
|
||||||
regionId: id,
|
regionId: id,
|
||||||
zoneId: region.zoneId,
|
zoneId: region.zoneId,
|
||||||
|
managerCode,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isPrimary: true
|
isPrimary: true
|
||||||
});
|
});
|
||||||
@ -703,17 +800,25 @@ export const deleteLocation = async (req: Request, res: Response) => {
|
|||||||
export const updateLocation = async (req: Request, res: Response) => {
|
export const updateLocation = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params; // This is the Area ID
|
const { id } = req.params; // This is the Area ID
|
||||||
const { name, code, stateName, city, openFrom, openTo, status, isActive, description } = req.body;
|
const { name, code, stateName, city, openFrom, openTo, status, isActive, description, districtId } = req.body;
|
||||||
|
|
||||||
const area = await db.Location.findByPk(id, {
|
const area = await db.Location.findByPk(id, {
|
||||||
include: [{ model: db.District, as: 'district' }]
|
include: [{ model: db.District, as: 'district' }]
|
||||||
});
|
});
|
||||||
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
|
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
|
||||||
|
|
||||||
const district = area.district;
|
let district = area.district;
|
||||||
|
|
||||||
|
if (districtId && (!district || String(district.id) !== String(districtId))) {
|
||||||
|
const nextDistrict = await db.District.findByPk(districtId);
|
||||||
|
if (!nextDistrict) {
|
||||||
|
return res.status(404).json({ success: false, message: 'District not found' });
|
||||||
|
}
|
||||||
|
district = nextDistrict;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Update District
|
// 1. Update District
|
||||||
if (district) {
|
if (district && !districtId) {
|
||||||
let stateId = req.body.stateId;
|
let stateId = req.body.stateId;
|
||||||
if (stateName && !stateId) {
|
if (stateName && !stateId) {
|
||||||
const [state] = await db.State.findOrCreate({
|
const [state] = await db.State.findOrCreate({
|
||||||
@ -734,6 +839,7 @@ export const updateLocation = async (req: Request, res: Response) => {
|
|||||||
// 2. Update Area
|
// 2. Update Area
|
||||||
await area.update({
|
await area.update({
|
||||||
name: name || area.name,
|
name: name || area.name,
|
||||||
|
districtId: district?.id || area.districtId,
|
||||||
city: city || area.city,
|
city: city || area.city,
|
||||||
isActive: isActive !== undefined ? isActive : area.isActive,
|
isActive: isActive !== undefined ? isActive : area.isActive,
|
||||||
openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom,
|
openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom,
|
||||||
@ -860,7 +966,7 @@ export const getZonalManagers = async (req: Request, res: Response) => {
|
|||||||
{
|
{
|
||||||
model: db.Role,
|
model: db.Role,
|
||||||
as: 'role',
|
as: 'role',
|
||||||
where: { roleCode: { [db.Sequelize.Op.in]: ['ZM', 'DD-ZM', 'ZBH'] } }
|
where: { roleCode: { [db.Sequelize.Op.in]: ['ZM', 'DD-ZM'] } }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: db.Region,
|
model: db.Region,
|
||||||
@ -879,7 +985,7 @@ export const getZonalManagers = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = (zms || []).map((u: any) => {
|
const result = (zms || []).map((u: any) => {
|
||||||
const rolePriority = ['DD-ZM', 'ZM', 'ZBH'];
|
const rolePriority = ['DD-ZM', 'ZM'];
|
||||||
const roleAssignments = (u.userRoles || []).filter((r: any) => rolePriority.includes(r.role?.roleCode));
|
const roleAssignments = (u.userRoles || []).filter((r: any) => rolePriority.includes(r.role?.roleCode));
|
||||||
|
|
||||||
const mainAssignment = [...roleAssignments].sort((a: any, b: any) => {
|
const mainAssignment = [...roleAssignments].sort((a: any, b: any) => {
|
||||||
@ -958,7 +1064,7 @@ export const saveZM = async (req: Request, res: Response) => {
|
|||||||
roleId: zmRole.id,
|
roleId: zmRole.id,
|
||||||
zoneId: zoneId || null,
|
zoneId: zoneId || null,
|
||||||
regionId: regionId,
|
regionId: regionId,
|
||||||
managerCode: zmCode || null,
|
managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isPrimary: true
|
isPrimary: true
|
||||||
});
|
});
|
||||||
@ -970,7 +1076,7 @@ export const saveZM = async (req: Request, res: Response) => {
|
|||||||
roleId: zmRole.id,
|
roleId: zmRole.id,
|
||||||
zoneId: zoneId || null,
|
zoneId: zoneId || null,
|
||||||
regionId: null,
|
regionId: null,
|
||||||
managerCode: zmCode || null,
|
managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isPrimary: true
|
isPrimary: true
|
||||||
});
|
});
|
||||||
@ -1074,7 +1180,7 @@ export const saveDDLead = async (req: Request, res: Response) => {
|
|||||||
userId,
|
userId,
|
||||||
roleId: leadRole.id,
|
roleId: leadRole.id,
|
||||||
zoneId: zoneId,
|
zoneId: zoneId,
|
||||||
managerCode: leadCode || null,
|
managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isPrimary: true
|
isPrimary: true
|
||||||
});
|
});
|
||||||
@ -1085,7 +1191,7 @@ export const saveDDLead = async (req: Request, res: Response) => {
|
|||||||
userId,
|
userId,
|
||||||
roleId: leadRole.id,
|
roleId: leadRole.id,
|
||||||
zoneId: null,
|
zoneId: null,
|
||||||
managerCode: leadCode || null,
|
managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null),
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isPrimary: true
|
isPrimary: true
|
||||||
});
|
});
|
||||||
|
|||||||
@ -123,6 +123,13 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
|||||||
stage: application.currentStage
|
stage: application.currentStage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure district/region/zone manager pointers are fresh, then auto-map evaluators
|
||||||
|
// so Application Details shows RBM/ZBH (and other mapped roles) immediately.
|
||||||
|
if (districtId) {
|
||||||
|
await syncLocationManagers(districtId);
|
||||||
|
await assignStageEvaluators(application.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Send Email (Async)
|
// Send Email (Async)
|
||||||
if (isOpportunityAvailable) {
|
if (isOpportunityAvailable) {
|
||||||
sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId)
|
sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWork
|
|||||||
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||||
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
||||||
|
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
|
||||||
import {
|
import {
|
||||||
isRegisteredConstitutionalChangeType,
|
isRegisteredConstitutionalChangeType,
|
||||||
normalizeToConstitutionalChangeType
|
normalizeToConstitutionalChangeType
|
||||||
@ -22,6 +23,11 @@ const STRUCTURE_TARGET_VALUES = new Set<string>(
|
|||||||
CONSTITUTIONAL_STRUCTURE_TARGET_OPTIONS.map((o) => o.value as string)
|
CONSTITUTIONAL_STRUCTURE_TARGET_OPTIONS.map((o) => o.value as string)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resolveConstitutionalUuid = async (id: string) => {
|
||||||
|
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'constitutional');
|
||||||
|
return resolvedId;
|
||||||
|
};
|
||||||
|
|
||||||
export const getMeta = async (_req: AuthRequest, res: Response) => {
|
export const getMeta = async (_req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
res.json({
|
res.json({
|
||||||
@ -261,11 +267,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
|||||||
export const getRequestById = async (req: AuthRequest, res: Response) => {
|
export const getRequestById = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
const resolvedId = await resolveConstitutionalUuid(String(id));
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
|
|
||||||
const request = await ConstitutionalChange.findOne({
|
const request = await ConstitutionalChange.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr },
|
where: { id: resolvedId },
|
||||||
include: [
|
include: [
|
||||||
{ model: Outlet, as: 'outlet' },
|
{ model: Outlet, as: 'outlet' },
|
||||||
{
|
{
|
||||||
@ -370,14 +375,13 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
|||||||
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||||
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
|
||||||
const rawAction = String(req.body.action || '').trim();
|
const rawAction = String(req.body.action || '').trim();
|
||||||
const actionNorm = rawAction.toLowerCase().replace(/\s+/g, ' ');
|
const actionNorm = rawAction.toLowerCase().replace(/\s+/g, ' ');
|
||||||
const comments = req.body.comments;
|
const comments = req.body.comments;
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
const resolvedId = await resolveConstitutionalUuid(String(id));
|
||||||
|
|
||||||
const request = await ConstitutionalChange.findOne({
|
const request = await ConstitutionalChange.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!request) return res.status(404).json({ success: false, message: 'Request not found' });
|
if (!request) return res.status(404).json({ success: false, message: 'Request not found' });
|
||||||
@ -523,12 +527,11 @@ export const uploadDocuments = async (req: AuthRequest, res: Response) => {
|
|||||||
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||||
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
|
||||||
const { documents } = req.body;
|
const { documents } = req.body;
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
const resolvedId = await resolveConstitutionalUuid(String(id));
|
||||||
|
|
||||||
const request = await ConstitutionalChange.findOne({
|
const request = await ConstitutionalChange.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
|
|||||||
@ -8,6 +8,12 @@ import { Op, Transaction } from 'sequelize';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import { formatDateTime } from '../../common/utils/dateUtils.js';
|
import { formatDateTime } from '../../common/utils/dateUtils.js';
|
||||||
|
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
|
||||||
|
|
||||||
|
const resolveRelocationUuid = async (id: string) => {
|
||||||
|
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'relocation');
|
||||||
|
return resolvedId;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to assign evaluators for relocation requests based on outlet location hierarchy
|
* Helper to assign evaluators for relocation requests based on outlet location hierarchy
|
||||||
@ -386,12 +392,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
|||||||
export const getRequestById = async (req: AuthRequest, res: Response) => {
|
export const getRequestById = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id as string;
|
const id = req.params.id as string;
|
||||||
|
const resolvedId = await resolveRelocationUuid(id);
|
||||||
// Check if id is a UUID or a requestId string
|
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id);
|
|
||||||
|
|
||||||
const request = await RelocationRequest.findOne({
|
const request = await RelocationRequest.findOne({
|
||||||
where: isUUID ? { id } : { requestId: id },
|
where: { id: resolvedId },
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Outlet,
|
model: Outlet,
|
||||||
@ -515,12 +519,10 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
|||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.replace(/\s+/g, '_');
|
.replace(/\s+/g, '_');
|
||||||
|
|
||||||
// Check if id is a UUID or a requestId string
|
const resolvedId = await resolveRelocationUuid(String(id));
|
||||||
const idStr = String(id);
|
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
|
|
||||||
const request = await RelocationRequest.findOne({
|
const request = await RelocationRequest.findOne({
|
||||||
where: isUUID ? { id: idStr } : { requestId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
@ -720,12 +722,10 @@ export const uploadDocuments = async (req: AuthRequest, res: Response) => {
|
|||||||
return res.status(400).json({ success: false, message: 'Document type is required' });
|
return res.status(400).json({ success: false, message: 'Document type is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const idStr = String(id);
|
const resolvedId = await resolveRelocationUuid(String(id));
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
|
|
||||||
// Only search by requestId since frontend sends requestId, not UUID
|
|
||||||
const request = await RelocationRequest.findOne({
|
const request = await RelocationRequest.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
@ -806,11 +806,10 @@ const applyRelocationDocumentDecision = async (
|
|||||||
|
|
||||||
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||||
|
|
||||||
const idStr = String(id);
|
const resolvedId = await resolveRelocationUuid(String(id));
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
|
|
||||||
const request = await RelocationRequest.findOne({
|
const request = await RelocationRequest.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return res.status(404).json({ success: false, message: 'Relocation request not found' });
|
return res.status(404).json({ success: false, message: 'Relocation request not found' });
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
AUDIT_ACTIONS,
|
AUDIT_ACTIONS,
|
||||||
ROLES,
|
ROLES,
|
||||||
REQUEST_TYPES,
|
REQUEST_TYPES,
|
||||||
|
FNF_STATUS,
|
||||||
RESIGNATION_DOCUMENT_TYPES,
|
RESIGNATION_DOCUMENT_TYPES,
|
||||||
RESIGNATION_DOCUMENT_STAGES
|
RESIGNATION_DOCUMENT_STAGES
|
||||||
} from '../../common/config/constants.js';
|
} from '../../common/config/constants.js';
|
||||||
@ -18,8 +19,13 @@ import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
|||||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||||
import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||||
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
||||||
|
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
|
||||||
|
|
||||||
// Removed generateResignationId and moved to NomenclatureService
|
// Removed generateResignationId and moved to NomenclatureService
|
||||||
|
const resolveResignationUuid = async (id: string) => {
|
||||||
|
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'resignation');
|
||||||
|
return resolvedId;
|
||||||
|
};
|
||||||
|
|
||||||
// Create resignation request (Dealer only)
|
// Create resignation request (Dealer only)
|
||||||
export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
@ -132,11 +138,10 @@ export const getResignations = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
export const getResignationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const getResignationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
|
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
|
where: { id: resolvedId },
|
||||||
include: [
|
include: [
|
||||||
{ model: db.Outlet, as: 'outlet' },
|
{ model: db.Outlet, as: 'outlet' },
|
||||||
{
|
{
|
||||||
@ -218,10 +223,9 @@ export const uploadResignationDocument = async (req: AuthRequest, res: Response,
|
|||||||
message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}`
|
message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const idStr = String(id);
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resignation) {
|
if (!resignation) {
|
||||||
@ -275,12 +279,11 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
try {
|
try {
|
||||||
if (!req.user) throw new Error('Unauthorized');
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
|
||||||
const { remarks } = req.body;
|
const { remarks } = req.body;
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
|
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
|
where: { id: resolvedId },
|
||||||
include: [
|
include: [
|
||||||
{ model: db.Outlet, as: 'outlet' },
|
{ model: db.Outlet, as: 'outlet' },
|
||||||
{ model: db.User, as: 'dealer', attributes: ['id', 'dealerId'] }
|
{ model: db.User, as: 'dealer', attributes: ['id', 'dealerId'] }
|
||||||
@ -317,6 +320,21 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
return res.status(400).json({ success: false, message: 'Cannot move to next stage from current state' });
|
return res.status(400).json({ success: false, message: 'Cannot move to next stage from current state' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sequence guard: resignation can be marked completed only after F&F settlement is complete.
|
||||||
|
if (
|
||||||
|
resignation.currentStage === RESIGNATION_STAGES.FNF_INITIATED &&
|
||||||
|
nextStage === RESIGNATION_STAGES.COMPLETED
|
||||||
|
) {
|
||||||
|
const fnf = await db.FnF.findOne({ where: { resignationId: resignation.id }, transaction });
|
||||||
|
if (!fnf || fnf.status !== FNF_STATUS.COMPLETED) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Cannot complete resignation. F&F settlement must be completed first.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sourceStage = resignation.currentStage;
|
const sourceStage = resignation.currentStage;
|
||||||
|
|
||||||
// Transition via Workflow Service
|
// Transition via Workflow Service
|
||||||
@ -405,16 +423,15 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
|
|||||||
try {
|
try {
|
||||||
if (!req.user) throw new Error('Unauthorized');
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
|
||||||
const { reason } = req.body;
|
const { reason } = req.body;
|
||||||
if (!reason) {
|
if (!reason) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
return res.status(400).json({ success: false, message: 'Rejection reason is required' });
|
return res.status(400).json({ success: false, message: 'Rejection reason is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
|
where: { id: resolvedId },
|
||||||
include: [{ model: db.Outlet, as: 'outlet' }]
|
include: [{ model: db.Outlet, as: 'outlet' }]
|
||||||
});
|
});
|
||||||
if (!resignation) {
|
if (!resignation) {
|
||||||
@ -457,12 +474,11 @@ export const withdrawResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
try {
|
try {
|
||||||
if (!req.user) throw new Error('Unauthorized');
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
|
||||||
const { reason } = req.body;
|
const { reason } = req.body;
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
|
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
|
where: { id: resolvedId },
|
||||||
include: [{ model: db.Outlet, as: 'outlet' }]
|
include: [{ model: db.Outlet, as: 'outlet' }]
|
||||||
});
|
});
|
||||||
if (!resignation) {
|
if (!resignation) {
|
||||||
@ -524,12 +540,11 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next:
|
|||||||
try {
|
try {
|
||||||
if (!req.user) throw new Error('Unauthorized');
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
|
||||||
const { targetStage, remarks } = req.body;
|
const { targetStage, remarks } = req.body;
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
|
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
if (!resignation) {
|
if (!resignation) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@ -590,8 +605,9 @@ export const assignResignation = async (req: AuthRequest, res: Response, next: N
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { assignTo, remarks } = req.body; // assignTo is a role code or specific userId
|
const { assignTo, remarks } = req.body; // assignTo is a role code or specific userId
|
||||||
|
|
||||||
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: { [Op.or]: [{ id }, { resignationId: id }] },
|
where: { id: resolvedId },
|
||||||
include: [{ model: db.User, as: 'dealer' }]
|
include: [{ model: db.User, as: 'dealer' }]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -687,12 +703,11 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
try {
|
try {
|
||||||
if (!req.user) throw new Error('Unauthorized');
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
|
||||||
const { department, status, remarks, amount, type } = req.body;
|
const { department, status, remarks, amount, type } = req.body;
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
const resolvedId = await resolveResignationUuid(String(id));
|
||||||
|
|
||||||
const resignation = await db.Resignation.findOne({
|
const resignation = await db.Resignation.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
if (!resignation) {
|
if (!resignation) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@ -850,6 +865,35 @@ export const updateResignationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
if (!req.user || !authorizedRoles.includes(req.user.roleCode as any)) {
|
if (!req.user || !authorizedRoles.includes(req.user.roleCode as any)) {
|
||||||
return res.status(403).json({ success: false, message: 'You do not have permission to push this request to F&F' });
|
return res.status(403).json({ success: false, message: 'You do not have permission to push this request to F&F' });
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
const resolvedId = await resolveResignationUuid(String(req.params.id));
|
||||||
|
const resignation = await db.Resignation.findByPk(resolvedId);
|
||||||
|
if (!resignation) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Resignation not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRS-aligned gate: F&F can start only after Legal completion artifacts.
|
||||||
|
if (resignation.currentStage !== RESIGNATION_STAGES.LEGAL) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Cannot trigger F&F from ${resignation.currentStage}. Move request to Legal stage first.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLegalStageDocument = await db.ResignationDocument.findOne({
|
||||||
|
where: {
|
||||||
|
resignationId: resignation.id,
|
||||||
|
stage: RESIGNATION_STAGES.LEGAL
|
||||||
|
},
|
||||||
|
attributes: ['id']
|
||||||
|
});
|
||||||
|
if (!hasLegalStageDocument) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Cannot trigger F&F. Legal-stage acceptance/communication document is required first.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
// Jump directly to F&F Initiation
|
// Jump directly to F&F Initiation
|
||||||
(req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED;
|
(req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED;
|
||||||
return approveResignation(req, res, next);
|
return approveResignation(req, res, next);
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
TERMINATION_DOCUMENT_TYPES,
|
TERMINATION_DOCUMENT_TYPES,
|
||||||
TERMINATION_DOCUMENT_STAGES
|
TERMINATION_DOCUMENT_STAGES
|
||||||
} from '../../common/config/constants.js';
|
} from '../../common/config/constants.js';
|
||||||
import { Op, Transaction } from 'sequelize';
|
import { Transaction } from 'sequelize';
|
||||||
import { AuthRequest } from '../../types/express.types.js';
|
import { AuthRequest } from '../../types/express.types.js';
|
||||||
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
||||||
|
|
||||||
@ -17,6 +17,12 @@ import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
|||||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||||
import { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
import { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||||
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
||||||
|
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
|
||||||
|
|
||||||
|
const resolveTerminationUuid = async (id: string) => {
|
||||||
|
const { resolvedId } = await resolveEntityUuidByType(db as any, id, 'termination');
|
||||||
|
return resolvedId;
|
||||||
|
};
|
||||||
|
|
||||||
// Create termination request
|
// Create termination request
|
||||||
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const createTermination = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
@ -102,11 +108,10 @@ export const getTerminations = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
export const getTerminationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const idStr = String(id);
|
const resolvedId = await resolveTerminationUuid(String(id));
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
|
|
||||||
const termination = await db.TerminationRequest.findOne({
|
const termination = await db.TerminationRequest.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr },
|
where: { id: resolvedId },
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: db.Dealer,
|
model: db.Dealer,
|
||||||
@ -184,10 +189,9 @@ export const uploadTerminationDocument = async (req: AuthRequest, res: Response,
|
|||||||
message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}`
|
message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const idStr = String(id);
|
const resolvedId = await resolveTerminationUuid(String(id));
|
||||||
const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(idStr);
|
|
||||||
const termination = await db.TerminationRequest.findOne({
|
const termination = await db.TerminationRequest.findOne({
|
||||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
where: { id: resolvedId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!termination) {
|
if (!termination) {
|
||||||
@ -240,8 +244,9 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
|
|||||||
if (!req.user) throw new Error('Unauthorized');
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { action, remarks } = req.body;
|
const { action, remarks } = req.body;
|
||||||
|
const resolvedId = await resolveTerminationUuid(String(id));
|
||||||
|
|
||||||
const termination = await db.TerminationRequest.findByPk(id);
|
const termination = await db.TerminationRequest.findByPk(resolvedId);
|
||||||
if (!termination) {
|
if (!termination) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
return res.status(404).json({ success: false, message: 'Termination not found' });
|
return res.status(404).json({ success: false, message: 'Termination not found' });
|
||||||
@ -394,8 +399,9 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
if (!req.user) throw new Error('Unauthorized');
|
if (!req.user) throw new Error('Unauthorized');
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { department, status, amount, type, remarks } = req.body;
|
const { department, status, amount, type, remarks } = req.body;
|
||||||
|
const resolvedId = await resolveTerminationUuid(String(id));
|
||||||
|
|
||||||
const termination = await db.TerminationRequest.findByPk(id);
|
const termination = await db.TerminationRequest.findByPk(resolvedId);
|
||||||
if (!termination) throw new Error('Termination request not found');
|
if (!termination) throw new Error('Termination request not found');
|
||||||
|
|
||||||
const clearances = { ...(termination.departmentalClearances || {}) };
|
const clearances = { ...(termination.departmentalClearances || {}) };
|
||||||
@ -412,7 +418,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
await termination.update({ departmentalClearances: clearances }, { transaction });
|
await termination.update({ departmentalClearances: clearances }, { transaction });
|
||||||
|
|
||||||
// Update individual clearance record for unified dashboard
|
// Update individual clearance record for unified dashboard
|
||||||
const fnf = await db.FnF.findOne({ where: { terminationRequestId: id } });
|
const fnf = await db.FnF.findOne({ where: { terminationRequestId: resolvedId } });
|
||||||
if (fnf) {
|
if (fnf) {
|
||||||
await db.FffClearance.update(
|
await db.FffClearance.update(
|
||||||
{ status: normalizedStatus, remarks, amount: Number(amount) || 0 },
|
{ status: normalizedStatus, remarks, amount: Number(amount) || 0 },
|
||||||
@ -423,7 +429,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
await db.TerminationAudit.create({
|
await db.TerminationAudit.create({
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
action: 'CLEARANCE_UPDATED',
|
action: 'CLEARANCE_UPDATED',
|
||||||
terminationRequestId: id,
|
terminationRequestId: resolvedId,
|
||||||
remarks: remarks || `Cleared ${department}`,
|
remarks: remarks || `Cleared ${department}`,
|
||||||
details: { department, status: normalizedStatus, amount }
|
details: { department, status: normalizedStatus, amount }
|
||||||
}, { transaction });
|
}, { transaction });
|
||||||
|
|||||||
@ -127,7 +127,8 @@ export class ResignationWorkflowService {
|
|||||||
[RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD,
|
[RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD,
|
||||||
[RESIGNATION_STAGES.NBH]: ROLES.NBH,
|
[RESIGNATION_STAGES.NBH]: ROLES.NBH,
|
||||||
[RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN,
|
[RESIGNATION_STAGES.DD_ADMIN]: ROLES.DD_ADMIN,
|
||||||
[RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN
|
[RESIGNATION_STAGES.LEGAL]: ROLES.LEGAL_ADMIN,
|
||||||
|
[RESIGNATION_STAGES.FNF_INITIATED]: ROLES.DD_ADMIN
|
||||||
};
|
};
|
||||||
|
|
||||||
const requiredRole = stageToRole[resignation.currentStage];
|
const requiredRole = stageToRole[resignation.currentStage];
|
||||||
|
|||||||
52
src/services/userRoleCode.service.ts
Normal file
52
src/services/userRoleCode.service.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Op } from 'sequelize';
|
||||||
|
import db from '../database/models/index.js';
|
||||||
|
|
||||||
|
const ROLE_CODE_PREFIX: Record<string, string> = {
|
||||||
|
ASM: 'ASM',
|
||||||
|
RBM: 'RBM',
|
||||||
|
RM: 'RBM',
|
||||||
|
'DD-ZM': 'ZM',
|
||||||
|
ZM: 'ZM',
|
||||||
|
ZBH: 'ZBH',
|
||||||
|
'DD Lead': 'DDL'
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCode = (value: string) =>
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.toUpperCase()
|
||||||
|
.replace(/\s+/g, '-');
|
||||||
|
|
||||||
|
const shouldGenerateCode = (roleCode?: string | null) => {
|
||||||
|
if (!roleCode) return false;
|
||||||
|
return Boolean(ROLE_CODE_PREFIX[roleCode]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveManagerCode = async (
|
||||||
|
roleId: string,
|
||||||
|
roleCode?: string | null,
|
||||||
|
providedCode?: string | null
|
||||||
|
): Promise<string | null> => {
|
||||||
|
const normalizedProvided = providedCode ? normalizeCode(providedCode) : null;
|
||||||
|
if (normalizedProvided) return normalizedProvided;
|
||||||
|
if (!shouldGenerateCode(roleCode)) return null;
|
||||||
|
|
||||||
|
const prefix = ROLE_CODE_PREFIX[roleCode as string];
|
||||||
|
const rows = await db.UserRole.findAll({
|
||||||
|
where: {
|
||||||
|
roleId,
|
||||||
|
managerCode: { [Op.iLike]: `${prefix}-%` }
|
||||||
|
},
|
||||||
|
attributes: ['managerCode']
|
||||||
|
});
|
||||||
|
|
||||||
|
let maxSeq = 0;
|
||||||
|
for (const row of rows as any[]) {
|
||||||
|
const code = String(row.managerCode || '');
|
||||||
|
const parts = code.split('-');
|
||||||
|
const seq = Number(parts[parts.length - 1]);
|
||||||
|
if (Number.isFinite(seq) && seq > maxSeq) maxSeq = seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix}-${String(maxSeq + 1).padStart(3, '0')}`;
|
||||||
|
};
|
||||||
@ -20,16 +20,16 @@ const PROSPECT_EMAIL = `ramesh_${timestamp}@gmail.com`;
|
|||||||
|
|
||||||
const EMAILS = {
|
const EMAILS = {
|
||||||
PROSPECT: PROSPECT_EMAIL,
|
PROSPECT: PROSPECT_EMAIL,
|
||||||
RBM_L1: 'rbm.ncr@royalenfield.com',
|
RBM_L1: 'manish@gmail.com',
|
||||||
ZM_L1: 'zm.ncr@royalenfield.com',
|
ZM_L1: 'piyush@gmail.com',
|
||||||
DD_LEAD: 'ddlead@royalenfield.com',
|
DD_LEAD: 'jaya@gmail.com',
|
||||||
ZBH: 'yashwin@gmail.com',
|
ZBH: 'manav@gmail.com',
|
||||||
NBH: 'nbh@royalenfield.com',
|
NBH: 'yashwin@gmail.com',
|
||||||
DD_HEAD: 'ddhead@royalenfield.com',
|
DD_HEAD: 'ganesh@gmail.com',
|
||||||
FDD: 'fdd@royalenfield.com',
|
FDD: 'fdd@gmail.com',
|
||||||
FINANCE: 'finance@royalenfield.com',
|
FINANCE: 'finance@gmail.com',
|
||||||
DD_ADMIN: 'lince@gmail.com',
|
DD_ADMIN: 'aman@gmail.com',
|
||||||
ASM: 'asm.sdelhi@royalenfield.com',
|
ASM: 'abhishek@gmail.com',
|
||||||
SALES: 'sales@royalenfield.com',
|
SALES: 'sales@royalenfield.com',
|
||||||
SERVICE: 'service@royalenfield.com',
|
SERVICE: 'service@royalenfield.com',
|
||||||
SPARES: 'spares@royalenfield.com',
|
SPARES: 'spares@royalenfield.com',
|
||||||
@ -123,7 +123,7 @@ async function prospectLogin(phone) {
|
|||||||
|
|
||||||
async function mockUploadDocument(appId, token, docType) {
|
async function mockUploadDocument(appId, token, docType) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const fileBuffer = fs.readFileSync('C:/Users/BACKPACKERS/Pictures/claim_document_type.PNG');
|
const fileBuffer = fs.readFileSync('/home/laxman-h/Pictures/Screenshots/Screenshot from 2026-03-26 10-08-00.png');
|
||||||
const blob = new Blob([fileBuffer], { type: 'image/png' });
|
const blob = new Blob([fileBuffer], { type: 'image/png' });
|
||||||
formData.append('file', blob, 'screenshot.png');
|
formData.append('file', blob, 'screenshot.png');
|
||||||
formData.append('documentType', docType);
|
formData.append('documentType', docType);
|
||||||
@ -347,48 +347,48 @@ async function triggerWorkflow() {
|
|||||||
await delay();
|
await delay();
|
||||||
|
|
||||||
// 6.3 FDD ASSIGNMENT
|
// 6.3 FDD ASSIGNMENT
|
||||||
log(6.3, 'Admin Assigning Application to FDD Agency...');
|
// log(6.3, 'Admin Assigning Application to FDD Agency...');
|
||||||
const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
// const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
||||||
await apiRequest('/fdd/assign', 'POST', {
|
// await apiRequest('/fdd/assign', 'POST', {
|
||||||
applicationId: applicationUUID,
|
// applicationId: applicationUUID,
|
||||||
assignedToAgency: fddUser.id
|
// assignedToAgency: fddUser.id
|
||||||
}, adminToken);
|
// }, adminToken);
|
||||||
log(6.3, 'FDD Agency assigned successfully.');
|
// log(6.3, 'FDD Agency assigned successfully.');
|
||||||
await delay();
|
// await delay();
|
||||||
|
|
||||||
// 7. FDD MILESTONE
|
// // 7. FDD MILESTONE
|
||||||
log(7, 'FDD Agency Discovery & Report Upload...');
|
// log(7, 'FDD Agency Discovery & Report Upload...');
|
||||||
const fddToken = await login(EMAILS.FDD);
|
// const fddToken = await login(EMAILS.FDD);
|
||||||
|
|
||||||
// FETCH ASSIGNMENT ID
|
// // FETCH ASSIGNMENT ID
|
||||||
const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
// const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
||||||
const assignmentId = assignmentRes.data.id;
|
// const assignmentId = assignmentRes.data.id;
|
||||||
log(7, `Found Assignment ID: ${assignmentId}`);
|
// log(7, `Found Assignment ID: ${assignmentId}`);
|
||||||
|
|
||||||
await apiRequest('/fdd/report', 'POST', {
|
// await apiRequest('/fdd/report', 'POST', {
|
||||||
assignmentId,
|
// assignmentId,
|
||||||
findings: 'Finance records clean.',
|
// findings: 'Finance records clean.',
|
||||||
recommendation: 'Approved'
|
// recommendation: 'Approved'
|
||||||
}, fddToken);
|
// }, fddToken);
|
||||||
|
|
||||||
log(7.1, 'Admin Approving FDD Final Stage...');
|
// log(7.1, 'Admin Approving FDD Final Stage...');
|
||||||
await apiRequest('/assessment/stage-decision', 'POST', {
|
// await apiRequest('/assessment/stage-decision', 'POST', {
|
||||||
applicationId: applicationUUID,
|
// applicationId: applicationUUID,
|
||||||
stageCode: 'FDD_VERIFICATION',
|
// stageCode: 'FDD_VERIFICATION',
|
||||||
decision: 'Approved',
|
// decision: 'Approved',
|
||||||
remarks: 'FDD documents verified.'
|
// remarks: 'FDD documents verified.'
|
||||||
}, adminToken);
|
// }, adminToken);
|
||||||
log(7, 'FDD Milestone Complete.');
|
// log(7, 'FDD Milestone Complete.');
|
||||||
await delay();
|
// await delay();
|
||||||
|
|
||||||
log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
// log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
||||||
const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
// const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
||||||
for (const doc of requiredDocs) {
|
// for (const doc of requiredDocs) {
|
||||||
await mockUploadDocument(applicationUUID, adminToken, doc);
|
// await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||||
}
|
// }
|
||||||
await delay(1000);
|
// await delay(1000);
|
||||||
|
|
||||||
// 7.5 LOI APPROVAL
|
// // 7.5 LOI APPROVAL
|
||||||
// log(7.5, 'LOI Generation & Approval...');
|
// log(7.5, 'LOI Generation & Approval...');
|
||||||
// const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
|
// const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||||
// const loiRequestId = loiRes.data.id;
|
// const loiRequestId = loiRes.data.id;
|
||||||
@ -418,10 +418,8 @@ async function triggerWorkflow() {
|
|||||||
// depositType: 'SECURITY_DEPOSIT',
|
// depositType: 'SECURITY_DEPOSIT',
|
||||||
// status: 'Verified'
|
// status: 'Verified'
|
||||||
// }, financeToken);
|
// }, financeToken);
|
||||||
// log(8, 'Security Deposit Verified.');
|
// log(8, 'Security Deposit Verified.')
|
||||||
// await delay();
|
// // 9. GENERATE DEALER CODES (align with backend gate: LOI Issued required)
|
||||||
|
|
||||||
// // 9. GENERATE DEALER CODES (NOW RETRY-SAFE WITH STATUS CHECK)
|
|
||||||
// let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
// let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||||
// log(9, `Current status before code generation: ${statusBeforeCodeGen}`);
|
// log(9, `Current status before code generation: ${statusBeforeCodeGen}`);
|
||||||
// log(9, 'Ensuring mandatory PAN/GST/Bank fields before code generation...');
|
// log(9, 'Ensuring mandatory PAN/GST/Bank fields before code generation...');
|
||||||
@ -442,6 +440,23 @@ async function triggerWorkflow() {
|
|||||||
// log(9, `Status after re-verify: ${statusBeforeCodeGen}`);
|
// log(9, `Status after re-verify: ${statusBeforeCodeGen}`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// // Current backend flow keeps app at "Security Details" until explicit admin transition.
|
||||||
|
// if (statusBeforeCodeGen === 'Security Details') {
|
||||||
|
// log(9, 'Applying admin transition from Security Details -> LOI Issued...');
|
||||||
|
// await apiRequest(`/onboarding/applications/${applicationUUID}/status`, 'PUT', {
|
||||||
|
// status: 'LOI Issued',
|
||||||
|
// stage: 'LOI',
|
||||||
|
// reason: 'E2E script alignment: unlock dealer code generation after Security Details checks.'
|
||||||
|
// }, adminToken);
|
||||||
|
// await delay();
|
||||||
|
// statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||||
|
// log(9, `Status after admin transition: ${statusBeforeCodeGen}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (statusBeforeCodeGen !== 'LOI Issued' && statusBeforeCodeGen !== 'Dealer Code Generation') {
|
||||||
|
// throw new Error(`Cannot generate codes: expected LOI Issued/Dealer Code Generation, got ${statusBeforeCodeGen}`);
|
||||||
|
// }
|
||||||
|
|
||||||
// log(9, 'Admin Generating SAP Dealer Codes...');
|
// log(9, 'Admin Generating SAP Dealer Codes...');
|
||||||
// await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
|
// await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
|
||||||
// log(9, 'Dealer Codes Generated.');
|
// log(9, 'Dealer Codes Generated.');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user