From ccba9d443b826f6dc75fb0eecacf323013de1248 Mon Sep 17 00:00:00 2001 From: elpatron Date: Wed, 8 Oct 2025 18:45:34 +0200 Subject: [PATCH] Fix ICS calendar issues and improve email template code quality - Fix ATTENDEE mailto to use customerEmail instead of customerName - Add icsEscape helper for proper RFC 5545 text escaping - Calculate ICS duration from treatments array instead of separate param - Add renderTreatmentList helper to reduce code duplication in email templates --- src/server/lib/email-templates.ts | 62 ++++++++++++++--- src/server/lib/email.ts | 39 ++++++++--- src/server/rpc/bookings.ts | 110 +++++++++++++++++++++--------- 3 files changed, 160 insertions(+), 51 deletions(-) diff --git a/src/server/lib/email-templates.ts b/src/server/lib/email-templates.ts index b5c95c8..ec01518 100644 --- a/src/server/lib/email-templates.ts +++ b/src/server/lib/email-templates.ts @@ -8,6 +8,27 @@ function formatDateGerman(dateString: string): string { return `${day}.${month}.${year}`; } +// Helper function to render treatment list HTML +function renderTreatmentList( + treatments: Array<{id: string; name: string; duration: number; price: number}>, + options: { showPrices: boolean } = { showPrices: true } +): string { + const totalDuration = treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = treatments.reduce((sum, t) => sum + t.price, 0); + + const treatmentItems = treatments.map(t => + options.showPrices + ? `
  • ${t.name} - ${t.duration} Min - ${t.price.toFixed(2)} €
  • ` + : `
  • ${t.name} - ${t.duration} Min - ${t.price.toFixed(2)} €
  • ` + ).join(''); + + const totalLine = options.showPrices + ? `
  • Gesamt: ${totalDuration} Min - ${totalPrice.toFixed(2)} €
  • ` + : `
  • Gesamt: ${totalDuration} Min - ${totalPrice.toFixed(2)} €
  • `; + + return `${treatmentItems}${totalLine}`; +} + let cachedLogoDataUrl: string | null = null; async function getLogoDataUrl(): Promise { @@ -86,8 +107,8 @@ async function renderBrandedEmail(title: string, bodyHtml: string): Promise`; } -export async function renderBookingPendingHTML(params: { name: string; date: string; time: string; statusUrl?: string }) { - const { name, date, time, statusUrl } = params; +export async function renderBookingPendingHTML(params: { name: string; date: string; time: string; statusUrl?: string; treatments: Array<{id: string; name: string; duration: number; price: number}> }) { + const { name, date, time, statusUrl, treatments } = params; const formattedDate = formatDateGerman(date); const domain = process.env.DOMAIN || 'localhost:5173'; const protocol = domain.includes('localhost') ? 'http' : 'https'; @@ -96,6 +117,12 @@ export async function renderBookingPendingHTML(params: { name: string; date: str const inner = `

    Hallo ${name},

    wir haben deine Anfrage für ${formattedDate} um ${time} erhalten.

    +
    +

    💅 Deine Behandlungen:

    +
      + ${renderTreatmentList(treatments, { showPrices: true })} +
    +

    Wir bestätigen deinen Termin in Kürze. Du erhältst eine weitere E-Mail, sobald der Termin bestätigt ist.

    ${statusUrl ? `
    @@ -113,8 +140,8 @@ export async function renderBookingPendingHTML(params: { name: string; date: str 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; +export async function renderBookingConfirmedHTML(params: { name: string; date: string; time: string; cancellationUrl?: string; reviewUrl?: string; treatments: Array<{id: string; name: string; duration: number; price: number}> }) { + const { name, date, time, cancellationUrl, reviewUrl, treatments } = params; const formattedDate = formatDateGerman(date); const domain = process.env.DOMAIN || 'localhost:5173'; const protocol = domain.includes('localhost') ? 'http' : 'https'; @@ -123,6 +150,12 @@ export async function renderBookingConfirmedHTML(params: { name: string; date: s const inner = `

    Hallo ${name},

    wir haben deinen Termin am ${formattedDate} um ${time} bestätigt.

    +
    +

    💅 Deine Behandlungen:

    +
      + ${renderTreatmentList(treatments, { showPrices: true })} +
    +

    Wir freuen uns auf dich!

    📋 Wichtiger Hinweis:

    @@ -152,8 +185,8 @@ export async function renderBookingConfirmedHTML(params: { name: string; date: s return renderBrandedEmail("Termin bestätigt", inner); } -export async function renderBookingCancelledHTML(params: { name: string; date: string; time: string }) { - const { name, date, time } = params; +export async function renderBookingCancelledHTML(params: { name: string; date: string; time: string; treatments: Array<{id: string; name: string; duration: number; price: number}> }) { + const { name, date, time, treatments } = params; const formattedDate = formatDateGerman(date); const domain = process.env.DOMAIN || 'localhost:5173'; const protocol = domain.includes('localhost') ? 'http' : 'https'; @@ -162,6 +195,12 @@ export async function renderBookingCancelledHTML(params: { name: string; date: s const inner = `

    Hallo ${name},

    dein Termin am ${formattedDate} um ${time} wurde abgesagt.

    +
    +

    💅 Abgesagte Behandlungen:

    +
      + ${renderTreatmentList(treatments, { showPrices: true })} +
    +

    Bitte buche einen neuen Termin. Bei Fragen helfen wir dir gerne weiter.

    📋 Rechtliche Informationen:

    @@ -176,13 +215,14 @@ export async function renderAdminBookingNotificationHTML(params: { name: string; date: string; time: string; - treatment: string; + treatments: Array<{id: string; name: string; duration: number; price: number}>; phone: string; notes?: string; hasInspirationPhoto: boolean; }) { - const { name, date, time, treatment, phone, notes, hasInspirationPhoto } = params; + const { name, date, time, treatments, phone, notes, hasInspirationPhoto } = params; const formattedDate = formatDateGerman(date); + const inner = `

    Hallo Admin,

    eine neue Buchungsanfrage ist eingegangen:

    @@ -191,7 +231,11 @@ export async function renderAdminBookingNotificationHTML(params: {
    • Name: ${name}
    • Telefon: ${phone}
    • -
    • Behandlung: ${treatment}
    • +
    • Behandlungen: +
        + ${renderTreatmentList(treatments, { showPrices: false })} +
      +
    • Datum: ${formattedDate}
    • Uhrzeit: ${time}
    • ${notes ? `
    • Notizen: ${notes}
    • ` : ''} diff --git a/src/server/lib/email.ts b/src/server/lib/email.ts index d378c4b..02b3f85 100644 --- a/src/server/lib/email.ts +++ b/src/server/lib/email.ts @@ -29,15 +29,27 @@ function formatDateForICS(date: string, time: string): string { return `${year}${month}${day}T${hours}${minutes}00`; } +// Helper function to escape text values for ICS files (RFC 5545) +function icsEscape(text: string): string { + return text + .replace(/\\/g, '\\\\') // Backslash must be escaped first + .replace(/;/g, '\\;') // Semicolon + .replace(/,/g, '\\,') // Comma + .replace(/\n/g, '\\n'); // Newline +} + // Helper function to create ICS (iCalendar) file content function createICSFile(params: { date: string; // YYYY-MM-DD time: string; // HH:MM - durationMinutes: number; customerName: string; - treatmentName: string; + customerEmail?: string; + treatments: Array<{id: string; name: string; duration: number; price: number}>; }): string { - const { date, time, durationMinutes, customerName, treatmentName } = params; + const { date, time, customerName, customerEmail, treatments } = params; + + // Calculate duration from treatments + const durationMinutes = treatments.reduce((sum, t) => sum + t.duration, 0); // Calculate start and end times in Europe/Berlin timezone const dtStart = formatDateForICS(date, time); @@ -57,6 +69,17 @@ function createICSFile(params: { const now = new Date(); const dtstamp = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + // Build treatments list for SUMMARY and DESCRIPTION + const treatmentNames = icsEscape(treatments.map(t => t.name).join(', ')); + const totalDuration = treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = treatments.reduce((sum, t) => sum + t.price, 0); + + const treatmentDetails = treatments.map(t => + `${icsEscape(t.name)} (${t.duration} Min, ${t.price.toFixed(2)} EUR)` + ).join('\\n'); + + const description = `Behandlungen:\\n${treatmentDetails}\\n\\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} EUR\\n\\nTermin bei Stargirlnails Kiel`; + // ICS content const icsContent = [ 'BEGIN:VCALENDAR', @@ -69,11 +92,11 @@ function createICSFile(params: { `DTSTAMP:${dtstamp}`, `DTSTART;TZID=Europe/Berlin:${dtStart}`, `DTEND;TZID=Europe/Berlin:${dtEnd}`, - `SUMMARY:${treatmentName} - Stargirlnails Kiel`, - `DESCRIPTION:Termin für ${treatmentName} bei Stargirlnails Kiel`, + `SUMMARY:${treatmentNames} - Stargirlnails Kiel`, + `DESCRIPTION:${description}`, 'LOCATION:Stargirlnails Kiel', `ORGANIZER;CN=Stargirlnails Kiel:mailto:${process.env.EMAIL_FROM?.match(/<(.+)>/)?.[1] || 'no-reply@stargirlnails.de'}`, - `ATTENDEE;CN=${customerName};RSVP=TRUE:mailto:${customerName}`, + ...(customerEmail ? [`ATTENDEE;CN=${customerName};RSVP=TRUE:mailto:${customerEmail}`] : []), 'STATUS:CONFIRMED', 'SEQUENCE:0', 'BEGIN:VALARM', @@ -187,9 +210,9 @@ export async function sendEmailWithAGBAndCalendar( calendarParams: { date: string; time: string; - durationMinutes: number; customerName: string; - treatmentName: string; + customerEmail?: string; + treatments: Array<{id: string; name: string; duration: number; price: number}>; } ): Promise<{ success: boolean }> { const agbBase64 = await getAGBPDFBase64(); diff --git a/src/server/rpc/bookings.ts b/src/server/rpc/bookings.ts index 77a9f71..1890887 100644 --- a/src/server/rpc/bookings.ts +++ b/src/server/rpc/bookings.ts @@ -363,12 +363,18 @@ const create = os name: input.customerName, date: input.appointmentDate, time: input.appointmentTime, - statusUrl: bookingUrl + statusUrl: bookingUrl, + treatments: input.treatments }); + + const treatmentsText = input.treatments.map(t => `- ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalDuration = input.treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = input.treatments.reduce((sum, t) => sum + t.price, 0); + await sendEmail({ to: input.customerEmail, subject: "Deine Terminanfrage ist eingegangen", - text: `Hallo ${input.customerName},\n\nwir haben deine Anfrage für ${formattedDate} um ${input.appointmentTime} erhalten. Wir bestätigen deinen Termin in Kürze. Du erhältst eine weitere E-Mail, sobald der Termin bestätigt ist.\n\nTermin-Status ansehen: ${bookingUrl}\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`, + text: `Hallo ${input.customerName},\n\nwir haben deine Anfrage für ${formattedDate} um ${input.appointmentTime} erhalten.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nWir bestätigen deinen Termin in Kürze. Du erhältst eine weitere E-Mail, sobald der Termin bestätigt ist.\n\nTermin-Status ansehen: ${bookingUrl}\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`, html, }).catch(() => {}); })(); @@ -377,25 +383,26 @@ const create = os void (async () => { if (!process.env.ADMIN_EMAIL) return; - // Build treatment list string - const treatmentsList = input.treatments.map(t => `${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join(', '); - const adminHtml = await renderAdminBookingNotificationHTML({ name: input.customerName, date: input.appointmentDate, time: input.appointmentTime, - treatment: treatmentsList, + treatments: input.treatments, phone: input.customerPhone || "Nicht angegeben", notes: input.notes, hasInspirationPhoto: !!input.inspirationPhoto }); const homepageUrl = generateUrl(); + const treatmentsText = input.treatments.map(t => ` - ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalDuration = input.treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = input.treatments.reduce((sum, t) => sum + t.price, 0); const adminText = `Neue Buchungsanfrage eingegangen:\n\n` + `Name: ${input.customerName}\n` + `Telefon: ${input.customerPhone || "Nicht angegeben"}\n` + - `Behandlungen: ${treatmentsList}\n` + + `Behandlungen:\n${treatmentsText}\n` + + `Gesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n` + `Datum: ${formatDateGerman(input.appointmentDate)}\n` + `Uhrzeit: ${input.appointmentTime}\n` + `${input.notes ? `Notizen: ${input.notes}\n` : ''}` + @@ -472,41 +479,48 @@ const updateStatus = os date: booking.appointmentDate, time: booking.appointmentTime, cancellationUrl: bookingUrl, // Now points to booking status page - reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`) + reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`), + treatments: booking.treatments }); - // Get treatment information for ICS file - const treatmentName = booking.treatments && booking.treatments.length > 0 - ? booking.treatments.map(t => t.name).join(', ') - : "Behandlung"; - const treatmentDuration = booking.treatments && booking.treatments.length > 0 - ? booking.treatments.reduce((sum, t) => sum + t.duration, 0) - : (booking.bookedDurationMinutes || 60); + const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = booking.treatments.reduce((sum, t) => sum + t.price, 0); if (booking.customerEmail) { await sendEmailWithAGBAndCalendar({ to: booking.customerEmail, subject: "Dein Termin wurde bestätigt - AGB im Anhang", - text: `Hallo ${booking.customerName},\n\nwir haben deinen Termin am ${formattedDate} um ${booking.appointmentTime} bestätigt.\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nTermin-Status ansehen und verwalten: ${bookingUrl}\nFalls du den Termin stornieren möchtest, kannst du das über den obigen Link tun.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nBis bald!\nStargirlnails Kiel`, + text: `Hallo ${booking.customerName},\n\nwir haben deinen Termin am ${formattedDate} um ${booking.appointmentTime} bestätigt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nTermin-Status ansehen und verwalten: ${bookingUrl}\nFalls du den Termin stornieren möchtest, kannst du das über den obigen Link tun.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nBis bald!\nStargirlnails Kiel`, html, bcc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined, }, { date: booking.appointmentDate, time: booking.appointmentTime, - durationMinutes: treatmentDuration, customerName: booking.customerName, - treatmentName: treatmentName + customerEmail: booking.customerEmail, + treatments: booking.treatments }); } } else if (input.status === "cancelled") { const formattedDate = formatDateGerman(booking.appointmentDate); const homepageUrl = generateUrl(); - const html = await renderBookingCancelledHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime }); + const html = await renderBookingCancelledHTML({ + name: booking.customerName, + date: booking.appointmentDate, + time: booking.appointmentTime, + treatments: booking.treatments + }); + + const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = booking.treatments.reduce((sum, t) => sum + t.price, 0); + if (booking.customerEmail) { await sendEmail({ to: booking.customerEmail, subject: "Dein Termin wurde abgesagt", - text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${formattedDate} um ${booking.appointmentTime} abgesagt. Bitte buche einen neuen Termin.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`, + text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${formattedDate} um ${booking.appointmentTime} abgesagt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nBitte buche einen neuen Termin.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`, html, bcc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined, }); @@ -550,11 +564,21 @@ const remove = os try { const formattedDate = formatDateGerman(booking.appointmentDate); const homepageUrl = generateUrl(); - const html = await renderBookingCancelledHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime }); + const html = await renderBookingCancelledHTML({ + name: booking.customerName, + date: booking.appointmentDate, + time: booking.appointmentTime, + treatments: booking.treatments + }); + + const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = booking.treatments.reduce((sum, t) => sum + t.price, 0); + await sendEmail({ to: booking.customerEmail, subject: "Dein Termin wurde abgesagt", - text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${formattedDate} um ${booking.appointmentTime} abgesagt. Bitte buche einen neuen Termin.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`, + text: `Hallo ${booking.customerName},\n\nleider wurde dein Termin am ${formattedDate} um ${booking.appointmentTime} abgesagt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nBitte buche einen neuen Termin.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nLiebe Grüße\nStargirlnails Kiel`, html, bcc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined, }); @@ -665,20 +689,24 @@ const createManual = os date: input.appointmentDate, time: input.appointmentTime, cancellationUrl: bookingUrl, - reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`) + reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`), + treatments: input.treatments }); + const treatmentsText = input.treatments.map(t => `- ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalPrice = input.treatments.reduce((sum, t) => sum + t.price, 0); + await sendEmailWithAGBAndCalendar({ to: input.customerEmail!, subject: "Dein Termin wurde bestätigt - AGB im Anhang", - text: `Hallo ${input.customerName},\n\nwir haben deinen Termin am ${formattedDate} um ${input.appointmentTime} bestätigt.\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nTermin-Status ansehen und verwalten: ${bookingUrl}\nFalls du den Termin stornieren möchtest, kannst du das über den obigen Link tun.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nBis bald!\nStargirlnails Kiel`, + text: `Hallo ${input.customerName},\n\nwir haben deinen Termin am ${formattedDate} um ${input.appointmentTime} bestätigt.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €\n\nWichtiger Hinweis: Die Allgemeinen Geschäftsbedingungen (AGB) findest du im Anhang dieser E-Mail. Bitte lies sie vor deinem Termin durch.\n\nTermin-Status ansehen und verwalten: ${bookingUrl}\nFalls du den Termin stornieren möchtest, kannst du das über den obigen Link tun.\n\nRechtliche Informationen: ${generateUrl('/legal')}\nZur Website: ${homepageUrl}\n\nBis bald!\nStargirlnails Kiel`, html, }, { date: input.appointmentDate, time: input.appointmentTime, - durationMinutes: totalDuration, customerName: input.customerName, - treatmentName: input.treatments.map(t => t.name).join(', ') + customerEmail: input.customerEmail, + treatments: input.treatments }); } catch (e) { console.error("Email send failed for manual booking:", e); @@ -843,21 +871,23 @@ export const router = { time: updated.appointmentTime, cancellationUrl: generateUrl(`/booking/${bookingAccessToken.token}`), reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`), + treatments: updated.treatments, }); - const treatmentName = updated.treatments && updated.treatments.length > 0 - ? updated.treatments.map(t => t.name).join(', ') - : "Behandlung"; + + const treatmentsText = updated.treatments.map(t => `- ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalPrice = updated.treatments.reduce((sum, t) => sum + t.price, 0); + await sendEmailWithAGBAndCalendar({ to: updated.customerEmail, subject: "Terminänderung bestätigt", - text: `Hallo ${updated.customerName}, dein neuer Termin ist am ${formatDateGerman(updated.appointmentDate)} um ${updated.appointmentTime}.`, + text: `Hallo ${updated.customerName}, dein neuer Termin ist am ${formatDateGerman(updated.appointmentDate)} um ${updated.appointmentTime}.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${duration} Min, ${totalPrice.toFixed(2)} €`, html, }, { date: updated.appointmentDate, time: updated.appointmentTime, - durationMinutes: duration, customerName: updated.customerName, - treatmentName: treatmentName, + customerEmail: updated.customerEmail, + treatments: updated.treatments, }).catch(() => {}); } @@ -898,11 +928,23 @@ export const router = { // Notify customer that original stays if (booking.customerEmail) { const bookingAccessToken = await queryClient.cancellation.createToken({ bookingId: booking.id }); + + const treatmentsText = booking.treatments.map(t => `- ${t.name} (${t.duration} Min, ${t.price.toFixed(2)} €)`).join('\n'); + const totalDuration = booking.treatments.reduce((sum, t) => sum + t.duration, 0); + const totalPrice = booking.treatments.reduce((sum, t) => sum + t.price, 0); + await sendEmail({ to: booking.customerEmail, subject: "Terminänderung abgelehnt", - text: `Du hast den Vorschlag zur Terminänderung abgelehnt. Dein ursprünglicher Termin am ${formatDateGerman(booking.appointmentDate)} um ${booking.appointmentTime} bleibt bestehen.`, - html: await renderBookingConfirmedHTML({ name: booking.customerName, date: booking.appointmentDate, time: booking.appointmentTime, cancellationUrl: generateUrl(`/booking/${bookingAccessToken.token}`), reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`) }), + text: `Du hast den Vorschlag zur Terminänderung abgelehnt. Dein ursprünglicher Termin am ${formatDateGerman(booking.appointmentDate)} um ${booking.appointmentTime} bleibt bestehen.\n\nBehandlungen:\n${treatmentsText}\n\nGesamt: ${totalDuration} Min, ${totalPrice.toFixed(2)} €`, + html: await renderBookingConfirmedHTML({ + name: booking.customerName, + date: booking.appointmentDate, + time: booking.appointmentTime, + cancellationUrl: generateUrl(`/booking/${bookingAccessToken.token}`), + reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`), + treatments: booking.treatments + }), }).catch(() => {}); }