316 lines
9.4 KiB
TypeScript
316 lines
9.4 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import { TatAlert } from '@models/TatAlert';
|
|
import { ApprovalLevel } from '@models/ApprovalLevel';
|
|
import { User } from '@models/User';
|
|
import { WorkflowRequest } from '@models/WorkflowRequest';
|
|
import logger from '@utils/logger';
|
|
import { sequelize } from '@config/database';
|
|
import { QueryTypes } from 'sequelize';
|
|
import { activityService } from '@services/activity.service';
|
|
import { getRequestMetadata } from '@utils/requestUtils';
|
|
import type { AuthenticatedRequest } from '../types/express';
|
|
|
|
/**
|
|
* Get TAT alerts for a specific request
|
|
*/
|
|
export const getTatAlertsByRequest = async (req: Request, res: Response) => {
|
|
try {
|
|
const { requestId } = req.params;
|
|
|
|
const alerts = await TatAlert.findAll({
|
|
where: { requestId },
|
|
include: [
|
|
{
|
|
model: ApprovalLevel,
|
|
as: 'level',
|
|
attributes: ['levelNumber', 'levelName', 'approverName', 'status']
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'approver',
|
|
attributes: ['userId', 'displayName', 'email', 'department']
|
|
}
|
|
],
|
|
order: [['alertSentAt', 'ASC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: alerts
|
|
});
|
|
} catch (error) {
|
|
logger.error('[TAT Controller] Error fetching TAT alerts:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch TAT alerts'
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get TAT alerts for a specific approval level
|
|
*/
|
|
export const getTatAlertsByLevel = async (req: Request, res: Response) => {
|
|
try {
|
|
const { levelId } = req.params;
|
|
|
|
const alerts = await TatAlert.findAll({
|
|
where: { levelId },
|
|
order: [['alertSentAt', 'ASC']]
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: alerts
|
|
});
|
|
} catch (error) {
|
|
logger.error('[TAT Controller] Error fetching TAT alerts by level:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch TAT alerts'
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get TAT compliance summary
|
|
*/
|
|
export const getTatComplianceSummary = async (req: Request, res: Response) => {
|
|
try {
|
|
const { startDate, endDate } = req.query;
|
|
|
|
let dateFilter = '';
|
|
if (startDate && endDate) {
|
|
dateFilter = `AND alert_sent_at BETWEEN '${startDate}' AND '${endDate}'`;
|
|
}
|
|
|
|
const summary = await sequelize.query(`
|
|
SELECT
|
|
COUNT(*) as total_alerts,
|
|
COUNT(CASE WHEN alert_type = 'TAT_50' THEN 1 END) as alerts_50,
|
|
COUNT(CASE WHEN alert_type = 'TAT_75' THEN 1 END) as alerts_75,
|
|
COUNT(CASE WHEN alert_type = 'TAT_100' THEN 1 END) as breaches,
|
|
COUNT(CASE WHEN was_completed_on_time = true THEN 1 END) as completed_on_time,
|
|
COUNT(CASE WHEN was_completed_on_time = false THEN 1 END) as completed_late,
|
|
ROUND(
|
|
COUNT(CASE WHEN was_completed_on_time = true THEN 1 END) * 100.0 /
|
|
NULLIF(COUNT(CASE WHEN was_completed_on_time IS NOT NULL THEN 1 END), 0),
|
|
2
|
|
) as compliance_percentage
|
|
FROM tat_alerts
|
|
WHERE 1=1 ${dateFilter}
|
|
`, { type: QueryTypes.SELECT });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: summary[0] || {}
|
|
});
|
|
} catch (error) {
|
|
logger.error('[TAT Controller] Error fetching TAT compliance summary:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch TAT compliance summary'
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get TAT breach report
|
|
*/
|
|
export const getTatBreachReport = async (req: Request, res: Response) => {
|
|
try {
|
|
const breaches = await sequelize.query(`
|
|
SELECT
|
|
ta.alert_id,
|
|
ta.request_id,
|
|
w.request_number,
|
|
w.title as request_title,
|
|
w.priority,
|
|
al.level_number,
|
|
al.approver_name,
|
|
ta.tat_hours_allocated,
|
|
ta.tat_hours_elapsed,
|
|
ta.alert_sent_at,
|
|
ta.completion_time,
|
|
ta.was_completed_on_time,
|
|
CASE
|
|
WHEN ta.completion_time IS NULL THEN 'Still Pending'
|
|
WHEN ta.was_completed_on_time = false THEN 'Completed Late'
|
|
ELSE 'Completed On Time'
|
|
END as completion_status
|
|
FROM tat_alerts ta
|
|
JOIN workflow_requests w ON ta.request_id = w.request_id
|
|
JOIN approval_levels al ON ta.level_id = al.level_id
|
|
WHERE ta.is_breached = true
|
|
ORDER BY ta.alert_sent_at DESC
|
|
LIMIT 100
|
|
`, { type: QueryTypes.SELECT });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: breaches
|
|
});
|
|
} catch (error) {
|
|
logger.error('[TAT Controller] Error fetching TAT breach report:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch TAT breach report'
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update breach reason for a TAT alert
|
|
*/
|
|
export const updateBreachReason = async (req: Request, res: Response) => {
|
|
try {
|
|
const { levelId } = req.params;
|
|
const { breachReason } = req.body;
|
|
const userId = (req as AuthenticatedRequest).user?.userId;
|
|
const requestMeta = getRequestMetadata(req);
|
|
|
|
if (!userId) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
error: 'Unauthorized'
|
|
});
|
|
}
|
|
|
|
if (!breachReason || typeof breachReason !== 'string' || breachReason.trim().length === 0) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Breach reason is required'
|
|
});
|
|
}
|
|
|
|
// Get the approval level to verify permissions
|
|
const level = await ApprovalLevel.findByPk(levelId);
|
|
if (!level) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Approval level not found'
|
|
});
|
|
}
|
|
|
|
// Get user to check role
|
|
const user = await User.findByPk(userId);
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'User not found'
|
|
});
|
|
}
|
|
|
|
const userRole = (user as any).role;
|
|
const approverId = (level as any).approverId;
|
|
|
|
// Check permissions: ADMIN, MANAGEMENT, or the approver
|
|
const hasPermission =
|
|
userRole === 'ADMIN' ||
|
|
userRole === 'MANAGEMENT' ||
|
|
approverId === userId;
|
|
|
|
if (!hasPermission) {
|
|
return res.status(403).json({
|
|
success: false,
|
|
error: 'You do not have permission to update breach reason'
|
|
});
|
|
}
|
|
|
|
// Get user details for activity logging
|
|
const userDisplayName = (user as any).displayName || (user as any).email || 'Unknown User';
|
|
const isUpdate = !!(level as any).breachReason; // Check if this is an update or first time
|
|
const levelNumber = (level as any).levelNumber;
|
|
const approverName = (level as any).approverName || 'Unknown Approver';
|
|
|
|
// Update breach reason directly in approval_levels table
|
|
await level.update({
|
|
breachReason: breachReason.trim()
|
|
});
|
|
|
|
// Reload to get updated data
|
|
await level.reload();
|
|
|
|
// Log activity for the request
|
|
const userRoleLabel = userRole === 'ADMIN' ? 'Admin' : userRole === 'MANAGEMENT' ? 'Management' : 'Approver';
|
|
await activityService.log({
|
|
requestId: level.requestId,
|
|
type: 'comment', // Using comment type for breach reason entry
|
|
user: {
|
|
userId: userId,
|
|
name: userDisplayName,
|
|
email: (user as any).email
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
action: isUpdate ? 'Updated TAT breach reason' : 'Added TAT breach reason',
|
|
details: `${userDisplayName} (${userRoleLabel}) ${isUpdate ? 'updated' : 'added'} TAT breach reason for ${approverName} (Level ${levelNumber}): "${breachReason.trim()}"`,
|
|
metadata: {
|
|
levelId: level.levelId,
|
|
levelNumber: levelNumber,
|
|
approverName: approverName,
|
|
breachReason: breachReason.trim(),
|
|
updatedByRole: userRole
|
|
},
|
|
ipAddress: requestMeta.ipAddress,
|
|
userAgent: requestMeta.userAgent
|
|
});
|
|
|
|
logger.info(`[TAT Controller] Breach reason ${isUpdate ? 'updated' : 'added'} for level ${levelId} by user ${userId} (${userRole})`);
|
|
|
|
return res.json({
|
|
success: true,
|
|
message: `Breach reason ${isUpdate ? 'updated' : 'added'} successfully`,
|
|
data: {
|
|
levelId: level.levelId,
|
|
breachReason: breachReason.trim()
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('[TAT Controller] Error updating breach reason:', error);
|
|
return res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to update breach reason'
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get approver TAT performance
|
|
*/
|
|
export const getApproverTatPerformance = async (req: Request, res: Response) => {
|
|
try {
|
|
const { approverId } = req.params;
|
|
|
|
const performance = await sequelize.query(`
|
|
SELECT
|
|
COUNT(DISTINCT ta.level_id) as total_approvals,
|
|
COUNT(CASE WHEN ta.alert_type = 'TAT_50' THEN 1 END) as alerts_50_received,
|
|
COUNT(CASE WHEN ta.alert_type = 'TAT_75' THEN 1 END) as alerts_75_received,
|
|
COUNT(CASE WHEN ta.is_breached = true THEN 1 END) as breaches,
|
|
AVG(ta.tat_hours_elapsed) as avg_hours_taken,
|
|
ROUND(
|
|
COUNT(CASE WHEN ta.was_completed_on_time = true THEN 1 END) * 100.0 /
|
|
NULLIF(COUNT(CASE WHEN ta.was_completed_on_time IS NOT NULL THEN 1 END), 0),
|
|
2
|
|
) as compliance_rate
|
|
FROM tat_alerts ta
|
|
WHERE ta.approver_id = :approverId
|
|
`, {
|
|
replacements: { approverId },
|
|
type: QueryTypes.SELECT
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: performance[0] || {}
|
|
});
|
|
} catch (error) {
|
|
logger.error('[TAT Controller] Error fetching approver TAT performance:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Failed to fetch approver TAT performance'
|
|
});
|
|
}
|
|
};
|
|
|