relocatin
flow integrated at cetrtain extent douments mapping , assigns mapping done documents application constrain removed
This commit is contained in:
parent
8d2a7874de
commit
28580b7fb0
@ -166,10 +166,15 @@ export const RELOCATION_TYPES = {
|
|||||||
|
|
||||||
// Relocation Stages
|
// Relocation Stages
|
||||||
export const RELOCATION_STAGES = {
|
export const RELOCATION_STAGES = {
|
||||||
DD_ADMIN_REVIEW: 'DD Admin Review',
|
ASM_REVIEW: 'ASM Review',
|
||||||
RBM_REVIEW: 'RBM Review',
|
RBM_REVIEW: 'RBM Review',
|
||||||
|
DD_ZM_REVIEW: 'DD ZM Review',
|
||||||
|
ZBH_REVIEW: 'ZBH Review',
|
||||||
|
DD_LEAD_REVIEW: 'DD Lead Review',
|
||||||
|
DD_HEAD_APPROVAL: 'DD Head Approval',
|
||||||
NBH_APPROVAL: 'NBH Approval',
|
NBH_APPROVAL: 'NBH Approval',
|
||||||
LEGAL_CLEARANCE: 'Legal Clearance',
|
LEGAL_CLEARANCE: 'Legal Clearance',
|
||||||
|
NBH_CLEARANCE_EOR: 'NBH Clearance with EOR',
|
||||||
COMPLETED: 'Completed',
|
COMPLETED: 'Completed',
|
||||||
REJECTED: 'Rejected'
|
REJECTED: 'Rejected'
|
||||||
} as const;
|
} as const;
|
||||||
@ -338,6 +343,18 @@ export const DOCUMENT_TYPES = {
|
|||||||
SECURITY_DEPOSIT_RECEIPT: 'Security Deposit Receipt',
|
SECURITY_DEPOSIT_RECEIPT: 'Security Deposit Receipt',
|
||||||
SECURITY_DEPOSIT_INITIAL: 'Initial Security Deposit Receipt',
|
SECURITY_DEPOSIT_INITIAL: 'Initial Security Deposit Receipt',
|
||||||
SECURITY_DEPOSIT_FINAL: 'Final Security Deposit Receipt',
|
SECURITY_DEPOSIT_FINAL: 'Final Security Deposit Receipt',
|
||||||
|
RELOCATION_PROPERTY_DOCS: 'Property documents for new location',
|
||||||
|
RELOCATION_LEASE_AGREEMENT: 'Lease/Rental agreement for new location',
|
||||||
|
RELOCATION_NOC_LANDLORD: 'NOC from current landlord',
|
||||||
|
RELOCATION_MUNICIPAL_APPROVALS: 'Municipal approvals',
|
||||||
|
RELOCATION_FIRE_SAFETY: 'Fire safety certificate',
|
||||||
|
RELOCATION_POLLUTION_CLEARANCE: 'Pollution clearance',
|
||||||
|
RELOCATION_LAYOUT_PLAN: 'Layout/Floor plan of new location',
|
||||||
|
RELOCATION_PHOTOS: 'Photos of new location',
|
||||||
|
RELOCATION_LOCALITY_MAP: 'Locality map',
|
||||||
|
RELOCATION_BUILDING_PLAN: 'Building plan approval',
|
||||||
|
RELOCATION_ELECTRICITY_DOCS: 'Electricity connection documents',
|
||||||
|
RELOCATION_WATER_DOCS: 'Water supply documents',
|
||||||
OTHER: 'Other'
|
OTHER: 'Other'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
34
src/common/utils/dateUtils.ts
Normal file
34
src/common/utils/dateUtils.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Common date formatting utility for backend modules
|
||||||
|
*/
|
||||||
|
export function formatDateTime(date: string | Date | number, format: 'full' | 'date' | 'time' = 'full') {
|
||||||
|
const d = new Date(date);
|
||||||
|
|
||||||
|
// Check for invalid date
|
||||||
|
if (isNaN(d.getTime())) return 'Invalid Date';
|
||||||
|
|
||||||
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (format === 'full' || format === 'time') {
|
||||||
|
options.hour = '2-digit';
|
||||||
|
options.minute = '2-digit';
|
||||||
|
options.hour12 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format === 'time') {
|
||||||
|
return d.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.toLocaleDateString('en-IN', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns current timestamp in YYYY-MM-DD HH:mm:ss format
|
||||||
|
*/
|
||||||
|
export function getCurrentTimestamp() {
|
||||||
|
return new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||||
|
}
|
||||||
@ -248,7 +248,8 @@ export default (sequelize: Sequelize) => {
|
|||||||
Application.hasMany(models.Document, {
|
Application.hasMany(models.Document, {
|
||||||
foreignKey: 'requestId',
|
foreignKey: 'requestId',
|
||||||
as: 'uploadedDocuments',
|
as: 'uploadedDocuments',
|
||||||
scope: { requestType: 'application' }
|
scope: { requestType: 'application' },
|
||||||
|
constraints: false
|
||||||
});
|
});
|
||||||
|
|
||||||
Application.hasMany(models.QuestionnaireResponse, { foreignKey: 'applicationId', as: 'questionnaireResponses' });
|
Application.hasMany(models.QuestionnaireResponse, { foreignKey: 'applicationId', as: 'questionnaireResponses' });
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { Model, DataTypes, Sequelize } from 'sequelize';
|
|||||||
|
|
||||||
export interface EorChecklistAttributes {
|
export interface EorChecklistAttributes {
|
||||||
id: string;
|
id: string;
|
||||||
applicationId: string;
|
applicationId: string | null;
|
||||||
|
relocationId: string | null;
|
||||||
auditorId: string | null;
|
auditorId: string | null;
|
||||||
auditDate: Date | null;
|
auditDate: Date | null;
|
||||||
status: string;
|
status: string;
|
||||||
@ -20,12 +21,20 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
applicationId: {
|
applicationId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: false,
|
allowNull: true,
|
||||||
references: {
|
references: {
|
||||||
model: 'applications',
|
model: 'applications',
|
||||||
key: 'id'
|
key: 'id'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
relocationId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'relocation_requests',
|
||||||
|
key: 'id'
|
||||||
|
}
|
||||||
|
},
|
||||||
auditorId: {
|
auditorId: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
@ -53,6 +62,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
|
|
||||||
(EorChecklist as any).associate = (models: any) => {
|
(EorChecklist as any).associate = (models: any) => {
|
||||||
EorChecklist.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
|
EorChecklist.belongsTo(models.Application, { foreignKey: 'applicationId', as: 'application' });
|
||||||
|
EorChecklist.belongsTo(models.RelocationRequest, { foreignKey: 'relocationId', as: 'relocation' });
|
||||||
EorChecklist.belongsTo(models.User, { foreignKey: 'auditorId', as: 'auditor' });
|
EorChecklist.belongsTo(models.User, { foreignKey: 'auditorId', as: 'auditor' });
|
||||||
|
|
||||||
EorChecklist.hasMany(models.EorChecklistItem, { foreignKey: 'checklistId', as: 'items' });
|
EorChecklist.hasMany(models.EorChecklistItem, { foreignKey: 'checklistId', as: 'items' });
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
currentStage: {
|
currentStage: {
|
||||||
type: DataTypes.ENUM(...Object.values(RELOCATION_STAGES)),
|
type: DataTypes.ENUM(...Object.values(RELOCATION_STAGES)),
|
||||||
defaultValue: RELOCATION_STAGES.DD_ADMIN_REVIEW
|
defaultValue: RELOCATION_STAGES.ASM_REVIEW
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
@ -140,6 +140,16 @@ export default (sequelize: Sequelize) => {
|
|||||||
scope: { requestType: 'relocation' },
|
scope: { requestType: 'relocation' },
|
||||||
constraints: false
|
constraints: false
|
||||||
});
|
});
|
||||||
|
RelocationRequest.hasMany(models.Document, {
|
||||||
|
foreignKey: 'requestId',
|
||||||
|
as: 'uploadedDocuments',
|
||||||
|
scope: { requestType: 'relocation' },
|
||||||
|
constraints: false
|
||||||
|
});
|
||||||
|
RelocationRequest.hasOne(models.EorChecklist, {
|
||||||
|
foreignKey: 'relocationId',
|
||||||
|
as: 'eorChecklist'
|
||||||
|
});
|
||||||
// Note: Participants are computed dynamically based on outlet location hierarchy
|
// Note: Participants are computed dynamically based on outlet location hierarchy
|
||||||
// See getRequestById in relocation.controller.ts
|
// See getRequestById in relocation.controller.ts
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { AuthRequest } from '../../types/express.types.js';
|
|||||||
|
|
||||||
export const getChecklist = async (req: Request, res: Response) => {
|
export const getChecklist = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { applicationId } = req.params;
|
const { applicationId, relocationId } = req.params;
|
||||||
let checklist = await EorChecklist.findOne({
|
let checklist = await EorChecklist.findOne({
|
||||||
where: { applicationId },
|
where: relocationId ? { relocationId } : { applicationId },
|
||||||
include: [{ model: EorChecklistItem, as: 'items', include: ['proofDocument'] }]
|
include: [{ model: EorChecklistItem, as: 'items', include: ['proofDocument'] }]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,32 +25,62 @@ export const getChecklist = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const createChecklist = async (req: AuthRequest, res: Response) => {
|
export const createChecklist = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { applicationId } = req.body;
|
const { applicationId, relocationId } = req.body;
|
||||||
|
|
||||||
const application = await db.Application.findByPk(applicationId);
|
if (!applicationId && !relocationId) {
|
||||||
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
|
return res.status(400).json({ success: false, message: 'applicationId or relocationId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationId) {
|
||||||
|
const application = await db.Application.findByPk(applicationId);
|
||||||
|
if (!application) return res.status(404).json({ success: false, message: 'Application not found' });
|
||||||
|
} else if (relocationId) {
|
||||||
|
const relocation = await db.RelocationRequest.findByPk(relocationId);
|
||||||
|
if (!relocation) return res.status(404).json({ success: false, message: 'Relocation request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
const [checklist, created] = await EorChecklist.findOrCreate({
|
const [checklist, created] = await EorChecklist.findOrCreate({
|
||||||
where: { applicationId },
|
where: relocationId ? { relocationId } : { applicationId },
|
||||||
defaults: { status: 'In Progress' }
|
defaults: {
|
||||||
|
status: 'In Progress',
|
||||||
|
applicationId: applicationId || null,
|
||||||
|
relocationId: relocationId || null
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (created) {
|
if (created) {
|
||||||
// Define Default Mandatory Items per SRS/Frontend
|
// Define Default Mandatory Items per SRS/Frontend
|
||||||
const defaultItems = [
|
let defaultItems = [];
|
||||||
{ itemType: 'Sales', description: 'Sales Standards' },
|
|
||||||
{ itemType: 'Service', description: 'Service & Spares' },
|
if (relocationId) {
|
||||||
{ itemType: 'IT', description: 'DMS infra' },
|
// Strictly per SRS Section 12.2.8 for Relocation
|
||||||
{ itemType: 'Training', description: 'Manpower Training' },
|
defaultItems = [
|
||||||
{ itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
|
{ itemType: 'Property', description: 'Property documents for new location' },
|
||||||
{ itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
|
{ itemType: 'Property', description: 'Lease / Rental agreement' },
|
||||||
{ itemType: 'Finance', description: 'Inventory Funding' },
|
{ itemType: 'Property', description: 'Layout / Floor plan of new location' },
|
||||||
{ itemType: 'IT', description: 'Virtual code availability' },
|
{ itemType: 'Infrastructure', description: 'Photos of new location' },
|
||||||
{ itemType: 'Finance', description: 'Vendor payments' },
|
{ itemType: 'Infrastructure', description: 'Locality map / Building plan approval' },
|
||||||
{ itemType: 'Marketing', description: 'Details for website submission' },
|
{ itemType: 'Statutory', description: 'NOC from current landlord' },
|
||||||
{ itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
|
{ itemType: 'Statutory', description: 'Municipal approvals (Fire safety / Pollution clearance)' },
|
||||||
{ itemType: 'IT', description: 'Auto ordering' }
|
{ itemType: 'Utility', description: 'Electricity & Water supply documents' }
|
||||||
];
|
];
|
||||||
|
} else {
|
||||||
|
// Onboarding Default
|
||||||
|
defaultItems = [
|
||||||
|
{ itemType: 'Sales', description: 'Sales Standards' },
|
||||||
|
{ itemType: 'Service', description: 'Service & Spares' },
|
||||||
|
{ itemType: 'IT', description: 'DMS infra' },
|
||||||
|
{ itemType: 'Training', description: 'Manpower Training' },
|
||||||
|
{ itemType: 'Statutory', description: 'Trade certificate with test ride bikes registration' },
|
||||||
|
{ itemType: 'Statutory', description: 'GST certificate including Accessories & Apparels billing' },
|
||||||
|
{ itemType: 'Finance', description: 'Inventory Funding' },
|
||||||
|
{ itemType: 'IT', description: 'Virtual code availability' },
|
||||||
|
{ itemType: 'Finance', description: 'Vendor payments' },
|
||||||
|
{ itemType: 'Marketing', description: 'Details for website submission' },
|
||||||
|
{ itemType: 'Insurance', description: 'Infra Insurance both Showroom and Service center' },
|
||||||
|
{ itemType: 'IT', description: 'Auto ordering' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const itemsData = defaultItems.map(item => ({
|
const itemsData = defaultItems.map(item => ({
|
||||||
...item,
|
...item,
|
||||||
@ -113,15 +143,25 @@ export const submitAudit = async (req: AuthRequest, res: Response) => {
|
|||||||
if (status === 'Completed') {
|
if (status === 'Completed') {
|
||||||
const checklist = await EorChecklist.findByPk(checklistId);
|
const checklist = await EorChecklist.findByPk(checklistId);
|
||||||
if (checklist) {
|
if (checklist) {
|
||||||
await db.Application.update({
|
if (checklist.applicationId) {
|
||||||
overallStatus: 'Approved',
|
await db.Application.update({
|
||||||
progressPercentage: 100
|
overallStatus: 'Approved',
|
||||||
}, { where: { id: checklist.applicationId } });
|
progressPercentage: 100
|
||||||
|
}, { where: { id: checklist.applicationId } });
|
||||||
|
|
||||||
// Update Progress Tracking
|
// Update Progress Tracking
|
||||||
const { updateApplicationProgress } = await import('../../common/utils/progress.js');
|
const { updateApplicationProgress } = await import('../../common/utils/progress.js');
|
||||||
await updateApplicationProgress(checklist.applicationId, 'EOR Complete', 'completed', 100);
|
await updateApplicationProgress(checklist.applicationId, 'EOR Complete', 'completed', 100);
|
||||||
await updateApplicationProgress(checklist.applicationId, 'Inauguration', 'active', 50);
|
await updateApplicationProgress(checklist.applicationId, 'Inauguration', 'active', 50);
|
||||||
|
} else if (checklist.relocationId) {
|
||||||
|
await db.RelocationRequest.update({
|
||||||
|
status: 'Completed',
|
||||||
|
progressPercentage: 100,
|
||||||
|
currentStage: 'Completed'
|
||||||
|
}, { where: { id: checklist.relocationId } });
|
||||||
|
|
||||||
|
// The workflow service can handle timeline/audit but here we just finalized the status
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import { authenticate } from '../../common/middleware/auth.js';
|
|||||||
|
|
||||||
router.use(authenticate as any);
|
router.use(authenticate as any);
|
||||||
|
|
||||||
router.get('/:applicationId', eorController.getChecklist);
|
router.get('/application/:applicationId', eorController.getChecklist);
|
||||||
|
router.get('/relocation/:relocationId', eorController.getChecklist);
|
||||||
router.post('/', eorController.createChecklist);
|
router.post('/', eorController.createChecklist);
|
||||||
router.post('/item/:checklistId', eorController.updateItem);
|
router.post('/item/:checklistId', eorController.updateItem);
|
||||||
router.post('/audit/:checklistId', eorController.submitAudit);
|
router.post('/audit/:checklistId', eorController.submitAudit);
|
||||||
|
|||||||
@ -272,6 +272,68 @@ export const updateSecurityDeposit = async (req: AuthRequest, res: Response) =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AUTOMATION: If Initial Payment is Verified, auto-approve "Finance" role in LOI Stage
|
||||||
|
if (depositType === 'INITIAL' && status === 'Verified') {
|
||||||
|
const LoiRequest = db.LoiRequest;
|
||||||
|
const LoiApproval = db.LoiApproval;
|
||||||
|
const StageApprovalAction = db.StageApprovalAction;
|
||||||
|
|
||||||
|
const loiReq = await LoiRequest.findOne({ where: { applicationId: application.id } });
|
||||||
|
if (loiReq) {
|
||||||
|
// 1. Update module-specific approval table
|
||||||
|
const financeApproval = await LoiApproval.findOne({
|
||||||
|
where: { requestId: loiReq.id, approverRole: 'Finance', action: 'Pending' }
|
||||||
|
});
|
||||||
|
if (financeApproval) {
|
||||||
|
await financeApproval.update({
|
||||||
|
action: 'Approved',
|
||||||
|
remarks: 'Auto-approved via Finance payment verification',
|
||||||
|
approverId: req.user?.id,
|
||||||
|
approvedAt: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update generic StageApprovalAction table
|
||||||
|
await StageApprovalAction.upsert({
|
||||||
|
applicationId: application.id,
|
||||||
|
stageCode: 'LOI_APPROVAL',
|
||||||
|
actorUserId: req.user?.id,
|
||||||
|
actorRole: 'Finance',
|
||||||
|
decision: 'Approved',
|
||||||
|
remarks: 'Auto-approved via Finance payment verification'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Check if LOI can now be fully approved (copied logic from loi.controller)
|
||||||
|
const policy = await db.StageApprovalPolicy.findOne({ where: { stageCode: 'LOI_APPROVAL' } });
|
||||||
|
const requiredRoles = policy?.requiredRoles || ['Finance', 'DD Head', 'NBH'];
|
||||||
|
|
||||||
|
const stageActions = await StageApprovalAction.findAll({
|
||||||
|
where: { applicationId: application.id, stageCode: 'LOI_APPROVAL' }
|
||||||
|
});
|
||||||
|
const approvedRoles = new Set(stageActions.filter((a: any) => a.decision === 'Approved').map((a: any) => a.actorRole));
|
||||||
|
const meetsMinApprovals = approvedRoles.size >= (policy?.minApprovals || 3);
|
||||||
|
const hasAllRequired = requiredRoles.every((r: string) => approvedRoles.has(r));
|
||||||
|
|
||||||
|
if (hasAllRequired && meetsMinApprovals && loiReq.status !== 'Approved') {
|
||||||
|
await loiReq.update({ status: 'Approved', approvedBy: req.user?.id, approvedAt: new Date() });
|
||||||
|
|
||||||
|
const mockFile = `LOI_${loiReq.id}.pdf`;
|
||||||
|
await db.LoiDocumentGenerated.findOrCreate({
|
||||||
|
where: { requestId: loiReq.id, documentType: 'LOI' },
|
||||||
|
defaults: {
|
||||||
|
fileName: mockFile,
|
||||||
|
filePath: `/uploads/loi/${mockFile}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await WorkflowService.transitionApplication(application, APPLICATION_STATUS.SECURITY_DETAILS, req.user?.id, {
|
||||||
|
reason: 'LOI Request fully approved via automated finance verification',
|
||||||
|
progressPercentage: 80
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ success: true, message: 'Security Deposit updated', data: deposit });
|
res.json({ success: true, message: 'Security Deposit updated', data: deposit });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update Security Deposit error:', error);
|
console.error('Update Security Deposit error:', error);
|
||||||
|
|||||||
@ -64,6 +64,7 @@ router.get('/area-managers', getAreaManagers);
|
|||||||
router.get('/asms', getASMs);
|
router.get('/asms', getASMs);
|
||||||
router.get('/zonal-managers', getZonalManagers);
|
router.get('/zonal-managers', getZonalManagers);
|
||||||
router.post('/zonal-managers', saveZM);
|
router.post('/zonal-managers', saveZM);
|
||||||
|
router.get('/dd-leads', getDDLeads);
|
||||||
router.post('/dd-leads', saveDDLead);
|
router.post('/dd-leads', saveDDLead);
|
||||||
router.get('/system-configs', getSystemConfigs);
|
router.get('/system-configs', getSystemConfigs);
|
||||||
router.post('/system-configs', saveSystemConfig);
|
router.post('/system-configs', saveSystemConfig);
|
||||||
|
|||||||
@ -50,7 +50,7 @@ export const getRequests = 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 where: any = {};
|
const where: any = {};
|
||||||
if (req.user.role === 'Dealer') {
|
if (req.user.roleCode === 'Dealer') {
|
||||||
where.dealerId = req.user.id;
|
where.dealerId = req.user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import db from '../../database/models/index.js';
|
import db from '../../database/models/index.js';
|
||||||
const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone } = db;
|
import { RelocationWorkflowService } from '../../services/RelocationWorkflowService.js';
|
||||||
|
const { RelocationRequest, Outlet, User, Worknote, District, Region, Zone, Document } = db;
|
||||||
import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js';
|
import { AUDIT_ACTIONS, ROLES, RELOCATION_STAGES } from '../../common/config/constants.js';
|
||||||
import { Op, Transaction } from 'sequelize';
|
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to assign evaluators for relocation requests based on outlet location hierarchy
|
* Helper to assign evaluators for relocation requests based on outlet location hierarchy
|
||||||
@ -48,22 +50,22 @@ const assignRelocationEvaluators = async (requestId: string, outletId: string) =
|
|||||||
|
|
||||||
// Stage 1: DD ASM (from district)
|
// Stage 1: DD ASM (from district)
|
||||||
if (district.asmId) {
|
if (district.asmId) {
|
||||||
evaluators.push({ id: district.asmId, role: 'ASM', stage: 'ASM_REVIEW' });
|
evaluators.push({ id: district.asmId, role: 'ASM', stage: RELOCATION_STAGES.ASM_REVIEW });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 2: RBM (from region)
|
// Stage 2: RBM (from region)
|
||||||
if (region && region.rbmId) {
|
if (region && region.rbmId) {
|
||||||
evaluators.push({ id: region.rbmId, role: 'RBM', stage: 'RBM_REVIEW' });
|
evaluators.push({ id: region.rbmId, role: 'RBM', stage: RELOCATION_STAGES.RBM_REVIEW });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 3: DD ZM (from district)
|
// Stage 3: DD ZM (from district)
|
||||||
if (district.zmId) {
|
if (district.zmId) {
|
||||||
evaluators.push({ id: district.zmId, role: 'DD-ZM', stage: 'DD_ZM_REVIEW' });
|
evaluators.push({ id: district.zmId, role: 'DD-ZM', stage: RELOCATION_STAGES.DD_ZM_REVIEW });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 4: ZBH (from zone)
|
// Stage 4: ZBH (from zone)
|
||||||
if (zone && zone.zbhId) {
|
if (zone && zone.zbhId) {
|
||||||
evaluators.push({ id: zone.zbhId, role: 'ZBH', stage: 'ZBH_REVIEW' });
|
evaluators.push({ id: zone.zbhId, role: 'ZBH', stage: RELOCATION_STAGES.ZBH_REVIEW });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 5: DD Lead (zone-scoped)
|
// Stage 5: DD Lead (zone-scoped)
|
||||||
@ -77,30 +79,35 @@ const assignRelocationEvaluators = async (requestId: string, outletId: string) =
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
if (ddLead) {
|
if (ddLead) {
|
||||||
evaluators.push({ id: ddLead.id, role: 'DD Lead', stage: 'DD_LEAD_REVIEW' });
|
evaluators.push({ id: ddLead.id, role: 'DD Lead', stage: RELOCATION_STAGES.DD_LEAD_REVIEW });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 6: NBH (national)
|
// Stage 6: DD Head (national)
|
||||||
const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } });
|
const ddHead = await User.findOne({ where: { roleCode: 'DD Head', status: 'active' } });
|
||||||
if (nbh) {
|
if (ddHead) {
|
||||||
evaluators.push({ id: nbh.id, role: 'NBH', stage: 'NBH_REVIEW' });
|
evaluators.push({ id: ddHead.id, role: 'DD Head', stage: RELOCATION_STAGES.DD_HEAD_APPROVAL });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 7: Legal (national)
|
// Stage 7: NBH Approval (national)
|
||||||
|
const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } });
|
||||||
|
if (nbh) {
|
||||||
|
evaluators.push({ id: nbh.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_APPROVAL });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 8: Legal Clearance (national)
|
||||||
const legal = await User.findOne({ where: { roleCode: 'Legal Admin', status: 'active' } });
|
const legal = await User.findOne({ where: { roleCode: 'Legal Admin', status: 'active' } });
|
||||||
if (legal) {
|
if (legal) {
|
||||||
evaluators.push({ id: legal.id, role: 'Legal', stage: 'LEGAL_CLEARANCE' });
|
evaluators.push({ id: legal.id, role: 'Legal', stage: RELOCATION_STAGES.LEGAL_CLEARANCE });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 9: NBH Clearance with EOR (national)
|
||||||
|
if (nbh) {
|
||||||
|
evaluators.push({ id: nbh.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_CLEARANCE_EOR });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[debug] Found ${evaluators.length} evaluators for relocation request`);
|
console.log(`[debug] Found ${evaluators.length} evaluators for relocation request`);
|
||||||
|
|
||||||
// Note: RequestParticipant table has FK to applications, not relocation_requests
|
|
||||||
// So we store evaluators directly in the relocation request's timeline/metadata
|
|
||||||
// and return them via the outlet's location hierarchy lookup
|
|
||||||
|
|
||||||
// Store evaluator info in a separate table or return via API
|
|
||||||
// For now, log and store in request metadata via timeline
|
|
||||||
const evaluatorInfo = evaluators.map(e => ({
|
const evaluatorInfo = evaluators.map(e => ({
|
||||||
userId: e.id,
|
userId: e.id,
|
||||||
role: e.role,
|
role: e.role,
|
||||||
@ -108,13 +115,9 @@ const assignRelocationEvaluators = async (requestId: string, outletId: string) =
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(`[debug] Evaluators assigned:`, evaluatorInfo);
|
console.log(`[debug] Evaluators assigned:`, evaluatorInfo);
|
||||||
console.log(`[debug] Successfully assigned ${evaluators.length} evaluators to relocation request`);
|
|
||||||
|
|
||||||
// Return evaluator info in response
|
|
||||||
return evaluatorInfo;
|
return evaluatorInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[debug] Error assigning relocation evaluators:', error);
|
console.error('[debug] Error assigning relocation evaluators:', error);
|
||||||
// Don't throw - assignment is non-critical
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -152,9 +155,9 @@ export const submitRequest = async (req: AuthRequest, res: Response) => {
|
|||||||
newDistrictId: newDistrictId || null,
|
newDistrictId: newDistrictId || null,
|
||||||
newStateId: newStateId || null,
|
newStateId: newStateId || null,
|
||||||
reason,
|
reason,
|
||||||
currentStage: RELOCATION_STAGES.DD_ADMIN_REVIEW as any,
|
currentStage: RELOCATION_STAGES.ASM_REVIEW,
|
||||||
status: 'Pending',
|
status: 'Pending ASM Review',
|
||||||
progressPercentage: 20,
|
progressPercentage: 10,
|
||||||
documents: [],
|
documents: [],
|
||||||
timeline: [{
|
timeline: [{
|
||||||
stage: 'Submitted',
|
stage: 'Submitted',
|
||||||
@ -183,7 +186,7 @@ export const getRequests = 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 where: any = {};
|
const where: any = {};
|
||||||
if (req.user.role === 'Dealer') {
|
if (req.user.roleCode === 'Dealer') {
|
||||||
where.dealerId = req.user.id;
|
where.dealerId = req.user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +196,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
|||||||
{
|
{
|
||||||
model: Outlet,
|
model: Outlet,
|
||||||
as: 'outlet',
|
as: 'outlet',
|
||||||
attributes: ['code', 'name'],
|
attributes: ['code', 'name', 'address', 'city', 'state', 'pincode'],
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: District,
|
model: District,
|
||||||
@ -218,7 +221,7 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
|||||||
// Filter requests based on user's role and location assignments
|
// Filter requests based on user's role and location assignments
|
||||||
const filteredRequests = requests.filter((request: any) => {
|
const filteredRequests = requests.filter((request: any) => {
|
||||||
// Dealers see only their own requests
|
// Dealers see only their own requests
|
||||||
if (req.user?.role === 'Dealer') {
|
if (req.user?.roleCode === 'Dealer') {
|
||||||
return request.dealerId === req.user.id;
|
return request.dealerId === req.user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,8 +250,20 @@ export const getRequests = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
return isAssigned;
|
return isAssigned;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enrich responses with currentLocation and proposedLocation for frontend
|
||||||
|
const enrichedRequests = filteredRequests.map((request: any) => {
|
||||||
|
const reqData = request.get({ plain: true });
|
||||||
|
const outlet = reqData.outlet;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...reqData,
|
||||||
|
currentLocation: outlet ? `${outlet.address}, ${outlet.city}, ${outlet.state} - ${outlet.pincode}` : 'N/A',
|
||||||
|
proposedLocation: `${reqData.newAddress}, ${reqData.newCity}, ${reqData.newState}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
res.json({ success: true, requests: filteredRequests });
|
res.json({ success: true, requests: enrichedRequests });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get relocation requests error:', error);
|
console.error('Get relocation requests error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error fetching requests' });
|
res.status(500).json({ success: false, message: 'Error fetching requests' });
|
||||||
@ -305,11 +320,11 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
|
|||||||
const region = district.region;
|
const region = district.region;
|
||||||
const zone = district.zone;
|
const zone = district.zone;
|
||||||
|
|
||||||
const evaluatorRoles = [
|
const evaluatorRoles: any[] = [
|
||||||
{ id: district.asmId, role: 'ASM', stage: 'ASM_REVIEW' },
|
{ id: district.asmId, roleCode: 'ASM', stage: RELOCATION_STAGES.ASM_REVIEW },
|
||||||
{ id: region?.rbmId, role: 'RBM', stage: 'RBM_REVIEW' },
|
{ id: region?.rbmId, roleCode: 'RBM', stage: RELOCATION_STAGES.RBM_REVIEW },
|
||||||
{ id: district.zmId, role: 'DD-ZM', stage: 'DD_ZM_REVIEW' },
|
{ id: district.zmId, roleCode: 'DD-ZM', stage: RELOCATION_STAGES.DD_ZM_REVIEW },
|
||||||
{ id: zone?.zbhId, role: 'ZBH', stage: 'ZBH_REVIEW' }
|
{ id: zone?.zbhId, roleCode: 'ZBH', stage: RELOCATION_STAGES.ZBH_REVIEW }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get DD Lead (zone-scoped)
|
// Get DD Lead (zone-scoped)
|
||||||
@ -320,18 +335,29 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
|
|||||||
model: db.UserRole,
|
model: db.UserRole,
|
||||||
as: 'userRoles',
|
as: 'userRoles',
|
||||||
where: { zoneId: zone.id, isActive: true }
|
where: { zoneId: zone.id, isActive: true }
|
||||||
}],
|
}]
|
||||||
attributes: ['id', 'fullName', 'email', 'roleCode']
|
|
||||||
});
|
});
|
||||||
if (ddLead) evaluatorRoles.push({ id: ddLead.id, role: 'DD Lead', stage: 'DD_LEAD_REVIEW' });
|
if (ddLead) evaluatorRoles.push({ id: ddLead.id, roleCode: 'DD Lead', stage: RELOCATION_STAGES.DD_LEAD_REVIEW });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get NBH and Legal (national)
|
// Get DD Head (national)
|
||||||
const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' }, attributes: ['id', 'fullName', 'email', 'roleCode'] });
|
const ddHead = await User.findOne({ where: { roleCode: 'DD Head', status: 'active' } });
|
||||||
if (nbh) evaluatorRoles.push({ id: nbh.id, role: 'NBH', stage: 'NBH_REVIEW' });
|
if (ddHead) evaluatorRoles.push({ id: ddHead.id, roleCode: 'DD Head', stage: RELOCATION_STAGES.DD_HEAD_APPROVAL });
|
||||||
|
|
||||||
const legal = await User.findOne({ where: { roleCode: 'Legal Admin', status: 'active' }, attributes: ['id', 'fullName', 'email', 'roleCode'] });
|
// Get NBH (national) - Approval Stage
|
||||||
if (legal) evaluatorRoles.push({ id: legal.id, role: 'Legal', stage: 'LEGAL_CLEARANCE' });
|
const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } });
|
||||||
|
if (nbh) {
|
||||||
|
evaluatorRoles.push({ id: nbh.id, roleCode: 'NBH', stage: RELOCATION_STAGES.NBH_APPROVAL });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Legal (national)
|
||||||
|
const legal = await User.findOne({ where: { roleCode: 'Legal Admin', status: 'active' } });
|
||||||
|
if (legal) evaluatorRoles.push({ id: legal.id, roleCode: 'Legal Admin', stage: RELOCATION_STAGES.LEGAL_CLEARANCE });
|
||||||
|
|
||||||
|
// Get NBH (national) - Final Clearance Stage
|
||||||
|
if (nbh) {
|
||||||
|
evaluatorRoles.push({ id: nbh.id, roleCode: 'NBH', stage: RELOCATION_STAGES.NBH_CLEARANCE_EOR });
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch user details for each evaluator
|
// Fetch user details for each evaluator
|
||||||
for (const evaluator of evaluatorRoles) {
|
for (const evaluator of evaluatorRoles) {
|
||||||
@ -342,7 +368,7 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
|
|||||||
id: `eval-${evaluator.stage}`,
|
id: `eval-${evaluator.stage}`,
|
||||||
userId: evaluator.id,
|
userId: evaluator.id,
|
||||||
participantType: 'reviewer',
|
participantType: 'reviewer',
|
||||||
metadata: { stage: evaluator.stage, role: evaluator.role, autoAssigned: true },
|
metadata: { stage: evaluator.stage, role: evaluator.roleCode, autoAssigned: true },
|
||||||
user
|
user
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -350,7 +376,10 @@ export const getRequestById = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enrich response with currentLocation and proposedLocation
|
||||||
const response = request.toJSON();
|
const response = request.toJSON();
|
||||||
|
(response as any).currentLocation = (response as any).outlet ? `${(response as any).outlet.address}, ${(response as any).outlet.city}, ${(response as any).outlet.state} - ${(response as any).outlet.pincode}` : 'N/A';
|
||||||
|
(response as any).proposedLocation = `${(response as any).newAddress}, ${(response as any).newCity}, ${(response as any).newState}`;
|
||||||
(response as any).participants = participants;
|
(response as any).participants = participants;
|
||||||
|
|
||||||
res.json({ success: true, request: response });
|
res.json({ success: true, request: response });
|
||||||
@ -367,51 +396,99 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { action, comments } = req.body;
|
const { action, comments } = req.body;
|
||||||
|
|
||||||
// Only search by requestId since frontend sends requestId, not UUID
|
// Normalize action to uppercase for service consistency (APPROVE/REJECT)
|
||||||
|
const normalizedAction = action?.toUpperCase() || '';
|
||||||
|
|
||||||
|
// 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 request = await RelocationRequest.findOne({
|
const request = await RelocationRequest.findOne({
|
||||||
where: {
|
where: isUUID ? { id: idStr } : { requestId: idStr }
|
||||||
requestId: id
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return res.status(404).json({ success: false, message: 'Request not found' });
|
return res.status(404).json({ success: false, message: 'Request not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. Authorization Check via Workflow Service
|
||||||
|
const canAction = await RelocationWorkflowService.canUserAction(request, req.user);
|
||||||
|
if (!canAction) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: `Forbidden: Your role (${req.user?.roleCode}) is not authorized to take actions for the current stage: ${request.currentStage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update status and current_stage based on action
|
// Update status and current_stage based on action
|
||||||
let newStatus = request.status;
|
let newStatus = request.status;
|
||||||
let newCurrentStage = request.currentStage;
|
let newCurrentStage = request.currentStage;
|
||||||
|
|
||||||
const stageFlow: Record<string, string> = {
|
const stageFlow: Record<string, string> = {
|
||||||
[RELOCATION_STAGES.DD_ADMIN_REVIEW]: RELOCATION_STAGES.RBM_REVIEW,
|
[RELOCATION_STAGES.ASM_REVIEW]: RELOCATION_STAGES.RBM_REVIEW,
|
||||||
[RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.NBH_APPROVAL,
|
[RELOCATION_STAGES.RBM_REVIEW]: RELOCATION_STAGES.DD_ZM_REVIEW,
|
||||||
|
[RELOCATION_STAGES.DD_ZM_REVIEW]: RELOCATION_STAGES.ZBH_REVIEW,
|
||||||
|
[RELOCATION_STAGES.ZBH_REVIEW]: RELOCATION_STAGES.DD_LEAD_REVIEW,
|
||||||
|
[RELOCATION_STAGES.DD_LEAD_REVIEW]: RELOCATION_STAGES.DD_HEAD_APPROVAL,
|
||||||
|
[RELOCATION_STAGES.DD_HEAD_APPROVAL]: RELOCATION_STAGES.NBH_APPROVAL,
|
||||||
[RELOCATION_STAGES.NBH_APPROVAL]: RELOCATION_STAGES.LEGAL_CLEARANCE,
|
[RELOCATION_STAGES.NBH_APPROVAL]: RELOCATION_STAGES.LEGAL_CLEARANCE,
|
||||||
[RELOCATION_STAGES.LEGAL_CLEARANCE]: RELOCATION_STAGES.COMPLETED
|
[RELOCATION_STAGES.LEGAL_CLEARANCE]: RELOCATION_STAGES.NBH_CLEARANCE_EOR,
|
||||||
|
[RELOCATION_STAGES.NBH_CLEARANCE_EOR]: RELOCATION_STAGES.COMPLETED
|
||||||
};
|
};
|
||||||
|
|
||||||
if (action === 'Approved') {
|
if (normalizedAction === 'APPROVE') {
|
||||||
newCurrentStage = stageFlow[request.currentStage] || request.currentStage;
|
newCurrentStage = stageFlow[request.currentStage] || request.currentStage;
|
||||||
newStatus = newCurrentStage === RELOCATION_STAGES.COMPLETED ? 'Completed' : `Pending ${newCurrentStage.replace('_', ' ')}`;
|
newStatus = newCurrentStage === RELOCATION_STAGES.COMPLETED ? 'Completed' : `Pending ${newCurrentStage}`;
|
||||||
} else if (action === 'Rejected') {
|
} else if (normalizedAction === 'REJECT') {
|
||||||
newStatus = 'Rejected';
|
newStatus = 'Rejected';
|
||||||
newCurrentStage = RELOCATION_STAGES.REJECTED;
|
newCurrentStage = RELOCATION_STAGES.REJECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a worknote entry
|
// 2. Perform transition via Workflow Service (handles request update, timeline, audit logs)
|
||||||
await Worknote.create({
|
const progressSteps = 9;
|
||||||
requestId: request.id,
|
const currentStepIndex = Object.keys(stageFlow).indexOf(request.currentStage);
|
||||||
requestType: 'relocation' as any,
|
const newProgress = normalizedAction === 'APPROVE'
|
||||||
userId: req.user.id,
|
? Math.min(Math.round(((currentStepIndex + 2) / progressSteps) * 100), 100)
|
||||||
content: comments,
|
: request.progressPercentage;
|
||||||
isInternal: true
|
|
||||||
|
await RelocationWorkflowService.transitionRelocation(request, newStatus, req.user?.id || null, {
|
||||||
|
reason: comments || 'No remarks provided',
|
||||||
|
stage: newCurrentStage,
|
||||||
|
action: normalizedAction,
|
||||||
|
progressPercentage: newProgress
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the request status and current stage
|
// 2.5 Auto-initiate EOR Checklist if moving to NBH_CLEARANCE_EOR
|
||||||
await request.update({
|
if (newCurrentStage === RELOCATION_STAGES.NBH_CLEARANCE_EOR && normalizedAction === 'APPROVE') {
|
||||||
status: newStatus,
|
try {
|
||||||
currentStage: newCurrentStage,
|
// Internal call to EOR controller logic (or we could use a service)
|
||||||
updatedAt: new Date()
|
// For now, simpler to just trigger the DB creation here or ensure controller handles it
|
||||||
});
|
const { createChecklist } = await import('../eor/eor.controller.js');
|
||||||
|
// We mock the req/res for internal call or just use the DB directly
|
||||||
|
await db.EorChecklist.findOrCreate({
|
||||||
|
where: { relocationId: request.id },
|
||||||
|
defaults: {
|
||||||
|
status: 'In Progress',
|
||||||
|
relocationId: request.id,
|
||||||
|
applicationId: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`[RelocationController] EOR Checklist initiated for ${request.requestId}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to auto-initiate EOR checklist:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create a worknote entry for the comment
|
||||||
|
if (comments) {
|
||||||
|
await Worknote.create({
|
||||||
|
requestId: request.id,
|
||||||
|
requestType: 'relocation' as any,
|
||||||
|
userId: req.user.id,
|
||||||
|
content: comments,
|
||||||
|
isInternal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
|
res.json({ success: true, message: `Request ${action.toLowerCase()} successfully` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -423,31 +500,165 @@ export const takeAction = async (req: AuthRequest, res: Response) => {
|
|||||||
export const uploadDocuments = async (req: AuthRequest, res: Response) => {
|
export const uploadDocuments = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { documents } = req.body;
|
const { documentType, stage } = req.body;
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return res.status(400).json({ success: false, message: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!documentType) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Document type is required' });
|
||||||
|
}
|
||||||
|
|
||||||
// Only search by requestId since frontend sends requestId, not UUID
|
// Only search by requestId since frontend sends requestId, not UUID
|
||||||
const request = await RelocationRequest.findOne({
|
const request = await RelocationRequest.findOne({
|
||||||
where: {
|
where: {
|
||||||
requestId: id
|
[Op.or]: [
|
||||||
|
{ id },
|
||||||
|
{ requestId: id }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return res.status(404).json({ success: false, message: 'Request not found' });
|
return res.status(404).json({ success: false, message: 'Relocation request not found' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Document Record
|
||||||
|
const newDoc = await Document.create({
|
||||||
|
requestId: request.id,
|
||||||
|
requestType: 'relocation',
|
||||||
|
documentType,
|
||||||
|
stage: stage || request.currentStage,
|
||||||
|
fileName: file.originalname,
|
||||||
|
filePath: file.path,
|
||||||
|
fileSize: file.size,
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
uploadedBy: req.user?.id,
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the documents JSON array in RelocationRequest for quick access/legacy compatibility
|
||||||
|
const currentDocuments = request.documents || [];
|
||||||
|
const updatedDocuments = [...currentDocuments, {
|
||||||
|
id: newDoc.id,
|
||||||
|
name: file.originalname,
|
||||||
|
type: documentType,
|
||||||
|
url: newDoc.filePath, // Critical for frontend preview/download
|
||||||
|
mimeType: newDoc.mimeType, // Useful for previewer
|
||||||
|
uploadedOn: new Date(),
|
||||||
|
uploadedBy: req.user?.fullName || 'System',
|
||||||
|
status: 'Pending Verification',
|
||||||
|
category: 'Relocation'
|
||||||
|
}];
|
||||||
|
|
||||||
await request.update({
|
await request.update({
|
||||||
documents: documents,
|
documents: updatedDocuments,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, message: 'Documents uploaded successfully' });
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Document uploaded successfully',
|
||||||
|
data: newDoc
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload documents error:', error);
|
console.error('Upload documents error:', error);
|
||||||
res.status(500).json({ success: false, message: 'Error uploading documents' });
|
res.status(500).json({ success: false, message: 'Error uploading documents' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const verifyDocument = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id, documentId } = req.params;
|
||||||
|
|
||||||
|
if (!req.user) return res.status(401).json({ success: false, message: 'Unauthorized' });
|
||||||
|
|
||||||
|
// Search by UUID or requestId for the request
|
||||||
|
const request = await RelocationRequest.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ id },
|
||||||
|
{ requestId: id }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Relocation request not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization: Non-dealers only, and ideally matching the current stage
|
||||||
|
const isInternal = req.user.roleCode !== 'Dealer';
|
||||||
|
if (!isInternal) {
|
||||||
|
return res.status(403).json({ success: false, message: 'Forbidden: Dealers cannot verify documents' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and update the Document record
|
||||||
|
const docRecord = await Document.findByPk(documentId);
|
||||||
|
if (docRecord) {
|
||||||
|
await docRecord.update({ status: 'Verified' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the document entry in the request's JSON JSON array
|
||||||
|
const currentDocuments = request.documents || [];
|
||||||
|
const documentIndex = currentDocuments.findIndex((d: any) => d.id === documentId);
|
||||||
|
|
||||||
|
if (documentIndex !== -1) {
|
||||||
|
currentDocuments[documentIndex].status = 'Verified';
|
||||||
|
currentDocuments[documentIndex].verifiedBy = req.user.fullName;
|
||||||
|
currentDocuments[documentIndex].verifiedOn = new Date();
|
||||||
|
|
||||||
|
// Add simple timeline log for document verification in same update
|
||||||
|
const updatedTimeline = [...(request.timeline || []), {
|
||||||
|
stage: request.currentStage,
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: req.user.fullName,
|
||||||
|
action: 'Document Verified',
|
||||||
|
remarks: `Verified document: ${currentDocuments[documentIndex].name}`
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Calculate progress percentage
|
||||||
|
const totalRequired = 12; // Standard relocation requirement
|
||||||
|
const verifiedCount = currentDocuments.filter((d: any) => d.status === 'Verified').length;
|
||||||
|
const progressPercentage = Math.min(Math.round((verifiedCount / totalRequired) * 100), 100);
|
||||||
|
|
||||||
|
// Update request status to 'In Progress' if it was 'Pending'
|
||||||
|
let newStatus = request.status;
|
||||||
|
if (request.status === 'Pending') {
|
||||||
|
newStatus = 'In Progress';
|
||||||
|
}
|
||||||
|
|
||||||
|
await request.update({
|
||||||
|
documents: currentDocuments,
|
||||||
|
timeline: updatedTimeline,
|
||||||
|
progressPercentage,
|
||||||
|
status: newStatus,
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force Sequelize to detect JSON changes
|
||||||
|
request.changed('documents', true);
|
||||||
|
request.changed('timeline', true);
|
||||||
|
await request.save();
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Document verified successfully',
|
||||||
|
document: currentDocuments[documentIndex],
|
||||||
|
progressPercentage,
|
||||||
|
status: newStatus
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(404).json({ success: false, message: 'Document not found in request tracker' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Verify document error:', error);
|
||||||
|
res.status(500).json({ success: false, message: 'Error verifying document' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function to calculate distance between two coordinates
|
// Helper function to calculate distance between two coordinates
|
||||||
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||||
const R = 6371; // Radius of Earth in km
|
const R = 6371; // Radius of Earth in km
|
||||||
|
|||||||
@ -3,12 +3,14 @@ const router = express.Router();
|
|||||||
|
|
||||||
import * as relocationController from './relocation.controller.js';
|
import * as relocationController from './relocation.controller.js';
|
||||||
import { authenticate } from '../../common/middleware/auth.js';
|
import { authenticate } from '../../common/middleware/auth.js';
|
||||||
|
import { uploadSingle } from '../../common/middleware/upload.js';
|
||||||
|
|
||||||
// Relocation routes
|
// Relocation routes
|
||||||
router.post('/', authenticate as any, relocationController.submitRequest);
|
router.post('/', authenticate as any, relocationController.submitRequest);
|
||||||
router.get('/', authenticate as any, relocationController.getRequests);
|
router.get('/', authenticate as any, relocationController.getRequests);
|
||||||
router.get('/:id', authenticate as any, relocationController.getRequestById);
|
router.get('/:id', authenticate as any, relocationController.getRequestById);
|
||||||
router.put('/:id/action', authenticate as any, relocationController.takeAction);
|
router.post('/:id/action', authenticate as any, relocationController.takeAction);
|
||||||
router.post('/:id/documents', authenticate as any, relocationController.uploadDocuments);
|
router.post('/:id/documents', authenticate as any, uploadSingle, relocationController.uploadDocuments);
|
||||||
|
router.post('/:id/documents/:documentId/verify', authenticate as any, relocationController.verifyDocument);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@ -139,7 +139,7 @@ export const getResignations = async (req: AuthRequest, res: Response, next: Nex
|
|||||||
// Build where clause based on user role
|
// Build where clause based on user role
|
||||||
let where: any = {};
|
let where: any = {};
|
||||||
|
|
||||||
if (req.user.role === ROLES.DEALER) {
|
if (req.user.roleCode === ROLES.DEALER) {
|
||||||
where.dealerId = req.user.id;
|
where.dealerId = req.user.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +219,7 @@ export const getResignationById = async (req: AuthRequest, res: Response, next:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check access permissions
|
// Check access permissions
|
||||||
if (req.user.role === ROLES.DEALER && resignation.dealerId !== req.user.id) {
|
if (req.user.roleCode === ROLES.DEALER && resignation.dealerId !== req.user.id) {
|
||||||
return res.status(403).json({
|
return res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Access denied'
|
message: 'Access denied'
|
||||||
|
|||||||
@ -13,14 +13,14 @@ router.use('/resignations', resignationRoutes);
|
|||||||
router.post('/constitutional', authenticate as any, constitutionalController.submitRequest);
|
router.post('/constitutional', authenticate as any, constitutionalController.submitRequest);
|
||||||
router.get('/constitutional', authenticate as any, constitutionalController.getRequests);
|
router.get('/constitutional', authenticate as any, constitutionalController.getRequests);
|
||||||
router.get('/constitutional/:id', authenticate as any, constitutionalController.getRequestById);
|
router.get('/constitutional/:id', authenticate as any, constitutionalController.getRequestById);
|
||||||
router.put('/constitutional/:id/action', authenticate as any, constitutionalController.takeAction);
|
router.post('/constitutional/:id/action', authenticate as any, constitutionalController.takeAction);
|
||||||
router.post('/constitutional/:id/documents', authenticate as any, constitutionalController.uploadDocuments);
|
router.post('/constitutional/:id/documents', authenticate as any, constitutionalController.uploadDocuments);
|
||||||
|
|
||||||
// Relocation submodule
|
// Relocation submodule
|
||||||
router.post('/relocation', authenticate as any, relocationController.submitRequest);
|
router.post('/relocation', authenticate as any, relocationController.submitRequest);
|
||||||
router.get('/relocation', authenticate as any, relocationController.getRequests);
|
router.get('/relocation', authenticate as any, relocationController.getRequests);
|
||||||
router.get('/relocation/:id', authenticate as any, relocationController.getRequestById);
|
router.get('/relocation/:id', authenticate as any, relocationController.getRequestById);
|
||||||
router.put('/relocation/:id/action', authenticate as any, relocationController.takeAction);
|
router.post('/relocation/:id/action', authenticate as any, relocationController.takeAction);
|
||||||
router.post('/relocation/:id/documents', authenticate as any, relocationController.uploadDocuments);
|
router.post('/relocation/:id/documents', authenticate as any, relocationController.uploadDocuments);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
91
src/services/RelocationWorkflowService.ts
Normal file
91
src/services/RelocationWorkflowService.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import db from '../database/models/index.js';
|
||||||
|
const { RelocationRequest, AuditLog, User } = db;
|
||||||
|
import { AUDIT_ACTIONS, RELOCATION_STAGES, ROLES } from '../common/config/constants.js';
|
||||||
|
|
||||||
|
export class RelocationWorkflowService {
|
||||||
|
/**
|
||||||
|
* Standardized method to transition a relocation request status
|
||||||
|
*/
|
||||||
|
static async transitionRelocation(request: any, targetStatus: string, userId: string | null = null, metadata: any = {}) {
|
||||||
|
const previousStatus = request.status;
|
||||||
|
const { reason, stage, progressPercentage, action } = metadata;
|
||||||
|
|
||||||
|
const updateData: any = {
|
||||||
|
status: targetStatus,
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update stage if provided and valid
|
||||||
|
if (stage && Object.values(RELOCATION_STAGES).includes(stage)) {
|
||||||
|
updateData.currentStage = stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress percentage if explicitly provided
|
||||||
|
if (progressPercentage !== undefined) {
|
||||||
|
updateData.progressPercentage = progressPercentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Update Request Record
|
||||||
|
await request.update(updateData);
|
||||||
|
|
||||||
|
// 2. Update Timeline (JSON array)
|
||||||
|
const user = userId ? await User.findByPk(userId) : null;
|
||||||
|
const timelineEntry = {
|
||||||
|
stage: stage || request.currentStage,
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: user ? user.fullName : 'System',
|
||||||
|
action: action || `Transitioned to ${targetStatus}`,
|
||||||
|
remarks: reason || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedTimeline = [...(request.timeline || []), timelineEntry];
|
||||||
|
await request.update({ timeline: updatedTimeline });
|
||||||
|
|
||||||
|
// 3. Create Audit Log
|
||||||
|
await AuditLog.create({
|
||||||
|
userId: userId,
|
||||||
|
action: action === 'REJECT' ? AUDIT_ACTIONS.REJECTED : AUDIT_ACTIONS.APPROVED,
|
||||||
|
entityType: 'relocation',
|
||||||
|
entityId: request.id,
|
||||||
|
newData: { status: targetStatus, stage: stage || request.currentStage, reason }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[RelocationWorkflowService] Transitioned Request ${request.requestId} to ${targetStatus}`);
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user is authorized to perform an action in the current stage
|
||||||
|
*/
|
||||||
|
static async canUserAction(request: any, user: any) {
|
||||||
|
if (!user) return false;
|
||||||
|
|
||||||
|
// Super Admin bypass
|
||||||
|
if (user.roleCode === ROLES.SUPER_ADMIN) return true;
|
||||||
|
|
||||||
|
const stageMapping: Record<string, string> = {
|
||||||
|
[RELOCATION_STAGES.ASM_REVIEW]: ROLES.ASM,
|
||||||
|
[RELOCATION_STAGES.RBM_REVIEW]: ROLES.RBM,
|
||||||
|
[RELOCATION_STAGES.DD_ZM_REVIEW]: ROLES.DD_ZM,
|
||||||
|
[RELOCATION_STAGES.ZBH_REVIEW]: ROLES.ZBH,
|
||||||
|
[RELOCATION_STAGES.DD_LEAD_REVIEW]: ROLES.DD_LEAD,
|
||||||
|
[RELOCATION_STAGES.DD_HEAD_APPROVAL]: ROLES.DD_HEAD,
|
||||||
|
[RELOCATION_STAGES.NBH_APPROVAL]: ROLES.NBH,
|
||||||
|
[RELOCATION_STAGES.LEGAL_CLEARANCE]: ROLES.LEGAL_ADMIN,
|
||||||
|
[RELOCATION_STAGES.NBH_CLEARANCE_EOR]: ROLES.NBH
|
||||||
|
};
|
||||||
|
|
||||||
|
const requiredRole = stageMapping[request.currentStage];
|
||||||
|
if (!requiredRole) return false;
|
||||||
|
|
||||||
|
// Role-based check
|
||||||
|
if (user.roleCode !== requiredRole) return false;
|
||||||
|
|
||||||
|
// Optional: Hierarchy check
|
||||||
|
// We could verify if the user is the SPECIFIC person assigned in participants
|
||||||
|
// but for now, any user with the correct role can act (consistent with simple RBAC)
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
verify_relocation_auth.ts
Normal file
55
verify_relocation_auth.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import db from './src/database/models/index.js';
|
||||||
|
import { RELOCATION_STAGES, ROLES } from './src/common/config/constants.js';
|
||||||
|
import { RelocationWorkflowService } from './src/services/RelocationWorkflowService.js';
|
||||||
|
|
||||||
|
const { RelocationRequest, User } = db;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
console.log('--- Relocation Authorization Verification ---');
|
||||||
|
|
||||||
|
// 1. Find a test request
|
||||||
|
const request = await RelocationRequest.findOne({
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
console.log('No relocation request found to test.');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Testing Request: ${request.requestId}, Current Stage: ${request.currentStage}`);
|
||||||
|
|
||||||
|
// 2. Find users with different roles
|
||||||
|
const asmUser = await User.findOne({ where: { roleCode: ROLES.ASM, status: 'active' } });
|
||||||
|
const rbmUser = await User.findOne({ where: { roleCode: ROLES.RBM, status: 'active' } });
|
||||||
|
const dealerUser = await User.findOne({ where: { roleCode: ROLES.DEALER, status: 'active' } });
|
||||||
|
const superAdmin = await User.findOne({ where: { roleCode: ROLES.SUPER_ADMIN, status: 'active' } });
|
||||||
|
|
||||||
|
// 3. Test ASM approval on ASM_REVIEW stage
|
||||||
|
if (request.currentStage === RELOCATION_STAGES.ASM_REVIEW) {
|
||||||
|
console.log('Testing ASM_REVIEW stage:');
|
||||||
|
|
||||||
|
const canASM = await RelocationWorkflowService.canUserAction(request, asmUser);
|
||||||
|
console.log(`- ASM can action: ${canASM} (Expected: true)`);
|
||||||
|
|
||||||
|
const canRBM = await RelocationWorkflowService.canUserAction(request, rbmUser);
|
||||||
|
console.log(`- RBM can action: ${canRBM} (Expected: false)`);
|
||||||
|
|
||||||
|
const canDealer = await RelocationWorkflowService.canUserAction(request, dealerUser);
|
||||||
|
console.log(`- Dealer can action: ${canDealer} (Expected: false)`);
|
||||||
|
|
||||||
|
const canSuperAdmin = await RelocationWorkflowService.canUserAction(request, superAdmin);
|
||||||
|
console.log(`- Super Admin can action: ${canSuperAdmin} (Expected: true)`);
|
||||||
|
} else {
|
||||||
|
console.log(`Request is in ${request.currentStage}. Please create a new request to test ASM_REVIEW.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
102
verify_relocation_workflow.ts
Normal file
102
verify_relocation_workflow.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import db from './src/database/models/index.js';
|
||||||
|
import { RELOCATION_STAGES } from './src/common/config/constants.js';
|
||||||
|
|
||||||
|
const { RelocationRequest, Outlet, User, District, Region, Zone } = db;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
console.log('--- Relocation Workflow Verification ---');
|
||||||
|
|
||||||
|
// 1. Find a test outlet
|
||||||
|
const outlet = await Outlet.findOne({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: District,
|
||||||
|
as: 'district',
|
||||||
|
include: [
|
||||||
|
{ model: Region, as: 'region' },
|
||||||
|
{ model: Zone, as: 'zone' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!outlet || !outlet.district) {
|
||||||
|
console.log('No outlet with district found to test.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Testing with Outlet: ${outlet.code}, District: ${outlet.district.name}`);
|
||||||
|
|
||||||
|
// Create a mock user if needed (the dealer)
|
||||||
|
const dealer = await User.findOne({ where: { roleCode: 'Dealer' } });
|
||||||
|
if (!dealer) {
|
||||||
|
console.log('No dealer found for testing.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { submitRequest } = await import('./src/modules/self-service/relocation.controller.js');
|
||||||
|
|
||||||
|
// We'll mock the Request/Response if we wanted to call the controller directly,
|
||||||
|
// but here we just want to verify if the evaluator logic would work.
|
||||||
|
|
||||||
|
// Let's just manually run the assignRelocationEvaluators logic (internal)
|
||||||
|
// Actually, I'll just check if the Controller's getRequestById would return participants correctly.
|
||||||
|
|
||||||
|
// Let's create a relocation request directly to test
|
||||||
|
const request = await RelocationRequest.create({
|
||||||
|
requestId: 'TEST-REL-' + Date.now(),
|
||||||
|
outletId: outlet.id,
|
||||||
|
dealerId: dealer.id,
|
||||||
|
relocationType: 'Within City',
|
||||||
|
newAddress: 'New Test Address',
|
||||||
|
newCity: 'Test City',
|
||||||
|
newState: 'Test State',
|
||||||
|
reason: 'Testing workflow assignment',
|
||||||
|
currentStage: RELOCATION_STAGES.ASM_REVIEW,
|
||||||
|
status: 'Pending ASM Review',
|
||||||
|
progressPercentage: 10,
|
||||||
|
documents: [],
|
||||||
|
timeline: []
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Created Test Request: ${request.requestId}`);
|
||||||
|
|
||||||
|
// Now call the logic that calculates participants (similar to getRequestById)
|
||||||
|
// We'll just look at the DB for now to see if DD Head and NBH (dual) would be assigned.
|
||||||
|
|
||||||
|
// Check DD Head
|
||||||
|
const ddHead = await User.findOne({ where: { roleCode: 'DD Head', status: 'active' } });
|
||||||
|
console.log(`DD Head found in DB: ${ddHead ? ddHead.fullName : 'NO'}`);
|
||||||
|
|
||||||
|
// Check NBH
|
||||||
|
const nbh = await User.findOne({ where: { roleCode: 'NBH', status: 'active' } });
|
||||||
|
console.log(`NBH found in DB: ${nbh ? nbh.fullName : 'NO'}`);
|
||||||
|
|
||||||
|
// Verify Evaluator Assignment Logic (Re-running a piece of it)
|
||||||
|
const evaluators = [];
|
||||||
|
evaluators.push({ id: outlet.district.asmId, role: 'ASM', stage: RELOCATION_STAGES.ASM_REVIEW });
|
||||||
|
evaluators.push({ id: ddHead?.id, role: 'DD Head', stage: RELOCATION_STAGES.DD_HEAD_APPROVAL });
|
||||||
|
evaluators.push({ id: nbh?.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_APPROVAL });
|
||||||
|
evaluators.push({ id: nbh?.id, role: 'NBH', stage: RELOCATION_STAGES.NBH_CLEARANCE });
|
||||||
|
|
||||||
|
console.log('Expected Evaluators for this request:');
|
||||||
|
evaluators.forEach(e => console.log(`- Role: ${e.role}, Stage: ${e.stage}, ID: ${e.id}`));
|
||||||
|
|
||||||
|
// Check for success: All must have IDs
|
||||||
|
const missing = evaluators.filter(e => !e.id);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.warn('WARNING: Missing some evaluators in the DB. Ensure they are seeded!');
|
||||||
|
missing.forEach(m => console.log(` Missing: ${m.role}`));
|
||||||
|
} else {
|
||||||
|
console.log('SUCCESS: All hierarchy and national evaluators identified.');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
Loading…
Reference in New Issue
Block a user