- 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
570 lines
17 KiB
TypeScript
570 lines
17 KiB
TypeScript
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,
|
||
});
|
||
}
|