csv filed issue on unique transaction no and also dealer claim templates enhanced

This commit is contained in:
laxmanhalaki 2026-03-13 14:31:27 +05:30
parent 89beffee2e
commit 9c003e9a16
15 changed files with 261 additions and 100 deletions

View File

@ -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

View File

@ -13,7 +13,7 @@
<!-- Preload essential fonts and icons --> <!-- Preload essential fonts and icons -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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/charts-vendor-BVfwAPj-.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js"> <link rel="modulepreload" crossorigin href="/assets/radix-vendor-CYvDqP9X.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js"> <link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">

View File

@ -3,7 +3,7 @@
*/ */
import { ApprovalRequestData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getApprovalRequestEmail(data: ApprovalRequestData): string { export function getApprovalRequestEmail(data: ApprovalRequestData): string {
@ -102,6 +102,9 @@ export function getApprovalRequestEmail(data: ApprovalRequestData): string {
</tr> </tr>
</table> </table>
<!-- Custom Message Section -->
${getCustomMessageSection(data.customMessage)}
<!-- Description (supports rich text HTML including tables) --> <!-- Description (supports rich text HTML including tables) -->
<div style="margin-bottom: 30px;"> <div style="margin-bottom: 30px;">
<h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">Description:</h3> <h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">Description:</h3>

View File

@ -5,7 +5,7 @@
*/ */
import { DealerProposalRequiredData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getDealerCompletionRequiredEmail(data: DealerProposalRequiredData): string { export function getDealerCompletionRequiredEmail(data: DealerProposalRequiredData): string {
@ -103,6 +103,9 @@ export function getDealerCompletionRequiredEmail(data: DealerProposalRequiredDat
</tr> </tr>
</table> </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;"> <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> <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;"> <ul style="margin: 0; padding: 0 0 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">

View File

@ -5,7 +5,7 @@
*/ */
import { DealerProposalRequiredData } from './types'; 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'; import { getBrandedHeader } from './branding.config';
export function getDealerProposalRequiredEmail(data: DealerProposalRequiredData): string { export function getDealerProposalRequiredEmail(data: DealerProposalRequiredData): string {
@ -152,6 +152,9 @@ export function getDealerProposalRequiredEmail(data: DealerProposalRequiredData)
</tr> </tr>
</table> </table>
<!-- Custom Message Section -->
${getCustomMessageSection(data.customMessage)}
<!-- Description (supports rich text HTML including tables) --> <!-- Description (supports rich text HTML including tables) -->
${data.requestDescription ? ` ${data.requestDescription ? `
<div style="margin-bottom: 30px;"> <div style="margin-bottom: 30px;">

View File

@ -32,7 +32,8 @@ export enum EmailNotificationType {
COMPLETION_DOCUMENTS_SUBMITTED = 'completion_documents_submitted', COMPLETION_DOCUMENTS_SUBMITTED = 'completion_documents_submitted',
EINVOICE_GENERATED = 'einvoice_generated', EINVOICE_GENERATED = 'einvoice_generated',
CREDIT_NOTE_SENT = 'credit_note_sent', CREDIT_NOTE_SENT = 'credit_note_sent',
ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added' ADDITIONAL_DOCUMENT_ADDED = 'additional_document_added',
RE_QUOTATION = 're_quotation',
} }
/** /**

View File

@ -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 * Generate action required section for workflow resumed
*/ */

View File

@ -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 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 http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<title>Request Rejected</title> <title>${data.isReturnedForRevision ? 'Request Returned for Revision' : 'Request Rejected'}</title>
${getResponsiveStyles()} ${getResponsiveStyles()}
</head> </head>
<body style="margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f4f4f4;"> <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;"> <td style="padding: 40px 0;">
<table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0"> <table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
${getEmailHeader(getBrandedHeader({ ${getEmailHeader(getBrandedHeader({
title: 'Request Rejected', title: data.isReturnedForRevision ? 'Revision Required' : 'Request Rejected',
...HeaderStyles.error ...(data.isReturnedForRevision ? HeaderStyles.warning : HeaderStyles.error)
}))} }))}
<tr> <tr>
<td style="padding: 40px 30px;"> <td style="padding: 40px 30px;">
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;"> <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>
<p style="margin: 0 0 30px; color: #666666; font-size: 16px; line-height: 1.6;"> <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> </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> <tr>
<td style="padding: 25px;"> <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"> <table role="presentation" style="width: 100%; border-collapse: collapse;" cellpadding="0" cellspacing="0">
<tr> <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> <strong>Request ID:</strong>
</td> </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} ${data.requestId}
</td> </td>
</tr> </tr>
<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>Rejected By:</strong> <strong>Action By:</strong>
</td> </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} ${data.approverName}
</td> </td>
</tr> </tr>
<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>Rejected On:</strong> <strong>Action On:</strong>
</td> </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} ${data.rejectionDate}
</td> </td>
</tr> </tr>
<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> <strong>Time:</strong>
</td> </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} ${data.rejectionTime}
</td> </td>
</tr> </tr>
<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> <strong>Request Type:</strong>
</td> </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} ${data.requestType}
</td> </td>
</tr> </tr>
@ -90,8 +92,8 @@ export function getRejectionNotificationEmail(data: RejectionNotificationData):
</table> </table>
<div style="margin-bottom: 30px;"> <div style="margin-bottom: 30px;">
<h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">Reason for Rejection:</h3> <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 #dc3545; border-radius: 4px; overflow-x: auto;"> <div style="padding: 15px; background-color: #f8f9fa; border-left: 4px solid ${data.isReturnedForRevision ? '#ffc107' : '#dc3545'}; border-radius: 4px; overflow-x: auto;">
${wrapRichText(data.rejectionReason)} ${wrapRichText(data.rejectionReason)}
</div> </div>
</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;"> <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> <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;"> <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> ${data.isReturnedForRevision
<li>Make necessary adjustments to your request</li> ? `<li>Review the requested changes carefully</li>
<li>Submit a new request with the required changes</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> <li>Contact ${data.approverName} for more clarification if needed</li>
</ul> </ul>
</div> </div>

View File

@ -47,6 +47,7 @@ export interface ApprovalRequestData extends BaseEmailData {
requestType: string; requestType: string;
requestDescription: string; requestDescription: string;
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
customMessage?: string;
} }
export interface MultiApproverRequestData extends ApprovalRequestData { export interface MultiApproverRequestData extends ApprovalRequestData {
@ -80,6 +81,7 @@ export interface RejectionNotificationData extends BaseEmailData {
rejectionTime: string; rejectionTime: string;
requestType: string; requestType: string;
rejectionReason: string; rejectionReason: string;
isReturnedForRevision?: boolean;
} }
export interface TATReminderData extends BaseEmailData { export interface TATReminderData extends BaseEmailData {
@ -250,6 +252,7 @@ export interface DealerProposalRequiredData extends BaseEmailData {
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
tatHours?: number; tatHours?: number;
dueDate?: string; dueDate?: string;
customMessage?: string;
} }
export interface AdditionalDocumentAddedData extends BaseEmailData { export interface AdditionalDocumentAddedData extends BaseEmailData {

View File

@ -3615,7 +3615,7 @@ export class DealerClaimService {
const csvData = invoiceItems.map((item: any) => { const csvData = invoiceItems.map((item: any) => {
const row: any = { const row: any = {
TRNS_UNIQ_NO: isNonGst ? '' : (item.transactionCode || ''), TRNS_UNIQ_NO: item.transactionCode || '',
CLAIM_NUMBER: requestNumber, CLAIM_NUMBER: requestNumber,
INV_NUMBER: invoice.invoiceNumber || '', INV_NUMBER: invoice.invoiceNumber || '',
DEALER_CODE: claimDetails.dealerCode, DEALER_CODE: claimDetails.dealerCode,

View File

@ -849,6 +849,23 @@ export class DealerClaimApprovalService {
tatPercentageUsed: 0 tatPercentageUsed: 0
} as any); } 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 userAgent: requestMetadata?.userAgent || undefined
}); });
// Notify the approver of the target level // Notify the approver of the target level (skip if specific re-quotation notification was already sent)
if (targetLevel.approverId) { if (targetLevel.approverId && !isReQuotation) {
await notificationService.sendToUsers([targetLevel.approverId], { 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'}`, 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, requestNumber: (wf as any).requestNumber,
requestId: level.requestId, requestId: level.requestId,
@ -903,15 +920,19 @@ export class DealerClaimApprovalService {
} }
// Notify initiator when request is returned // Notify initiator when request is returned
await notificationService.sendToUsers([(wf as any).initiatorId], { // Skip if they are the same as the dealer (who already got a more specific notification)
title: `Request Returned: ${(wf as any).requestNumber}`, const dealerId = targetLevel.approverId;
body: `Request "${(wf as any).title}" has been returned to level ${targetLevel.levelNumber} for revision. Reason: ${action.rejectionReason || action.comments || 'No reason provided'}`, if ((wf as any).initiatorId && (wf as any).initiatorId !== dealerId) {
requestNumber: (wf as any).requestNumber, await notificationService.sendToUsers([(wf as any).initiatorId], {
requestId: level.requestId, title: `Request Returned: ${(wf as any).requestNumber}`,
url: `/request/${(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'}`,
type: 'rejection', requestNumber: (wf as any).requestNumber,
priority: 'MEDIUM' requestId: level.requestId,
}); url: `/request/${(wf as any).requestNumber}`,
type: 'rejection',
priority: 'MEDIUM'
});
}
} }
// Emit real-time update to all users viewing this request // Emit real-time update to all users viewing this request

View File

@ -92,26 +92,6 @@ export class DealerClaimEmailService implements IWorkflowEmailService {
(levelName.includes('completion') || levelName.includes('documents')) && (levelName.includes('completion') || levelName.includes('documents')) &&
!levelName.includes('proposal'); // Explicitly exclude proposal !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 // Route to appropriate template
if (isDealerCompletionStep) { if (isDealerCompletionStep) {
logger.info(`[DealerClaimEmail] ✅ DEALER COMPLETION step - sending completion documents required email`); logger.info(`[DealerClaimEmail] ✅ DEALER COMPLETION step - sending completion documents required email`);
@ -224,6 +204,18 @@ export class DealerClaimEmailService implements IWorkflowEmailService {
where: { requestId: requestData.requestId } 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 // Enrich requestData with dealer claim-specific information
const enrichedRequestData = { const enrichedRequestData = {
...requestData, ...requestData,
@ -256,7 +248,8 @@ export class DealerClaimEmailService implements IWorkflowEmailService {
approverData, approverData,
initiatorData, initiatorData,
false, // isMultiLevel = false for dealer claim workflows false, // isMultiLevel = false for dealer claim workflows
undefined // No approval chain needed undefined, // No approval chain needed
stageInstructions // Pass as customMessage (contextual instruction)
); );
} }

View File

@ -144,7 +144,8 @@ export class EmailNotificationService {
approverData: any, approverData: any,
initiatorData: any, initiatorData: any,
isMultiLevel: boolean, isMultiLevel: boolean,
approvalChain?: any[] approvalChain?: any[],
customMessage?: string
): Promise<void> { ): Promise<void> {
try { try {
// Check preferences // Check preferences
@ -185,7 +186,8 @@ export class EmailNotificationService {
totalApprovers: approvalChain.length, totalApprovers: approvalChain.length,
approversList: chainData, approversList: chainData,
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name companyName: CompanyInfo.name,
customMessage
}; };
const html = getMultiApproverRequestEmail(data); const html = getMultiApproverRequestEmail(data);
@ -214,7 +216,8 @@ export class EmailNotificationService {
requestDate: this.formatDate(requestData.createdAt), requestDate: this.formatDate(requestData.createdAt),
requestTime: this.formatTime(requestData.createdAt), requestTime: this.formatTime(requestData.createdAt),
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name companyName: CompanyInfo.name,
customMessage
}; };
const html = getApprovalRequestEmail(data); const html = getApprovalRequestEmail(data);
@ -298,6 +301,14 @@ export class EmailNotificationService {
rejectionReason: string rejectionReason: string
): Promise<void> { ): Promise<void> {
try { 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 // Use override for high-priority emails
const canSend = await shouldSendEmailWithOverride( const canSend = await shouldSendEmailWithOverride(
initiatorData.userId, initiatorData.userId,
@ -319,11 +330,14 @@ export class EmailNotificationService {
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType), requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
rejectionReason, rejectionReason,
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name companyName: CompanyInfo.name,
isReturnedForRevision
}; };
const html = getRejectionNotificationEmail(data); 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({ const result = await emailService.sendEmail({
to: initiatorData.email, to: initiatorData.email,
@ -927,7 +941,8 @@ export class EmailNotificationService {
requestData: any, requestData: any,
dealerData: any, dealerData: any,
initiatorData: any, initiatorData: any,
claimData?: any claimData?: any,
customMessage?: string
): Promise<void> { ): Promise<void> {
try { try {
const canSend = await shouldSendEmail( const canSend = await shouldSendEmail(
@ -944,7 +959,7 @@ export class EmailNotificationService {
let dueDate: string | undefined; let dueDate: string | undefined;
if (claimData?.tatHours) { if (claimData?.tatHours) {
const dueDateObj = dayjs().add(claimData.tatHours, 'hour'); 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 = { const data: DealerProposalRequiredData = {
@ -965,7 +980,8 @@ export class EmailNotificationService {
tatHours: claimData?.tatHours, tatHours: claimData?.tatHours,
dueDate: dueDate, dueDate: dueDate,
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl),
companyName: CompanyInfo.name companyName: CompanyInfo.name,
customMessage
}; };
const html = getDealerProposalRequiredEmail(data); 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, requestData: any,
dealerData: any, dealerData: any,
initiatorData: any, initiatorData: any,
claimData?: any claimData: any,
rejectionReason: string
): Promise<void> { ): Promise<void> {
try { try {
const canSend = await shouldSendEmail( const canSend = await shouldSendEmail(
dealerData.userId, dealerData.userId,
EmailNotificationType.APPROVAL_REQUEST // Use approval_request type for preferences EmailNotificationType.RE_QUOTATION
); );
if (!canSend) { 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; return;
} }
@ -1011,7 +1029,7 @@ export class EmailNotificationService {
let dueDate: string | undefined; let dueDate: string | undefined;
if (claimData?.tatHours) { if (claimData?.tatHours) {
const dueDateObj = dayjs().add(claimData.tatHours, 'hour'); 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 = { const data: DealerProposalRequiredData = {
@ -1032,7 +1050,77 @@ export class EmailNotificationService {
tatHours: claimData?.tatHours, tatHours: claimData?.tatHours,
dueDate: dueDate, dueDate: dueDate,
viewDetailsLink: getViewDetailsLink(requestData.requestNumber, this.frontendUrl), 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); const html = getDealerCompletionRequiredEmail(data);

View File

@ -315,6 +315,7 @@ class NotificationService {
'form16_debit_note': null, 'form16_debit_note': null,
'form16_alert_submit': null, 'form16_alert_submit': null,
'form16_reminder': null, 'form16_reminder': null,
're_quotation': EmailNotificationType.RE_QUOTATION,
}; };
const emailType = emailTypeMap[payload.type || '']; const emailType = emailTypeMap[payload.type || ''];
@ -482,12 +483,16 @@ class NotificationService {
order: [['levelNumber', 'ASC']] order: [['levelNumber', 'ASC']]
}); });
// Find the level that matches this approver - PRIORITIZE PENDING LEVEL // Find the level that matches this approver and the current request level
// This ensures that if a user has multiple steps (e.g., Step 1 and Step 2), // This ensures we pick the step that actually needs action (e.g. Step 1 for re-quotation)
// we pick the one that actually needs action (Step 2) rather than the first one (Step 1) const currentLevelNumber = requestData.currentLevel;
let matchingLevel = allLevels.find((l: any) => l.approverId === userId && l.status === 'PENDING'); 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) { if (!matchingLevel) {
matchingLevel = allLevels.find((l: any) => l.approverId === userId); matchingLevel = allLevels.find((l: any) => l.approverId === userId);
} }
@ -1138,6 +1143,25 @@ class NotificationService {
} }
break; 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: default:
logger.info(`[Email] No email configured for notification type: ${notificationType}`); logger.info(`[Email] No email configured for notification type: ${notificationType}`);
} }