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 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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/radix-vendor-GwO0o3Qg.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-BTBPSQfW.js">
|
||||
|
||||
@ -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',
|
||||
SUMMARY_SHARED = 'summary_shared'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -36,4 +36,5 @@ export { getCompletionDocumentsSubmittedEmail } from './completionDocumentsSubmi
|
||||
export { getEInvoiceGeneratedEmail } from './einvoiceGenerated.template';
|
||||
export { getCreditNoteSentEmail } from './creditNoteSent.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;
|
||||
}
|
||||
|
||||
export interface SummarySharedData extends BaseEmailData {
|
||||
initiatorName: string;
|
||||
sharedByName: string;
|
||||
sharedDate: string;
|
||||
sharedTime: string;
|
||||
requestNumber: string;
|
||||
requestType: string;
|
||||
}
|
||||
|
||||
export interface SpectatorAddedData extends BaseEmailData {
|
||||
spectatorName: 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 { approvalActionSchema, approvalParamsSchema } from '../validators/approval.validator';
|
||||
import { asyncHandler } from '../middlewares/errorHandler.middleware';
|
||||
import { requireParticipantTypes } from '../middlewares/authorization.middleware';
|
||||
import { requireParticipantTypes, requireApproverAdditionRights } from '../middlewares/authorization.middleware';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
@ -737,6 +737,7 @@ router.get('/work-notes/attachments/:attachmentId/download',
|
||||
router.post('/:id/participants/approver',
|
||||
authenticateToken,
|
||||
validateParams(workflowParamsSchema),
|
||||
requireApproverAdditionRights(),
|
||||
asyncHandler(async (req: any, res: Response) => {
|
||||
const workflowService = new WorkflowService();
|
||||
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
|
||||
router.post('/:id/approvers/at-level',
|
||||
authenticateToken,
|
||||
requireParticipantTypes(['INITIATOR', 'APPROVER']), // Only initiator or approvers can add new approvers
|
||||
requireApproverAdditionRights(),
|
||||
validateParams(workflowParamsSchema),
|
||||
asyncHandler(async (req: any, res: Response) => {
|
||||
const workflowService = new WorkflowService();
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
getSpectatorAddedEmail,
|
||||
getApproverSkippedEmail,
|
||||
getRequestClosedEmail,
|
||||
getSummarySharedEmail,
|
||||
getDealerProposalSubmittedEmail,
|
||||
getDealerProposalRequiredEmail,
|
||||
getDealerCompletionRequiredEmail,
|
||||
@ -44,6 +45,7 @@ import {
|
||||
SpectatorAddedData,
|
||||
ApproverSkippedData,
|
||||
RequestClosedData,
|
||||
SummarySharedData,
|
||||
DealerProposalSubmittedData,
|
||||
DealerProposalRequiredData,
|
||||
ActivityCreatedData,
|
||||
@ -1440,6 +1442,60 @@ export class EmailNotificationService {
|
||||
// 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
|
||||
|
||||
@ -25,7 +25,7 @@ interface NotificationPayload {
|
||||
|
||||
const appDomain = process.env.APP_DOMAIN || 'royalenfield.com';
|
||||
|
||||
class NotificationService {
|
||||
export class NotificationService {
|
||||
private userIdToSubscriptions: Map<string, PushSubscription[]> = new Map();
|
||||
|
||||
configure(vapidPublicKey?: string, vapidPrivateKey?: string, mailto?: string) {
|
||||
@ -307,7 +307,8 @@ class NotificationService {
|
||||
'einvoice_generated': EmailNotificationType.EINVOICE_GENERATED,
|
||||
'credit_note_sent': EmailNotificationType.CREDIT_NOTE_SENT,
|
||||
'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 || ''];
|
||||
@ -808,6 +809,22 @@ class NotificationService {
|
||||
}
|
||||
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':
|
||||
{
|
||||
// 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 logger from '@utils/logger';
|
||||
import dayjs from 'dayjs';
|
||||
import { NotificationService } from './notification.service';
|
||||
|
||||
export class SummaryService {
|
||||
/**
|
||||
@ -674,6 +675,36 @@ export class SummaryService {
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (error) {
|
||||
logger.error(`[Summary] Failed to share summary ${summaryId}:`, error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user