488 lines
16 KiB
TypeScript
488 lines
16 KiB
TypeScript
import { Response, NextFunction } from 'express';
|
|
import db from '../../database/models/index.js';
|
|
import logger from '../../common/utils/logger.js';
|
|
import { RESIGNATION_STAGES, AUDIT_ACTIONS, ROLES } from '../../common/config/constants.js';
|
|
import { Op, Transaction } from 'sequelize';
|
|
import { AuthRequest } from '../../types/express.types.js';
|
|
import ExternalMocksService from '../../common/utils/externalMocks.service.js';
|
|
|
|
// Generate unique resignation ID
|
|
const generateResignationId = async (): Promise<string> => {
|
|
const count = await db.Resignation.count();
|
|
return `RES-${String(count + 1).padStart(3, '0')}`;
|
|
};
|
|
|
|
// Calculate progress percentage based on stage
|
|
const calculateProgress = (stage: string): number => {
|
|
const stageProgress: Record<string, number> = {
|
|
[RESIGNATION_STAGES.ASM]: 15,
|
|
[RESIGNATION_STAGES.RBM]: 30,
|
|
[RESIGNATION_STAGES.ZBH]: 40,
|
|
[RESIGNATION_STAGES.DD_LEAD]: 50,
|
|
[RESIGNATION_STAGES.NBH]: 60,
|
|
[RESIGNATION_STAGES.DD_ADMIN]: 65,
|
|
[RESIGNATION_STAGES.LEGAL]: 70,
|
|
[RESIGNATION_STAGES.SPARES_CLEARANCE]: 75,
|
|
[RESIGNATION_STAGES.SERVICE_CLEARANCE]: 80,
|
|
[RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: 85,
|
|
[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)
|
|
export const createResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
|
|
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,
|
|
submittedOn: new Date(),
|
|
documents: [],
|
|
timeline: [{
|
|
stage: 'Submitted',
|
|
timestamp: new Date(),
|
|
user: req.user.fullName,
|
|
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,
|
|
action: AUDIT_ACTIONS.CREATED,
|
|
entityType: 'resignation',
|
|
entityId: resignation.id
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
|
|
logger.info(`Resignation request created: ${resignationId} by dealer: ${req.user.email}`);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Resignation request submitted successfully',
|
|
resignationId: resignation.resignationId,
|
|
resignation: resignation.toJSON()
|
|
});
|
|
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error creating resignation:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Get resignations list (role-based filtering)
|
|
export const getResignations = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
const { status, page = '1', limit = '10' } = req.query as { status?: string, page?: string, limit?: string };
|
|
const offset = (parseInt(page) - 1) * parseInt(limit);
|
|
|
|
// Build where clause based on user role
|
|
let where: any = {};
|
|
|
|
if (req.user.role === ROLES.DEALER) {
|
|
where.dealerId = req.user.id;
|
|
}
|
|
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
// Get resignations
|
|
const { count, rows: resignations } = await db.Resignation.findAndCountAll({
|
|
where,
|
|
include: [
|
|
{
|
|
model: db.Outlet,
|
|
as: 'outlet'
|
|
},
|
|
{
|
|
model: db.User,
|
|
as: 'dealer',
|
|
attributes: ['id', 'fullName', 'email', 'mobileNumber']
|
|
}
|
|
],
|
|
order: [['createdAt', 'DESC']],
|
|
limit: parseInt(limit),
|
|
offset: offset
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
resignations,
|
|
pagination: {
|
|
total: count,
|
|
page: parseInt(page),
|
|
pages: Math.ceil(count / parseInt(limit)),
|
|
limit: parseInt(limit)
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Error fetching resignations:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Get resignation details
|
|
export const getResignationById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
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', 'fullName', 'email', 'mobileNumber']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
model: db.Worknote,
|
|
as: 'worknotes'
|
|
}
|
|
],
|
|
order: [[{ model: db.Worknote, as: 'worknotes' }, 'createdAt', '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)
|
|
export const approveResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
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: Record<string, string> = {
|
|
[RESIGNATION_STAGES.ASM]: RESIGNATION_STAGES.RBM,
|
|
[RESIGNATION_STAGES.RBM]: RESIGNATION_STAGES.ZBH,
|
|
[RESIGNATION_STAGES.ZBH]: RESIGNATION_STAGES.DD_LEAD,
|
|
[RESIGNATION_STAGES.DD_LEAD]: RESIGNATION_STAGES.NBH,
|
|
[RESIGNATION_STAGES.NBH]: RESIGNATION_STAGES.DD_ADMIN,
|
|
[RESIGNATION_STAGES.DD_ADMIN]: RESIGNATION_STAGES.LEGAL,
|
|
[RESIGNATION_STAGES.LEGAL]: RESIGNATION_STAGES.SPARES_CLEARANCE,
|
|
[RESIGNATION_STAGES.SPARES_CLEARANCE]: RESIGNATION_STAGES.SERVICE_CLEARANCE,
|
|
[RESIGNATION_STAGES.SERVICE_CLEARANCE]: RESIGNATION_STAGES.ACCOUNTS_CLEARANCE,
|
|
[RESIGNATION_STAGES.ACCOUNTS_CLEARANCE]: 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.fullName,
|
|
action: 'Approved',
|
|
remarks
|
|
}];
|
|
|
|
await resignation.update({
|
|
currentStage: nextStage as any,
|
|
status: nextStage === RESIGNATION_STAGES.COMPLETED ? 'Completed' : `${nextStage} Review`,
|
|
progressPercentage: calculateProgress(nextStage),
|
|
timeline
|
|
}, { transaction });
|
|
|
|
// If completed, update outlet status and sync with SAP
|
|
if (nextStage === RESIGNATION_STAGES.COMPLETED) {
|
|
await (resignation as any).outlet.update({
|
|
status: 'Closed'
|
|
}, { transaction });
|
|
|
|
// Trigger Mock SAP Sync
|
|
ExternalMocksService.mockSyncDealerStatusToSap((resignation as any).outlet.code, 'Inactive')
|
|
.catch(err => logger.error('Error syncing resignation completion to SAP:', err));
|
|
}
|
|
|
|
// If F&F Initiated, create F&F record and fetch mock SAP dues
|
|
if (nextStage === RESIGNATION_STAGES.FNF_INITIATED) {
|
|
const sapDues = await ExternalMocksService.mockGetFinancialDuesFromSap((resignation as any).outlet.code);
|
|
|
|
const fnf = await db.FnF.create({
|
|
resignationId: resignation.id,
|
|
outletId: resignation.outletId,
|
|
dealerId: resignation.dealerId,
|
|
status: 'Initiated',
|
|
totalReceivables: sapDues.data.outstandingInvoices,
|
|
totalPayables: sapDues.data.securityDeposit,
|
|
netAmount: sapDues.data.securityDeposit - sapDues.data.outstandingInvoices
|
|
}, { transaction });
|
|
|
|
// Create initial line items from SAP data
|
|
await db.FnFLineItem.bulkCreate([
|
|
{
|
|
fnfId: fnf.id,
|
|
itemType: 'Receivable',
|
|
description: 'Outstanding Invoices from SAP',
|
|
department: 'Finance',
|
|
amount: sapDues.data.outstandingInvoices,
|
|
addedBy: req.user.id
|
|
},
|
|
{
|
|
fnfId: fnf.id,
|
|
itemType: 'Payable',
|
|
description: 'Security Deposit from SAP',
|
|
department: 'Finance',
|
|
amount: sapDues.data.securityDeposit,
|
|
addedBy: req.user.id
|
|
}
|
|
], { transaction });
|
|
|
|
logger.info(`F&F record and mock line items created for resignation: ${resignation.resignationId}`);
|
|
}
|
|
|
|
// Create audit log
|
|
await db.AuditLog.create({
|
|
userId: req.user.id,
|
|
action: AUDIT_ACTIONS.APPROVED,
|
|
entityType: 'resignation',
|
|
entityId: resignation.id
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
|
|
logger.info(`Resignation ${resignation.resignationId} approved to ${nextStage} by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Resignation approved successfully',
|
|
nextStage,
|
|
resignation
|
|
});
|
|
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error approving resignation:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Reject resignation
|
|
export const rejectResignation = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
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.fullName,
|
|
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 as any).outlet.update({
|
|
status: 'Active'
|
|
}, { transaction });
|
|
|
|
// Create audit log
|
|
await db.AuditLog.create({
|
|
userId: req.user.id,
|
|
action: AUDIT_ACTIONS.REJECTED,
|
|
entityType: 'resignation',
|
|
entityId: resignation.id
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
|
|
logger.info(`Resignation ${resignation.resignationId} rejected by ${req.user.email}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Resignation rejected',
|
|
resignation
|
|
});
|
|
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error rejecting resignation:', error);
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
// Update departmental clearance
|
|
export const updateClearance = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
const transaction: Transaction = await db.sequelize.transaction();
|
|
try {
|
|
if (!req.user) throw new Error('Unauthorized');
|
|
const { id } = req.params;
|
|
const { department, cleared, remarks } = req.body;
|
|
|
|
const resignation = await db.Resignation.findByPk(id);
|
|
if (!resignation) {
|
|
await transaction.rollback();
|
|
return res.status(404).json({ success: false, message: 'Resignation not found' });
|
|
}
|
|
|
|
const clearances = { ...resignation.departmentalClearances, [department]: cleared };
|
|
|
|
await resignation.update({
|
|
departmentalClearances: clearances,
|
|
timeline: [...resignation.timeline, {
|
|
stage: resignation.currentStage,
|
|
timestamp: new Date(),
|
|
user: req.user.fullName,
|
|
action: cleared ? `Cleared ${department}` : `Revoked ${department} clearance`,
|
|
remarks
|
|
}]
|
|
}, { transaction });
|
|
|
|
await transaction.commit();
|
|
res.json({ success: true, message: `Clearance updated for ${department}`, resignation });
|
|
} catch (error) {
|
|
if (transaction) await transaction.rollback();
|
|
logger.error('Error updating clearance:', error);
|
|
next(error);
|
|
}
|
|
};
|