Files
Atlas/lib/email-service.ts
Julia Wehden a2c95c70e7 feat: Equipment-System, Buchungsbearbeitung, Kundenadresse, LexOffice-Fix
- Vintage Modell hinzugefuegt
- Equipment Multi-Select (Neue Buchung + Bearbeitung)
- Kundenadresse in Formularen
- Bearbeiten-Seite fuer Buchungen
- Abbau-Zeiten in Formularen und Uebersicht
- Vertrag PDF nur bei Privatkunden
- LexOffice Kontakt-Erstellung Fix (BUSINESS)
- Zurueck-Pfeil auf Touren-Seite
2026-03-19 16:21:55 +01:00

570 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import nodemailer from 'nodemailer';
import path from 'path';
import { readFile } from 'fs/promises';
interface LocationSmtpConfig {
host: string;
port: number;
user: string;
password: string;
secure: boolean;
from: string;
}
function createTransporter(config: LocationSmtpConfig) {
return nodemailer.createTransport({
host: config.host,
port: config.port,
secure: config.secure,
auth: {
user: config.user,
pass: config.password,
},
});
}
interface SendEmailOptions {
to: string;
subject: string;
text: string;
html: string;
smtpConfig: LocationSmtpConfig;
attachments?: {
filename: string;
content?: Buffer;
path?: string;
}[];
}
export async function sendEmail(options: SendEmailOptions) {
const emailEnabled = process.env.EMAIL_ENABLED !== 'false';
const testMode = process.env.TEST_MODE === 'true';
const testRecipient = process.env.TEST_EMAIL_RECIPIENT;
// E-Mail komplett deaktiviert
if (!emailEnabled) {
console.log('📧 [EMAIL DISABLED] E-Mail würde gesendet an:', options.to);
console.log(' Betreff:', options.subject);
console.log(' Von:', options.smtpConfig.from);
console.log(' ⚠️ EMAIL_ENABLED=false - Kein echter Versand!');
return { success: true, messageId: 'test-disabled', mode: 'disabled' };
}
// Test-Modus: Umleitung an Test-E-Mail
let actualRecipient = options.to;
if (testMode && testRecipient) {
console.log('🧪 [TEST MODE] E-Mail umgeleitet!');
console.log(' Original-Empfänger:', options.to);
console.log(' Test-Empfänger:', testRecipient);
actualRecipient = testRecipient;
// Füge Hinweis in Betreff ein
options.subject = `[TEST] ${options.subject}`;
// Füge Hinweis in E-Mail ein
options.html = `
<div style="background: #FEF3C7; border: 2px solid #F59E0B; padding: 15px; margin-bottom: 20px; border-radius: 8px;">
<p style="margin: 0; color: #92400E; font-weight: bold;">🧪 TEST-MODUS AKTIV</p>
<p style="margin: 5px 0 0 0; color: #92400E; font-size: 14px;">
Diese E-Mail wäre ursprünglich an <strong>${options.to}</strong> gegangen.
</p>
</div>
${options.html}
`;
}
try {
const transport = createTransporter(options.smtpConfig);
const info = await transport.sendMail({
from: options.smtpConfig.from,
to: actualRecipient,
subject: options.subject,
text: options.text,
html: options.html,
attachments: options.attachments,
});
console.log('✅ Email sent:', info.messageId, 'from:', options.smtpConfig.from);
if (testMode) {
console.log(' 🧪 TEST MODE - E-Mail an:', actualRecipient, '(Original:', options.to, ')');
}
return { success: true, messageId: info.messageId, mode: testMode ? 'test' : 'production' };
} catch (error: any) {
console.error('❌ Email send error:', error);
throw error;
}
}
export async function sendContractEmail(
booking: any,
contractPdfPath: string
) {
const location = booking.location;
if (!location || !location.smtpHost || !location.smtpPassword) {
throw new Error(`SMTP not configured for location: ${location?.name || 'Unknown'}`);
}
const smtpConfig: LocationSmtpConfig = {
host: location.smtpHost,
port: location.smtpPort || 465,
user: location.smtpUser || location.contactEmail,
password: location.smtpPassword,
secure: location.smtpSecure !== false,
from: `SaveTheMoment ${location.name} <${location.contactEmail}>`,
};
const signToken = Buffer.from(`${booking.id}-${Date.now()}`).toString('base64url');
const signUrl = `${process.env.NEXTAUTH_URL}/contract/sign/${signToken}`;
const subject = `Ihr Mietvertrag für ${booking.eventLocation || 'Ihr Event'}`;
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #DC2626 0%, #EC4899 100%);
color: white;
padding: 30px;
text-align: center;
border-radius: 10px 10px 0 0;
}
.content {
background: #f9fafb;
padding: 30px;
border-radius: 0 0 10px 10px;
}
.button {
display: inline-block;
background: linear-gradient(135deg, #DC2626 0%, #EC4899 100%);
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 8px;
margin: 20px 0;
font-weight: bold;
}
.details {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
border-left: 4px solid #DC2626;
}
.footer {
text-align: center;
color: #666;
font-size: 12px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
}
</style>
</head>
<body>
<div class="header">
<h1>🎉 SaveTheMoment ${location.name}</h1>
<p>Ihr Mietvertrag ist bereit!</p>
</div>
<div class="content">
<p>Hallo ${booking.customerName},</p>
<p>vielen Dank für Ihre Buchung bei SaveTheMoment ${location.name}! Wir freuen uns sehr, Teil Ihres besonderen Anlasses zu sein.</p>
<div class="details">
<h3>📋 Buchungsdetails</h3>
<p><strong>Buchungsnummer:</strong> ${booking.bookingNumber}</p>
<p><strong>Event-Datum:</strong> ${new Date(booking.eventDate).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}</p>
<p><strong>Location:</strong> ${booking.eventLocation || booking.eventAddress}</p>
<p><strong>Fotobox:</strong> ${booking.photobox?.model || 'N/A'}</p>
</div>
<p>Im Anhang finden Sie Ihren Mietvertrag als PDF-Datei.</p>
<p><strong>Nächste Schritte:</strong></p>
<ol>
<li>Bitte lesen Sie den Vertrag sorgfältig durch</li>
<li>Signieren Sie den Vertrag online oder drucken Sie ihn aus und senden Sie ihn zurück</li>
<li>Nach Erhalt der Unterschrift ist Ihre Buchung verbindlich bestätigt</li>
</ol>
<center>
<a href="${signUrl}" class="button">
✍️ Vertrag online signieren
</a>
</center>
<p>Alternativ können Sie den Vertrag auch ausdrucken, unterschreiben und uns per E-Mail oder Post zurücksenden.</p>
<p>Bei Fragen stehen wir Ihnen jederzeit gerne zur Verfügung!</p>
<p>Mit freundlichen Grüßen<br>
Ihr SaveTheMoment Team ${location.name}</p>
</div>
<div class="footer">
<p>SaveTheMoment ${location.name}<br>
E-Mail: ${location.contactEmail}<br>
Web: ${location.websiteUrl || 'www.savethemoment.photos'}</p>
<p style="color: #999; font-size: 11px;">
Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht direkt auf diese E-Mail.
</p>
</div>
</body>
</html>
`.trim();
const text = `
Hallo ${booking.customerName},
vielen Dank für Ihre Buchung bei SaveTheMoment ${location.name}!
Buchungsdetails:
- Buchungsnummer: ${booking.bookingNumber}
- Event-Datum: ${new Date(booking.eventDate).toLocaleDateString('de-DE')}
- Location: ${booking.eventLocation || booking.eventAddress}
Im Anhang finden Sie Ihren Mietvertrag als PDF-Datei.
Sie können den Vertrag online signieren unter:
${signUrl}
Oder drucken Sie ihn aus und senden Sie ihn uns zurück.
Bei Fragen stehen wir Ihnen jederzeit zur Verfügung!
Mit freundlichen Grüßen
Ihr SaveTheMoment Team ${location.name}
---
SaveTheMoment ${location.name}
E-Mail: ${location.contactEmail}
Web: ${location.websiteUrl || 'www.savethemoment.photos'}
`.trim();
let pdfBuffer: Buffer;
try {
pdfBuffer = await readFile(path.join(process.cwd(), 'public', contractPdfPath));
} catch (error) {
console.error('Failed to read contract PDF:', error);
throw new Error('Contract PDF not found');
}
return sendEmail({
to: booking.customerEmail,
subject,
text,
html,
smtpConfig,
attachments: [
{
filename: `Mietvertrag_${booking.bookingNumber}.pdf`,
content: pdfBuffer,
},
],
});
}
export async function sendInitialBookingEmail(
booking: any,
quotationPdf: Buffer,
contractPdf: Buffer
) {
const location = booking.location;
if (!location || !location.smtpHost || !location.smtpPassword) {
throw new Error(`SMTP not configured for location: ${location?.name || 'Unknown'}`);
}
const smtpConfig: LocationSmtpConfig = {
host: location.smtpHost,
port: location.smtpPort || 465,
user: location.smtpUser || location.contactEmail,
password: location.smtpPassword,
secure: location.smtpSecure !== false,
from: `SaveTheMoment ${location.name} <${location.contactEmail}>`,
};
const signToken = Buffer.from(`${booking.id}-${Date.now()}`).toString('base64url');
const signUrl = `${process.env.NEXTAUTH_URL}/contract/sign/${signToken}`;
const subject = `Ihre Anfrage bei SaveTheMoment ${location.name} - ${booking.bookingNumber}`;
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #DC2626 0%, #EC4899 100%);
color: white;
padding: 30px;
text-align: center;
border-radius: 10px 10px 0 0;
}
.content {
background: #f9fafb;
padding: 30px;
border-radius: 0 0 10px 10px;
}
.button {
display: inline-block;
background: linear-gradient(135deg, #DC2626 0%, #EC4899 100%);
color: white;
padding: 15px 30px;
text-decoration: none;
border-radius: 8px;
margin: 20px 0;
font-weight: bold;
}
.details {
background: white;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
border-left: 4px solid #DC2626;
}
.price-box {
background: #FEF3C7;
border: 2px solid #F59E0B;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
text-align: center;
}
.footer {
text-align: center;
color: #666;
font-size: 12px;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
}
</style>
</head>
<body>
<div class="header">
<h1>🎉 SaveTheMoment ${location.name}</h1>
<p>Vielen Dank für Ihre Anfrage!</p>
</div>
<div class="content">
<p>Hallo ${booking.customerName},</p>
<p>herzlichen Dank für Ihre Anfrage! Wir freuen uns sehr, dass Sie sich für SaveTheMoment entschieden haben.</p>
<div class="details">
<h3>📋 Ihre Buchungsdetails</h3>
<p><strong>Buchungsnummer:</strong> ${booking.bookingNumber}</p>
<p><strong>Event-Datum:</strong> ${new Date(booking.eventDate).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}</p>
<p><strong>Event-Location:</strong> ${booking.eventLocation || booking.eventAddress}</p>
<p><strong>Fotobox:</strong> ${booking.photobox?.model || 'N/A'}</p>
${booking.distance ? `<p><strong>Entfernung:</strong> ${booking.distance.toFixed(1)} km (einfach)</p>` : ''}
</div>
${booking.calculatedPrice ? `
<div class="price-box">
<h3 style="margin: 0 0 10px 0; color: #92400E;">💰 Gesamtpreis</h3>
<p style="font-size: 28px; font-weight: bold; margin: 0; color: #92400E;">
${booking.calculatedPrice.toFixed(2)}
</p>
<p style="font-size: 14px; margin: 5px 0 0 0; color: #92400E;">inkl. 19% MwSt.</p>
</div>
` : ''}
<p><strong>📎 Im Anhang finden Sie:</strong></p>
<ol>
<li><strong>Ihr persönliches Angebot</strong> mit allen Details und Positionen</li>
<li><strong>Ihren Mietvertrag</strong> zum Durchlesen</li>
</ol>
<p><strong>✅ Nächste Schritte:</strong></p>
<ol>
<li>Prüfen Sie bitte das Angebot und den Mietvertrag</li>
<li>Signieren Sie den Vertrag online oder laden Sie ihn unterschrieben hoch</li>
<li>Nach Ihrer Unterschrift wird Ihre Buchung verbindlich bestätigt</li>
</ol>
<center>
<a href="${signUrl}" class="button">
✍️ Vertrag jetzt online signieren
</a>
</center>
<p>Alternativ können Sie den Vertrag auch ausdrucken, unterschreiben und uns per E-Mail zurücksenden.</p>
<p><strong>Haben Sie Fragen oder Änderungswünsche?</strong><br>
Antworten Sie einfach auf diese E-Mail wir sind für Sie da!</p>
<p>Mit freundlichen Grüßen<br>
Ihr SaveTheMoment Team ${location.name}</p>
</div>
<div class="footer">
<p>SaveTheMoment ${location.name}<br>
E-Mail: ${location.contactEmail}<br>
Web: ${location.websiteUrl || 'www.savethemoment.photos'}</p>
<p style="color: #999; font-size: 11px;">
Diese E-Mail wurde automatisch generiert.
</p>
</div>
</body>
</html>
`.trim();
const text = `
Hallo ${booking.customerName},
vielen Dank für Ihre Anfrage bei SaveTheMoment ${location.name}!
Buchungsdetails:
- Buchungsnummer: ${booking.bookingNumber}
- Event-Datum: ${new Date(booking.eventDate).toLocaleDateString('de-DE')}
- Location: ${booking.eventLocation || booking.eventAddress}
- Fotobox: ${booking.photobox?.model || 'N/A'}
${booking.distance ? `- Entfernung: ${booking.distance.toFixed(1)} km` : ''}
${booking.calculatedPrice ? `\nGesamtpreis: ${booking.calculatedPrice.toFixed(2)} € (inkl. 19% MwSt.)` : ''}
Im Anhang finden Sie:
1. Ihr persönliches Angebot
2. Ihren Mietvertrag
Nächste Schritte:
1. Prüfen Sie bitte das Angebot und den Mietvertrag
2. Signieren Sie den Vertrag online: ${signUrl}
3. Nach Ihrer Unterschrift wird Ihre Buchung verbindlich bestätigt
Bei Fragen stehen wir Ihnen jederzeit zur Verfügung!
Mit freundlichen Grüßen
Ihr SaveTheMoment Team ${location.name}
---
SaveTheMoment ${location.name}
E-Mail: ${location.contactEmail}
Web: ${location.websiteUrl || 'www.savethemoment.photos'}
`.trim();
return sendEmail({
to: booking.customerEmail,
subject,
text,
html,
smtpConfig,
attachments: [
{
filename: `Angebot_${booking.bookingNumber}.pdf`,
content: quotationPdf,
},
{
filename: `Mietvertrag_${booking.bookingNumber}.pdf`,
content: contractPdf,
},
],
});
}
export async function sendBookingConfirmationEmail(booking: any) {
const location = booking.location;
if (!location || !location.smtpHost || !location.smtpPassword) {
throw new Error(`SMTP not configured for location: ${location?.name || 'Unknown'}`);
}
const smtpConfig: LocationSmtpConfig = {
host: location.smtpHost,
port: location.smtpPort || 465,
user: location.smtpUser || location.contactEmail,
password: location.smtpPassword,
secure: location.smtpSecure !== false,
from: `SaveTheMoment ${location.name} <${location.contactEmail}>`,
};
const subject = `Buchungsbestätigung - ${booking.bookingNumber}`;
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #DC2626 0%, #EC4899 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }
.content { background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px; }
.details { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #DC2626; }
</style>
</head>
<body>
<div class="header">
<h1>✅ Buchung bestätigt!</h1>
</div>
<div class="content">
<p>Hallo ${booking.customerName},</p>
<p>Ihre Buchung bei SaveTheMoment ${location.name} wurde erfolgreich bestätigt!</p>
<div class="details">
<h3>Buchungsdetails</h3>
<p><strong>Buchungsnummer:</strong> ${booking.bookingNumber}</p>
<p><strong>Event-Datum:</strong> ${new Date(booking.eventDate).toLocaleDateString('de-DE')}</p>
<p><strong>Location:</strong> ${booking.eventLocation || booking.eventAddress}</p>
</div>
<p>Wir freuen uns auf Ihr Event!</p>
<p>Mit freundlichen Grüßen<br>Ihr SaveTheMoment Team ${location.name}</p>
</div>
</body>
</html>
`;
const text = `
Hallo ${booking.customerName},
Ihre Buchung bei SaveTheMoment ${location.name} wurde erfolgreich bestätigt!
Buchungsnummer: ${booking.bookingNumber}
Event-Datum: ${new Date(booking.eventDate).toLocaleDateString('de-DE')}
Location: ${booking.eventLocation || booking.eventAddress}
Wir freuen uns auf Ihr Event!
Mit freundlichen Grüßen
Ihr SaveTheMoment Team ${location.name}
`;
return sendEmail({
to: booking.customerEmail,
subject,
text,
html,
smtpConfig,
});
}