template preview modified
This commit is contained in:
parent
90fe2c8e87
commit
db02d6eb01
@ -279,16 +279,85 @@ export class WorkflowController {
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate file names if they exceed database column limits (255 chars)
|
||||
const MAX_FILE_NAME_LENGTH = 255;
|
||||
const originalFileName = file.originalname;
|
||||
let truncatedOriginalFileName = originalFileName;
|
||||
|
||||
if (originalFileName.length > MAX_FILE_NAME_LENGTH) {
|
||||
// Preserve file extension when truncating
|
||||
const ext = path.extname(originalFileName);
|
||||
const nameWithoutExt = path.basename(originalFileName, ext);
|
||||
const maxNameLength = MAX_FILE_NAME_LENGTH - ext.length;
|
||||
|
||||
if (maxNameLength > 0) {
|
||||
truncatedOriginalFileName = nameWithoutExt.substring(0, maxNameLength) + ext;
|
||||
} else {
|
||||
// If extension itself is too long, just use the extension
|
||||
truncatedOriginalFileName = ext.substring(0, MAX_FILE_NAME_LENGTH);
|
||||
}
|
||||
|
||||
logger.warn('[Workflow] File name truncated to fit database column', {
|
||||
originalLength: originalFileName.length,
|
||||
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: path.basename(file.filename || file.originalname),
|
||||
originalFileName: file.originalname,
|
||||
fileName: truncatedFileName,
|
||||
originalFileName: truncatedOriginalFileName,
|
||||
fileType: extension,
|
||||
fileExtension: extension,
|
||||
fileSize: file.size,
|
||||
filePath: gcsFilePath, // Store GCS path or local path
|
||||
storageUrl: storageUrl, // Store GCS URL or local URL
|
||||
storageUrl: finalStorageUrl, // Store GCS URL or local URL (null if too long)
|
||||
mimeType: file.mimetype,
|
||||
checksum,
|
||||
isGoogleDoc: false,
|
||||
@ -300,6 +369,24 @@ export class WorkflowController {
|
||||
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
|
||||
const requestMeta = getRequestMetadata(req);
|
||||
@ -320,6 +407,13 @@ export class WorkflowController {
|
||||
ResponseHandler.success(res, { requestId: workflow.requestId, documents: docs }, 'Workflow created with documents', 201);
|
||||
} catch (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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ export class ApprovalService {
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
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(),
|
||||
action: 'AI Conclusion Generated',
|
||||
details: 'AI-powered conclusion remark generated for review by initiator',
|
||||
@ -291,7 +291,7 @@ export class ApprovalService {
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
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(),
|
||||
action: 'Summary Auto-Generated',
|
||||
details: 'Request summary auto-generated after final approval',
|
||||
@ -339,15 +339,15 @@ export class ApprovalService {
|
||||
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;
|
||||
await notificationService.sendToUsers([initiatorId], {
|
||||
title: `Request Approved - Closure Pending`,
|
||||
body: `Your request "${(wf as any).title}" has been fully approved. Please review and finalize the conclusion remark to close the request.`,
|
||||
title: `Request Approved - All Approvals Complete`,
|
||||
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,
|
||||
requestId: level.requestId,
|
||||
url: `/request/${(wf as any).requestNumber}`,
|
||||
type: 'approval_pending_closure',
|
||||
type: 'approval',
|
||||
priority: 'HIGH',
|
||||
actionRequired: true
|
||||
});
|
||||
@ -425,14 +425,8 @@ export class ApprovalService {
|
||||
);
|
||||
|
||||
logger.info(`Approved level ${level.levelNumber}. Activated next level ${nextLevelNumber} for workflow ${level.requestId}`);
|
||||
// Notify next approver
|
||||
if (wf && nextLevel) {
|
||||
await notificationService.sendToUsers([ (nextLevel as any).approverId ], {
|
||||
title: `Action required: ${(wf as any).requestNumber}`,
|
||||
body: `${(wf as any).title}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
url: `/request/${(wf as any).requestNumber}`
|
||||
});
|
||||
|
||||
// Log approval activity
|
||||
activityService.log({
|
||||
requestId: level.requestId,
|
||||
type: 'approval',
|
||||
@ -443,6 +437,32 @@ export class ApprovalService {
|
||||
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
|
||||
if (wf && nextLevel) {
|
||||
await notificationService.sendToUsers([ (nextLevel as any).approverId ], {
|
||||
title: `Action required: ${(wf as any).requestNumber}`,
|
||||
body: `${(wf as any).title}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
requestId: level.requestId,
|
||||
url: `/request/${(wf as any).requestNumber}`,
|
||||
type: 'assignment',
|
||||
priority: 'HIGH',
|
||||
actionRequired: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No next level found but not final approver - this shouldn't happen
|
||||
@ -528,12 +548,34 @@ export class ApprovalService {
|
||||
for (const p of participants as any[]) {
|
||||
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}`,
|
||||
body: `${(wf as any).title}`,
|
||||
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)
|
||||
|
||||
@ -90,7 +90,7 @@ export class EmailNotificationService {
|
||||
requestTitle: requestData.title,
|
||||
initiatorName: initiatorData.displayName || initiatorData.email,
|
||||
firstApproverName: firstApproverData.displayName || firstApproverData.email,
|
||||
requestType: requestData.requestType || 'General',
|
||||
requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
|
||||
priority: requestData.priority || 'MEDIUM',
|
||||
requestDate: this.formatDate(requestData.createdAt),
|
||||
requestTime: this.formatTime(requestData.createdAt),
|
||||
@ -157,7 +157,7 @@ export class EmailNotificationService {
|
||||
requestId: requestData.requestNumber,
|
||||
approverName: approverData.displayName || approverData.email,
|
||||
initiatorName: initiatorData.displayName || initiatorData.email,
|
||||
requestType: requestData.requestType || 'General',
|
||||
requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
|
||||
requestDescription: requestData.description || '',
|
||||
priority: requestData.priority || 'MEDIUM',
|
||||
requestDate: this.formatDate(requestData.createdAt),
|
||||
@ -188,7 +188,7 @@ export class EmailNotificationService {
|
||||
requestId: requestData.requestNumber,
|
||||
approverName: approverData.displayName || approverData.email,
|
||||
initiatorName: initiatorData.displayName || initiatorData.email,
|
||||
requestType: requestData.requestType || 'General',
|
||||
requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
|
||||
requestDescription: requestData.description || '',
|
||||
priority: requestData.priority || 'MEDIUM',
|
||||
requestDate: this.formatDate(requestData.createdAt),
|
||||
@ -243,7 +243,7 @@ export class EmailNotificationService {
|
||||
approverName: approverData.displayName || approverData.email,
|
||||
approvalDate: this.formatDate(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,
|
||||
isFinalApproval,
|
||||
nextApproverName: nextApproverData?.displayName || nextApproverData?.email,
|
||||
@ -296,7 +296,7 @@ export class EmailNotificationService {
|
||||
approverName: approverData.displayName || approverData.email,
|
||||
rejectionDate: this.formatDate(approverData.rejectedAt || new Date()),
|
||||
rejectionTime: this.formatTime(approverData.rejectedAt || new Date()),
|
||||
requestType: requestData.requestType || 'General',
|
||||
requestType: requestData.templateType || requestData.requestType || 'CUSTOM',
|
||||
rejectionReason,
|
||||
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
|
||||
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...
|
||||
}
|
||||
|
||||
|
||||
@ -275,7 +275,8 @@ class NotificationService {
|
||||
'status_change': null,
|
||||
'ai_conclusion_generated': null,
|
||||
'summary_generated': null,
|
||||
'workflow_paused': null, // Conditional - handled separately
|
||||
'workflow_paused': EmailNotificationType.WORKFLOW_PAUSED,
|
||||
'approver_skipped': EmailNotificationType.APPROVER_SKIPPED,
|
||||
'pause_retriggered': null
|
||||
};
|
||||
|
||||
@ -358,8 +359,17 @@ class NotificationService {
|
||||
|
||||
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(
|
||||
requestData,
|
||||
requestDataWithFirstTat,
|
||||
initiatorData,
|
||||
firstApprover ? firstApprover.toJSON() : null
|
||||
);
|
||||
@ -407,7 +417,7 @@ class NotificationService {
|
||||
requestId: payload.requestId,
|
||||
status: 'APPROVED'
|
||||
},
|
||||
order: [['approvedAt', 'DESC']]
|
||||
order: [['actionDate', 'DESC'], ['levelEndTime', 'DESC']]
|
||||
});
|
||||
|
||||
const allLevels = await ApprovalLevel.findAll({
|
||||
@ -421,9 +431,21 @@ class NotificationService {
|
||||
const nextLevel = isFinalApproval ? null : allLevels.find((l: any) => l.status === 'PENDING');
|
||||
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(
|
||||
requestData,
|
||||
user, // Approver who just approved
|
||||
approverData, // Approver who just approved
|
||||
initiatorData,
|
||||
isFinalApproval,
|
||||
nextApprover ? nextApprover.toJSON() : undefined
|
||||
@ -520,6 +542,55 @@ class NotificationService {
|
||||
}
|
||||
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:
|
||||
logger.info(`[Email] No email configured for notification type: ${notificationType}`);
|
||||
}
|
||||
|
||||
@ -182,18 +182,23 @@ export class PauseService {
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'workflow_paused',
|
||||
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], {
|
||||
title: 'Workflow Paused Successfully',
|
||||
body: `You have paused request "${title}". It will automatically resume on ${dayjs(resumeDate).format('MMM DD, YYYY')}.`,
|
||||
requestId,
|
||||
requestNumber,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'workflow_paused',
|
||||
type: 'status_change', // Use status_change to avoid email for self-action
|
||||
priority: 'MEDIUM',
|
||||
actionRequired: false
|
||||
});
|
||||
@ -210,7 +215,12 @@ export class PauseService {
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'workflow_paused',
|
||||
priority: 'HIGH',
|
||||
actionRequired: false
|
||||
actionRequired: false,
|
||||
metadata: {
|
||||
pauseReason: reason,
|
||||
resumeDate: resumeDate.toISOString(),
|
||||
pausedBy: userId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -459,7 +469,11 @@ export class PauseService {
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'workflow_resumed',
|
||||
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}`,
|
||||
type: 'workflow_resumed',
|
||||
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) {
|
||||
await notificationService.sendToUsers([userId], {
|
||||
title: 'Workflow Resumed Successfully',
|
||||
@ -486,7 +504,7 @@ export class PauseService {
|
||||
requestId,
|
||||
requestNumber,
|
||||
url: `/request/${requestNumber}`,
|
||||
type: 'workflow_resumed',
|
||||
type: 'status_change', // Use status_change to avoid email for self-action
|
||||
priority: 'MEDIUM',
|
||||
actionRequired: isResumedByApprover
|
||||
});
|
||||
|
||||
@ -221,13 +221,31 @@ export class WorkflowService {
|
||||
// Update workflow current level
|
||||
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
|
||||
await notificationService.sendToUsers([(nextLevel as any).approverId], {
|
||||
title: 'Request Escalated',
|
||||
body: `Previous approver was skipped. Request ${(workflow as any).requestNumber} is now awaiting your approval.`,
|
||||
requestId,
|
||||
requestNumber: (workflow as any).requestNumber,
|
||||
url: `/request/${(workflow as any).requestNumber}`
|
||||
url: `/request/${(workflow as any).requestNumber}`,
|
||||
type: 'assignment',
|
||||
priority: 'HIGH',
|
||||
actionRequired: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user