From 180a5b88b8f162b096f0ef9edcdc8c69234dc05c Mon Sep 17 00:00:00 2001 From: elpatron Date: Tue, 30 Sep 2025 12:13:16 +0200 Subject: [PATCH] feat: Add inspiration photo attachment to admin booking notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create sendEmailWithInspirationPhoto() function to handle photo attachments - Add renderAdminBookingNotificationHTML() template for admin notifications - Extract photo extension and content from base64 data URLs - Generate unique filenames with customer name and timestamp - Send separate admin notification email with photo attachment - Include comprehensive booking details in admin email - Add visual indicators for photo availability in email template - Support both HTML and text versions of admin notifications - Handle cases where no photo is uploaded gracefully - Import treatments KV to get treatment names for admin emails Features: - Inspiration photos automatically attached to admin notifications - Structured admin email with all booking details - Photo filename includes customer name and timestamp - Fallback handling for missing photos - German localization for admin notifications - Visual photo availability indicators (✅/❌) Changes: - email.ts: Add sendEmailWithInspirationPhoto() function - email-templates.ts: Add renderAdminBookingNotificationHTML() template - bookings.ts: Send admin notifications with photo attachments --- src/server/lib/email-templates.ts | 32 ++++++++++++++++ src/server/lib/email.ts | 31 +++++++++++++++ src/server/rpc/bookings.ts | 64 +++++++++++++++++++++++++++++-- 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/server/lib/email-templates.ts b/src/server/lib/email-templates.ts index e3edfcb..966b45a 100644 --- a/src/server/lib/email-templates.ts +++ b/src/server/lib/email-templates.ts @@ -91,4 +91,36 @@ export async function renderBookingCancelledHTML(params: { name: string; date: s 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 = ` +

Hallo Admin,

+

eine neue Buchungsanfrage ist eingegangen:

+
+

📅 Buchungsdetails:

+ +
+

Bitte logge dich in das Admin-Panel ein, um die Buchung zu bestätigen oder abzulehnen.

+

Liebe Grüße,
Stargirlnails System

+ `; + return renderBrandedEmail("Neue Buchungsanfrage - Admin-Benachrichtigung", inner); +} + diff --git a/src/server/lib/email.ts b/src/server/lib/email.ts index 14ba5d5..6ba3c5e 100644 --- a/src/server/lib/email.ts +++ b/src/server/lib/email.ts @@ -89,4 +89,35 @@ export async function sendEmailWithAGB(params: SendEmailParams): Promise<{ succe return sendEmail(params); } +export async function sendEmailWithInspirationPhoto( + params: SendEmailParams, + photoData: string, + customerName: string +): Promise<{ success: boolean }> { + if (!photoData) { + return sendEmail(params); + } + + // Extract file extension from base64 data URL + const match = photoData.match(/data:image\/([^;]+);base64,(.+)/); + if (!match) { + console.warn("Invalid photo data format"); + return sendEmail(params); + } + + const [, extension, base64Content] = match; + const filename = `inspiration_${customerName.replace(/[^a-zA-Z0-9]/g, '_')}_${Date.now()}.${extension}`; + + params.attachments = [ + ...(params.attachments || []), + { + filename, + content: base64Content, + type: `image/${extension}` + } + ]; + + return sendEmail(params); +} + diff --git a/src/server/rpc/bookings.ts b/src/server/rpc/bookings.ts index 4a5e019..700795a 100644 --- a/src/server/rpc/bookings.ts +++ b/src/server/rpc/bookings.ts @@ -3,8 +3,8 @@ import { z } from "zod"; import { randomUUID } from "crypto"; import { createKV } from "@/server/lib/create-kv"; import { createKV as createAvailabilityKV } from "@/server/lib/create-kv"; -import { sendEmail, sendEmailWithAGB } from "@/server/lib/email"; -import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML } from "@/server/lib/email-templates"; +import { sendEmail, sendEmailWithAGB, sendEmailWithInspirationPhoto } from "@/server/lib/email"; +import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML, renderAdminBookingNotificationHTML } from "@/server/lib/email-templates"; // Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy function formatDateGerman(dateString: string): string { @@ -41,6 +41,19 @@ type Availability = { }; const availabilityKV = createAvailabilityKV("availability"); +// Import treatments KV for admin notifications +import { createKV as createTreatmentsKV } from "@/server/lib/create-kv"; +type Treatment = { + id: string; + name: string; + description: string; + price: number; + duration: number; + category: string; + createdAt: string; +}; +const treatmentsKV = createTreatmentsKV("treatments"); + const create = os .input(BookingSchema.omit({ id: true, createdAt: true, status: true })) .handler(async ({ input }) => { @@ -84,9 +97,54 @@ const create = os 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.\n\nLiebe Grüße\nStargirlnails Kiel`, html, - cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined, }).catch(() => {}); })(); + + // Notify admin: new booking request (with photo if available) + void (async () => { + if (!process.env.ADMIN_EMAIL) return; + + // Get treatment name from KV + const allTreatments = await treatmentsKV.getAllItems(); + const treatment = allTreatments.find(t => t.id === input.treatmentId); + const treatmentName = treatment?.name || "Unbekannte Behandlung"; + + const adminHtml = await renderAdminBookingNotificationHTML({ + name: input.customerName, + date: input.appointmentDate, + time: input.appointmentTime, + treatment: treatmentName, + phone: input.customerPhone, + notes: input.notes, + hasInspirationPhoto: !!input.inspirationPhoto + }); + + const adminText = `Neue Buchungsanfrage eingegangen:\n\n` + + `Name: ${input.customerName}\n` + + `Telefon: ${input.customerPhone}\n` + + `Behandlung: ${treatmentName}\n` + + `Datum: ${formatDateGerman(input.appointmentDate)}\n` + + `Uhrzeit: ${input.appointmentTime}\n` + + `${input.notes ? `Notizen: ${input.notes}\n` : ''}` + + `Inspiration-Foto: ${input.inspirationPhoto ? 'Im Anhang verfügbar' : 'Kein Foto hochgeladen'}\n\n` + + `Bitte logge dich in das Admin-Panel ein, um die Buchung zu bearbeiten.`; + + if (input.inspirationPhoto) { + await sendEmailWithInspirationPhoto({ + to: process.env.ADMIN_EMAIL, + subject: `Neue Buchungsanfrage - ${input.customerName}`, + text: adminText, + html: adminHtml, + }, input.inspirationPhoto, input.customerName).catch(() => {}); + } else { + await sendEmail({ + to: process.env.ADMIN_EMAIL, + subject: `Neue Buchungsanfrage - ${input.customerName}`, + text: adminText, + html: adminHtml, + }).catch(() => {}); + } + })(); return booking; });