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",
|
||||
"reset:stable": "tsx scripts/reset_db_stable.ts",
|
||||
"seed": "tsx scripts/seed_normalized_data.ts",
|
||||
"seed:roles": "tsx scripts/seed-roles.ts",
|
||||
"seed:permissions": "tsx scripts/seed-permissions.ts",
|
||||
"seed:approval-policies": "tsx scripts/seed-approval-policies.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",
|
||||
"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: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",
|
||||
"seed:questionnaire": "tsx src/scripts/seedQuestionnaire.ts",
|
||||
"test": "jest",
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dotenv/config';
|
||||
import db from '../src/database/models/index.js';
|
||||
import bcrypt from 'bcryptjs';
|
||||
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;
|
||||
|
||||
@ -84,6 +85,7 @@ async function masterReset() {
|
||||
// Map assignments based on role category (Regional vs Granular)
|
||||
const isRegionalRole = [ROLES.RBM, ROLES.DD_ZM, ROLES.ZBH].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({
|
||||
userId: user.id,
|
||||
@ -92,7 +94,8 @@ async function masterReset() {
|
||||
isPrimary: true,
|
||||
zoneId: isRegionalRole || isGranularRole ? zone.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 { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src/modules/master/syncHierarchy.service.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;
|
||||
|
||||
@ -41,9 +42,17 @@ async function seed() {
|
||||
const mapUserRole = async (userRec: any, roleCode: string, assignment: any = {}) => {
|
||||
const role = await Role.findOne({ where: { roleCode } });
|
||||
if (role) {
|
||||
const managerCode = await resolveManagerCode(role.id, roleCode, null);
|
||||
await UserRole.findOrCreate({
|
||||
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
|
||||
const regions = [
|
||||
{ name: 'NCR Region', zoneName: 'North Zone' },
|
||||
{ name: 'Punjab Region', zoneName: 'North Zone' },
|
||||
{ name: 'Karnataka Region', zoneName: 'South Zone' },
|
||||
{ name: 'Tamil Nadu Region', zoneName: 'South Zone' }
|
||||
{ name: 'NCR Region', zoneName: 'North Zone', code: 'NZ-R1' },
|
||||
{ name: 'Punjab Region', zoneName: 'North Zone', code: 'NZ-R2' },
|
||||
{ name: 'Karnataka Region', zoneName: 'South Zone', code: 'SZ-R1' },
|
||||
{ name: 'Tamil Nadu Region', zoneName: 'South Zone', code: 'SZ-R2' }
|
||||
];
|
||||
const regionMap: Record<string, any> = {};
|
||||
for (const r of regions) {
|
||||
const zone = zoneMap[r.zoneName];
|
||||
const [region] = await Region.findOrCreate({
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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 { AuthRequest } from '../../types/express.types.js';
|
||||
import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../master/syncHierarchy.service.js';
|
||||
import { resolveManagerCode } from '../../services/userRoleCode.service.js';
|
||||
|
||||
const upsertUserAssignments = async (
|
||||
userId: string,
|
||||
@ -23,6 +24,7 @@ const upsertUserAssignments = async (
|
||||
|
||||
const role = await Role.findOne({ where: { roleCode } });
|
||||
if (!role) continue;
|
||||
const managerCode = await resolveManagerCode(role.id, roleCode, null);
|
||||
|
||||
const createdRole = await db.UserRole.create({
|
||||
userId,
|
||||
@ -30,7 +32,7 @@ const upsertUserAssignments = async (
|
||||
districtId: assignment.locationId || assignment.districtId || null,
|
||||
zoneId: assignment.zoneId || null,
|
||||
regionId: assignment.regionId || null,
|
||||
managerCode: assignment.managerCode || assignment.asmCode || null,
|
||||
managerCode,
|
||||
isPrimary: assignment.isPrimary !== undefined ? Boolean(assignment.isPrimary) : i === 0,
|
||||
isActive: assignment.isActive !== undefined ? Boolean(assignment.isActive) : true,
|
||||
effectiveFrom: assignment.effectiveFrom || null,
|
||||
@ -353,6 +355,13 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
||||
// Hash default password
|
||||
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
|
||||
const user = await User.create({
|
||||
fullName,
|
||||
@ -365,7 +374,7 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
||||
mobileNumber,
|
||||
department,
|
||||
designation,
|
||||
districtId: locationId
|
||||
districtId: safeDistrictId
|
||||
});
|
||||
|
||||
if (Array.isArray(assignments) && assignments.length > 0) {
|
||||
@ -375,13 +384,14 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
||||
if (targetRole) {
|
||||
for (const distId of districts) {
|
||||
const sampleDistrict = await db.District.findByPk(distId);
|
||||
const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null);
|
||||
await db.UserRole.create({
|
||||
userId: user.id,
|
||||
roleId: targetRole.id,
|
||||
districtId: distId,
|
||||
zoneId: sampleDistrict?.zoneId || null,
|
||||
regionId: sampleDistrict?.regionId || null,
|
||||
managerCode: asmCode || null,
|
||||
managerCode,
|
||||
isPrimary: false,
|
||||
isActive: true,
|
||||
assignedBy: req.user?.id || null
|
||||
@ -401,12 +411,13 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
for (const regId of finalRegionIds) {
|
||||
const region = await db.Region.findByPk(regId);
|
||||
const managerCode = await resolveManagerCode(targetRole.id, roleCode, null);
|
||||
await db.UserRole.create({
|
||||
userId: user.id,
|
||||
roleId: targetRole.id,
|
||||
regionId: regId,
|
||||
zoneId: region?.zoneId || null,
|
||||
managerCode: zmCode || null,
|
||||
managerCode,
|
||||
isActive: true,
|
||||
assignedBy: req.user?.id || null
|
||||
});
|
||||
@ -419,10 +430,12 @@ export const createUser = async (req: AuthRequest, res: Response) => {
|
||||
} else if (roleCode) {
|
||||
const role = await Role.findOne({ where: { roleCode } });
|
||||
if (role) {
|
||||
const managerCode = await resolveManagerCode(role.id, roleCode, null);
|
||||
await db.UserRole.create({
|
||||
userId: user.id,
|
||||
roleId: role.id,
|
||||
locationId: locationId || null,
|
||||
districtId: safeDistrictId,
|
||||
managerCode,
|
||||
isPrimary: true,
|
||||
isActive: true,
|
||||
assignedBy: req.user?.id || null
|
||||
@ -542,13 +555,14 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
for (const distId of districts) {
|
||||
const sampleDistrict = await db.District.findByPk(distId);
|
||||
const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null);
|
||||
await db.UserRole.create({
|
||||
userId: id,
|
||||
roleId: targetRole.id,
|
||||
districtId: distId,
|
||||
zoneId: sampleDistrict?.zoneId || null,
|
||||
regionId: sampleDistrict?.regionId || null,
|
||||
managerCode: asmCode || null,
|
||||
managerCode,
|
||||
isActive: true,
|
||||
assignedBy: req.user?.id || null
|
||||
});
|
||||
@ -569,12 +583,13 @@ export const updateUser = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
for (const regId of finalRegionIds) {
|
||||
const region = await db.Region.findByPk(regId);
|
||||
const managerCode = await resolveManagerCode(targetRole.id, roleCode, null);
|
||||
await db.UserRole.create({
|
||||
userId: id,
|
||||
roleId: targetRole.id,
|
||||
regionId: regId,
|
||||
zoneId: region?.zoneId || null,
|
||||
managerCode: zmCode || null,
|
||||
managerCode,
|
||||
isActive: true,
|
||||
assignedBy: req.user?.id || null
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
||||
import db from '../../database/models/index.js';
|
||||
const { AuditLog, User } = db;
|
||||
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
|
||||
const ACTION_DESCRIPTIONS: Record<string, string> = {
|
||||
@ -216,16 +216,12 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
// Dynamic Table Switching based on Module
|
||||
// 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') {
|
||||
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({
|
||||
where: { resignationId: resolvedResignationId },
|
||||
where: { resignationId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -233,13 +229,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
count = result.count;
|
||||
logs = result.rows;
|
||||
} 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({
|
||||
where: { terminationRequestId: resolvedTerminationId },
|
||||
where: { terminationRequestId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -247,27 +238,17 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
count = result.count;
|
||||
logs = result.rows;
|
||||
} 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({
|
||||
where: { fnfId: resolvedFnfId },
|
||||
where: { fnfId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
});
|
||||
count = result.count;
|
||||
logs = result.rows;
|
||||
} else if (type === 'constitutional_change') {
|
||||
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);
|
||||
} else if (type === 'constitutional') {
|
||||
const result = await db.ConstitutionalAudit.findAndCountAll({
|
||||
where: { constitutionalChangeId: resolvedConstitutionalId },
|
||||
where: { constitutionalChangeId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -275,13 +256,8 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
count = result.count;
|
||||
logs = result.rows;
|
||||
} 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({
|
||||
where: { relocationRequestId: resolvedRelocationId },
|
||||
where: { relocationRequestId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName', 'email'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -291,7 +267,7 @@ export const getAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||
} else {
|
||||
console.log(`[AuditController] Falling back to global AuditLog for type: ${type}`);
|
||||
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'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: limitNum, offset
|
||||
@ -343,73 +319,49 @@ export const getAuditSummary = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
let totalLogs = 0;
|
||||
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
|
||||
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);
|
||||
totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedResignationId } });
|
||||
totalLogs = await db.ResignationAudit.count({ where: { resignationId: resolvedId } });
|
||||
latestLog = await db.ResignationAudit.findOne({
|
||||
where: { resignationId: resolvedResignationId },
|
||||
where: { resignationId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} 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);
|
||||
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedTerminationId } });
|
||||
totalLogs = await db.TerminationAudit.count({ where: { terminationRequestId: resolvedId } });
|
||||
latestLog = await db.TerminationAudit.findOne({
|
||||
where: { terminationRequestId: resolvedTerminationId },
|
||||
where: { terminationRequestId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} 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);
|
||||
totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedFnfId } });
|
||||
totalLogs = await db.FnFAudit.count({ where: { fnfId: resolvedId } });
|
||||
latestLog = await db.FnFAudit.findOne({
|
||||
where: { fnfId: resolvedFnfId },
|
||||
where: { fnfId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} else if (type === 'constitutional' || type === 'constitutional_change') {
|
||||
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);
|
||||
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedConstitutionalId } });
|
||||
} else if (type === 'constitutional') {
|
||||
totalLogs = await db.ConstitutionalAudit.count({ where: { constitutionalChangeId: resolvedId } });
|
||||
latestLog = await db.ConstitutionalAudit.findOne({
|
||||
where: { constitutionalChangeId: resolvedConstitutionalId },
|
||||
where: { constitutionalChangeId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} else if (type === 'relocation' || type === 'relocation_request') {
|
||||
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);
|
||||
totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedRelocationId } });
|
||||
} else if (type === 'relocation') {
|
||||
totalLogs = await db.RelocationAudit.count({ where: { relocationRequestId: resolvedId } });
|
||||
latestLog = await db.RelocationAudit.findOne({
|
||||
where: { relocationRequestId: resolvedRelocationId },
|
||||
where: { relocationRequestId: resolvedId },
|
||||
include: [{ model: db.User, as: 'user', attributes: ['id', 'fullName'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
} 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({
|
||||
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'] }],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Response } from 'express';
|
||||
import { Op } from 'sequelize';
|
||||
import db from '../../database/models/index.js';
|
||||
const {
|
||||
Worknote, User, WorkNoteTag, WorkNoteAttachment, DocumentVersion, RequestParticipant, Application, AuditLog,
|
||||
@ -7,6 +6,7 @@ const {
|
||||
} = db;
|
||||
import { AuthRequest } from '../../types/express.types.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 { getIO } from '../../common/utils/socket.js';
|
||||
import * as NotificationService from '../../common/utils/notification.service.js';
|
||||
@ -66,38 +66,12 @@ const stitchWorknoteAttachments = async (worknotes: any[]) => {
|
||||
return Promise.all(notePromises);
|
||||
};
|
||||
|
||||
/** Resolve REQ-… vs UUID and align constitutional aliases with `REQUEST_TYPES.CONSTITUTIONAL`. */
|
||||
async function resolveWorknoteRequestKeys(rawId: string, rawType: string) {
|
||||
const id = String(rawId || '');
|
||||
let t = String(rawType || 'application').toLowerCase();
|
||||
if (t === 'constitutional_change') t = 'constitutional-change';
|
||||
let resolvedId = id;
|
||||
|
||||
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 };
|
||||
function worknoteListWhere(rawId: string, resolvedId: string, normalizedType: string) {
|
||||
const idVariants = Array.from(new Set([String(rawId || '').trim(), String(resolvedId || '').trim()].filter(Boolean)));
|
||||
const variants = requestTypeQueryVariants(normalizedType);
|
||||
const requestIdWhere = idVariants.length > 1 ? { [db.Sequelize.Op.in]: idVariants } : idVariants[0];
|
||||
if (variants.length > 1) return { requestId: requestIdWhere, requestType: { [db.Sequelize.Op.in]: variants } };
|
||||
return { requestId: requestIdWhere, requestType: variants[0] };
|
||||
}
|
||||
|
||||
// --- Worknotes ---
|
||||
@ -105,7 +79,7 @@ function worknoteListWhere(resolvedId: string, normalizedType: string) {
|
||||
export const addWorknote = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
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 });
|
||||
|
||||
// Debug: Log participants
|
||||
@ -240,8 +214,8 @@ export const addWorknote = async (req: AuthRequest, res: Response) => {
|
||||
export const getWorknotes = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { requestId, requestType } = req.query as any;
|
||||
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType);
|
||||
const where = worknoteListWhere(resolvedId, normalizedType);
|
||||
const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
|
||||
const where = worknoteListWhere(String(requestId || ''), resolvedId, normalizedType);
|
||||
|
||||
const worknotes = await Worknote.findAll({
|
||||
where,
|
||||
@ -264,7 +238,7 @@ export const uploadWorknoteAttachment = async (req: any, res: Response) => {
|
||||
try {
|
||||
const file = req.file;
|
||||
const { requestId, requestType } = req.body;
|
||||
const { resolvedId, normalizedType } = await resolveWorknoteRequestKeys(requestId, requestType);
|
||||
const { resolvedId, normalizedType } = await resolveEntityUuidByType(db as any, requestId, requestType);
|
||||
|
||||
if (!file) {
|
||||
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 db from '../../database/models/index.js';
|
||||
import { ROLES } from '../../common/config/constants.js';
|
||||
import { resolveManagerCode } from '../../services/userRoleCode.service.js';
|
||||
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) ---
|
||||
export const getAreas = async (req: Request, res: Response) => {
|
||||
try {
|
||||
@ -135,7 +177,41 @@ export const getDistricts = async (req: Request, res: Response) => {
|
||||
|
||||
export const createDistrict = async (req: Request, res: Response) => {
|
||||
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' });
|
||||
|
||||
// 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) => {
|
||||
try {
|
||||
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body;
|
||||
const { name, parentId, zoneId, managerId, districts, districtIds } = req.body;
|
||||
const targetZoneId = zoneId || parentId;
|
||||
if (!name) return res.status(400).json({ success: false, message: 'Region name is required' });
|
||||
if (!targetZoneId) return res.status(400).json({ success: false, message: 'Zone is required for region creation' });
|
||||
|
||||
const region = await db.Region.create({ name, code, zoneId: targetZoneId });
|
||||
const generatedCode = await nextRegionCode(targetZoneId);
|
||||
const region = await db.Region.create({ name, code: generatedCode, zoneId: targetZoneId });
|
||||
|
||||
// 1. Assign Manager
|
||||
// 1. Assign Manager (RBM is the regional manager role; keep RM as legacy fallback)
|
||||
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) {
|
||||
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({
|
||||
userId: managerId,
|
||||
roleId: rmRole.id,
|
||||
regionId: region.id,
|
||||
zoneId: targetZoneId,
|
||||
managerCode,
|
||||
isActive: true,
|
||||
isPrimary: true
|
||||
});
|
||||
@ -321,32 +405,45 @@ export const createRegion = async (req: Request, res: Response) => {
|
||||
export const updateRegion = async (req: Request, res: Response) => {
|
||||
try {
|
||||
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 region = await db.Region.findByPk(id);
|
||||
if (!region) return res.status(404).json({ success: false, message: 'Region not found' });
|
||||
|
||||
const nextZoneId = targetZoneId || region.zoneId;
|
||||
const zoneChanged = Boolean(targetZoneId && targetZoneId !== region.zoneId);
|
||||
const generatedCode =
|
||||
zoneChanged || !region.code
|
||||
? await nextRegionCode(nextZoneId, id as string)
|
||||
: region.code;
|
||||
|
||||
await region.update({
|
||||
name,
|
||||
code,
|
||||
zoneId: targetZoneId || region.zoneId
|
||||
code: generatedCode,
|
||||
zoneId: nextZoneId
|
||||
});
|
||||
|
||||
// 1. Update Manager
|
||||
// 1. Update Manager (RBM is the regional manager role; keep RM as legacy fallback)
|
||||
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) {
|
||||
// Deactivate old RMs for this region
|
||||
await db.UserRole.update({ isActive: false }, {
|
||||
where: { regionId: id, roleId: rmRole.id }
|
||||
});
|
||||
// Assign new RM
|
||||
const managerCode = await resolveManagerCode(rmRole.id, rmRole.roleCode, null);
|
||||
await db.UserRole.create({
|
||||
userId: managerId,
|
||||
roleId: rmRole.id,
|
||||
regionId: id,
|
||||
zoneId: region.zoneId,
|
||||
managerCode,
|
||||
isActive: true,
|
||||
isPrimary: true
|
||||
});
|
||||
@ -703,17 +800,25 @@ export const deleteLocation = async (req: Request, res: Response) => {
|
||||
export const updateLocation = async (req: Request, res: Response) => {
|
||||
try {
|
||||
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, {
|
||||
include: [{ model: db.District, as: 'district' }]
|
||||
});
|
||||
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
|
||||
if (district) {
|
||||
if (district && !districtId) {
|
||||
let stateId = req.body.stateId;
|
||||
if (stateName && !stateId) {
|
||||
const [state] = await db.State.findOrCreate({
|
||||
@ -734,6 +839,7 @@ export const updateLocation = async (req: Request, res: Response) => {
|
||||
// 2. Update Area
|
||||
await area.update({
|
||||
name: name || area.name,
|
||||
districtId: district?.id || area.districtId,
|
||||
city: city || area.city,
|
||||
isActive: isActive !== undefined ? isActive : area.isActive,
|
||||
openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom,
|
||||
@ -860,7 +966,7 @@ export const getZonalManagers = async (req: Request, res: Response) => {
|
||||
{
|
||||
model: db.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,
|
||||
@ -879,7 +985,7 @@ export const getZonalManagers = async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
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 mainAssignment = [...roleAssignments].sort((a: any, b: any) => {
|
||||
@ -958,7 +1064,7 @@ export const saveZM = async (req: Request, res: Response) => {
|
||||
roleId: zmRole.id,
|
||||
zoneId: zoneId || null,
|
||||
regionId: regionId,
|
||||
managerCode: zmCode || null,
|
||||
managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null),
|
||||
isActive: true,
|
||||
isPrimary: true
|
||||
});
|
||||
@ -970,7 +1076,7 @@ export const saveZM = async (req: Request, res: Response) => {
|
||||
roleId: zmRole.id,
|
||||
zoneId: zoneId || null,
|
||||
regionId: null,
|
||||
managerCode: zmCode || null,
|
||||
managerCode: await resolveManagerCode(zmRole.id, 'DD-ZM', null),
|
||||
isActive: true,
|
||||
isPrimary: true
|
||||
});
|
||||
@ -1074,7 +1180,7 @@ export const saveDDLead = async (req: Request, res: Response) => {
|
||||
userId,
|
||||
roleId: leadRole.id,
|
||||
zoneId: zoneId,
|
||||
managerCode: leadCode || null,
|
||||
managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null),
|
||||
isActive: true,
|
||||
isPrimary: true
|
||||
});
|
||||
@ -1085,7 +1191,7 @@ export const saveDDLead = async (req: Request, res: Response) => {
|
||||
userId,
|
||||
roleId: leadRole.id,
|
||||
zoneId: null,
|
||||
managerCode: leadCode || null,
|
||||
managerCode: await resolveManagerCode(leadRole.id, 'DD Lead', null),
|
||||
isActive: true,
|
||||
isPrimary: true
|
||||
});
|
||||
|
||||
@ -123,6 +123,13 @@ export const submitApplication = async (req: AuthRequest, res: Response) => {
|
||||
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)
|
||||
if (isOpportunityAvailable) {
|
||||
sendOpportunityEmail(email, applicantName, city || preferredLocation, applicationId)
|
||||
|
||||
@ -13,6 +13,7 @@ import { ConstitutionalWorkflowService } from '../../services/ConstitutionalWork
|
||||
import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
||||
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
|
||||
import {
|
||||
isRegisteredConstitutionalChangeType,
|
||||
normalizeToConstitutionalChangeType
|
||||
@ -22,6 +23,11 @@ const STRUCTURE_TARGET_VALUES = new Set<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) => {
|
||||
try {
|
||||
res.json({
|
||||
@ -261,11 +267,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
||||
export const getRequestById = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
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 resolvedId = await resolveConstitutionalUuid(String(id));
|
||||
|
||||
const request = await ConstitutionalChange.findOne({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr },
|
||||
where: { id: resolvedId },
|
||||
include: [
|
||||
{ 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' });
|
||||
|
||||
const { id } = req.params;
|
||||
const idStr = String(id);
|
||||
const rawAction = String(req.body.action || '').trim();
|
||||
const actionNorm = rawAction.toLowerCase().replace(/\s+/g, ' ');
|
||||
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({
|
||||
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' });
|
||||
@ -523,12 +527,11 @@ export const uploadDocuments = async (req: AuthRequest, res: Response) => {
|
||||
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
|
||||
const { id } = req.params;
|
||||
const idStr = String(id);
|
||||
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({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
|
||||
@ -8,6 +8,12 @@ import { Op, Transaction } from 'sequelize';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AuthRequest } from '../../types/express.types.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
|
||||
@ -386,12 +392,10 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
||||
export const getRequestById = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const id = req.params.id as string;
|
||||
|
||||
// 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 resolvedId = await resolveRelocationUuid(id);
|
||||
|
||||
const request = await RelocationRequest.findOne({
|
||||
where: isUUID ? { id } : { requestId: id },
|
||||
where: { id: resolvedId },
|
||||
include: [
|
||||
{
|
||||
model: Outlet,
|
||||
@ -515,12 +519,10 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
||||
.toUpperCase()
|
||||
.replace(/\s+/g, '_');
|
||||
|
||||
// Check if id is a UUID or a requestId string
|
||||
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 resolvedId = await resolveRelocationUuid(String(id));
|
||||
|
||||
const request = await RelocationRequest.findOne({
|
||||
where: isUUID ? { id: idStr } : { requestId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
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 resolvedId = await resolveRelocationUuid(String(id));
|
||||
|
||||
// Only search by requestId since frontend sends requestId, not UUID
|
||||
const request = await RelocationRequest.findOne({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
@ -806,11 +806,10 @@ const applyRelocationDocumentDecision = async (
|
||||
|
||||
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||
|
||||
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 resolvedId = await resolveRelocationUuid(String(id));
|
||||
|
||||
const request = await RelocationRequest.findOne({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
if (!request) {
|
||||
return res.status(404).json({ success: false, message: 'Relocation request not found' });
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
AUDIT_ACTIONS,
|
||||
ROLES,
|
||||
REQUEST_TYPES,
|
||||
FNF_STATUS,
|
||||
RESIGNATION_DOCUMENT_TYPES,
|
||||
RESIGNATION_DOCUMENT_STAGES
|
||||
} from '../../common/config/constants.js';
|
||||
@ -18,8 +19,13 @@ import { NomenclatureService } from '../../common/utils/nomenclature.js';
|
||||
import { ParticipantService } from '../../services/ParticipantService.js';
|
||||
import { getResignationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.js';
|
||||
import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js';
|
||||
import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js';
|
||||
|
||||
// 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)
|
||||
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) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
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 resolvedId = await resolveResignationUuid(String(id));
|
||||
|
||||
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' },
|
||||
{
|
||||
@ -218,10 +223,9 @@ export const uploadResignationDocument = async (req: AuthRequest, res: Response,
|
||||
message: `Invalid stage. Allowed values: ${RESIGNATION_DOCUMENT_STAGES.join(', ')}`
|
||||
});
|
||||
}
|
||||
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 resolvedId = await resolveResignationUuid(String(id));
|
||||
const resignation = await db.Resignation.findOne({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
|
||||
if (!resignation) {
|
||||
@ -275,12 +279,11 @@ export const approveResignation = async (req: AuthRequest, res: Response, next:
|
||||
try {
|
||||
if (!req.user) throw new Error('Unauthorized');
|
||||
const { id } = req.params;
|
||||
const idStr = String(id);
|
||||
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({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
|
||||
where: { id: resolvedId },
|
||||
include: [
|
||||
{ model: db.Outlet, as: 'outlet' },
|
||||
{ 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' });
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Transition via Workflow Service
|
||||
@ -405,16 +423,15 @@ export const rejectResignation = async (req: AuthRequest, res: Response, next: N
|
||||
try {
|
||||
if (!req.user) throw new Error('Unauthorized');
|
||||
const { id } = req.params;
|
||||
const idStr = String(id);
|
||||
const { reason } = req.body;
|
||||
if (!reason) {
|
||||
await transaction.rollback();
|
||||
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({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
|
||||
where: { id: resolvedId },
|
||||
include: [{ model: db.Outlet, as: 'outlet' }]
|
||||
});
|
||||
if (!resignation) {
|
||||
@ -457,12 +474,11 @@ export const withdrawResignation = async (req: AuthRequest, res: Response, next:
|
||||
try {
|
||||
if (!req.user) throw new Error('Unauthorized');
|
||||
const { id } = req.params;
|
||||
const idStr = String(id);
|
||||
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({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr },
|
||||
where: { id: resolvedId },
|
||||
include: [{ model: db.Outlet, as: 'outlet' }]
|
||||
});
|
||||
if (!resignation) {
|
||||
@ -524,12 +540,11 @@ export const sendBackResignation = async (req: AuthRequest, res: Response, next:
|
||||
try {
|
||||
if (!req.user) throw new Error('Unauthorized');
|
||||
const { id } = req.params;
|
||||
const idStr = String(id);
|
||||
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({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
if (!resignation) {
|
||||
await transaction.rollback();
|
||||
@ -590,8 +605,9 @@ export const assignResignation = async (req: AuthRequest, res: Response, next: N
|
||||
const { id } = req.params;
|
||||
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({
|
||||
where: { [Op.or]: [{ id }, { resignationId: id }] },
|
||||
where: { id: resolvedId },
|
||||
include: [{ model: db.User, as: 'dealer' }]
|
||||
});
|
||||
|
||||
@ -687,12 +703,11 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
||||
try {
|
||||
if (!req.user) throw new Error('Unauthorized');
|
||||
const { id } = req.params;
|
||||
const idStr = String(id);
|
||||
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({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { resignationId: idStr }] } : { resignationId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
if (!resignation) {
|
||||
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)) {
|
||||
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
|
||||
(req as any).targetStage = RESIGNATION_STAGES.FNF_INITIATED;
|
||||
return approveResignation(req, res, next);
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
TERMINATION_DOCUMENT_TYPES,
|
||||
TERMINATION_DOCUMENT_STAGES
|
||||
} from '../../common/config/constants.js';
|
||||
import { Op, Transaction } from 'sequelize';
|
||||
import { Transaction } from 'sequelize';
|
||||
import { AuthRequest } from '../../types/express.types.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 { getTerminationStatusForStage, normalizeClearanceStatus } from '../../common/utils/offboardingStatus.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
|
||||
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) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
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 resolvedId = await resolveTerminationUuid(String(id));
|
||||
|
||||
const termination = await db.TerminationRequest.findOne({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr },
|
||||
where: { id: resolvedId },
|
||||
include: [
|
||||
{
|
||||
model: db.Dealer,
|
||||
@ -184,10 +189,9 @@ export const uploadTerminationDocument = async (req: AuthRequest, res: Response,
|
||||
message: `Invalid stage. Allowed values: ${TERMINATION_DOCUMENT_STAGES.join(', ')}`
|
||||
});
|
||||
}
|
||||
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 resolvedId = await resolveTerminationUuid(String(id));
|
||||
const termination = await db.TerminationRequest.findOne({
|
||||
where: isUUID ? { [Op.or]: [{ id: idStr }, { requestId: idStr }] } : { requestId: idStr }
|
||||
where: { id: resolvedId }
|
||||
});
|
||||
|
||||
if (!termination) {
|
||||
@ -240,8 +244,9 @@ export const updateTerminationStatus = async (req: AuthRequest, res: Response, n
|
||||
if (!req.user) throw new Error('Unauthorized');
|
||||
const { id } = req.params;
|
||||
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) {
|
||||
await transaction.rollback();
|
||||
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');
|
||||
const { id } = req.params;
|
||||
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');
|
||||
|
||||
const clearances = { ...(termination.departmentalClearances || {}) };
|
||||
@ -412,7 +418,7 @@ export const updateClearance = async (req: AuthRequest, res: Response, next: Nex
|
||||
await termination.update({ departmentalClearances: clearances }, { transaction });
|
||||
|
||||
// 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) {
|
||||
await db.FffClearance.update(
|
||||
{ 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({
|
||||
userId: req.user.id,
|
||||
action: 'CLEARANCE_UPDATED',
|
||||
terminationRequestId: id,
|
||||
terminationRequestId: resolvedId,
|
||||
remarks: remarks || `Cleared ${department}`,
|
||||
details: { department, status: normalizedStatus, amount }
|
||||
}, { transaction });
|
||||
|
||||
@ -127,7 +127,8 @@ export class ResignationWorkflowService {
|
||||
[RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD,
|
||||
[RESIGNATION_STAGES.NBH]: ROLES.NBH,
|
||||
[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];
|
||||
|
||||
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 = {
|
||||
PROSPECT: PROSPECT_EMAIL,
|
||||
RBM_L1: 'rbm.ncr@royalenfield.com',
|
||||
ZM_L1: 'zm.ncr@royalenfield.com',
|
||||
DD_LEAD: 'ddlead@royalenfield.com',
|
||||
ZBH: 'yashwin@gmail.com',
|
||||
NBH: 'nbh@royalenfield.com',
|
||||
DD_HEAD: 'ddhead@royalenfield.com',
|
||||
FDD: 'fdd@royalenfield.com',
|
||||
FINANCE: 'finance@royalenfield.com',
|
||||
DD_ADMIN: 'lince@gmail.com',
|
||||
ASM: 'asm.sdelhi@royalenfield.com',
|
||||
RBM_L1: 'manish@gmail.com',
|
||||
ZM_L1: 'piyush@gmail.com',
|
||||
DD_LEAD: 'jaya@gmail.com',
|
||||
ZBH: 'manav@gmail.com',
|
||||
NBH: 'yashwin@gmail.com',
|
||||
DD_HEAD: 'ganesh@gmail.com',
|
||||
FDD: 'fdd@gmail.com',
|
||||
FINANCE: 'finance@gmail.com',
|
||||
DD_ADMIN: 'aman@gmail.com',
|
||||
ASM: 'abhishek@gmail.com',
|
||||
SALES: 'sales@royalenfield.com',
|
||||
SERVICE: 'service@royalenfield.com',
|
||||
SPARES: 'spares@royalenfield.com',
|
||||
@ -123,7 +123,7 @@ async function prospectLogin(phone) {
|
||||
|
||||
async function mockUploadDocument(appId, token, docType) {
|
||||
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' });
|
||||
formData.append('file', blob, 'screenshot.png');
|
||||
formData.append('documentType', docType);
|
||||
@ -347,48 +347,48 @@ async function triggerWorkflow() {
|
||||
await delay();
|
||||
|
||||
// 6.3 FDD ASSIGNMENT
|
||||
log(6.3, 'Admin Assigning Application to FDD Agency...');
|
||||
const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
||||
await apiRequest('/fdd/assign', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
assignedToAgency: fddUser.id
|
||||
}, adminToken);
|
||||
log(6.3, 'FDD Agency assigned successfully.');
|
||||
await delay();
|
||||
// log(6.3, 'Admin Assigning Application to FDD Agency...');
|
||||
// const fddUser = users.data.find(u => u.email === EMAILS.FDD);
|
||||
// await apiRequest('/fdd/assign', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// assignedToAgency: fddUser.id
|
||||
// }, adminToken);
|
||||
// log(6.3, 'FDD Agency assigned successfully.');
|
||||
// await delay();
|
||||
|
||||
// 7. FDD MILESTONE
|
||||
log(7, 'FDD Agency Discovery & Report Upload...');
|
||||
const fddToken = await login(EMAILS.FDD);
|
||||
// // 7. FDD MILESTONE
|
||||
// log(7, 'FDD Agency Discovery & Report Upload...');
|
||||
// const fddToken = await login(EMAILS.FDD);
|
||||
|
||||
// FETCH ASSIGNMENT ID
|
||||
const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
||||
const assignmentId = assignmentRes.data.id;
|
||||
log(7, `Found Assignment ID: ${assignmentId}`);
|
||||
// // FETCH ASSIGNMENT ID
|
||||
// const assignmentRes = await apiRequest(`/fdd/${applicationUUID}`, 'GET', null, fddToken);
|
||||
// const assignmentId = assignmentRes.data.id;
|
||||
// log(7, `Found Assignment ID: ${assignmentId}`);
|
||||
|
||||
await apiRequest('/fdd/report', 'POST', {
|
||||
assignmentId,
|
||||
findings: 'Finance records clean.',
|
||||
recommendation: 'Approved'
|
||||
}, fddToken);
|
||||
// await apiRequest('/fdd/report', 'POST', {
|
||||
// assignmentId,
|
||||
// findings: 'Finance records clean.',
|
||||
// recommendation: 'Approved'
|
||||
// }, fddToken);
|
||||
|
||||
log(7.1, 'Admin Approving FDD Final Stage...');
|
||||
await apiRequest('/assessment/stage-decision', 'POST', {
|
||||
applicationId: applicationUUID,
|
||||
stageCode: 'FDD_VERIFICATION',
|
||||
decision: 'Approved',
|
||||
remarks: 'FDD documents verified.'
|
||||
}, adminToken);
|
||||
log(7, 'FDD Milestone Complete.');
|
||||
await delay();
|
||||
// log(7.1, 'Admin Approving FDD Final Stage...');
|
||||
// await apiRequest('/assessment/stage-decision', 'POST', {
|
||||
// applicationId: applicationUUID,
|
||||
// stageCode: 'FDD_VERIFICATION',
|
||||
// decision: 'Approved',
|
||||
// remarks: 'FDD documents verified.'
|
||||
// }, adminToken);
|
||||
// log(7, 'FDD Milestone Complete.');
|
||||
// await delay();
|
||||
|
||||
log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
||||
const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
||||
for (const doc of requiredDocs) {
|
||||
await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
}
|
||||
await delay(1000);
|
||||
// log(7.4, 'Uploading mandatory documents prior to LOI generation...');
|
||||
// const requiredDocs = ['CIBIL Report', 'Proposed Site City Map', 'Bank Statement', 'GST Certificate', 'PAN Card'];
|
||||
// for (const doc of requiredDocs) {
|
||||
// await mockUploadDocument(applicationUUID, adminToken, doc);
|
||||
// }
|
||||
// await delay(1000);
|
||||
|
||||
// 7.5 LOI APPROVAL
|
||||
// // 7.5 LOI APPROVAL
|
||||
// log(7.5, 'LOI Generation & Approval...');
|
||||
// const loiRes = await apiRequest('/loi/request', 'POST', { applicationId: applicationUUID }, adminToken);
|
||||
// const loiRequestId = loiRes.data.id;
|
||||
@ -418,10 +418,8 @@ async function triggerWorkflow() {
|
||||
// depositType: 'SECURITY_DEPOSIT',
|
||||
// status: 'Verified'
|
||||
// }, financeToken);
|
||||
// log(8, 'Security Deposit Verified.');
|
||||
// await delay();
|
||||
|
||||
// // 9. GENERATE DEALER CODES (NOW RETRY-SAFE WITH STATUS CHECK)
|
||||
// log(8, 'Security Deposit Verified.')
|
||||
// // 9. GENERATE DEALER CODES (align with backend gate: LOI Issued required)
|
||||
// let statusBeforeCodeGen = await getApplicationStatus(applicationUUID, adminToken);
|
||||
// log(9, `Current status before code generation: ${statusBeforeCodeGen}`);
|
||||
// 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}`);
|
||||
// }
|
||||
|
||||
// // 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...');
|
||||
// await apiRequest(`/onboarding/applications/${applicationUUID}/generate-codes`, 'POST', {}, adminToken);
|
||||
// log(9, 'Dealer Codes Generated.');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user