shared summary notify and template added for it current approver allowed to add approver
This commit is contained in:
parent
b5f3513bd1
commit
02c5187232
@ -1 +1 @@
|
|||||||
import{a as s}from"./index-hYhqmPqT.js";import"./radix-vendor-GwO0o3Qg.js";import"./charts-vendor-waDbLeao.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-3qilyUHW.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-Bmv9jJki.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-BmeYOVo5.js";import"./radix-vendor-GwO0o3Qg.js";import"./charts-vendor-waDbLeao.js";import"./utils-vendor-BTBPSQfW.js";import"./ui-vendor-3qilyUHW.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-Bmv9jJki.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 -->
|
<!-- 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-hYhqmPqT.js"></script>
|
<script type="module" crossorigin src="/assets/index-BmeYOVo5.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-waDbLeao.js">
|
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-waDbLeao.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-GwO0o3Qg.js">
|
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-GwO0o3Qg.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||||
|
|||||||
@ -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',
|
||||||
|
SUMMARY_SHARED = 'summary_shared'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -36,4 +36,5 @@ export { getCompletionDocumentsSubmittedEmail } from './completionDocumentsSubmi
|
|||||||
export { getEInvoiceGeneratedEmail } from './einvoiceGenerated.template';
|
export { getEInvoiceGeneratedEmail } from './einvoiceGenerated.template';
|
||||||
export { getCreditNoteSentEmail } from './creditNoteSent.template';
|
export { getCreditNoteSentEmail } from './creditNoteSent.template';
|
||||||
export { getAdditionalDocumentAddedEmail } from './additionalDocumentAdded.template';
|
export { getAdditionalDocumentAddedEmail } from './additionalDocumentAdded.template';
|
||||||
|
export { getSummarySharedEmail } from './summaryShared.template';
|
||||||
|
|
||||||
|
|||||||
116
src/emailtemplates/summaryShared.template.ts
Normal file
116
src/emailtemplates/summaryShared.template.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Summary Shared Email Template
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SummarySharedData } from './types';
|
||||||
|
import { getEmailFooter, getEmailHeader, HeaderStyles, getResponsiveStyles, getEmailContainerStyles } from './helpers';
|
||||||
|
import { getBrandedHeader } from './branding.config';
|
||||||
|
|
||||||
|
export function getSummarySharedEmail(data: SummarySharedData): string {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<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 Summary Shared</title>
|
||||||
|
${getResponsiveStyles()}
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; background-color: #f4f4f4;">
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f4f4f4;" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 40px 0;">
|
||||||
|
<table role="presentation" class="email-container" style="${getEmailContainerStyles()}" cellpadding="0" cellspacing="0">
|
||||||
|
${getEmailHeader(getBrandedHeader({
|
||||||
|
title: 'Summary Shared',
|
||||||
|
...HeaderStyles.info
|
||||||
|
}))}
|
||||||
|
|
||||||
|
<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: #007bff;">${data.recipientName}</strong>,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 0 0 30px; color: #666666; font-size: 16px; line-height: 1.6;">
|
||||||
|
<strong>${data.sharedByName}</strong> has shared a summary for request <strong>${data.requestNumber}</strong> with you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse; background-color: #f8f9fa; border-radius: 6px; margin-bottom: 30px;" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 25px;">
|
||||||
|
<h2 style="margin: 0 0 20px; color: #333333; font-size: 18px; font-weight: 600;">Request Details</h2>
|
||||||
|
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse;" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666666; font-size: 14px; width: 140px;">
|
||||||
|
<strong>Request ID:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
|
||||||
|
${data.requestNumber}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
|
||||||
|
<strong>Title:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
|
||||||
|
${data.requestTitle || 'N/A'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
|
||||||
|
<strong>Initiator:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
|
||||||
|
${data.initiatorName}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
|
||||||
|
<strong>Shared On:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
|
||||||
|
${data.sharedDate} at ${data.sharedTime}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; color: #666666; font-size: 14px;">
|
||||||
|
<strong>Type:</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; color: #333333; font-size: 14px;">
|
||||||
|
${data.requestType}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<a href="${data.viewDetailsLink}" class="cta-button" style="display: inline-block; padding: 15px 40px; background-color: #1a1a1a; color: #ffffff; text-decoration: none; text-align: center; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); min-width: 200px;">
|
||||||
|
View Shared Summary
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6; text-align: center;">
|
||||||
|
You have been granted access to view the summary and conclusion of this request.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
${getEmailFooter(data.companyName)}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -137,6 +137,15 @@ export interface RequestClosedData extends BaseEmailData {
|
|||||||
documentsCount: number;
|
documentsCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SummarySharedData extends BaseEmailData {
|
||||||
|
initiatorName: string;
|
||||||
|
sharedByName: string;
|
||||||
|
sharedDate: string;
|
||||||
|
sharedTime: string;
|
||||||
|
requestNumber: string;
|
||||||
|
requestType: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SpectatorAddedData extends BaseEmailData {
|
export interface SpectatorAddedData extends BaseEmailData {
|
||||||
spectatorName: string;
|
spectatorName: string;
|
||||||
addedByName?: string;
|
addedByName?: string;
|
||||||
|
|||||||
@ -256,3 +256,68 @@ export function hasAdminAccess(user: any): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware: requireApproverAdditionRights
|
||||||
|
*
|
||||||
|
* Restricts adding new approvers to:
|
||||||
|
* 1. The Initiator
|
||||||
|
* 2. The Current Active Approver (status IN_PROGRESS)
|
||||||
|
* 3. Only for CUSTOM/NON_TEMPLATIZED workflows
|
||||||
|
*/
|
||||||
|
export function requireApproverAdditionRights() {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const userId: string | undefined = (req as any).user?.userId || (req as any).user?.id;
|
||||||
|
const requestIdentifier: string | undefined = (req.params as any)?.id;
|
||||||
|
if (!userId || !requestIdentifier) {
|
||||||
|
return res.status(403).json({ success: false, error: 'Forbidden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve requestIdentifier to actual requestId (UUID)
|
||||||
|
const workflow = await findWorkflowByIdentifier(requestIdentifier);
|
||||||
|
if (!workflow) {
|
||||||
|
return res.status(404).json({ success: false, error: 'Workflow not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Scope Check: Exclude Dealer Claims and Template-based workflows
|
||||||
|
const templateType = (workflow as any).templateType;
|
||||||
|
const workflowType = (workflow as any).workflowType;
|
||||||
|
|
||||||
|
const isCustom = templateType === 'CUSTOM' || workflowType === 'NON_TEMPLATIZED';
|
||||||
|
|
||||||
|
if (!isCustom) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Dynamic approver addition is not allowed for this workflow type.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Permission Check: Initiator
|
||||||
|
if ((workflow as any).initiatorId === userId) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Permission Check: Current Active Approver
|
||||||
|
const { ApprovalLevel } = await import('@models/ApprovalLevel');
|
||||||
|
const currentApprover = await ApprovalLevel.findOne({
|
||||||
|
where: {
|
||||||
|
requestId: (workflow as any).requestId,
|
||||||
|
approverId: userId,
|
||||||
|
status: 'IN_PROGRESS'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentApprover) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Only the initiator or the current active approver can add new approvers.'
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Approver addition rights check failed:', err);
|
||||||
|
return res.status(500).json({ success: false, error: 'Authorization check failed' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { validateBody, validateParams } from '../middlewares/validate.middleware
|
|||||||
import { createWorkflowSchema, updateWorkflowSchema, workflowParamsSchema } from '../validators/workflow.validator';
|
import { createWorkflowSchema, updateWorkflowSchema, workflowParamsSchema } from '../validators/workflow.validator';
|
||||||
import { approvalActionSchema, approvalParamsSchema } from '../validators/approval.validator';
|
import { approvalActionSchema, approvalParamsSchema } from '../validators/approval.validator';
|
||||||
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
||||||
import { requireParticipantTypes } from '../middlewares/authorization.middleware';
|
import { requireParticipantTypes, requireApproverAdditionRights } from '../middlewares/authorization.middleware';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
@ -737,6 +737,7 @@ router.get('/work-notes/attachments/:attachmentId/download',
|
|||||||
router.post('/:id/participants/approver',
|
router.post('/:id/participants/approver',
|
||||||
authenticateToken,
|
authenticateToken,
|
||||||
validateParams(workflowParamsSchema),
|
validateParams(workflowParamsSchema),
|
||||||
|
requireApproverAdditionRights(),
|
||||||
asyncHandler(async (req: any, res: Response) => {
|
asyncHandler(async (req: any, res: Response) => {
|
||||||
const workflowService = new WorkflowService();
|
const workflowService = new WorkflowService();
|
||||||
const wf = await (workflowService as any).findWorkflowByIdentifier(req.params.id);
|
const wf = await (workflowService as any).findWorkflowByIdentifier(req.params.id);
|
||||||
@ -814,7 +815,7 @@ router.post('/:id/approvals/:levelId/skip',
|
|||||||
// Add approver at specific level with level shifting
|
// Add approver at specific level with level shifting
|
||||||
router.post('/:id/approvers/at-level',
|
router.post('/:id/approvers/at-level',
|
||||||
authenticateToken,
|
authenticateToken,
|
||||||
requireParticipantTypes(['INITIATOR', 'APPROVER']), // Only initiator or approvers can add new approvers
|
requireApproverAdditionRights(),
|
||||||
validateParams(workflowParamsSchema),
|
validateParams(workflowParamsSchema),
|
||||||
asyncHandler(async (req: any, res: Response) => {
|
asyncHandler(async (req: any, res: Response) => {
|
||||||
const workflowService = new WorkflowService();
|
const workflowService = new WorkflowService();
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
getSpectatorAddedEmail,
|
getSpectatorAddedEmail,
|
||||||
getApproverSkippedEmail,
|
getApproverSkippedEmail,
|
||||||
getRequestClosedEmail,
|
getRequestClosedEmail,
|
||||||
|
getSummarySharedEmail,
|
||||||
getDealerProposalSubmittedEmail,
|
getDealerProposalSubmittedEmail,
|
||||||
getDealerProposalRequiredEmail,
|
getDealerProposalRequiredEmail,
|
||||||
getDealerCompletionRequiredEmail,
|
getDealerCompletionRequiredEmail,
|
||||||
@ -44,6 +45,7 @@ import {
|
|||||||
SpectatorAddedData,
|
SpectatorAddedData,
|
||||||
ApproverSkippedData,
|
ApproverSkippedData,
|
||||||
RequestClosedData,
|
RequestClosedData,
|
||||||
|
SummarySharedData,
|
||||||
DealerProposalSubmittedData,
|
DealerProposalSubmittedData,
|
||||||
DealerProposalRequiredData,
|
DealerProposalRequiredData,
|
||||||
ActivityCreatedData,
|
ActivityCreatedData,
|
||||||
@ -1440,6 +1442,60 @@ export class EmailNotificationService {
|
|||||||
// Don't throw - email failure shouldn't block document upload
|
// Don't throw - email failure shouldn't block document upload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 11. Send Summary Shared Email
|
||||||
|
*/
|
||||||
|
async sendSummaryShared(
|
||||||
|
requestData: any,
|
||||||
|
recipientData: any,
|
||||||
|
initiatorData: any,
|
||||||
|
sharedByData: any,
|
||||||
|
sharedSummaryId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const canSend = await shouldSendEmail(
|
||||||
|
recipientData.userId,
|
||||||
|
EmailNotificationType.SUMMARY_SHARED
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canSend) {
|
||||||
|
logger.info(`Email skipped (preferences): Summary Shared for ${recipientData.email}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: SummarySharedData = {
|
||||||
|
recipientName: recipientData.displayName || recipientData.email,
|
||||||
|
requestId: requestData.requestNumber,
|
||||||
|
requestTitle: requestData.title,
|
||||||
|
requestNumber: requestData.requestNumber,
|
||||||
|
initiatorName: initiatorData.displayName || initiatorData.email,
|
||||||
|
sharedByName: sharedByData.displayName || sharedByData.email,
|
||||||
|
sharedDate: this.formatDate(new Date()),
|
||||||
|
sharedTime: this.formatTime(new Date()),
|
||||||
|
requestType: getTemplateTypeLabel(requestData.templateType || requestData.requestType),
|
||||||
|
viewDetailsLink: sharedSummaryId
|
||||||
|
? `${this.frontendUrl}/shared-summaries/${sharedSummaryId}`
|
||||||
|
: `${this.frontendUrl}/request/${requestData.requestNumber}/summary`,
|
||||||
|
companyName: CompanyInfo.name
|
||||||
|
};
|
||||||
|
|
||||||
|
const html = getSummarySharedEmail(data);
|
||||||
|
const subject = `${requestData.requestNumber} - ${requestData.title} - Request Summary Shared with You`;
|
||||||
|
|
||||||
|
const result = await emailService.sendEmail({
|
||||||
|
to: recipientData.email,
|
||||||
|
subject,
|
||||||
|
html
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.previewUrl) {
|
||||||
|
logger.info(`📧 Summary Shared Email Preview: ${result.previewUrl}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send Summary Shared email:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
|||||||
@ -25,7 +25,7 @@ interface NotificationPayload {
|
|||||||
|
|
||||||
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
|
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
|
||||||
|
|
||||||
class NotificationService {
|
export class NotificationService {
|
||||||
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
|
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
|
||||||
|
|
||||||
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
|
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
|
||||||
@ -307,7 +307,8 @@ class NotificationService {
|
|||||||
'einvoice_generated': EmailNotificationType.EINVOICE_GENERATED,
|
'einvoice_generated': EmailNotificationType.EINVOICE_GENERATED,
|
||||||
'credit_note_sent': EmailNotificationType.CREDIT_NOTE_SENT,
|
'credit_note_sent': EmailNotificationType.CREDIT_NOTE_SENT,
|
||||||
'pause_retrigger_request': EmailNotificationType.WORKFLOW_PAUSED, // Use same template as pause
|
'pause_retrigger_request': EmailNotificationType.WORKFLOW_PAUSED, // Use same template as pause
|
||||||
'pause_retriggered': null
|
'pause_retriggered': null,
|
||||||
|
'summary_shared': EmailNotificationType.SUMMARY_SHARED
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailType = emailTypeMap[payload.type || ''];
|
const emailType = emailTypeMap[payload.type || ''];
|
||||||
@ -808,6 +809,22 @@ class NotificationService {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'summary_shared':
|
||||||
|
{
|
||||||
|
const sharedByUserId = payload.metadata?.sharedBy;
|
||||||
|
const sharedSummaryId = payload.metadata?.sharedSummaryId;
|
||||||
|
const sharedByUser = sharedByUserId ? await User.findByPk(sharedByUserId) : null;
|
||||||
|
|
||||||
|
await emailNotificationService.sendSummaryShared(
|
||||||
|
requestData,
|
||||||
|
user, // Recipient
|
||||||
|
initiatorData, // Original request initiator
|
||||||
|
sharedByUser ? sharedByUser.toJSON() : { displayName: 'System' },
|
||||||
|
sharedSummaryId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'pause_retrigger_request':
|
case 'pause_retrigger_request':
|
||||||
{
|
{
|
||||||
// This is when initiator requests approver to resume a paused workflow
|
// This is when initiator requests approver to resume a paused workflow
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import '@models/index'; // Ensure associations are loaded
|
|||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import logger from '@utils/logger';
|
import logger from '@utils/logger';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
|
||||||
export class SummaryService {
|
export class SummaryService {
|
||||||
/**
|
/**
|
||||||
@ -674,6 +675,36 @@ export class SummaryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[Summary] Shared summary ${summaryId} with ${sharedSummaries.length} users`);
|
logger.info(`[Summary] Shared summary ${summaryId} with ${sharedSummaries.length} users`);
|
||||||
|
|
||||||
|
// 🔔 TRIGGER NOTIFICATIONS
|
||||||
|
try {
|
||||||
|
const notificationService = new NotificationService();
|
||||||
|
const request = await WorkflowRequest.findByPk((summary as any).requestId);
|
||||||
|
const sharedByUser = await User.findByPk(sharedBy);
|
||||||
|
|
||||||
|
if (request && sharedSummaries.length > 0) {
|
||||||
|
for (const shared of sharedSummaries) {
|
||||||
|
await notificationService.sendToUsers([shared.sharedWith], {
|
||||||
|
title: 'Summary Shared',
|
||||||
|
body: `${sharedByUser?.displayName || 'Someone'} shared a summary for request ${request.requestNumber} with you.`,
|
||||||
|
requestId: (request as any).requestId,
|
||||||
|
requestNumber: (request as any).requestNumber,
|
||||||
|
type: 'summary_shared',
|
||||||
|
url: `/shared-summaries/${shared.sharedSummaryId}`,
|
||||||
|
metadata: {
|
||||||
|
sharedBy,
|
||||||
|
summaryId,
|
||||||
|
sharedSummaryId: shared.sharedSummaryId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger.info(`[Summary] Triggered individual notifications for ${sharedSummaries.length} users`);
|
||||||
|
}
|
||||||
|
} catch (notifyError) {
|
||||||
|
logger.error('[Summary] Failed to trigger notifications for shared summary:', notifyError);
|
||||||
|
// Don't throw - sharing was successful
|
||||||
|
}
|
||||||
|
|
||||||
return sharedSummaries;
|
return sharedSummaries;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[Summary] Failed to share summary ${summaryId}:`, error);
|
logger.error(`[Summary] Failed to share summary ${summaryId}:`, error);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user