csv filed issue on unique transaction no and also dealer claim templates enhanced
This commit is contained in:
parent
89beffee2e
commit
9c003e9a16
@ -1 +1 @@
|
||||
import{a as s}from"./index-DDuRVIKn.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CxsBWvVP.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-BATWUvr6.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
import{a as s}from"./index-DK9CP9m9.js";import"./radix-vendor-CYvDqP9X.js";import"./charts-vendor-BVfwAPj-.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-CxsBWvVP.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-BATWUvr6.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
|
||||
File diff suppressed because one or more lines are too long
@ -13,7 +13,7 @@
|
||||
<!-- Preload essential fonts and icons -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<script type="module" crossorigin src="/assets/index-DDuRVIKn.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DK9CP9m9.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-BVfwAPj-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { ApprovalRequestData } from './types';
|
||||
import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles } from './helpers';
|
||||
import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles, getCustomMessageSection } from './helpers';
|
||||
import { getBrandedHeader } from './branding.config';
|
||||
|
||||
export function getApprovalRequestEmail(data: ApprovalRequestData): string {
|
||||
@ -102,6 +102,9 @@ export function getApprovalRequestEmail(data: ApprovalRequestData): string {
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Custom Message Section -->
|
||||
${getCustomMessageSection(data.customMessage)}
|
||||
|
||||
<!-- Description (supports rich text HTML including tables) -->
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">Description:</h3>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { DealerProposalRequiredData } from './types';
|
||||
import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles } from './helpers';
|
||||
import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles, getCustomMessageSection } from './helpers';
|
||||
import { getBrandedHeader } from './branding.config';
|
||||
|
||||
export function getDealerCompletionRequiredEmail(data: DealerProposalRequiredData): string {
|
||||
@ -103,6 +103,9 @@ export function getDealerCompletionRequiredEmail(data: DealerProposalRequiredDat
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Custom Message Section -->
|
||||
${getCustomMessageSection(data.customMessage)}
|
||||
|
||||
<div style="padding: 20px; background-color: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px; margin-bottom: 30px;">
|
||||
<h3 style="margin: 0 0 10px; color: #856404; font-size: 16px; font-weight: 600;">What You Need to Submit:</h3>
|
||||
<ul style="margin: 0; padding: 0 0 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { DealerProposalRequiredData } from './types';
|
||||
import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles } from './helpers';
|
||||
import { getEmailFooter, getPrioritySection, getEmailHeader, HeaderStyles, getResponsiveStyles, wrapRichText, getEmailContainerStyles, getCustomMessageSection } from './helpers';
|
||||
import { getBrandedHeader } from './branding.config';
|
||||
|
||||
export function getDealerProposalRequiredEmail(data: DealerProposalRequiredData): string {
|
||||
@ -152,6 +152,9 @@ export function getDealerProposalRequiredEmail(data: DealerProposalRequiredData)
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Custom Message Section -->
|
||||
${getCustomMessageSection(data.customMessage)}
|
||||
|
||||
<!-- Description (supports rich text HTML including tables) -->
|
||||
${data.requestDescription ? `
|
||||
<div style="margin-bottom: 30px;">
|
||||
|
||||
@ -32,7 +32,8 @@ export enum EmailNotificationType {
|
||||
COMPLETION_DOCUMENTS_SUBMITTED = 'completion_documents_submitted',
|
||||
EINVOICE_GENERATED = 'einvoice_generated',
|
||||
CREDIT_NOTE_SENT = 'credit_note_sent',
|
||||
ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added'
|
||||
ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added',
|
||||
RE_QUOTATION = 're_quotation',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -799,6 +799,22 @@ export function getRoleDescription(role: 'Approver' | 'Spectator'): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate custom message section (e.g., for re-quotation notes)
|
||||
*/
|
||||
export function getCustomMessageSection(message?: string): string {
|
||||
if (!message) return '';
|
||||
|
||||
return `
|
||||
<div style="margin-bottom: 30px; padding: 20px; background-color: #f0f7ff; border-left: 4px solid #667eea; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px; color: #333333; font-size: 16px; font-weight: 600;">Note/Instructions:</h3>
|
||||
<p style="margin: 0; color: #444444; font-size: 15px; line-height: 1.6; font-style: italic;">
|
||||
"${message}"
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate action required section for workflow resumed
|
||||
*/
|
||||
|
||||
@ -15,7 +15,7 @@ export function getRejectionNotificationEmail(data: RejectionNotificationData):
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<title>Request Rejected</title>
|
||||
<title>${data.isReturnedForRevision ? 'Request Returned for Revision' : 'Request Rejected'}</title>
|
||||
${getResponsiveStyles()}
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f4f4f4;">
|
||||
@ -24,63 +24,65 @@ export function getRejectionNotificationEmail(data: RejectionNotificationData):
|
||||
<td style="padding: 40px 0;">
|
||||
<table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
|
||||
${getEmailHeader(getBrandedHeader({
|
||||
title: 'Request Rejected',
|
||||
...HeaderStyles.error
|
||||
title: data.isReturnedForRevision ? 'Revision Required' : 'Request Rejected',
|
||||
...(data.isReturnedForRevision ? HeaderStyles.warning : HeaderStyles.error)
|
||||
}))}
|
||||
|
||||
<tr>
|
||||
<td style="padding: 40px 30px;">
|
||||
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
|
||||
Dear <strong style="color: #dc3545;">${data.initiatorName}</strong>,
|
||||
Dear <strong style="color: ${data.isReturnedForRevision ? '#856404' : '#dc3545'};">${data.initiatorName}</strong>,
|
||||
</p>
|
||||
|
||||
<p style="margin: 0 0 30px; color: #666666; font-size: 16px; line-height: 1.6;">
|
||||
We regret to inform you that your request has been <strong style="color: #dc3545;">rejected</strong> by <strong>${data.approverName}</strong>.
|
||||
${data.isReturnedForRevision
|
||||
? `Your request has been <strong>returned for revision</strong> by <strong>${data.approverName}</strong>.`
|
||||
: `We regret to inform you that your request has been <strong style="color: #dc3545;">rejected</strong> by <strong>${data.approverName}</strong>.`}
|
||||
</p>
|
||||
|
||||
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 6px; margin-bottom: 30px;" cellpadding="0" cellspacing="0">
|
||||
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: ${data.isReturnedForRevision ? '#fff3cd' : '#f8d7da'}; border: 1px solid ${data.isReturnedForRevision ? '#ffeeba' : '#f5c6cb'}; border-radius: 6px; margin-bottom: 30px;" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding: 25px;">
|
||||
<h2 style="margin: 0 0 20px; color: #721c24; font-size: 18px; font-weight: 600;">Request Summary</h2>
|
||||
<h2 style="margin: 0 0 20px; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 18px; font-weight: 600;">Request Summary</h2>
|
||||
|
||||
<table role="presentation" style="width: 100%; border-collapse: collapse;" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px; width: 140px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px; width: 140px;">
|
||||
<strong>Request ID:</strong>
|
||||
</td>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
${data.requestId}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<strong>Rejected By:</strong>
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
<strong>Action By:</strong>
|
||||
</td>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
${data.approverName}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<strong>Rejected On:</strong>
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
<strong>Action On:</strong>
|
||||
</td>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
${data.rejectionDate}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
<strong>Time:</strong>
|
||||
</td>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
${data.rejectionTime}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
<strong>Request Type:</strong>
|
||||
</td>
|
||||
<td style="padding: 8px 0; color: #721c24; font-size: 14px;">
|
||||
<td style="padding: 8px 0; color: ${data.isReturnedForRevision ? '#856404' : '#721c24'}; font-size: 14px;">
|
||||
${data.requestType}
|
||||
</td>
|
||||
</tr>
|
||||
@ -90,8 +92,8 @@ export function getRejectionNotificationEmail(data: RejectionNotificationData):
|
||||
</table>
|
||||
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">Reason for Rejection:</h3>
|
||||
<div style="padding: 15px; background-color: #f8f9fa; border-left: 4px solid #dc3545; border-radius: 4px; overflow-x: auto;">
|
||||
<h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">${data.isReturnedForRevision ? 'Reason for Revision:' : 'Reason for Rejection:'}</h3>
|
||||
<div style="padding: 15px; background-color: #f8f9fa; border-left: 4px solid ${data.isReturnedForRevision ? '#ffc107' : '#dc3545'}; border-radius: 4px; overflow-x: auto;">
|
||||
${wrapRichText(data.rejectionReason)}
|
||||
</div>
|
||||
</div>
|
||||
@ -99,9 +101,13 @@ export function getRejectionNotificationEmail(data: RejectionNotificationData):
|
||||
<div style="padding: 20px; background-color: #fff3cd; border-left: 4px solid #ffc107; border-radius: 4px; margin-bottom: 30px;">
|
||||
<h3 style="margin: 0 0 10px; color: #856404; font-size: 16px; font-weight: 600;">What You Can Do:</h3>
|
||||
<ul style="margin: 10px 0 0 0; padding-left: 20px; color: #856404; font-size: 14px; line-height: 1.8;">
|
||||
<li>Review the rejection reason carefully</li>
|
||||
<li>Make necessary adjustments to your request</li>
|
||||
<li>Submit a new request with the required changes</li>
|
||||
${data.isReturnedForRevision
|
||||
? `<li>Review the requested changes carefully</li>
|
||||
<li>Adjust the proposal or documents as needed</li>
|
||||
<li>Resubmit the request for approval</li>`
|
||||
: `<li>Review the rejection reason carefully</li>
|
||||
<li>Make necessary adjustments to your request</li>
|
||||
<li>Submit a new request with the required changes</li>`}
|
||||
<li>Contact ${data.approverName} for more clarification if needed</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -47,6 +47,7 @@ export interface ApprovalRequestData extends BaseEmailData {
|
||||
requestType: string;
|
||||
requestDescription: string;
|
||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
customMessage?: string;
|
||||
}
|
||||
|
||||
export interface MultiApproverRequestData extends ApprovalRequestData {
|
||||
@ -80,6 +81,7 @@ export interface RejectionNotificationData extends BaseEmailData {
|
||||
rejectionTime: string;
|
||||
requestType: string;
|
||||
rejectionReason: string;
|
||||
isReturnedForRevision?: boolean;
|
||||
}
|
||||
|
||||
export interface TATReminderData extends BaseEmailData {
|
||||
@ -250,6 +252,7 @@ export interface DealerProposalRequiredData extends BaseEmailData {
|
||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
tatHours?: number;
|
||||
dueDate?: string;
|
||||
customMessage?: string;
|
||||
}
|
||||
|
||||
export interface AdditionalDocumentAddedData extends BaseEmailData {
|
||||
|
||||
@ -3615,7 +3615,7 @@ export class DealerClaimService {
|
||||
|
||||
const csvData = invoiceItems.map((item: any) => {
|
||||
const row: any = {
|
||||
TRNS_UNIQ_NO: isNonGst ? '' : (item.transactionCode || ''),
|
||||
TRNS_UNIQ_NO: item.transactionCode || '',
|
||||
CLAIM_NUMBER: requestNumber,
|
||||
INV_NUMBER: invoice.invoiceNumber || '',
|
||||
DEALER_CODE: claimDetails.dealerCode,
|
||||
|
||||
@ -849,6 +849,23 @@ export class DealerClaimApprovalService {
|
||||
tatPercentageUsed: 0
|
||||
} as any);
|
||||
}
|
||||
|
||||
// Trigger Re-Quotation Notification (Email + Push)
|
||||
if (step1.approverId) {
|
||||
await notificationService.sendToUsers([step1.approverId], {
|
||||
title: `Revised Quotation Requested: ${(wf as any).requestNumber}`,
|
||||
body: action.rejectionReason || action.comments || 'Please revise and resubmit your quotation.',
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
requestId: level.requestId,
|
||||
url: `/request/${(wf as any).requestNumber}`,
|
||||
type: 're_quotation',
|
||||
priority: 'HIGH',
|
||||
actionRequired: true,
|
||||
metadata: {
|
||||
rejectionReason: action.rejectionReason || action.comments
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -888,10 +905,10 @@ export class DealerClaimApprovalService {
|
||||
userAgent: requestMetadata?.userAgent || undefined
|
||||
});
|
||||
|
||||
// Notify the approver of the target level
|
||||
if (targetLevel.approverId) {
|
||||
// Notify the approver of the target level (skip if specific re-quotation notification was already sent)
|
||||
if (targetLevel.approverId && !isReQuotation) {
|
||||
await notificationService.sendToUsers([targetLevel.approverId], {
|
||||
title: `Request Returned: ${(wf as any).requestNumber}`,
|
||||
title: `Request Returned for Revision: ${(wf as any).requestNumber}`,
|
||||
body: `Request "${(wf as any).title}" has been returned to your level for revision. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
requestId: level.requestId,
|
||||
@ -903,15 +920,19 @@ export class DealerClaimApprovalService {
|
||||
}
|
||||
|
||||
// Notify initiator when request is returned
|
||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||
title: `Request Returned: ${(wf as any).requestNumber}`,
|
||||
body: `Request "${(wf as any).title}" has been returned to level ${targetLevel.levelNumber} for revision. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
requestId: level.requestId,
|
||||
url: `/request/${(wf as any).requestNumber}`,
|
||||
type: 'rejection',
|
||||
priority: 'MEDIUM'
|
||||
});
|
||||
// Skip if they are the same as the dealer (who already got a more specific notification)
|
||||
const dealerId = targetLevel.approverId;
|
||||
if ((wf as any).initiatorId && (wf as any).initiatorId !== dealerId) {
|
||||
await notificationService.sendToUsers([(wf as any).initiatorId], {
|
||||
title: `Request Returned: ${(wf as any).requestNumber}`,
|
||||
body: `Request "${(wf as any).title}" has been returned to Step ${targetLevel.levelNumber} for revision. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`,
|
||||
requestNumber: (wf as any).requestNumber,
|
||||
requestId: level.requestId,
|
||||
url: `/request/${(wf as any).requestNumber}`,
|
||||
type: 'rejection',
|
||||
priority: 'MEDIUM'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Emit real-time update to all users viewing this request
|
||||
|
||||
@ -92,26 +92,6 @@ export class DealerClaimEmailService implements IWorkflowEmailService {
|
||||
(levelName.includes('completion') || levelName.includes('documents')) &&
|
||||
!levelName.includes('proposal'); // Explicitly exclude proposal
|
||||
|
||||
// Safety check: If proposal already submitted, don't send proposal email
|
||||
// This prevents sending proposal email if levelName somehow matches both conditions
|
||||
if (isDealerProposalStep && requestData.requestId) {
|
||||
try {
|
||||
const { DealerProposalDetails } = await import('@models/DealerProposalDetails');
|
||||
const existingProposal = await DealerProposalDetails.findOne({
|
||||
where: { requestId: requestData.requestId }
|
||||
});
|
||||
if (existingProposal) {
|
||||
logger.warn(`[DealerClaimEmail] ⚠️ Proposal already submitted but levelName indicates proposal step. Forcing completion step.`);
|
||||
// If proposal exists, this MUST be completion step, not proposal
|
||||
await this.sendDealerCompletionRequiredEmail(requestData, approverUser, initiatorData, level);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`[DealerClaimEmail] Error checking proposal:`, e);
|
||||
// Continue with normal flow if check fails
|
||||
}
|
||||
}
|
||||
|
||||
// Route to appropriate template
|
||||
if (isDealerCompletionStep) {
|
||||
logger.info(`[DealerClaimEmail] ✅ DEALER COMPLETION step - sending completion documents required email`);
|
||||
@ -224,6 +204,18 @@ export class DealerClaimEmailService implements IWorkflowEmailService {
|
||||
where: { requestId: requestData.requestId }
|
||||
});
|
||||
|
||||
// Determine stage-specific instructions
|
||||
let stageInstructions = '';
|
||||
const name = (currentLevel?.levelName || '').toLowerCase();
|
||||
|
||||
if (name.includes('evaluation') || name.includes('requestor evaluation')) {
|
||||
stageInstructions = 'Please evaluate the proposal submitted by the dealer. Verify the activity details and estimated budget.';
|
||||
} else if (name.includes('lead') || name.includes('department lead')) {
|
||||
stageInstructions = 'The requestor has evaluated this proposal and recommended it for your approval. Please review and provide your authorization.';
|
||||
} else if (name.includes('claim approval') && name.includes('requestor')) {
|
||||
stageInstructions = 'The dealer has submitted completion documents. Please verify the expenses and documents before providing final claim approval.';
|
||||
}
|
||||
|
||||
// Enrich requestData with dealer claim-specific information
|
||||
const enrichedRequestData = {
|
||||
...requestData,
|
||||
@ -256,7 +248,8 @@ export class DealerClaimEmailService implements IWorkflowEmailService {
|
||||
approverData,
|
||||
initiatorData,
|
||||
false, // isMultiLevel = false for dealer claim workflows
|
||||
undefined // No approval chain needed
|
||||
undefined, // No approval chain needed
|
||||
stageInstructions // Pass as customMessage (contextual instruction)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -144,7 +144,8 @@ export class EmailNotificationService {
|
||||
approverData: any,
|
||||
initiatorData: any,
|
||||
isMultiLevel: boolean,
|
||||
approvalChain?: any[]
|
||||
approvalChain?: any[],
|
||||
customMessage?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Check preferences
|
||||
@ -185,7 +186,8 @@ export class EmailNotificationService {
|
||||
totalApprovers: approvalChain.length,
|
||||
approversList: chainData,
|
||||
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
|
||||
companyName: CompanyInfo.name
|
||||
companyName: CompanyInfo.name,
|
||||
customMessage
|
||||
};
|
||||
|
||||
const html = getMultiApproverRequestEmail(data);
|
||||
@ -214,7 +216,8 @@ export class EmailNotificationService {
|
||||
requestDate: this.formatDate(requestData.createdAt),
|
||||
requestTime: this.formatTime(requestData.createdAt),
|
||||
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
|
||||
companyName: CompanyInfo.name
|
||||
companyName: CompanyInfo.name,
|
||||
customMessage
|
||||
};
|
||||
|
||||
const html = getApprovalRequestEmail(data);
|
||||
@ -298,6 +301,14 @@ export class EmailNotificationService {
|
||||
rejectionReason: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 1. Skip if approver is the same as initiator
|
||||
if (approverData.userId && initiatorData.userId && approverData.userId === initiatorData.userId) {
|
||||
logger.info(`Email skipped: Approver ${approverData.userId} is the initiator, no need to notify self of rejection/return`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isReturnedForRevision = rejectionReason.includes('Revised Quotation Requested');
|
||||
|
||||
// Use override for high-priority emails
|
||||
const canSend = await shouldSendEmailWithOverride(
|
||||
initiatorData.userId,
|
||||
@ -319,11 +330,14 @@ export class EmailNotificationService {
|
||||
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
|
||||
rejectionReason,
|
||||
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
|
||||
companyName: CompanyInfo.name
|
||||
companyName: CompanyInfo.name,
|
||||
isReturnedForRevision
|
||||
};
|
||||
|
||||
const html = getRejectionNotificationEmail(data);
|
||||
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Rejected`;
|
||||
const subject = isReturnedForRevision
|
||||
? `${requestData.requestNumber} - ${requestData.title} - Request Returned for Revision`
|
||||
: `${requestData.requestNumber} - ${requestData.title} - Request Rejected`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: initiatorData.email,
|
||||
@ -927,7 +941,8 @@ export class EmailNotificationService {
|
||||
requestData: any,
|
||||
dealerData: any,
|
||||
initiatorData: any,
|
||||
claimData?: any
|
||||
claimData?: any,
|
||||
customMessage?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const canSend = await shouldSendEmail(
|
||||
@ -944,7 +959,7 @@ export class EmailNotificationService {
|
||||
let dueDate: string | undefined;
|
||||
if (claimData?.tatHours) {
|
||||
const dueDateObj = dayjs().add(claimData.tatHours, 'hour');
|
||||
dueDate = dueDateObj.format('MMMM D, YYYY [at] h:mm A');
|
||||
dueDate = dueDateObj.format('MMMM D, YYYY');
|
||||
}
|
||||
|
||||
const data: DealerProposalRequiredData = {
|
||||
@ -965,7 +980,8 @@ export class EmailNotificationService {
|
||||
tatHours: claimData?.tatHours,
|
||||
dueDate: dueDate,
|
||||
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
|
||||
companyName: CompanyInfo.name
|
||||
companyName: CompanyInfo.name,
|
||||
customMessage
|
||||
};
|
||||
|
||||
const html = getDealerProposalRequiredEmail(data);
|
||||
@ -988,22 +1004,24 @@ export class EmailNotificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 12b. Send Dealer Completion Documents Required Email
|
||||
* 12a. Send Re-Quotation Required Email
|
||||
* (Uses Proposal Required template with custom subject and rejection reason)
|
||||
*/
|
||||
async sendDealerCompletionRequired(
|
||||
async sendReQuotationRequired(
|
||||
requestData: any,
|
||||
dealerData: any,
|
||||
initiatorData: any,
|
||||
claimData?: any
|
||||
claimData: any,
|
||||
rejectionReason: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const canSend = await shouldSendEmail(
|
||||
dealerData.userId,
|
||||
EmailNotificationType.APPROVAL_REQUEST // Use approval_request type for preferences
|
||||
EmailNotificationType.RE_QUOTATION
|
||||
);
|
||||
|
||||
if (!canSend) {
|
||||
logger.info(`Email skipped (preferences): Dealer Completion Required for ${dealerData.email}`);
|
||||
logger.info(`Email skipped (preferences): Re-Quotation Required for ${dealerData.email}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1011,7 +1029,7 @@ export class EmailNotificationService {
|
||||
let dueDate: string | undefined;
|
||||
if (claimData?.tatHours) {
|
||||
const dueDateObj = dayjs().add(claimData.tatHours, 'hour');
|
||||
dueDate = dueDateObj.format('MMMM D, YYYY [at] h:mm A');
|
||||
dueDate = dueDateObj.format('MMMM D, YYYY');
|
||||
}
|
||||
|
||||
const data: DealerProposalRequiredData = {
|
||||
@ -1032,7 +1050,77 @@ export class EmailNotificationService {
|
||||
tatHours: claimData?.tatHours,
|
||||
dueDate: dueDate,
|
||||
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
|
||||
companyName: CompanyInfo.name
|
||||
companyName: CompanyInfo.name,
|
||||
customMessage: rejectionReason
|
||||
};
|
||||
|
||||
const html = getDealerProposalRequiredEmail(data);
|
||||
const subject = `[${requestData.requestNumber}] Revised Quotation Requested - ${data.activityName}`;
|
||||
|
||||
const result = await emailService.sendEmail({
|
||||
to: dealerData.email,
|
||||
subject,
|
||||
html
|
||||
});
|
||||
|
||||
if (result.previewUrl) {
|
||||
logger.info(`📧 Re-Quotation Required Email Preview: ${result.previewUrl}`);
|
||||
}
|
||||
logger.info(`✅ Re-Quotation Required email sent to ${dealerData.email} for request ${requestData.requestNumber}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to send Re-Quotation Required email:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 12b. Send Dealer Completion Documents Required Email
|
||||
*/
|
||||
async sendDealerCompletionRequired(
|
||||
requestData: any,
|
||||
dealerData: any,
|
||||
initiatorData: any,
|
||||
claimData?: any,
|
||||
customMessage?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const canSend = await shouldSendEmail(
|
||||
dealerData.userId,
|
||||
EmailNotificationType.APPROVAL_REQUEST // Use approval_request type for preferences
|
||||
);
|
||||
|
||||
if (!canSend) {
|
||||
logger.info(`Email skipped (preferences): Dealer Completion Required for ${dealerData.email}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate due date from TAT if available
|
||||
let dueDate: string | undefined;
|
||||
if (claimData?.tatHours) {
|
||||
const dueDateObj = dayjs().add(claimData.tatHours, 'hour');
|
||||
dueDate = dueDateObj.format('MMMM D, YYYY');
|
||||
}
|
||||
|
||||
const data: DealerProposalRequiredData = {
|
||||
recipientName: dealerData.displayName || dealerData.email,
|
||||
requestId: requestData.requestNumber,
|
||||
requestTitle: requestData.title,
|
||||
dealerName: dealerData.displayName || dealerData.email || claimData?.dealerName || 'Dealer',
|
||||
initiatorName: initiatorData.displayName || initiatorData.email,
|
||||
activityName: claimData?.activityName || requestData.title,
|
||||
activityType: claimData?.activityType || 'N/A',
|
||||
activityDate: claimData?.activityDate ? this.formatDate(claimData.activityDate) : undefined,
|
||||
location: claimData?.location,
|
||||
estimatedBudget: claimData?.estimatedBudget,
|
||||
requestDate: this.formatDate(requestData.createdAt),
|
||||
requestTime: this.formatTime(requestData.createdAt),
|
||||
requestDescription: requestData.description || '',
|
||||
priority: requestData.priority || 'MEDIUM',
|
||||
tatHours: claimData?.tatHours,
|
||||
dueDate: dueDate,
|
||||
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
|
||||
companyName: CompanyInfo.name,
|
||||
customMessage
|
||||
};
|
||||
|
||||
const html = getDealerCompletionRequiredEmail(data);
|
||||
|
||||
@ -315,6 +315,7 @@ class NotificationService {
|
||||
'form16_debit_note': null,
|
||||
'form16_alert_submit': null,
|
||||
'form16_reminder': null,
|
||||
're_quotation': EmailNotificationType.RE_QUOTATION,
|
||||
};
|
||||
|
||||
const emailType = emailTypeMap[payload.type || ''];
|
||||
@ -482,12 +483,16 @@ class NotificationService {
|
||||
order: [['levelNumber', 'ASC']]
|
||||
});
|
||||
|
||||
// Find the level that matches this approver - PRIORITIZE PENDING LEVEL
|
||||
// This ensures that if a user has multiple steps (e.g., Step 1 and Step 2),
|
||||
// we pick the one that actually needs action (Step 2) rather than the first one (Step 1)
|
||||
let matchingLevel = allLevels.find((l: any) => l.approverId === userId && l.status === 'PENDING');
|
||||
// Find the level that matches this approver and the current request level
|
||||
// This ensures we pick the step that actually needs action (e.g. Step 1 for re-quotation)
|
||||
const currentLevelNumber = requestData.currentLevel;
|
||||
let matchingLevel = allLevels.find((l: any) => l.approverId === userId && l.levelNumber === currentLevelNumber);
|
||||
|
||||
// Fallback to any level if no exact match found (e.g. if currentLevel isn't set yet)
|
||||
if (!matchingLevel) {
|
||||
matchingLevel = allLevels.find((l: any) => l.approverId === userId && l.status === 'PENDING');
|
||||
}
|
||||
|
||||
// Fallback to any level if no pending level found (though for assignment there should be one)
|
||||
if (!matchingLevel) {
|
||||
matchingLevel = allLevels.find((l: any) => l.approverId === userId);
|
||||
}
|
||||
@ -1138,6 +1143,25 @@ class NotificationService {
|
||||
}
|
||||
break;
|
||||
|
||||
case 're_quotation':
|
||||
{
|
||||
const dealerData = payload.metadata?.dealerData || { userId: null, email: payload.metadata?.dealerEmail, displayName: payload.metadata?.dealerName };
|
||||
const rejectionReason = payload.body || payload.metadata?.rejectionReason || 'Revised Quotation Requested';
|
||||
|
||||
// Get claim info if available
|
||||
const { DealerClaimDetails } = await import('@models/index');
|
||||
const claim = await DealerClaimDetails.findOne({ where: { requestId: payload.requestId } });
|
||||
|
||||
await emailNotificationService.sendReQuotationRequired(
|
||||
requestData,
|
||||
dealerData,
|
||||
initiatorData,
|
||||
claim ? claim.toJSON() : {},
|
||||
rejectionReason
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.info(`[Email] No email configured for notification type: ${notificationType}`);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user