feat: Add inspiration photo attachment to admin booking notifications

- 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
This commit is contained in:
2025-09-30 12:13:16 +02:00
parent bcfc481578
commit 180a5b88b8
3 changed files with 124 additions and 3 deletions

View File

@@ -91,4 +91,36 @@ export async function renderBookingCancelledHTML(params: { name: string; date: s
return renderBrandedEmail("Termin abgesagt", inner); 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);
}

View File

@@ -89,4 +89,35 @@ export async function sendEmailWithAGB(params: SendEmailParams): Promise<{ succe
return sendEmail(params); 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);
}

View File

@@ -3,8 +3,8 @@ import { z } from "zod";
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { createKV } from "@/server/lib/create-kv"; import { createKV } from "@/server/lib/create-kv";
import { createKV as createAvailabilityKV } from "@/server/lib/create-kv"; import { createKV as createAvailabilityKV } from "@/server/lib/create-kv";
import { sendEmail, sendEmailWithAGB } from "@/server/lib/email"; import { sendEmail, sendEmailWithAGB, sendEmailWithInspirationPhoto } from "@/server/lib/email";
import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML } from "@/server/lib/email-templates"; import { renderBookingPendingHTML, renderBookingConfirmedHTML, renderBookingCancelledHTML, renderAdminBookingNotificationHTML } from "@/server/lib/email-templates";
// Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy // Helper function to convert date from yyyy-mm-dd to dd.mm.yyyy
function formatDateGerman(dateString: string): string { function formatDateGerman(dateString: string): string {
@@ -41,6 +41,19 @@ type Availability = {
}; };
const availabilityKV = createAvailabilityKV<Availability>("availability"); const availabilityKV = createAvailabilityKV<Availability>("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<Treatment>("treatments");
const create = os const create = os
.input(BookingSchema.omit({ id: true, createdAt: true, status: true })) .input(BookingSchema.omit({ id: true, createdAt: true, status: true }))
.handler(async ({ input }) => { .handler(async ({ input }) => {
@@ -84,9 +97,54 @@ const create = os
subject: "Deine Terminanfrage ist eingegangen", 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`, 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, html,
cc: process.env.ADMIN_EMAIL ? [process.env.ADMIN_EMAIL] : undefined,
}).catch(() => {}); }).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; return booking;
}); });