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:
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user