const db = require('../models'); const logger = require('../utils/logger'); const { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } = require('../config/constants'); const { Op } = require('sequelize'); // Generate unique resignation ID const generateResignationId = async () => { const count = await db.Resignation.count(); return `RES-${String(count + 1).padStart(3, '0')}`; }; // Calculate progress percentage based on stage const calculateProgress = (stage) => { const stageProgress = { [RESIGNATION_STAGES.ASM]: 15, [RESIGNATION_STAGES.RBM]: 30, [RESIGNATION_STAGES.ZBH]: 45, [RESIGNATION_STAGES.NBH]: 60, [RESIGNATION_STAGES.DD_ADMIN]: 70, [RESIGNATION_STAGES.LEGAL]: 80, [RESIGNATION_STAGES.FINANCE]: 90, [RESIGNATION_STAGES.FNF_INITIATED]: 95, [RESIGNATION_STAGES.COMPLETED]: 100, [RESIGNATION_STAGES.REJECTED]: 0 }; return stageProgress[stage] || 0; }; // Create resignation request (Dealer only) exports.createResignation = async (req, res, next) => { const transaction = await db.sequelize.transaction(); try { const { outletId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo } = req.body; const dealerId = req.user.id; // Verify outlet belongs to dealer const outlet = await db.Outlet.findOne({ where: { id: outletId, dealerId } }); if (!outlet) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Outlet not found or does not belong to you' }); } // Check if outlet already has active resignation const existingResignation = await db.Resignation.findOne({ where: { outletId, status: { [Op.notIn]: ['Completed', 'Rejected'] } } }); if (existingResignation) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'This outlet already has an active resignation request' }); } // Generate resignation ID const resignationId = await generateResignationId(); // Create resignation const resignation = await db.Resignation.create({ resignationId, outletId, dealerId, resignationType, lastOperationalDateSales, lastOperationalDateServices, reason, additionalInfo, currentStage: RESIGNATION_STAGES.ASM, status: 'ASM Review', progressPercentage: 15, timeline: [{ stage: 'Submitted', timestamp: new Date(), user: req.user.name, action: 'Resignation request submitted' }] }, { transaction }); // Update outlet status await outlet.update({ status: 'Pending Resignation' }, { transaction }); // Create audit log await db.AuditLog.create({ userId: req.user.id, userName: req.user.name, userRole: req.user.role, action: AUDIT_ACTIONS.CREATED, entityType: 'resignation', entityId: resignation.id, changes: { created: resignation.toJSON() } }, { transaction }); await transaction.commit(); logger.info(`Resignation request created: ${resignationId} by dealer: ${req.user.email}`); // TODO: Send email notification to ASM/DD Admin res.status(201).json({ success: true, message: 'Resignation request submitted successfully', resignationId: resignation.resignationId, resignation: resignation.toJSON() }); } catch (error) { await transaction.rollback(); logger.error('Error creating resignation:', error); next(error); } }; // Get resignations list (role-based filtering) exports.getResignations = async (req, res, next) => { try { const { status, region, zone, page = 1, limit = 10 } = req.query; const offset = (page - 1) * limit; // Build where clause based on user role let where = {}; if (req.user.role === ROLES.DEALER) { // Dealers see only their resignations where.dealerId = req.user.id; } else if (req.user.region && ![ROLES.NBH, ROLES.DD_HEAD, ROLES.DD_LEAD, ROLES.SUPER_ADMIN].includes(req.user.role)) { // Regional users see resignations in their region where['$outlet.region$'] = req.user.region; } if (status) { where.status = status; } // Get resignations const { count, rows: resignations } = await db.Resignation.findAndCountAll({ where, include: [ { model: db.Outlet, as: 'outlet', attributes: ['id', 'code', 'name', 'type', 'city', 'state', 'region', 'zone'] }, { model: db.User, as: 'dealer', attributes: ['id', 'name', 'email', 'phone'] } ], order: [['submittedOn', 'DESC']], limit: parseInt(limit), offset: parseInt(offset) }); res.json({ success: true, resignations, pagination: { total: count, page: parseInt(page), pages: Math.ceil(count / limit), limit: parseInt(limit) } }); } catch (error) { logger.error('Error fetching resignations:', error); next(error); } }; // Get resignation details exports.getResignationById = async (req, res, next) => { try { const { id } = req.params; const resignation = await db.Resignation.findOne({ where: { id }, include: [ { model: db.Outlet, as: 'outlet', include: [ { model: db.User, as: 'dealer', attributes: ['id', 'name', 'email', 'phone'] } ] }, { model: db.Worknote, as: 'worknotes', order: [['timestamp', 'DESC']] } ] }); if (!resignation) { return res.status(404).json({ success: false, message: 'Resignation not found' }); } // Check access permissions if (req.user.role === ROLES.DEALER && resignation.dealerId !== req.user.id) { return res.status(403).json({ success: false, message: 'Access denied' }); } res.json({ success: true, resignation }); } catch (error) { logger.error('Error fetching resignation details:', error); next(error); } }; // Approve resignation (move to next stage) exports.approveResignation = async (req, res, next) => { const transaction = await db.sequelize.transaction(); try { const { id } = req.params; const { remarks } = req.body; const resignation = await db.Resignation.findByPk(id, { include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } // Determine next stage based on current stage const stageFlow = { [RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM, [RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH, [RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.NBH, [RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN, [RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL, [RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.FINANCE, [RESIGNATION_STAGES.FINANCE]: RESIGNATION_STAGES.FNF_INITIATED, [RESIGNATION_STAGES.FNF_INITIATED]: RESIGNATION_STAGES.COMPLETED }; const nextStage = stageFlow[resignation.currentStage]; if (!nextStage) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Cannot approve from current stage' }); } // Update resignation const timeline = [...resignation.timeline, { stage: nextStage, timestamp: new Date(), user: req.user.name, action: 'Approved', remarks }]; await resignation.update({ currentStage: nextStage, status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`, progressPercentage: calculateProgress(nextStage), timeline }, { transaction }); // If completed, update outlet status if (nextStage === RESIGNATION_STAGES.COMPLETED) { await resignation.outlet.update({ status: 'Closed' }, { transaction }); } // Create audit log await db.AuditLog.create({ userId: req.user.id, userName: req.user.name, userRole: req.user.role, action: AUDIT_ACTIONS.APPROVED, entityType: 'resignation', entityId: resignation.id, changes: { from: resignation.currentStage, to: nextStage, remarks } }, { transaction }); await transaction.commit(); logger.info(`Resignation ${resignation.resignationId} approved to ${nextStage} by ${req.user.email}`); // TODO: Send email notification to next approver res.json({ success: true, message: 'Resignation approved successfully', nextStage, resignation }); } catch (error) { await transaction.rollback(); logger.error('Error approving resignation:', error); next(error); } }; // Reject resignation exports.rejectResignation = async (req, res, next) => { const transaction = await db.sequelize.transaction(); try { const { id } = req.params; const { reason } = req.body; if (!reason) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'Rejection reason is required' }); } const resignation = await db.Resignation.findByPk(id, { include: [{ model: db.Outlet, as: 'outlet' }] }); if (!resignation) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Resignation not found' }); } // Update resignation const timeline = [...resignation.timeline, { stage: 'Rejected', timestamp: new Date(), user: req.user.name, action: 'Rejected', reason }]; await resignation.update({ currentStage: RESIGNATION_STAGES.REJECTED, status: 'Rejected', progressPercentage: 0, rejectionReason: reason, timeline }, { transaction }); // Update outlet status back to Active await resignation.outlet.update({ status: 'Active' }, { transaction }); // Create audit log await db.AuditLog.create({ userId: req.user.id, userName: req.user.name, userRole: req.user.role, action: AUDIT_ACTIONS.REJECTED, entityType: 'resignation', entityId: resignation.id, changes: { reason } }, { transaction }); await transaction.commit(); logger.info(`Resignation ${resignation.resignationId} rejected by ${req.user.email}`); // TODO: Send email notification to dealer res.json({ success: true, message: 'Resignation rejected', resignation }); } catch (error) { await transaction.rollback(); logger.error('Error rejecting resignation:', error); next(error); } }; module.exports = exports;