xpeditis2.0/apps/backend/src/infrastructure/email/templates/email-templates.ts
2026-02-05 11:53:22 +01:00

685 lines
22 KiB
TypeScript

/**
* Email Templates Service
*
* Renders email templates using MJML and Handlebars
*/
import { Injectable } from '@nestjs/common';
import mjml2html from 'mjml';
import Handlebars from 'handlebars';
@Injectable()
export class EmailTemplates {
/**
* Render booking confirmation email
*/
async renderBookingConfirmation(data: {
bookingNumber: string;
bookingDetails: any;
}): Promise<string> {
const mjmlTemplate = `
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" />
<mj-text font-size="14px" color="#333333" line-height="1.6" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f4f4f4">
<mj-section background-color="#ffffff" padding="20px">
<mj-column>
<mj-text font-size="24px" font-weight="bold" color="#0066cc">
Booking Confirmation
</mj-text>
<mj-divider border-color="#0066cc" />
<mj-text font-size="16px">
Your booking has been confirmed successfully!
</mj-text>
<mj-text>
<strong>Booking Number:</strong> {{bookingNumber}}
</mj-text>
<mj-text>
Thank you for using Xpeditis. Your booking confirmation is attached as a PDF.
</mj-text>
<mj-button background-color="#0066cc" href="{{dashboardUrl}}">
View in Dashboard
</mj-button>
</mj-column>
</mj-section>
<mj-section background-color="#f4f4f4" padding="10px">
<mj-column>
<mj-text font-size="12px" color="#666666" align="center">
© 2025 Xpeditis. All rights reserved.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
`;
const { html } = mjml2html(mjmlTemplate);
const template = Handlebars.compile(html);
return template(data);
}
/**
* Render verification email
*/
async renderVerificationEmail(data: { verifyUrl: string }): Promise<string> {
const mjmlTemplate = `
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f4f4f4">
<mj-section background-color="#ffffff" padding="20px">
<mj-column>
<mj-text font-size="24px" font-weight="bold" color="#0066cc">
Verify Your Email
</mj-text>
<mj-divider border-color="#0066cc" />
<mj-text>
Welcome to Xpeditis! Please verify your email address to get started.
</mj-text>
<mj-button background-color="#0066cc" href="{{verifyUrl}}">
Verify Email Address
</mj-button>
<mj-text font-size="12px" color="#666666">
If you didn't create an account, you can safely ignore this email.
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#f4f4f4" padding="10px">
<mj-column>
<mj-text font-size="12px" color="#666666" align="center">
© 2025 Xpeditis. All rights reserved.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
`;
const { html } = mjml2html(mjmlTemplate);
const template = Handlebars.compile(html);
return template(data);
}
/**
* Render password reset email
*/
async renderPasswordResetEmail(data: { resetUrl: string }): Promise<string> {
const mjmlTemplate = `
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f4f4f4">
<mj-section background-color="#ffffff" padding="20px">
<mj-column>
<mj-text font-size="24px" font-weight="bold" color="#0066cc">
Reset Your Password
</mj-text>
<mj-divider border-color="#0066cc" />
<mj-text>
You requested to reset your password. Click the button below to set a new password.
</mj-text>
<mj-button background-color="#0066cc" href="{{resetUrl}}">
Reset Password
</mj-button>
<mj-text font-size="12px" color="#666666">
This link will expire in 1 hour. If you didn't request this, please ignore this email.
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#f4f4f4" padding="10px">
<mj-column>
<mj-text font-size="12px" color="#666666" align="center">
© 2025 Xpeditis. All rights reserved.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
`;
const { html } = mjml2html(mjmlTemplate);
const template = Handlebars.compile(html);
return template(data);
}
/**
* Render welcome email
*/
async renderWelcomeEmail(data: { firstName: string; dashboardUrl: string }): Promise<string> {
const mjmlTemplate = `
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f4f4f4">
<mj-section background-color="#ffffff" padding="20px">
<mj-column>
<mj-text font-size="24px" font-weight="bold" color="#0066cc">
Welcome to Xpeditis, {{firstName}}!
</mj-text>
<mj-divider border-color="#0066cc" />
<mj-text>
We're excited to have you on board. Xpeditis helps you search and book maritime freight with ease.
</mj-text>
<mj-text>
<strong>Get started:</strong>
</mj-text>
<mj-text>
• Search for shipping rates<br/>
• Compare carriers and prices<br/>
• Book containers online<br/>
• Track your shipments
</mj-text>
<mj-button background-color="#0066cc" href="{{dashboardUrl}}">
Go to Dashboard
</mj-button>
</mj-column>
</mj-section>
<mj-section background-color="#f4f4f4" padding="10px">
<mj-column>
<mj-text font-size="12px" color="#666666" align="center">
© 2025 Xpeditis. All rights reserved.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
`;
const { html } = mjml2html(mjmlTemplate);
const template = Handlebars.compile(html);
return template(data);
}
/**
* Render user invitation email
*/
async renderUserInvitation(data: {
organizationName: string;
inviterName: string;
tempPassword: string;
loginUrl: string;
}): Promise<string> {
const mjmlTemplate = `
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f4f4f4">
<mj-section background-color="#ffffff" padding="20px">
<mj-column>
<mj-text font-size="24px" font-weight="bold" color="#0066cc">
You've Been Invited!
</mj-text>
<mj-divider border-color="#0066cc" />
<mj-text>
{{inviterName}} has invited you to join <strong>{{organizationName}}</strong> on Xpeditis.
</mj-text>
<mj-text>
<strong>Your temporary password:</strong> {{tempPassword}}
</mj-text>
<mj-text font-size="12px" color="#ff6600">
Please change your password after your first login.
</mj-text>
<mj-button background-color="#0066cc" href="{{loginUrl}}">
Login Now
</mj-button>
</mj-column>
</mj-section>
<mj-section background-color="#f4f4f4" padding="10px">
<mj-column>
<mj-text font-size="12px" color="#666666" align="center">
© 2025 Xpeditis. All rights reserved.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
`;
const { html } = mjml2html(mjmlTemplate);
const template = Handlebars.compile(html);
return template(data);
}
/**
* Render CSV booking request email
*/
async renderCsvBookingRequest(data: {
bookingId: string;
bookingNumber?: string;
documentPassword?: string;
origin: string;
destination: string;
volumeCBM: number;
weightKG: number;
palletCount: number;
priceUSD: number;
priceEUR: number;
primaryCurrency: string;
transitDays: number;
containerType: string;
documents: Array<{
type: string;
fileName: string;
}>;
acceptUrl: string;
rejectUrl: string;
}): Promise<string> {
// Register Handlebars helper for equality check
Handlebars.registerHelper('eq', function (a, b) {
return a === b;
});
const htmlTemplate = `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nouvelle demande de réservation</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Arial', 'Helvetica', sans-serif;
background-color: #f4f6f8;
color: #333;
}
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.header {
background: linear-gradient(135deg, #045a8d, #00bcd4);
color: #ffffff;
padding: 30px 20px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
}
.header p {
margin: 5px 0 0;
font-size: 14px;
opacity: 0.9;
}
.content {
padding: 30px 20px;
}
.section-title {
font-size: 20px;
font-weight: 700;
color: #045a8d;
margin-bottom: 15px;
border-bottom: 2px solid #00bcd4;
padding-bottom: 8px;
}
.details-table {
width: 100%;
margin: 20px 0;
border-collapse: collapse;
}
.details-table td {
padding: 12px 10px;
border-bottom: 1px solid #e0e0e0;
}
.details-table td:first-child {
font-weight: 700;
color: #045a8d;
width: 40%;
}
.details-table td:last-child {
color: #333;
}
.price-highlight {
font-size: 24px;
font-weight: 700;
color: #00aa00;
}
.price-secondary {
font-size: 14px;
color: #666;
}
.documents-section {
background-color: #f9f9f9;
padding: 20px;
border-radius: 6px;
margin: 20px 0;
}
.documents-section ul {
list-style: none;
padding: 0;
margin: 10px 0 0;
}
.documents-section li {
padding: 8px 0;
font-size: 14px;
}
.documents-section li:before {
content: '📄 ';
margin-right: 8px;
}
.action-buttons {
text-align: center;
margin: 30px 0;
}
.action-buttons p {
font-size: 16px;
font-weight: 700;
margin-bottom: 15px;
}
.button-container {
display: flex;
justify-content: space-around;
gap: 15px;
flex-wrap: wrap;
}
.btn {
display: inline-block;
padding: 15px 30px;
font-size: 16px;
font-weight: 700;
text-decoration: none;
border-radius: 6px;
color: #ffffff;
text-align: center;
min-width: 200px;
}
.btn-accept {
background-color: #00aa00;
}
.btn-accept:hover {
background-color: #008800;
}
.btn-reject {
background-color: #cc0000;
}
.btn-reject:hover {
background-color: #aa0000;
}
.important-notice {
background-color: #fff8e1;
border-left: 4px solid #f57c00;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}
.important-notice p {
margin: 0;
font-size: 14px;
color: #666;
}
.important-notice strong {
color: #f57c00;
}
.footer {
background-color: #f4f6f8;
padding: 20px;
text-align: center;
font-size: 12px;
color: #666;
}
.footer p {
margin: 5px 0;
}
.booking-reference {
font-size: 14px;
font-weight: 700;
color: #045a8d;
}
@media only screen and (max-width: 600px) {
.container {
margin: 10px;
border-radius: 0;
}
.header h1 {
font-size: 22px;
}
.button-container {
flex-direction: column;
}
.btn {
width: 100%;
margin: 5px 0;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<h1>🚢 Nouvelle demande de réservation</h1>
<p>Xpeditis</p>
</div>
<!-- Content -->
<div class="content">
<p style="font-size: 16px; margin-bottom: 20px;">
Bonjour,
</p>
<p style="font-size: 14px; line-height: 1.6; margin-bottom: 20px;">
Vous avez reçu une nouvelle demande de réservation via Xpeditis. Veuillez examiner les détails ci-dessous et confirmer ou refuser cette demande.
</p>
{{#if bookingNumber}}
<!-- Booking Reference Box -->
<div style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border: 2px solid #0284c7; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center;">
<p style="margin: 0 0 10px 0; font-size: 14px; color: #0369a1;">Numéro de devis</p>
<p style="margin: 0; font-size: 28px; font-weight: bold; color: #0c4a6e; letter-spacing: 2px;">{{bookingNumber}}</p>
{{#if documentPassword}}
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #bae6fd;">
<p style="margin: 0 0 5px 0; font-size: 12px; color: #0369a1;">🔐 Mot de passe pour accéder aux documents</p>
<p style="margin: 0; font-size: 20px; font-weight: bold; color: #0c4a6e; background: white; display: inline-block; padding: 8px 16px; border-radius: 4px; letter-spacing: 3px;">{{documentPassword}}</p>
<p style="margin: 10px 0 0 0; font-size: 11px; color: #64748b;">Conservez ce mot de passe, il vous sera demandé pour télécharger les documents</p>
</div>
{{/if}}
</div>
{{/if}}
<!-- Booking Details -->
<div class="section-title">📋 Détails du transport</div>
<table class="details-table">
<tr>
<td>Route</td>
<td>{{origin}} → {{destination}}</td>
</tr>
<tr>
<td>Volume</td>
<td>{{volumeCBM}} CBM</td>
</tr>
<tr>
<td>Poids</td>
<td>{{weightKG}} kg</td>
</tr>
<tr>
<td>Palettes</td>
<td>{{palletCount}}</td>
</tr>
<tr>
<td>Type de conteneur</td>
<td>{{containerType}}</td>
</tr>
<tr>
<td>Transit</td>
<td>{{transitDays}} jours</td>
</tr>
<tr>
<td>Prix</td>
<td>
<span class="price-highlight">
{{#if (eq primaryCurrency "EUR")}}
{{priceEUR}} EUR
{{else}}
{{priceUSD}} USD
{{/if}}
</span>
<br>
<span class="price-secondary">
{{#if (eq primaryCurrency "EUR")}}
(≈ {{priceUSD}} USD)
{{else}}
(≈ {{priceEUR}} EUR)
{{/if}}
</span>
</td>
</tr>
</table>
<!-- Documents Section -->
<div class="documents-section">
<div class="section-title">📄 Documents fournis</div>
<ul>
{{#each documents}}
<li><strong>{{this.type}}:</strong> {{this.fileName}}</li>
{{/each}}
</ul>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<p>Veuillez confirmer votre décision :</p>
<div class="button-container">
<a href="{{acceptUrl}}" class="btn btn-accept">✓ Accepter la demande</a>
<a href="{{rejectUrl}}" class="btn btn-reject">✗ Refuser la demande</a>
</div>
</div>
<!-- Important Notice -->
<div class="important-notice">
<p>
<strong>⚠️ Important</strong><br>
Cette demande expire automatiquement dans <strong>7 jours</strong> si aucune action n'est prise. Merci de répondre dans les meilleurs délais.
</p>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p class="booking-reference">Référence de réservation : {{bookingId}}</p>
<p>© 2025 Xpeditis. Tous droits réservés.</p>
<p>Cet email a été envoyé automatiquement. Merci de ne pas y répondre directement.</p>
</div>
</div>
</body>
</html>
`;
const template = Handlebars.compile(htmlTemplate);
return template(data);
}
/**
* Render invitation email with registration link
*/
async renderInvitationWithToken(data: {
firstName: string;
lastName: string;
organizationName: string;
inviterName: string;
invitationLink: string;
expiresAt: string;
}): Promise<string> {
const mjmlTemplate = `
<mjml>
<mj-head>
<mj-attributes>
<mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" />
</mj-attributes>
</mj-head>
<mj-body background-color="#f4f4f4">
<mj-section background-color="#ffffff" padding="40px 20px">
<mj-column>
<mj-text font-size="28px" font-weight="bold" color="#0066cc" align="center">
🚢 Bienvenue sur Xpeditis !
</mj-text>
<mj-divider border-color="#0066cc" border-width="3px" padding="20px 0" />
<mj-text font-size="16px" line-height="1.8">
Bonjour <strong>{{firstName}} {{lastName}}</strong>,
</mj-text>
<mj-text font-size="16px" line-height="1.8">
{{inviterName}} vous invite à rejoindre <strong>{{organizationName}}</strong> sur la plateforme Xpeditis.
</mj-text>
<mj-text font-size="15px" color="#666666" line-height="1.6">
Xpeditis est la solution complète pour gérer vos expéditions maritimes en ligne. Recherchez des tarifs, réservez des containers et suivez vos envois en temps réel.
</mj-text>
<mj-spacer height="30px" />
<mj-button
background-color="#0066cc"
href="{{invitationLink}}"
font-size="16px"
font-weight="bold"
border-radius="5px"
padding="15px 40px"
>
Créer mon compte
</mj-button>
<mj-spacer height="20px" />
<mj-text font-size="13px" color="#999999" align="center">
Ou copiez ce lien dans votre navigateur:
</mj-text>
<mj-text font-size="12px" color="#0066cc" align="center">
{{invitationLink}}
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#fff8e1" padding="20px">
<mj-column>
<mj-text font-size="14px" color="#f57c00" font-weight="bold">
⏱️ Cette invitation expire le {{expiresAt}}
</mj-text>
<mj-text font-size="13px" color="#666666">
Créez votre compte avant cette date pour rejoindre votre organisation.
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#f4f4f4" padding="20px">
<mj-column>
<mj-text font-size="12px" color="#666666" align="center">
© 2025 Xpeditis. Tous droits réservés.
</mj-text>
<mj-text font-size="11px" color="#999999" align="center">
Si vous n'avez pas sollicité cette invitation, vous pouvez ignorer cet email.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
`;
const { html } = mjml2html(mjmlTemplate);
const template = Handlebars.compile(html);
return template(data);
}
}