418 lines
11 KiB
JavaScript
418 lines
11 KiB
JavaScript
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;
|