diff --git a/src/modules/self-service/resignation.controller.ts b/src/modules/self-service/resignation.controller.ts index 81e1cfc..77bc20c 100644 --- a/src/modules/self-service/resignation.controller.ts +++ b/src/modules/self-service/resignation.controller.ts @@ -22,6 +22,7 @@ import { getResignationStatusForStage, normalizeClearanceStatus } from '../../co import { resolveEntityUuidByType } from '../../common/utils/requestResolver.js'; import { OFFBOARDING_ACTIONS } from '../../common/config/constants.js'; import { validateOffboardingAction, getPreviousStage } from '../../common/utils/offboardingWorkflow.utils.js'; +import { writeWorkflowActivityWorknote } from '../../common/utils/workflowWorknote.js'; // Removed generateResignationId and moved to NomenclatureService const resolveResignationUuid = async (id: string) => { @@ -44,7 +45,7 @@ export const createResignation = async (req: AuthRequest, res: Response, next: N } const existingResignation = await db.Resignation.findOne({ - where: { outletId, status: { [Op.notIn]: ['Completed', 'Rejected'] } } + where: { outletId, status: { [Op.notIn]: ['Completed', 'Rejected', 'Withdrawn', 'Revoked'] } } }); if (existingResignation) { await transaction.rollback(); @@ -400,6 +401,66 @@ export const approveResignation = async (req: AuthRequest, res: Response, next: const sourceStage = resignation.currentStage; + // JOINT APPROVAL LOGIC FOR RBM STAGE + if (sourceStage === RESIGNATION_STAGES.RBM) { + // Log the current user's approval in audit + await db.ResignationAudit.create({ + userId: req.user.id, + resignationId: resignation.id, + action: 'PARTIAL_APPROVE', + remarks: `Partial approval by ${req.user.roleCode}${remarks ? ': ' + remarks : ''}`, + details: { roleCode: req.user.roleCode, stage: sourceStage } + }, { transaction }); + + // Ensure worknote is added for this partial approval + if (remarks) { + await writeWorkflowActivityWorknote({ + requestId: resignation.id, + requestType: 'resignation', + userId: req.user.id, + noteText: `Approved: ${remarks}`, + noteType: 'internal' + }); + } + + // Check if both RBM and DD_ZM have approved + const requiredRoles = [ROLES.RBM, ROLES.DD_ZM]; + const partialLogs = await db.ResignationAudit.findAll({ + where: { + resignationId: resignation.id, + action: 'PARTIAL_APPROVE' + }, + transaction + }); + + const approvedRoles = new Set( + partialLogs.map((log: any) => log.details?.roleCode) + ); + + const hasAllRequiredApprovals = requiredRoles.every(role => approvedRoles.has(role)); + + if (!hasAllRequiredApprovals) { + // Append to timeline directly without transitioning the stage + const timelineEntry = { + stage: sourceStage, + targetStage: nextStage, + timestamp: new Date(), + user: req.user.fullName, + action: `Approved by ${req.user.roleCode}`, + remarks: remarks || '' + }; + const updatedTimeline = [...(resignation.timeline || []), timelineEntry]; + await resignation.update({ timeline: updatedTimeline }, { transaction }); + + await transaction.commit(); + return res.json({ + success: true, + message: 'Approval recorded. Waiting for the other required approver (RBM or DD-ZM).', + resignation + }); + } + } + // Transition via Workflow Service await ResignationWorkflowService.transitionResignation(resignation, nextStage, req.user.id, { remarks, diff --git a/src/services/ResignationWorkflowService.ts b/src/services/ResignationWorkflowService.ts index 8082d33..7aa0d26 100644 --- a/src/services/ResignationWorkflowService.ts +++ b/src/services/ResignationWorkflowService.ts @@ -148,9 +148,9 @@ export class ResignationWorkflowService { if (!user) return false; if (user.roleCode === ROLES.SUPER_ADMIN) return true; - const stageToRole: Record = { + const stageToRole: Record = { [RESIGNATION_STAGES.ASM]: ROLES.ASM, - [RESIGNATION_STAGES.RBM]: ROLES.RBM, + [RESIGNATION_STAGES.RBM]: [ROLES.RBM, ROLES.DD_ZM], [RESIGNATION_STAGES.ZBH]: ROLES.ZBH, [RESIGNATION_STAGES.DD_LEAD]: ROLES.DD_LEAD, [RESIGNATION_STAGES.NBH]: ROLES.NBH, @@ -160,6 +160,9 @@ export class ResignationWorkflowService { }; const requiredRole = stageToRole[resignation.currentStage]; + if (Array.isArray(requiredRole)) { + return requiredRole.includes(user.roleCode); + } return user.roleCode === requiredRole; } } diff --git a/trigger-resignation.js b/trigger-resignation.js index 1e335aa..331618f 100644 --- a/trigger-resignation.js +++ b/trigger-resignation.js @@ -14,6 +14,7 @@ const EMAILS = { DEALER: args.dealerEmail, ASM: 'abhishek@royalenfield.com', RBM: 'manish@royalenfield.com', + DD_ZM: 'piyush@royalenfield.com', ZBH: 'manav@royalenfield.com', DD_LEAD: 'jaya@royalenfield.com', NBH: 'yashwin@royalenfield.com', @@ -158,13 +159,14 @@ async function run() { await delay(); const approvals = [ - { name: 'ASM', email: EMAILS.ASM, remarks: 'Verified physical assets and dealer intent. Recommended for resignation.' }, - { name: 'RBM', email: EMAILS.RBM, remarks: 'No active sales pipeline issues in the territory. Transition plan discussed.' }, - { name: 'ZBH', email: EMAILS.ZBH, remarks: 'Zone-level resource reallocation planned. Approved.' }, - { name: 'DD Lead', email: EMAILS.DD_LEAD, remarks: 'Dealer development criteria satisfied for exit.' }, - { name: 'NBH', email: EMAILS.NBH, remarks: 'HQ clearance granted. Proceed with F&F initiation.' }, - { name: 'DD Admin', email: EMAILS.DD_ADMIN, remarks: 'Administrative checks complete. Initiating termination of commercial contract.' }, - { name: 'Legal Admin', email: EMAILS.LEGAL, remarks: 'Legal documentation verified. No active litigation found.' } + { stage: 'ASM', name: 'ASM', email: EMAILS.ASM, remarks: 'Verified physical assets and dealer intent. Recommended for resignation.' }, + { stage: 'RBM', name: 'RBM', email: EMAILS.RBM, remarks: 'No active sales pipeline issues in the territory. Transition plan discussed.' }, + { stage: 'RBM', name: 'DD-ZM', email: EMAILS.DD_ZM, remarks: 'Joint approval evaluated and verified by DD-ZM.' }, + { stage: 'ZBH', name: 'ZBH', email: EMAILS.ZBH, remarks: 'Zone-level resource reallocation planned. Approved.' }, + { stage: 'DD Lead', name: 'DD Lead', email: EMAILS.DD_LEAD, remarks: 'Dealer development criteria satisfied for exit.' }, + { stage: 'NBH', name: 'NBH', email: EMAILS.NBH, remarks: 'HQ clearance granted. Proceed with F&F initiation.' }, + { stage: 'DD Admin', name: 'DD Admin', email: EMAILS.DD_ADMIN, remarks: 'Administrative checks complete. Initiating termination of commercial contract.' }, + { stage: 'Legal', name: 'Legal Admin', email: EMAILS.LEGAL, remarks: 'Legal documentation verified. No active litigation found.' } ]; // Fetch resignation data to determine current stage for skipping @@ -174,13 +176,17 @@ async function run() { console.log(`Current Stage: ${currentStage}`); const stageOrder = [ - 'ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal Admin', 'F&F Initiated', 'Completed' + 'ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed' ]; - const startIndex = stageOrder.indexOf(currentStage) === -1 ? 0 : stageOrder.indexOf(currentStage); + let startStageIndex = stageOrder.indexOf(currentStage) === -1 ? 0 : stageOrder.indexOf(currentStage); + const startingStage = stageOrder[startStageIndex]; + + let startApproveIndex = approvals.findIndex(a => a.stage === startingStage); + if (startApproveIndex === -1) startApproveIndex = 0; let currentStep = 2; - for (let i = startIndex; i < approvals.length; i++) { + for (let i = startApproveIndex; i < approvals.length; i++) { const actor = approvals[i]; log(currentStep, `${actor.name} (${actor.email}) approving...`); const token = await login(actor.email);