322 lines
18 KiB
TypeScript
322 lines
18 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
// Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy
|
|
function formatDateGerman(dateString: string): string {
|
|
const [year, month, day] = dateString.split('-');
|
|
return `${day}.${month}.${year}`;
|
|
}
|
|
|
|
let cachedLogoDataUrl: string | null = null;
|
|
|
|
async function getLogoDataUrl(): Promise<string | null> {
|
|
if (cachedLogoDataUrl) return cachedLogoDataUrl;
|
|
try {
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const logoPath = resolve(__dirname, "../../../assets/stargilnails_logo_transparent.png");
|
|
const buf = await readFile(logoPath);
|
|
const base64 = buf.toString("base64");
|
|
cachedLogoDataUrl = `data:image/png;base64,${base64}`;
|
|
return cachedLogoDataUrl;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function renderBrandedEmail(title: string, bodyHtml: string): Promise<string> {
|
|
const logo = await getLogoDataUrl();
|
|
const domain = process.env.DOMAIN || 'localhost:5173';
|
|
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
const homepageUrl = `${protocol}://${domain}`;
|
|
|
|
return `
|
|
<div style="font-family: Arial, sans-serif; color: #0f172a; background:#fdf2f8; padding:24px;">
|
|
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:640px; margin:0 auto; background:#ffffff; border-radius:12px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
|
<tr>
|
|
<td style="padding:24px 24px 0 24px; text-align:center;">
|
|
${logo ? `<img src="${logo}" alt="Stargirlnails" style="width:120px; height:auto; display:inline-block;" />` : `<div style=\"font-size:24px\">💅</div>`}
|
|
<h1 style="margin:16px 0 0 0; font-size:22px; color:#db2777;">${title}</h1>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:16px 24px 24px 24px;">
|
|
<div style="font-size:16px; line-height:1.6; color:#334155;">
|
|
${bodyHtml}
|
|
</div>
|
|
<hr style="border:none; border-top:1px solid #f1f5f9; margin:24px 0" />
|
|
<div style="text-align:center; margin-bottom:16px;">
|
|
<a href="${homepageUrl}" style="display: inline-block; background-color: #db2777; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 14px;">Zur Website</a>
|
|
</div>
|
|
<div style="font-size:12px; color:#64748b; text-align:center;">
|
|
© ${new Date().getFullYear()} Stargirlnails Kiel • Professional Nail Care
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
export async function renderBookingPendingHTML(params: { name: string; date: string; time: string; statusUrl?: string }) {
|
|
const { name, date, time, statusUrl } = params;
|
|
const formattedDate = formatDateGerman(date);
|
|
const domain = process.env.DOMAIN || 'localhost:5173';
|
|
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
const legalUrl = `${protocol}://${domain}/legal`;
|
|
|
|
const inner = `
|
|
<p>Hallo ${name},</p>
|
|
<p>wir haben deine Anfrage für <strong>${formattedDate}</strong> um <strong>${time}</strong> erhalten.</p>
|
|
<p>Wir bestätigen deinen Termin in Kürze. Du erhältst eine weitere E-Mail, sobald der Termin bestätigt ist.</p>
|
|
${statusUrl ? `
|
|
<div style="background-color: #fef9f5; border-left: 4px solid #f59e0b; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #f59e0b;">⏳ Termin-Status ansehen:</p>
|
|
<p style="margin: 8px 0 12px 0; color: #475569;">Du kannst den aktuellen Status deiner Buchung jederzeit einsehen:</p>
|
|
<a href="${statusUrl}" style="display: inline-block; background-color: #f59e0b; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Status ansehen</a>
|
|
</div>
|
|
` : ''}
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #3b82f6; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #3b82f6;">📋 Rechtliche Informationen:</p>
|
|
<p style="margin: 8px 0 12px 0; color: #475569;">Weitere Informationen findest du in unserem <a href="${legalUrl}" style="color: #3b82f6; text-decoration: underline;">Impressum und Datenschutz</a>.</p>
|
|
</div>
|
|
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
|
`;
|
|
return renderBrandedEmail("Deine Terminanfrage ist eingegangen", inner);
|
|
}
|
|
|
|
export async function renderBookingConfirmedHTML(params: { name: string; date: string; time: string; cancellationUrl?: string; reviewUrl?: string }) {
|
|
const { name, date, time, cancellationUrl, reviewUrl } = params;
|
|
const formattedDate = formatDateGerman(date);
|
|
const domain = process.env.DOMAIN || 'localhost:5173';
|
|
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
const legalUrl = `${protocol}://${domain}/legal`;
|
|
|
|
const inner = `
|
|
<p>Hallo ${name},</p>
|
|
<p>wir haben deinen Termin am <strong>${formattedDate}</strong> um <strong>${time}</strong> bestätigt.</p>
|
|
<p>Wir freuen uns auf dich!</p>
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #db2777; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #db2777;">📋 Wichtiger Hinweis:</p>
|
|
<p style="margin: 8px 0 0 0; color: #475569;">Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.</p>
|
|
</div>
|
|
${cancellationUrl ? `
|
|
<div style="background-color: #fef9f5; border-left: 4px solid #db2777; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #db2777;">📅 Termin verwalten:</p>
|
|
<p style="margin: 8px 0 12px 0; color: #475569;">Du kannst deinen Termin-Status einsehen und bei Bedarf stornieren:</p>
|
|
<a href="${cancellationUrl}" style="display: inline-block; background-color: #db2777; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Termin ansehen & verwalten</a>
|
|
</div>
|
|
` : ''}
|
|
${reviewUrl ? `
|
|
<div style="background-color: #eff6ff; border-left: 4px solid #3b82f6; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #3b82f6;">⭐ Bewertung abgeben:</p>
|
|
<p style="margin: 8px 0 12px 0; color: #475569;">Nach deinem Termin würden wir uns über deine Bewertung freuen!</p>
|
|
<a href="${reviewUrl}" style="display: inline-block; background-color: #3b82f6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: 600;">Bewertung schreiben</a>
|
|
<p style="margin: 12px 0 0 0; color: #64748b; font-size: 13px;">Du kannst deine Bewertung nach dem Termin über diesen Link abgeben.</p>
|
|
</div>
|
|
` : ''}
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #3b82f6; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #3b82f6;">📋 Rechtliche Informationen:</p>
|
|
<p style="margin: 8px 0 12px 0; color: #475569;">Weitere Informationen findest du in unserem <a href="${legalUrl}" style="color: #3b82f6; text-decoration: underline;">Impressum und Datenschutz</a>.</p>
|
|
</div>
|
|
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
|
`;
|
|
return renderBrandedEmail("Termin bestätigt", inner);
|
|
}
|
|
|
|
export async function renderBookingCancelledHTML(params: { name: string; date: string; time: string }) {
|
|
const { name, date, time } = params;
|
|
const formattedDate = formatDateGerman(date);
|
|
const domain = process.env.DOMAIN || 'localhost:5173';
|
|
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
|
const legalUrl = `${protocol}://${domain}/legal`;
|
|
|
|
const inner = `
|
|
<p>Hallo ${name},</p>
|
|
<p>dein Termin am <strong>${formattedDate}</strong> um <strong>${time}</strong> wurde abgesagt.</p>
|
|
<p>Bitte buche einen neuen Termin. Bei Fragen helfen wir dir gerne weiter.</p>
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #3b82f6; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #3b82f6;">📋 Rechtliche Informationen:</p>
|
|
<p style="margin: 8px 0 12px 0; color: #475569;">Weitere Informationen findest du in unserem <a href="${legalUrl}" style="color: #3b82f6; text-decoration: underline;">Impressum und Datenschutz</a>.</p>
|
|
</div>
|
|
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
|
`;
|
|
return renderBrandedEmail("Termin abgesagt", inner);
|
|
}
|
|
|
|
export async function renderAdminBookingNotificationHTML(params: {
|
|
name: string;
|
|
date: string;
|
|
time: string;
|
|
treatment: string;
|
|
phone: string;
|
|
notes?: string;
|
|
hasInspirationPhoto: boolean;
|
|
}) {
|
|
const { name, date, time, treatment, phone, notes, hasInspirationPhoto } = params;
|
|
const formattedDate = formatDateGerman(date);
|
|
const inner = `
|
|
<p>Hallo Admin,</p>
|
|
<p>eine neue Buchungsanfrage ist eingegangen:</p>
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #db2777; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #db2777;">📅 Buchungsdetails:</p>
|
|
<ul style="margin: 8px 0 0 0; color: #475569; list-style: none; padding: 0;">
|
|
<li><strong>Name:</strong> ${name}</li>
|
|
<li><strong>Telefon:</strong> ${phone}</li>
|
|
<li><strong>Behandlung:</strong> ${treatment}</li>
|
|
<li><strong>Datum:</strong> ${formattedDate}</li>
|
|
<li><strong>Uhrzeit:</strong> ${time}</li>
|
|
${notes ? `<li><strong>Notizen:</strong> ${notes}</li>` : ''}
|
|
<li><strong>Inspiration-Foto:</strong> ${hasInspirationPhoto ? '✅ Im Anhang verfügbar' : '❌ Kein Foto hochgeladen'}</li>
|
|
</ul>
|
|
</div>
|
|
<p>Bitte logge dich in das Admin-Panel ein, um die Buchung zu bestätigen oder abzulehnen.</p>
|
|
<p>Liebe Grüße,<br/>Stargirlnails System</p>
|
|
`;
|
|
return renderBrandedEmail("Neue Buchungsanfrage - Admin-Benachrichtigung", inner);
|
|
}
|
|
|
|
|
|
export async function renderBookingRescheduleProposalHTML(params: {
|
|
name: string;
|
|
originalDate: string;
|
|
originalTime: string;
|
|
proposedDate: string;
|
|
proposedTime: string;
|
|
treatmentName: string;
|
|
acceptUrl: string;
|
|
declineUrl: string;
|
|
expiresAt: string;
|
|
}) {
|
|
const formattedOriginalDate = formatDateGerman(params.originalDate);
|
|
const formattedProposedDate = formatDateGerman(params.proposedDate);
|
|
const expiryDate = new Date(params.expiresAt);
|
|
const formattedExpiry = `${expiryDate.toLocaleDateString('de-DE')} ${expiryDate.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`;
|
|
|
|
const inner = `
|
|
<p>Hallo ${params.name},</p>
|
|
<p>wir müssen deinen Termin leider verschieben. Hier ist unser Vorschlag:</p>
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #f59e0b; padding: 16px; margin: 20px 0; border-radius: 4px;">
|
|
<p style="margin: 0; font-weight: 600; color: #92400e;">📅 Übersicht</p>
|
|
<table role="presentation" cellspacing="0" cellpadding="0" style="width:100%; margin-top:8px; font-size:14px; color:#475569;">
|
|
<tr>
|
|
<td style="padding:6px 0; width:45%"><strong>Alter Termin</strong></td>
|
|
<td style="padding:6px 0;">${formattedOriginalDate} um ${params.originalTime} Uhr</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:6px 0; width:45%"><strong>Neuer Vorschlag</strong></td>
|
|
<td style="padding:6px 0; color:#b45309;"><strong>${formattedProposedDate} um ${params.proposedTime} Uhr</strong></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:6px 0; width:45%"><strong>Behandlung</strong></td>
|
|
<td style="padding:6px 0;">${params.treatmentName}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
<div style="background-color: #fffbeb; border-left: 4px solid #f59e0b; padding: 12px; margin: 16px 0; border-radius: 4px; color:#92400e;">
|
|
⏰ Bitte antworte bis ${formattedExpiry}.
|
|
</div>
|
|
<div style="text-align:center; margin: 20px 0;">
|
|
<a href="${params.acceptUrl}" style="display:inline-block; background-color:#16a34a; color:#ffffff; padding:12px 18px; border-radius:8px; text-decoration:none; font-weight:700; margin-right:8px;">Neuen Termin akzeptieren</a>
|
|
<a href="${params.declineUrl}" style="display:inline-block; background-color:#dc2626; color:#ffffff; padding:12px 18px; border-radius:8px; text-decoration:none; font-weight:700;">Termin ablehnen</a>
|
|
</div>
|
|
<div style="background-color: #f8fafc; border-left: 4px solid #10b981; padding: 12px; margin: 16px 0; border-radius: 4px; color:#065f46;">
|
|
Wenn du den Vorschlag ablehnst, bleibt dein ursprünglicher Termin bestehen und wir kontaktieren dich für eine alternative Lösung.
|
|
</div>
|
|
<p>Falls du einen komplett neuen Termin buchen möchtest, kannst du deinen aktuellen Termin stornieren und einen neuen Termin auf unserer Website buchen.</p>
|
|
<p>Liebe Grüße,<br/>Stargirlnails Kiel</p>
|
|
`;
|
|
return renderBrandedEmail("Terminänderung vorgeschlagen", inner);
|
|
}
|
|
|
|
export async function renderAdminRescheduleDeclinedHTML(params: {
|
|
customerName: string;
|
|
originalDate: string;
|
|
originalTime: string;
|
|
proposedDate: string;
|
|
proposedTime: string;
|
|
treatmentName: string;
|
|
customerEmail?: string;
|
|
customerPhone?: string;
|
|
}) {
|
|
const inner = `
|
|
<p>Hallo Admin,</p>
|
|
<p>der Kunde <strong>${params.customerName}</strong> hat den Terminänderungsvorschlag abgelehnt.</p>
|
|
<div style="background-color:#f8fafc; border-left:4px solid #ef4444; padding:16px; margin:16px 0; border-radius:4px;">
|
|
<ul style="margin:0; padding:0; list-style:none; color:#475569; font-size:14px;">
|
|
<li><strong>Kunde:</strong> ${params.customerName}</li>
|
|
${params.customerEmail ? `<li><strong>E-Mail:</strong> ${params.customerEmail}</li>` : ''}
|
|
${params.customerPhone ? `<li><strong>Telefon:</strong> ${params.customerPhone}</li>` : ''}
|
|
<li><strong>Behandlung:</strong> ${params.treatmentName}</li>
|
|
<li><strong>Ursprünglicher Termin:</strong> ${formatDateGerman(params.originalDate)} um ${params.originalTime} Uhr (bleibt bestehen)</li>
|
|
<li><strong>Abgelehnter Vorschlag:</strong> ${formatDateGerman(params.proposedDate)} um ${params.proposedTime} Uhr</li>
|
|
</ul>
|
|
</div>
|
|
<p>Bitte kontaktiere den Kunden, um eine alternative Lösung zu finden.</p>
|
|
`;
|
|
return renderBrandedEmail("Kunde hat Terminänderung abgelehnt", inner);
|
|
}
|
|
|
|
export async function renderAdminRescheduleAcceptedHTML(params: {
|
|
customerName: string;
|
|
originalDate: string;
|
|
originalTime: string;
|
|
newDate: string;
|
|
newTime: string;
|
|
treatmentName: string;
|
|
}) {
|
|
const inner = `
|
|
<p>Hallo Admin,</p>
|
|
<p>der Kunde <strong>${params.customerName}</strong> hat den Terminänderungsvorschlag akzeptiert.</p>
|
|
<div style="background-color:#ecfeff; border-left:4px solid #10b981; padding:16px; margin:16px 0; border-radius:4px;">
|
|
<ul style="margin:0; padding:0; list-style:none; color:#475569; font-size:14px;">
|
|
<li><strong>Kunde:</strong> ${params.customerName}</li>
|
|
<li><strong>Behandlung:</strong> ${params.treatmentName}</li>
|
|
<li><strong>Alter Termin:</strong> ${formatDateGerman(params.originalDate)} um ${params.originalTime} Uhr</li>
|
|
<li><strong>Neuer Termin:</strong> ${formatDateGerman(params.newDate)} um ${params.newTime} Uhr ✅</li>
|
|
</ul>
|
|
</div>
|
|
<p>Der Termin wurde automatisch aktualisiert.</p>
|
|
`;
|
|
return renderBrandedEmail("Kunde hat Terminänderung akzeptiert", inner);
|
|
}
|
|
|
|
export async function renderAdminRescheduleExpiredHTML(params: {
|
|
expiredProposals: Array<{
|
|
customerName: string;
|
|
originalDate: string;
|
|
originalTime: string;
|
|
proposedDate: string;
|
|
proposedTime: string;
|
|
treatmentName: string;
|
|
customerEmail?: string;
|
|
customerPhone?: string;
|
|
expiredAt: string;
|
|
}>;
|
|
}) {
|
|
const inner = `
|
|
<p>Hallo Admin,</p>
|
|
<p><strong>${params.expiredProposals.length} Terminänderungsvorschlag${params.expiredProposals.length > 1 ? 'e' : ''} ${params.expiredProposals.length > 1 ? 'sind' : 'ist'} abgelaufen</strong> und wurde${params.expiredProposals.length > 1 ? 'n' : ''} automatisch entfernt.</p>
|
|
<div style="background-color:#fef2f2; border-left:4px solid #ef4444; padding:16px; margin:16px 0; border-radius:4px;">
|
|
<p style="margin:0 0 12px 0; font-weight:600; color:#dc2626;">⚠️ Abgelaufene Vorschläge:</p>
|
|
${params.expiredProposals.map(proposal => `
|
|
<div style="background-color:#ffffff; border:1px solid #fecaca; border-radius:4px; padding:12px; margin:8px 0;">
|
|
<ul style="margin:0; padding:0; list-style:none; color:#475569; font-size:13px;">
|
|
<li><strong>Kunde:</strong> ${proposal.customerName}</li>
|
|
${proposal.customerEmail ? `<li><strong>E-Mail:</strong> ${proposal.customerEmail}</li>` : ''}
|
|
${proposal.customerPhone ? `<li><strong>Telefon:</strong> ${proposal.customerPhone}</li>` : ''}
|
|
<li><strong>Behandlung:</strong> ${proposal.treatmentName}</li>
|
|
<li><strong>Ursprünglicher Termin:</strong> ${formatDateGerman(proposal.originalDate)} um ${proposal.originalTime} Uhr</li>
|
|
<li><strong>Vorgeschlagener Termin:</strong> ${formatDateGerman(proposal.proposedDate)} um ${proposal.proposedTime} Uhr</li>
|
|
<li><strong>Abgelaufen am:</strong> ${new Date(proposal.expiredAt).toLocaleString('de-DE')}</li>
|
|
</ul>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<p style="color:#dc2626; font-weight:600;">Bitte kontaktiere die Kunden, um eine alternative Lösung zu finden.</p>
|
|
<p>Die ursprünglichen Termine bleiben bestehen.</p>
|
|
`;
|
|
return renderBrandedEmail("Abgelaufene Terminänderungsvorschläge", inner);
|
|
}
|
|
|