template preview modified

This commit is contained in:
laxmanhalaki 2025-12-24 14:20:13 +05:30
parent 90fe2c8e87
commit db02d6eb01
6 changed files with 403 additions and 57 deletions

View File

@ -279,27 +279,114 @@ export class WorkflowController {
} }
} }
const doc = await Document.create({ // Truncate file names if they exceed database column limits (255 chars)
requestId: workflow.requestId, const MAX_FILE_NAME_LENGTH = 255;
uploadedBy: userId, const originalFileName = file.originalname;
fileName: path.basename(file.filename || file.originalname), let truncatedOriginalFileName = originalFileName;
originalFileName: file.originalname,
fileType: extension, if (originalFileName.length > MAX_FILE_NAME_LENGTH) {
fileExtension: extension, // Preserve file extension when truncating
fileSize: file.size, const ext = path.extname(originalFileName);
filePath: gcsFilePath, // Store GCS path or local path const nameWithoutExt = path.basename(originalFileName, ext);
storageUrl: storageUrl, // Store GCS URL or local URL const maxNameLength = MAX_FILE_NAME_LENGTH - ext.length;
mimeType: file.mimetype,
checksum, if (maxNameLength > 0) {
isGoogleDoc: false, truncatedOriginalFileName = nameWithoutExt.substring(0, maxNameLength) + ext;
googleDocUrl: null as any, } else {
category: category || 'OTHER', // If extension itself is too long, just use the extension
version: 1, truncatedOriginalFileName = ext.substring(0, MAX_FILE_NAME_LENGTH);
parentDocumentId: null as any, }
isDeleted: false,
downloadCount: 0, logger.warn('[Workflow] File name truncated to fit database column', {
} as any); originalLength: originalFileName.length,
docs.push(doc); truncatedLength: truncatedOriginalFileName.length,
originalName: originalFileName.substring(0, 100) + '...',
truncatedName: truncatedOriginalFileName,
});
}
// Generate fileName (basename of the generated file name in GCS)
const generatedFileName = path.basename(gcsFilePath);
let truncatedFileName = generatedFileName;
if (generatedFileName.length > MAX_FILE_NAME_LENGTH) {
const ext = path.extname(generatedFileName);
const nameWithoutExt = path.basename(generatedFileName, ext);
const maxNameLength = MAX_FILE_NAME_LENGTH - ext.length;
if (maxNameLength > 0) {
truncatedFileName = nameWithoutExt.substring(0, maxNameLength) + ext;
} else {
truncatedFileName = ext.substring(0, MAX_FILE_NAME_LENGTH);
}
logger.warn('[Workflow] Generated file name truncated', {
originalLength: generatedFileName.length,
truncatedLength: truncatedFileName.length,
});
}
// Check if storageUrl exceeds database column limit (500 chars)
const MAX_STORAGE_URL_LENGTH = 500;
let finalStorageUrl = storageUrl;
if (storageUrl && storageUrl.length > MAX_STORAGE_URL_LENGTH) {
logger.warn('[Workflow] Storage URL exceeds database column limit, storing null', {
originalLength: storageUrl.length,
maxLength: MAX_STORAGE_URL_LENGTH,
urlPrefix: storageUrl.substring(0, 100),
filePath: gcsFilePath,
});
// For signed URLs, store null and generate on-demand later
finalStorageUrl = null as any;
}
logger.info('[Workflow] Creating document record', {
fileName: truncatedOriginalFileName,
filePath: gcsFilePath,
storageUrl: finalStorageUrl ? 'present' : 'null (too long)',
requestId: workflow.requestId
});
try {
const doc = await Document.create({
requestId: workflow.requestId,
uploadedBy: userId,
fileName: truncatedFileName,
originalFileName: truncatedOriginalFileName,
fileType: extension,
fileExtension: extension,
fileSize: file.size,
filePath: gcsFilePath, // Store GCS path or local path
storageUrl: finalStorageUrl, // Store GCS URL or local URL (null if too long)
mimeType: file.mimetype,
checksum,
isGoogleDoc: false,
googleDocUrl: null as any,
category: category || 'OTHER',
version: 1,
parentDocumentId: null as any,
isDeleted: false,
downloadCount: 0,
} as any);
docs.push(doc);
logger.info('[Workflow] Document record created successfully', {
documentId: doc.documentId,
fileName: file.originalname,
});
} catch (docError) {
const docErrorMessage = docError instanceof Error ? docError.message : 'Unknown error';
const docErrorStack = docError instanceof Error ? docError.stack : undefined;
logger.error('[Workflow] Failed to create document record', {
error: docErrorMessage,
stack: docErrorStack,
fileName: file.originalname,
requestId: workflow.requestId,
filePath: gcsFilePath,
storageUrl: storageUrl,
});
// Re-throw to be caught by outer catch block
throw docError;
}
// Log document upload activity // Log document upload activity
const requestMeta = getRequestMetadata(req); const requestMeta = getRequestMetadata(req);
@ -320,6 +407,13 @@ export class WorkflowController {
ResponseHandler.success(res, { requestId: workflow.requestId, documents: docs }, 'Workflow created with documents', 201); ResponseHandler.success(res, { requestId: workflow.requestId, documents: docs }, 'Workflow created with documents', 201);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : undefined;
logger.error('[WorkflowController] createWorkflowMultipart failed', {
error: errorMessage,
stack: errorStack,
userId: req.user?.userId,
filesCount: (req as any).files?.length || 0,
});
ResponseHandler.error(res, 'Failed to create workflow', 400, errorMessage); ResponseHandler.error(res, 'Failed to create workflow', 400, errorMessage);
} }
} }

