Re_Backend/src/emailtemplates/helpers.ts

869 lines
26 KiB
TypeScript

/**
* Email Template Helper Functions
*
* Reusable functions for generating dynamic email sections
*/
import { ApprovalChainItem } from './types';
/**
* Get rich text styles for email-safe HTML rendering
* Styles common HTML elements from rich text editors
*/
export function getRichTextStyles(): string {
return `
<style>
/* Rich text content styles */
.rich-text-content p {
margin: 0 0 12px 0;
color: #333333;
font-size: 14px;
line-height: 1.6;
}
.rich-text-content p:last-child {
margin-bottom: 0;
}
.rich-text-content strong,
.rich-text-content b {
font-weight: 600;
color: #1a1a1a;
}
.rich-text-content em,
.rich-text-content i {
font-style: italic;
}
.rich-text-content u {
text-decoration: underline;
}
.rich-text-content a {
color: #667eea;
text-decoration: underline;
}
.rich-text-content ul,
.rich-text-content ol {
margin: 0 0 12px 0;
padding-left: 25px;
color: #333333;
font-size: 14px;
line-height: 1.6;
}
.rich-text-content li {
margin-bottom: 8px;
}
.rich-text-content h1,
.rich-text-content h2,
.rich-text-content h3,
.rich-text-content h4 {
margin: 0 0 12px 0;
color: #1a1a1a;
font-weight: 600;
line-height: 1.3;
}
.rich-text-content h1 { font-size: 20px; }
.rich-text-content h2 { font-size: 18px; }
.rich-text-content h3 { font-size: 16px; }
.rich-text-content h4 { font-size: 14px; }
.rich-text-content blockquote {
margin: 0 0 12px 0;
padding: 12px 15px;
border-left: 3px solid #667eea;
background-color: #f8f9fa;
color: #666666;
font-style: italic;
}
.rich-text-content code {
padding: 2px 6px;
background-color: #f4f4f4;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #667eea;
}
.rich-text-content pre {
margin: 0 0 12px 0;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
}
.rich-text-content hr {
border: none;
border-top: 1px solid #e9ecef;
margin: 20px 0;
}
.rich-text-content img {
max-width: 100%;
height: auto;
display: block;
margin: 12px 0;
}
/* Table styles for rich text content */
.rich-text-content table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
background-color: #ffffff;
border: 1px solid #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.rich-text-content table thead {
background-color: #f8f9fa;
}
.rich-text-content table th {
padding: 12px 15px;
text-align: left;
font-weight: 600;
color: #1a1a1a;
font-size: 14px;
border-bottom: 2px solid #e9ecef;
background-color: #f8f9fa;
}
.rich-text-content table td {
padding: 10px 15px;
color: #333333;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
vertical-align: top;
}
.rich-text-content table tbody tr:last-child td {
border-bottom: none;
}
.rich-text-content table tbody tr:hover {
background-color: #f8f9fa;
}
.rich-text-content table tbody tr:nth-child(even) {
background-color: #fafafa;
}
.rich-text-content table tbody tr:nth-child(even):hover {
background-color: #f0f0f0;
}
/* Mobile adjustments for rich text */
@media only screen and (max-width: 600px) {
.rich-text-content p,
.rich-text-content ul,
.rich-text-content ol {
font-size: 13px !important;
}
.rich-text-content h1 { font-size: 18px !important; }
.rich-text-content h2 { font-size: 16px !important; }
.rich-text-content h3 { font-size: 15px !important; }
.rich-text-content h4 { font-size: 14px !important; }
/* Make tables scrollable on mobile - keep table structure */
.rich-text-content table {
width: 100% !important;
max-width: 100% !important;
display: table !important;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.rich-text-content table th,
.rich-text-content table td {
padding: 8px 12px !important;
font-size: 13px !important;
}
}
</style>
`;
}
/**
* Wrap rich text content with proper styling
* Use this for descriptions and comments from rich text editors
* Enhanced to support tables with inline styles for email client compatibility
*/
export function wrapRichText(htmlContent: string): string {
if (!htmlContent) return '';
// Process tables to add inline styles for email client compatibility
// Email clients often strip CSS classes, so we need inline styles
let processedContent = htmlContent;
// Add inline styles to tables for better email client support
processedContent = processedContent.replace(
/<table([^>]*)>/gi,
(match, attrs) => {
// Check if style attribute already exists
if (attrs && attrs.includes('style=')) {
return match; // Keep existing styles
}
return `<table${attrs} style="width: 100%; border-collapse: collapse; margin: 12px 0; background-color: #ffffff; border: 1px solid #e9ecef;">`;
}
);
// Add inline styles to table headers
processedContent = processedContent.replace(
/<th([^>]*)>/gi,
(match, attrs) => {
if (attrs && attrs.includes('style=')) {
return match;
}
return `<th${attrs} style="padding: 12px 15px; text-align: left; font-weight: 600; color: #1a1a1a; font-size: 14px; border-bottom: 2px solid #e9ecef; background-color: #f8f9fa;">`;
}
);
// Add inline styles to table cells
processedContent = processedContent.replace(
/<td([^>]*)>/gi,
(match, attrs) => {
if (attrs && attrs.includes('style=')) {
return match;
}
return `<td${attrs} style="padding: 10px 15px; color: #333333; font-size: 14px; border-bottom: 1px solid #f0f0f0; vertical-align: top;">`;
}
);
// Add inline styles to table rows for hover effect (email-safe)
processedContent = processedContent.replace(
/<tr([^>]*)>/gi,
(match, attrs) => {
if (attrs && attrs.includes('style=')) {
return match;
}
return `<tr${attrs} style="border-bottom: 1px solid #f0f0f0;">`;
}
);
// Add inline styles to thead
processedContent = processedContent.replace(
/<thead([^>]*)>/gi,
(match, attrs) => {
if (attrs && attrs.includes('style=')) {
return match;
}
return `<thead${attrs} style="background-color: #f8f9fa;">`;
}
);
// Add inline styles to tbody (if present)
processedContent = processedContent.replace(
/<tbody([^>]*)>/gi,
(match, attrs) => {
if (attrs && attrs.includes('style=')) {
return match;
}
return `<tbody${attrs} style="">`;
}
);
// Add inline styles to tfoot (if present)
processedContent = processedContent.replace(
/<tfoot([^>]*)>/gi,
(match, attrs) => {
if (attrs && attrs.includes('style=')) {
return match;
}
return `<tfoot${attrs} style="background-color: #f8f9fa; font-weight: 600;">`;
}
);
return `
<div class="rich-text-content" style="color: #666666; font-size: 14px; line-height: 1.6;">
${processedContent}
</div>
`;
}
/**
* Get inline styles for email container table
* This ensures width is preserved when emails are forwarded
* Email clients often strip CSS classes, so inline styles are critical
*/
export function getEmailContainerStyles(): string {
return 'width: 95%; max-width: 1200px; min-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);';
}
/**
* Generate all email styles (responsive + rich text)
* Desktop-first design (optimized for browser) with mobile responsive breakpoints
*/
export function getResponsiveStyles(): string {
return `
${getRichTextStyles()}
<style>
/* Reset styles */
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
table {
border-collapse: collapse !important;
}
/* Desktop-first base styles */
.email-container {
width: 95% !important;
max-width: 1200px !important;
min-width: 600px !important; /* Prevent shrinking below 600px when forwarded */
}
/* Force full width for forwarded emails - use inline styles in templates */
table.email-container {
width: 95% !important;
max-width: 1200px !important;
min-width: 600px !important;
}
/* Wrapper table to force full width even when forwarded */
.email-wrapper {
width: 100% !important;
max-width: 100% !important;
}
.email-content {
padding: 50px 40px;
}
.email-header {
padding: 40px 40px 35px;
}
.email-footer {
padding: 30px 40px;
}
/* Desktop typography */
.header-title {
font-size: 24px;
letter-spacing: 0.5px;
line-height: 1.3;
}
.header-subtitle {
font-size: 14px;
}
/* Desktop detail tables - side by side */
.detail-table {
width: 100%;
}
.detail-table td {
font-size: 15px;
padding: 10px 0;
display: table-cell;
width: auto;
vertical-align: top;
}
.detail-label {
width: 200px;
font-weight: 600;
color: #666666;
}
.detail-box {
padding: 30px;
}
/* Desktop button styles */
.cta-button {
display: inline-block;
padding: 16px 45px;
font-size: 16px;
min-width: 220px;
}
/* Tablet responsive styles */
@media only screen and (max-width: 1200px) {
.email-container {
width: 95% !important;
max-width: 95% !important;
}
.email-content {
padding: 40px 30px !important;
}
.email-header {
padding: 35px 30px 30px !important;
}
.email-footer {
padding: 25px 30px !important;
}
}
/* Mobile responsive styles */
@media only screen and (max-width: 600px) {
/* Container adjustments */
.email-container {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
border-radius: 0 !important;
}
/* Header adjustments */
.email-header {
padding: 25px 20px 30px !important;
}
/* Content adjustments */
.email-content {
padding: 30px 20px !important;
}
/* Footer adjustments */
.email-footer {
padding: 25px 20px !important;
}
/* Logo responsive */
.logo-img {
width: 180px !important;
height: auto !important;
max-width: 90% !important;
}
/* Typography adjustments */
.header-title {
font-size: 20px !important;
letter-spacing: 0.5px !important;
line-height: 1.4 !important;
}
.header-subtitle {
font-size: 12px !important;
}
/* Detail tables - stack on mobile */
.detail-box {
padding: 20px 15px !important;
}
.detail-table td {
font-size: 14px !important;
padding: 8px 0 !important;
display: block !important;
width: 100% !important;
}
.detail-label {
font-weight: 600 !important;
margin-bottom: 4px !important;
width: 100% !important;
}
/* Button adjustments */
.cta-button {
display: block !important;
width: 100% !important;
max-width: 100% !important;
padding: 16px 20px !important;
font-size: 16px !important;
box-sizing: border-box !important;
min-width: auto !important;
}
/* Section adjustments */
.info-section {
padding: 18px 15px !important;
margin-bottom: 20px !important;
}
.section-title {
font-size: 16px !important;
}
.section-text {
font-size: 14px !important;
line-height: 1.6 !important;
}
/* List items */
.info-section ul {
padding-left: 20px !important;
font-size: 14px !important;
}
.info-section li {
margin-bottom: 8px !important;
}
}
/* Very small screens (320px) */
@media only screen and (max-width: 400px) {
.logo-img {
width: 150px !important;
}
.header-title {
font-size: 18px !important;
}
.email-content {
padding: 25px 15px !important;
}
}
</style>
`;
}
/**
* Email header configuration types
*/
export interface EmailHeaderConfig {
title: string;
subtitle?: string;
gradientFrom: string;
gradientTo: string;
logoUrl?: string;
logoAlt?: string;
logoWidth?: number;
logoHeight?: number;
productName?: string; // e.g., "RE Flow"
}
/**
* Generate professional email header with Royal Enfield branding
* Black background with logo, product name, and gold accent line
* Fully responsive for mobile devices
*/
export function getEmailHeader(config: EmailHeaderConfig): string {
const logoWidth = config.logoWidth || 220;
const logoHeight = config.logoHeight || 65;
const logoSection = config.logoUrl ? `
<div style="margin-bottom: ${config.productName ? '15px' : '20px'};">
<img
src="${config.logoUrl}"
alt="${config.logoAlt || 'Company Logo'}"
class="logo-img"
style="width: ${logoWidth}px; height: auto; max-width: 100%; display: inline-block;"
/>
</div>
` : '';
const productNameSection = config.productName ? `
<p style="margin: 0 0 20px; color: #DEB219; font-size: 16px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase;">
${config.productName}
</p>
` : '';
const dividerLine = `
<div style="width: 100%; height: 3px; background-color: #DEB219; margin: ${config.logoUrl || config.productName ? '20px 0 25px' : '0 0 25px'};"></div>
`;
const subtitleSection = config.subtitle ? `
<p class="header-subtitle" style="margin: 12px 0 0; color: #DEB219; font-size: 14px; font-weight: 500; letter-spacing: 0.5px; text-transform: uppercase;">${config.subtitle}</p>
` : '';
return `
<tr>
<td class="email-header" style="background-color: #1a1a1a; padding: ${config.logoUrl || config.productName ? '30px 30px 35px' : '35px 30px'}; text-align: center; border-radius: 8px 8px 0 0;">
${logoSection}
${productNameSection}
${dividerLine}
<h1 class="header-title" style="margin: 0; color: #ffffff; font-size: 26px; font-weight: 600; letter-spacing: 1.5px; line-height: 1.3; text-transform: uppercase;">${config.title}</h1>
${subtitleSection}
</td>
</tr>
`;
}
/**
* Royal Enfield Brand Colors (Official)
*/
export const BrandColors = {
red: '#DB281B', // Royal Enfield Red (Official)
black: '#1a1a1a', // Royal Enfield Black
gold: '#DEB219', // Royal Enfield Gold
darkRed: '#A51F16', // Darker red for accents
darkGold: '#C89F16', // Darker gold for accents
white: '#ffffff' // White for text
};
/**
* Predefined header styles using Royal Enfield brand colors
*/
export const HeaderStyles = {
// Primary - Red & Black (Main brand colors)
primary: {
gradientFrom: BrandColors.red,
gradientTo: BrandColors.darkRed
},
// Information/Neutral - Black & Gold accent
info: {
gradientFrom: BrandColors.black,
gradientTo: '#2d2d2d'
},
// Success - Black with gold accent
success: {
gradientFrom: BrandColors.black,
gradientTo: '#2d2d2d'
},
// Error/Critical - Red (Brand red)
error: {
gradientFrom: BrandColors.red,
gradientTo: BrandColors.darkRed
},
// Warning - Gold
warning: {
gradientFrom: BrandColors.gold,
gradientTo: BrandColors.darkGold
},
// Neutral - Black
neutral: {
gradientFrom: BrandColors.black,
gradientTo: '#2d2d2d'
},
// Info Secondary - Dark grey
infoSecondary: {
gradientFrom: '#424242',
gradientTo: '#1a1a1a'
},
// Complete - Black with gold
complete: {
gradientFrom: BrandColors.black,
gradientTo: '#2d2d2d'
}
};
/**
* Generate priority alert section
*/
export function getPrioritySection(priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'): string {
if (priority === 'HIGH' || priority === 'CRITICAL') {
return `
<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;">High Priority</h3>
<p style="margin: 0; color: #856404; font-size: 14px; line-height: 1.6;">
This request has been marked as ${priority} priority and requires prompt attention.
</p>
</div>
`;
}
return '';
}
/**
* Generate approval chain visualization
*/
export function getApprovalChain(approvers: ApprovalChainItem[]): string {
return approvers.map(approver => {
let icon = '';
let textColor = '#333333';
let status = '';
switch (approver.status) {
case 'approved':
icon = `<span style="display: inline-block; width: 30px; height: 30px; background-color: #28a745; color: #ffffff; text-align: center; line-height: 30px; border-radius: 50%; font-size: 14px; margin-right: 10px; font-weight: bold;">✓</span>`;
status = approver.date ? `Approved on ${approver.date}` : 'Approved';
break;
case 'current':
icon = `<span style="display: inline-block; width: 30px; height: 30px; background-color: #667eea; color: #ffffff; text-align: center; line-height: 30px; border-radius: 50%; font-size: 14px; margin-right: 10px; font-weight: bold;">${approver.levelNumber}</span>`;
textColor = '#667eea';
status = 'Pending (Your Turn)';
break;
case 'pending':
icon = `<span style="display: inline-block; width: 30px; height: 30px; background-color: #ffc107; color: #ffffff; text-align: center; line-height: 30px; border-radius: 50%; font-size: 14px; margin-right: 10px; font-weight: bold;">${approver.levelNumber}</span>`;
status = 'Pending';
break;
case 'awaiting':
icon = `<span style="display: inline-block; width: 30px; height: 30px; background-color: #cccccc; color: #ffffff; text-align: center; line-height: 30px; border-radius: 50%; font-size: 14px; margin-right: 10px; font-weight: bold;">${approver.levelNumber}</span>`;
textColor = '#999999';
status = 'Awaiting';
break;
}
return `
<div style="padding: 10px 0; border-bottom: 1px solid #e9ecef;">
${icon}
<strong style="color: ${textColor};">${approver.name}</strong> - ${status}
</div>
`;
}).join('');
}
/**
* Generate next steps section for approval confirmation
*/
export function getNextStepsSection(isFinalApproval: boolean, nextApproverName?: string): string {
if (isFinalApproval) {
return `
<div style="padding: 20px; background-color: #e7f3ff; border-left: 4px solid #0066cc; border-radius: 4px; margin-bottom: 30px;">
<h3 style="margin: 0 0 10px; color: #004085; font-size: 16px; font-weight: 600;">Next Steps</h3>
<p style="margin: 0; color: #004085; font-size: 14px; line-height: 1.6;">
All approvals are complete! Please review the request and add any conclusion remarks before closing it.
</p>
</div>
`;
} else if (nextApproverName) {
return `
<div style="padding: 20px; background-color: #e7f3ff; border-left: 4px solid #0066cc; border-radius: 4px; margin-bottom: 30px;">
<h3 style="margin: 0 0 10px; color: #004085; font-size: 16px; font-weight: 600;">Next Steps</h3>
<p style="margin: 0; color: #004085; font-size: 14px; line-height: 1.6;">
The request has been forwarded to <strong>${nextApproverName}</strong> for the next level of approval.
You'll be notified when the request progresses.
</p>
</div>
`;
}
return '';
}
/**
* Generate permissions section for participant added
*/
export function getPermissionsContent(role: 'Approver' | 'Spectator'): string {
if (role === 'Approver') {
return `
<ul style="margin: 10px 0 0 0; padding-left: 20px; color: #004085; font-size: 14px; line-height: 1.8;">
<li>Review request details and documents</li>
<li>Approve or reject the request</li>
<li>Add comments and work notes</li>
<li>View approval workflow and history</li>
<li>Receive real-time notifications</li>
</ul>
`;
} else {
return `
<ul style="margin: 10px 0 0 0; padding-left: 20px; color: #004085; font-size: 14px; line-height: 1.8;">
<li>View request details and documents</li>
<li>Add comments and work notes</li>
<li>View approval workflow progress</li>
<li>Receive status update notifications</li>
<li>Cannot approve or reject the request</li>
</ul>
`;
}
}
/**
* Generate conclusion section for closed requests (supports rich text)
*/
export function getConclusionSection(conclusionRemark?: string): string {
if (conclusionRemark) {
return `
<div style="margin-bottom: 30px;">
<h3 style="margin: 0 0 15px; color: #333333; font-size: 16px; font-weight: 600;">Conclusion Remarks:</h3>
<div style="padding: 15px; background-color: #f8f9fa; border-left: 4px solid #6f42c1; border-radius: 4px; overflow-x: auto;">
${wrapRichText(conclusionRemark)}
</div>
</div>
`;
}
return '';
}
/**
* Generate role description for participant added
*/
export function getRoleDescription(role: 'Approver' | 'Spectator'): string {
if (role === 'Approver') {
return 'You can now review and take action on this request.';
} else {
return 'You can now view this request and participate in discussions.';
}
}
/**
* Generate action required section for workflow resumed
*/
export function getActionRequiredSection(isApprover: boolean): string {
if (isApprover) {
return `
<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;">Action Required</h3>
<p style="margin: 0; color: #856404; font-size: 14px; line-height: 1.6;">
This request requires your immediate attention. Please review and take action to keep the workflow moving forward.
</p>
</div>
`;
}
return '';
}
/**
* Common email footer with optional branding
*/
export interface EmailFooterConfig {
companyName: string;
companyWebsite?: string;
supportEmail?: string;
additionalLinks?: Array<{ text: string; url: string }>;
}
export function getEmailFooter(config: EmailFooterConfig | string): string {
// Backward compatibility - if string is passed, use it as companyName
const footerConfig: EmailFooterConfig = typeof config === 'string'
? { companyName: config }
: config;
const supportSection = footerConfig.supportEmail ? `
<p style="margin: 10px 0 0; color: #666666; font-size: 13px;">
Need help? Contact us at <a href="mailto:${footerConfig.supportEmail}" style="color: #667eea; text-decoration: none;">${footerConfig.supportEmail}</a>
</p>
` : '';
const linksSection = footerConfig.additionalLinks && footerConfig.additionalLinks.length > 0 ? `
<p style="margin: 15px 0 0; color: #999999; font-size: 12px;">
${footerConfig.additionalLinks.map(link =>
`<a href="${link.url}" style="color: #667eea; text-decoration: none; margin: 0 10px;">${link.text}</a>`
).join(' | ')}
</p>
` : '';
const companyLink = footerConfig.companyWebsite
? `<a href="${footerConfig.companyWebsite}" style="color: #999999; text-decoration: none;">${footerConfig.companyName}</a>`
: footerConfig.companyName;
return `
<tr>
<td style="padding: 30px; background-color: #f8f9fa; border-radius: 0 0 8px 8px; text-align: center; border-top: 1px solid #e9ecef;">
<p style="margin: 0 0 10px; color: #666666; font-size: 13px; line-height: 1.6;">
This is an automated notification. Please do not reply to this email.
</p>
${supportSection}
${linksSection}
<p style="margin: ${linksSection ? '15px' : '10px'} 0 0; color: #999999; font-size: 12px;">
© 2025 ${companyLink}. All rights reserved.
</p>
</td>
</tr>
`;
}