View File

@ -260,7 +260,7 @@ export class ApprovalService {
activityService.log({ activityService.log({
requestId: level.requestId, requestId: level.requestId,
type: 'ai_conclusion_generated', type: 'ai_conclusion_generated',
user: { userId: 'system', name: 'System' }, user: { userId: null as any, name: 'System' }, // Use null instead of 'system' for UUID field
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
action: 'AI Conclusion Generated', action: 'AI Conclusion Generated',
details: 'AI-powered conclusion remark generated for review by initiator', details: 'AI-powered conclusion remark generated for review by initiator',
@ -291,7 +291,7 @@ export class ApprovalService {
activityService.log({ activityService.log({
requestId: level.requestId, requestId: level.requestId,
type: 'summary_generated', type: 'summary_generated',
user: { userId: 'system', name: 'System' }, user: { userId: null as any, name: 'System' }, // Use null instead of 'system' for UUID field
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
action: 'Summary Auto-Generated', action: 'Summary Auto-Generated',
details: 'Request summary auto-generated after final approval', details: 'Request summary auto-generated after final approval',
@ -339,15 +339,15 @@ export class ApprovalService {
targetUserIds.add(p.userId); // Includes spectators targetUserIds.add(p.userId); // Includes spectators
} }
// Send notification to initiator (with action required) // Send notification to initiator about final approval (triggers email)
const initiatorId = (wf as any).initiatorId; const initiatorId = (wf as any).initiatorId;
await notificationService.sendToUsers([initiatorId], { await notificationService.sendToUsers([initiatorId], {
title: `Request Approved - Closure Pending`, title: `Request Approved - All Approvals Complete`,
body: `Your request "${(wf as any).title}" has been fully approved. Please review and finalize the conclusion remark to close the request.`, body: `Your request "${(wf as any).title}" has been fully approved by all approvers. Please review and finalize the conclusion remark to close the request.`,
requestNumber: (wf as any).requestNumber, requestNumber: (wf as any).requestNumber,
requestId: level.requestId, requestId: level.requestId,
url: `/request/${(wf as any).requestNumber}`, url: `/request/${(wf as any).requestNumber}`,
type: 'approval_pending_closure', type: 'approval',
priority: 'HIGH', priority: 'HIGH',
actionRequired: true actionRequired: true
}); });
@ -425,23 +425,43 @@ export class ApprovalService {
); );
logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`); logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
// Log approval activity
activityService.log({
requestId: level.requestId,
type: 'approval',
user: { userId: level.approverId, name: level.approverName },
timestamp: new Date().toISOString(),
action: 'Approved',
details: `Request approved and forwarded to ${(nextLevel as any).approverName || (nextLevel as any).approverEmail} by ${level.approverName || level.approverEmail}`,
ipAddress: requestMetadata?.ipAddress || undefined,
userAgent: requestMetadata?.userAgent || undefined
});
// Notify initiator about the approval (triggers email)
if (wf) {
await notificationService.sendToUsers([(wf as any).initiatorId], {
title: `Request Approved - Level ${level.levelNumber}`,
body: `Your request "${(wf as any).title}" has been approved by ${level.approverName || level.approverEmail} and forwarded to the next approver.`,
requestNumber: (wf as any).requestNumber,
requestId: level.requestId,
url: `/request/${(wf as any).requestNumber}`,
type: 'approval',
priority: 'MEDIUM'
});
}
// Notify next approver // Notify next approver
if (wf && nextLevel) { if (wf && nextLevel) {
await notificationService.sendToUsers([ (nextLevel as any).approverId ], { await notificationService.sendToUsers([ (nextLevel as any).approverId ], {
title: `Action required: ${(wf as any).requestNumber}`, title: `Action required: ${(wf as any).requestNumber}`,
body: `${(wf as any).title}`, body: `${(wf as any).title}`,
requestNumber: (wf as any).requestNumber, requestNumber: (wf as any).requestNumber,
url: `/request/${(wf as any).requestNumber}`
});
activityService.log({
requestId: level.requestId, requestId: level.requestId,
type: 'approval', url: `/request/${(wf as any).requestNumber}`,
user: { userId: level.approverId, name: level.approverName }, type: 'assignment',
timestamp: new Date().toISOString(), priority: 'HIGH',
action: 'Approved', actionRequired: true
details: `Request approved and forwarded to ${(nextLevel as any).approverName || (nextLevel as any).approverEmail} by ${level.approverName || level.approverEmail}`,
ipAddress: requestMetadata?.ipAddress || undefined,
userAgent: requestMetadata?.userAgent || undefined
}); });
} }
} else { } else {
@ -528,12 +548,34 @@ export class ApprovalService {
for (const p of participants as any[]) { for (const p of participants as any[]) {
targetUserIds.add(p.userId); targetUserIds.add(p.userId);
} }
await notificationService.sendToUsers(Array.from(targetUserIds), {
// Send notification to initiator with type 'rejection' to trigger email
await notificationService.sendToUsers([(wf as any).initiatorId], {
title: `Rejected: ${(wf as any).requestNumber}`, title: `Rejected: ${(wf as any).requestNumber}`,
body: `${(wf as any).title}`, body: `${(wf as any).title}`,
requestNumber: (wf as any).requestNumber, requestNumber: (wf as any).requestNumber,
url: `/request/${(wf as any).requestNumber}` requestId: level.requestId,
url: `/request/${(wf as any).requestNumber}`,
type: 'rejection',
priority: 'HIGH',
metadata: {
rejectionReason: action.rejectionReason || action.comments || 'No reason provided'
}
}); });
// Send notification to other participants (spectators) for transparency (no email, just in-app)
const participantUserIds = Array.from(targetUserIds).filter(id => id !== (wf as any).initiatorId);
if (participantUserIds.length > 0) {
await notificationService.sendToUsers(participantUserIds, {
title: `Rejected: ${(wf as any).requestNumber}`,
body: `Request "${(wf as any).title}" has been rejected.`,
requestNumber: (wf as any).requestNumber,
requestId: level.requestId,
url: `/request/${(wf as any).requestNumber}`,
type: 'status_change', // Use status_change to avoid triggering emails for participants
priority: 'MEDIUM'
});
}
} }
// Generate AI conclusion remark ASYNCHRONOUSLY for rejected requests (similar to approved) // Generate AI conclusion remark ASYNCHRONOUSLY for rejected requests (similar to approved)

View File

@ -90,7 +90,7 @@ export class EmailNotificationService {
requestTitle: requestData.title, requestTitle: requestData.title,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: initiatorData.displayName || initiatorData.email,
firstApproverName: firstApproverData.displayName || firstApproverData.email, firstApproverName: firstApproverData.displayName || firstApproverData.email,
requestType: requestData.requestType || 'General', requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
priority: requestData.priority || 'MEDIUM', priority: requestData.priority || 'MEDIUM',
requestDate: this.formatDate(requestData.createdAt), requestDate: this.formatDate(requestData.createdAt),
requestTime: this.formatTime(requestData.createdAt), requestTime: this.formatTime(requestData.createdAt),
@ -157,7 +157,7 @@ export class EmailNotificationService {
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: initiatorData.displayName || initiatorData.email,
requestType: requestData.requestType || 'General', requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
requestDescription: requestData.description || '', requestDescription: requestData.description || '',
priority: requestData.priority || 'MEDIUM', priority: requestData.priority || 'MEDIUM',
requestDate: this.formatDate(requestData.createdAt), requestDate: this.formatDate(requestData.createdAt),
@ -188,7 +188,7 @@ export class EmailNotificationService {
requestId: requestData.requestNumber, requestId: requestData.requestNumber,
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
initiatorName: initiatorData.displayName || initiatorData.email, initiatorName: initiatorData.displayName || initiatorData.email,
requestType: requestData.requestType || 'General', requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
requestDescription: requestData.description || '', requestDescription: requestData.description || '',
priority: requestData.priority || 'MEDIUM', priority: requestData.priority || 'MEDIUM',
requestDate: this.formatDate(requestData.createdAt), requestDate: this.formatDate(requestData.createdAt),
@ -243,7 +243,7 @@ export class EmailNotificationService {
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
approvalDate: this.formatDate(approverData.approvedAt || new Date()), approvalDate: this.formatDate(approverData.approvedAt || new Date()),
approvalTime: this.formatTime(approverData.approvedAt || new Date()), approvalTime: this.formatTime(approverData.approvedAt || new Date()),
requestType: requestData.requestType || 'General', requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
approverComments: approverData.comments || undefined, approverComments: approverData.comments || undefined,
isFinalApproval, isFinalApproval,
nextApproverName: nextApproverData?.displayName || nextApproverData?.email, nextApproverName: nextApproverData?.displayName || nextApproverData?.email,
@ -296,7 +296,7 @@ export class EmailNotificationService {
approverName: approverData.displayName || approverData.email, approverName: approverData.displayName || approverData.email,
rejectionDate: this.formatDate(approverData.rejectedAt || new Date()), rejectionDate: this.formatDate(approverData.rejectedAt || new Date()),
rejectionTime: this.formatTime(approverData.rejectedAt || new Date()), rejectionTime: this.formatTime(approverData.rejectedAt || new Date()),
requestType: requestData.requestType || 'General', requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
rejectionReason, rejectionReason,
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name companyName: CompanyInfo.name
@ -575,6 +575,109 @@ export class EmailNotificationService {
} }
} }
/**
* 9. Send Approver Skipped Email
*/
async sendApproverSkipped(
requestData: any,
skippedApproverData: any,
skippedByData: any,
nextApproverData: any,
skipReason: string
): Promise<void> {
try {
const canSend = await shouldSendEmail(
skippedApproverData.userId,
EmailNotificationType.APPROVER_SKIPPED
);
if (!canSend) {
logger.info(`Email skipped (preferences): Approver Skipped for ${skippedApproverData.email}`);
return;
}
const data: ApproverSkippedData = {
recipientName: skippedApproverData.displayName || skippedApproverData.email,
requestId: requestData.requestNumber,
requestTitle: requestData.title,
skippedApproverName: skippedApproverData.displayName || skippedApproverData.email,
skippedByName: skippedByData.displayName || skippedByData.email,
skippedDate: this.formatDate(new Date()),
skippedTime: this.formatTime(new Date()),
nextApproverName: nextApproverData?.displayName || nextApproverData?.email || 'Next Approver',
skipReason: skipReason || 'Not provided',
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name
};
const html = getApproverSkippedEmail(data);
const subject = `[${requestData.requestNumber}] Approver Skipped`;
const result = await emailService.sendEmail({
to: skippedApproverData.email,
subject,
html
});
if (result.previewUrl) {
logger.info(`📧 Approver Skipped Email Preview: ${result.previewUrl}`);
}
} catch (error) {
logger.error('Failed to send Approver Skipped email:', error);
}
}
/**
* 10. Send Workflow Paused Email
*/
async sendWorkflowPaused(
requestData: any,
recipientData: any,
pausedByData: any,
pauseReason: string,
resumeDate: Date | string
): Promise<void> {
try {
const canSend = await shouldSendEmail(
recipientData.userId,
EmailNotificationType.WORKFLOW_PAUSED
);
if (!canSend) {
logger.info(`Email skipped (preferences): Workflow Paused for ${recipientData.email}`);
return;
}
const data: WorkflowPausedData = {
recipientName: recipientData.displayName || recipientData.email,
requestId: requestData.requestNumber,
requestTitle: requestData.title,
pausedByName: pausedByData?.displayName || pausedByData?.email || 'System',
pausedDate: this.formatDate(new Date()),
pausedTime: this.formatTime(new Date()),
resumeDate: this.formatDate(resumeDate),
pauseReason: pauseReason || 'Not provided',
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name
};
const html = getWorkflowPausedEmail(data);
const subject = `[${requestData.requestNumber}] Workflow Paused`;
const result = await emailService.sendEmail({
to: recipientData.email,
subject,
html
});
if (result.previewUrl) {
logger.info(`📧 Workflow Paused Email Preview: ${result.previewUrl}`);
}
} catch (error) {
logger.error('Failed to send Workflow Paused email:', error);
}
}
// Add more email methods as needed... // Add more email methods as needed...
} }

View File

@ -275,7 +275,8 @@ class NotificationService {
'status_change': null, 'status_change': null,
'ai_conclusion_generated': null, 'ai_conclusion_generated': null,
'summary_generated': null, 'summary_generated': null,
'workflow_paused': null, // Conditional - handled separately 'workflow_paused': EmailNotificationType.WORKFLOW_PAUSED,
'approver_skipped': EmailNotificationType.APPROVER_SKIPPED,
'pause_retriggered': null 'pause_retriggered': null
}; };
@ -358,8 +359,17 @@ class NotificationService {
const firstApprover = firstLevel ? await User.findByPk((firstLevel as any).approverId) : null; const firstApprover = firstLevel ? await User.findByPk((firstLevel as any).approverId) : null;
// Get first approver's TAT hours (not total TAT)
const firstApproverTatHours = firstLevel ? (firstLevel as any).tatHours : null;
// Add first approver's TAT to requestData for the email
const requestDataWithFirstTat = {
...requestData,
tatHours: firstApproverTatHours || (requestData as any).totalTatHours || 24
};
await emailNotificationService.sendRequestCreated( await emailNotificationService.sendRequestCreated(
requestData, requestDataWithFirstTat,
initiatorData, initiatorData,
firstApprover ? firstApprover.toJSON() : null firstApprover ? firstApprover.toJSON() : null
); );
@ -407,7 +417,7 @@ class NotificationService {
requestId: payload.requestId, requestId: payload.requestId,
status: 'APPROVED' status: 'APPROVED'
}, },
order: [['approvedAt', 'DESC']] order: [['actionDate', 'DESC'], ['levelEndTime', 'DESC']]
}); });
const allLevels = await ApprovalLevel.findAll({ const allLevels = await ApprovalLevel.findAll({
@ -421,9 +431,21 @@ class NotificationService {
const nextLevel = isFinalApproval ? null : allLevels.find((l: any) => l.status === 'PENDING'); const nextLevel = isFinalApproval ? null : allLevels.find((l: any) => l.status === 'PENDING');
const nextApprover = nextLevel ? await User.findByPk((nextLevel as any).approverId) : null; const nextApprover = nextLevel ? await User.findByPk((nextLevel as any).approverId) : null;
// Get the approver who just approved from the approved level
let approverData = user; // Fallback to user if we can't find the approver
if (approvedLevel) {
const approverUser = await User.findByPk((approvedLevel as any).approverId);
if (approverUser) {
approverData = approverUser.toJSON();
// Add approval metadata
(approverData as any).approvedAt = (approvedLevel as any).actionDate;
(approverData as any).comments = (approvedLevel as any).comments;
}
}
await emailNotificationService.sendApprovalConfirmation( await emailNotificationService.sendApprovalConfirmation(
requestData, requestData,
user, // Approver who just approved approverData, // Approver who just approved
initiatorData, initiatorData,
isFinalApproval, isFinalApproval,
nextApprover ? nextApprover.toJSON() : undefined nextApprover ? nextApprover.toJSON() : undefined
@ -520,6 +542,55 @@ class NotificationService {
} }
break; break;
case 'approver_skipped':
{
const skippedLevel = await ApprovalLevel.findOne({
where: {
requestId: payload.requestId,
isSkipped: true
},
order: [['skippedAt', 'DESC']]
});
const nextLevel = await ApprovalLevel.findOne({
where: {
requestId: payload.requestId,
status: 'PENDING'
},
order: [['levelNumber', 'ASC']]
});
const nextApprover = nextLevel ? await User.findByPk((nextLevel as any).approverId) : null;
const skippedBy = payload.metadata?.skippedBy ? await User.findByPk(payload.metadata.skippedBy) : null;
const skippedApprover = skippedLevel ? await User.findByPk((skippedLevel as any).approverId) : null;
if (skippedApprover) {
await emailNotificationService.sendApproverSkipped(
requestData,
skippedApprover.toJSON(),
skippedBy ? skippedBy.toJSON() : { userId: null, displayName: 'System', email: 'system' },
nextApprover ? nextApprover.toJSON() : null,
payload.metadata?.skipReason || (skippedLevel as any)?.skipReason || 'Not provided'
);
}
}
break;
case 'workflow_paused':
{
const pausedBy = payload.metadata?.pausedBy ? await User.findByPk(payload.metadata.pausedBy) : null;
const resumeDate = payload.metadata?.resumeDate || new Date();
await emailNotificationService.sendWorkflowPaused(
requestData,
user,
pausedBy ? pausedBy.toJSON() : { userId: null, displayName: 'System', email: 'system' },
payload.metadata?.pauseReason || 'Not provided',
resumeDate
);
}
break;
default: default:
logger.info(`[Email] No email configured for notification type: ${notificationType}`); logger.info(`[Email] No email configured for notification type: ${notificationType}`);
} }

View File

@ -182,18 +182,23 @@ export class PauseService {
url: `/request/${requestNumber}`, url: `/request/${requestNumber}`,
type: 'workflow_paused', type: 'workflow_paused',
priority: 'HIGH', priority: 'HIGH',
actionRequired: false actionRequired: false,
metadata: {
pauseReason: reason,
resumeDate: resumeDate.toISOString(),
pausedBy: userId
}
}); });
} }
// Notify the user who paused (confirmation) // Notify the user who paused (confirmation) - no email for self-action
await notificationService.sendToUsers([userId], { await notificationService.sendToUsers([userId], {
title: 'Workflow Paused Successfully', title: 'Workflow Paused Successfully',
body: `You have paused request "${title}". It will automatically resume on ${dayjs(resumeDate).format('MMM DD, YYYY')}.`, body: `You have paused request "${title}". It will automatically resume on ${dayjs(resumeDate).format('MMM DD, YYYY')}.`,
requestId, requestId,
requestNumber, requestNumber,
url: `/request/${requestNumber}`, url: `/request/${requestNumber}`,
type: 'workflow_paused', type: 'status_change', // Use status_change to avoid email for self-action
priority: 'MEDIUM', priority: 'MEDIUM',
actionRequired: false actionRequired: false
}); });
@ -210,7 +215,12 @@ export class PauseService {
url: `/request/${requestNumber}`, url: `/request/${requestNumber}`,
type: 'workflow_paused', type: 'workflow_paused',
priority: 'HIGH', priority: 'HIGH',
actionRequired: false actionRequired: false,
metadata: {
pauseReason: reason,
resumeDate: resumeDate.toISOString(),
pausedBy: userId
}
}); });
} }
@ -459,7 +469,11 @@ export class PauseService {
url: `/request/${requestNumber}`, url: `/request/${requestNumber}`,
type: 'workflow_resumed', type: 'workflow_resumed',
priority: 'HIGH', priority: 'HIGH',
actionRequired: false actionRequired: false,
metadata: {
resumedBy: userId ? { userId, name: resumeUserName } : null,
pauseDuration: pauseDuration
}
}); });
} }
@ -474,11 +488,15 @@ export class PauseService {
url: `/request/${requestNumber}`, url: `/request/${requestNumber}`,
type: 'workflow_resumed', type: 'workflow_resumed',
priority: 'HIGH', priority: 'HIGH',
actionRequired: true actionRequired: true,
metadata: {
resumedBy: userId ? { userId, name: resumeUserName } : null,
pauseDuration: pauseDuration
}
}); });
} }
// Send confirmation to the user who resumed (if manual resume) // Send confirmation to the user who resumed (if manual resume) - no email for self-action
if (userId) { if (userId) {
await notificationService.sendToUsers([userId], { await notificationService.sendToUsers([userId], {
title: 'Workflow Resumed Successfully', title: 'Workflow Resumed Successfully',
@ -486,7 +504,7 @@ export class PauseService {
requestId, requestId,
requestNumber, requestNumber,
url: `/request/${requestNumber}`, url: `/request/${requestNumber}`,
type: 'workflow_resumed', type: 'status_change', // Use status_change to avoid email for self-action
priority: 'MEDIUM', priority: 'MEDIUM',
actionRequired: isResumedByApprover actionRequired: isResumedByApprover
}); });

View File

@ -221,13 +221,31 @@ export class WorkflowService {
// Update workflow current level // Update workflow current level
await workflow.update({ currentLevel: nextLevelNumber }); await workflow.update({ currentLevel: nextLevelNumber });
// Notify skipped approver (triggers email)
await notificationService.sendToUsers([(level as any).approverId], {
title: 'Approver Skipped',
body: `You have been skipped in request ${(workflow as any).requestNumber}. The workflow has moved to the next approver.`,
requestId,
requestNumber: (workflow as any).requestNumber,
url: `/request/${(workflow as any).requestNumber}`,
type: 'approver_skipped',
priority: 'MEDIUM',
metadata: {
skipReason: skipReason,
skippedBy: skippedBy
}
});
// Notify next approver // Notify next approver
await notificationService.sendToUsers([(nextLevel as any).approverId], { await notificationService.sendToUsers([(nextLevel as any).approverId], {
title: 'Request Escalated', title: 'Request Escalated',
body: `Previous approver was skipped. Request ${(workflow as any).requestNumber} is now awaiting your approval.`, body: `Previous approver was skipped. Request ${(workflow as any).requestNumber} is now awaiting your approval.`,
requestId, requestId,
requestNumber: (workflow as any).requestNumber, requestNumber: (workflow as any).requestNumber,
url: `/request/${(workflow as any).requestNumber}` url: `/request/${(workflow as any).requestNumber}`,
type: 'assignment',
priority: 'HIGH',
actionRequired: true
}); });
} }