diff --git a/src/client/components/booking-form.tsx b/src/client/components/booking-form.tsx index 5fa5b4e..e91ed01 100644 --- a/src/client/components/booking-form.tsx +++ b/src/client/components/booking-form.tsx @@ -1,9 +1,12 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { queryClient } from "@/client/rpc-client"; +// Feature flag for multi-treatments availability API compatibility +const USE_MULTI_TREATMENTS_AVAILABILITY = false; + export function BookingForm() { - const [selectedTreatment, setSelectedTreatment] = useState(""); + const [selectedTreatments, setSelectedTreatments] = useState>([]); const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [customerPhone, setCustomerPhone] = useState(""); @@ -62,29 +65,80 @@ export function BookingForm() { queryClient.treatments.live.list.experimental_liveOptions() ); - // Dynamische Verfügbarkeitsabfrage für das gewählte Datum und die Behandlung + // Comment 3: Compute total duration and price once per render + const totalDuration = useMemo( + () => selectedTreatments.reduce((sum, t) => sum + t.duration, 0), + [selectedTreatments] + ); + + const totalPrice = useMemo( + () => selectedTreatments.reduce((sum, t) => sum + t.price, 0), + [selectedTreatments] + ); + + // Comment 1: Dynamische Verfügbarkeitsabfrage mit Kompatibilitäts-Fallback + const availabilityQueryInput = USE_MULTI_TREATMENTS_AVAILABILITY + ? { date: appointmentDate, treatmentIds: selectedTreatments.map(t => t.id) } + : { date: appointmentDate, treatmentId: selectedTreatments[0]?.id ?? "" }; + + const availabilityQueryEnabled = USE_MULTI_TREATMENTS_AVAILABILITY + ? !!appointmentDate && selectedTreatments.length > 0 + : !!appointmentDate && selectedTreatments.length > 0; + const { data: availableTimes, isLoading, isFetching, error } = useQuery({ ...queryClient.recurringAvailability.getAvailableTimes.queryOptions({ - input: { - date: appointmentDate, - treatmentId: selectedTreatment - } + input: availabilityQueryInput as any }), - enabled: !!appointmentDate && !!selectedTreatment + enabled: availabilityQueryEnabled }); const { mutate: createBooking, isPending } = useMutation( queryClient.bookings.create.mutationOptions() ); - const selectedTreatmentData = treatments?.find((t) => t.id === selectedTreatment); - - // Clear selectedTime when treatment changes - const handleTreatmentChange = (treatmentId: string) => { - setSelectedTreatment(treatmentId); + // Comment 2: Handle treatment checkbox toggle with functional state updates + const handleTreatmentToggle = (treatment: {id: string, name: string, duration: number, price: number}) => { + setSelectedTreatments((prev) => { + const isSelected = prev.some(t => t.id === treatment.id); + + if (isSelected) { + // Remove from selection + return prev.filter(t => t.id !== treatment.id); + } else if (prev.length < 3) { + // Add to selection (only if limit not reached) + return [...prev, { + id: treatment.id, + name: treatment.name, + duration: treatment.duration, + price: treatment.price + }]; + } + + // Return unchanged if limit reached + return prev; + }); + + // Clear selected time when treatments change setSelectedTime(""); }; + // Comment 4: Reconcile selectedTreatments when treatments list changes + useEffect(() => { + if (!treatments) return; + + setSelectedTreatments((prev) => { + const validTreatments = prev.filter((selected) => + treatments.some((t) => t.id === selected.id) + ); + + // Only update state if something changed to avoid unnecessary re-renders + if (validTreatments.length !== prev.length) { + return validTreatments; + } + return prev; + }); + }, [treatments]); + // Clear selectedTime when it becomes invalid useEffect(() => { if (selectedTime && availableTimes && !availableTimes.includes(selectedTime)) { @@ -173,7 +227,7 @@ export function BookingForm() { setErrorMessage(""); // Clear any previous error messages // console.log("Form submitted with data:", { - // selectedTreatment, + // selectedTreatments, // customerName, // customerEmail, // customerPhone, @@ -182,8 +236,12 @@ export function BookingForm() { // agbAccepted // }); - if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !selectedTime) { - setErrorMessage("Bitte fülle alle erforderlichen Felder aus."); + if (selectedTreatments.length === 0 || !customerName || !customerEmail || !customerPhone || !appointmentDate || !selectedTime) { + if (selectedTreatments.length === 0) { + setErrorMessage("Bitte wähle mindestens eine Behandlung aus."); + } else { + setErrorMessage("Bitte fülle alle erforderlichen Felder aus."); + } return; } if (!agbAccepted) { @@ -198,7 +256,7 @@ export function BookingForm() { // Email validation now handled in backend before booking creation const appointmentTime = selectedTime; // console.log("Creating booking with data:", { - // treatmentId: selectedTreatment, + // treatments: selectedTreatments, // customerName, // customerEmail, // customerPhone, @@ -209,7 +267,7 @@ export function BookingForm() { // }); createBooking( { - treatmentId: selectedTreatment, + treatments: selectedTreatments, customerName, customerEmail, customerPhone, @@ -220,7 +278,7 @@ export function BookingForm() { }, { onSuccess: () => { - setSelectedTreatment(""); + setSelectedTreatments([]); setCustomerName(""); setCustomerEmail(""); setCustomerPhone(""); @@ -265,24 +323,65 @@ export function BookingForm() {
{/* Treatment Selection */}
- - - {selectedTreatmentData && ( -

{selectedTreatmentData.description}

+
+ + + {selectedTreatments.length} von 3 ausgewählt + +
+ + {/* Checkbox List Container */} +
+ {treatments?.map((treatment) => { + const isSelected = selectedTreatments.some(t => t.id === treatment.id); + const isDisabled = selectedTreatments.length >= 3 && !isSelected; + + return ( +
+ handleTreatmentToggle({ + id: treatment.id, + name: treatment.name, + duration: treatment.duration, + price: treatment.price + })} + className="h-4 w-4 text-pink-600 border-gray-300 rounded flex-shrink-0 mt-1" + /> + +
+ ); + })} +
+ + {/* Treatment Descriptions */} + {selectedTreatments.length > 0 && ( +
+ {selectedTreatments.map((selectedTreatment) => { + const fullTreatment = treatments?.find(t => t.id === selectedTreatment.id); + return fullTreatment ? ( +

+ {fullTreatment.name}: {fullTreatment.description} +

+ ) : null; + })} +
+ )} + + {/* Live Calculation Display */} + {selectedTreatments.length > 0 && ( +
+

+ 📊 Gesamt: {totalDuration} Min | {(totalPrice / 100).toFixed(2)} € +

+
)}
@@ -350,7 +449,7 @@ export function BookingForm() { value={selectedTime} onChange={(e) => setSelectedTime(e.target.value)} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" - disabled={!appointmentDate || !selectedTreatment || isLoading || isFetching} + disabled={!appointmentDate || selectedTreatments.length === 0 || isLoading || isFetching} required > @@ -360,23 +459,23 @@ export function BookingForm() { ))} - {appointmentDate && selectedTreatment && isLoading && ( + {appointmentDate && selectedTreatments.length > 0 && isLoading && (

Lade verfügbare Zeiten...

)} - {appointmentDate && selectedTreatment && error && ( + {appointmentDate && selectedTreatments.length > 0 && error && (

Fehler beim Laden der verfügbaren Zeiten. Bitte versuche es erneut.

)} - {appointmentDate && selectedTreatment && !isLoading && !isFetching && !error && (!availableTimes || availableTimes.length === 0) && ( + {appointmentDate && selectedTreatments.length > 0 && !isLoading && !isFetching && !error && (!availableTimes || availableTimes.length === 0) && (

Keine verfügbaren Zeiten für dieses Datum. Bitte wähle ein anderes Datum.

)} - {selectedTreatmentData && ( -

Dauer: {selectedTreatmentData.duration} Minuten

+ {selectedTreatments.length > 0 && ( +

Gesamtdauer: {totalDuration} Minuten

)} diff --git a/src/server/rpc/bookings.ts b/src/server/rpc/bookings.ts index af6c19c..77a9f71 100644 --- a/src/server/rpc/bookings.ts +++ b/src/server/rpc/bookings.ts @@ -44,7 +44,7 @@ function isDateInTimeOffPeriod(date: string, periods: TimeOffPeriod[]): boolean async function validateBookingAgainstRules( date: string, time: string, - treatmentDuration: number + totalDuration: number ): Promise { // Parse date to get day of week const [year, month, day] = date.split('-').map(Number); @@ -69,7 +69,7 @@ async function validateBookingAgainstRules( // Check if booking time falls within any rule's time span const bookingStartMinutes = parseTime(time); - const bookingEndMinutes = bookingStartMinutes + treatmentDuration; + const bookingEndMinutes = bookingStartMinutes + totalDuration; const isWithinRules = matchingRules.some(rule => { const ruleStartMinutes = parseTime(rule.startTime); @@ -88,7 +88,7 @@ async function validateBookingAgainstRules( async function checkBookingConflicts( date: string, time: string, - treatmentDuration: number, + totalDuration: number, excludeBookingId?: string ): Promise { const allBookings = await kv.getAllItems(); @@ -99,10 +99,10 @@ async function checkBookingConflicts( ); const bookingStartMinutes = parseTime(time); - const bookingEndMinutes = bookingStartMinutes + treatmentDuration; + const bookingEndMinutes = bookingStartMinutes + totalDuration; - // Cache treatment durations by ID to avoid N+1 lookups - const uniqueTreatmentIds = [...new Set(dateBookings.map(booking => booking.treatmentId))]; + // Cache treatment durations by ID to avoid N+1 lookups (for backward compatibility with old bookings) + const uniqueTreatmentIds = [...new Set(dateBookings.filter(b => b.treatmentId).map(booking => booking.treatmentId!))]; const treatmentDurationMap = new Map(); for (const treatmentId of uniqueTreatmentIds) { @@ -112,10 +112,21 @@ async function checkBookingConflicts( // Check for overlaps with existing bookings for (const existingBooking of dateBookings) { - // Use cached duration or fallback to bookedDurationMinutes if available - let existingDuration = treatmentDurationMap.get(existingBooking.treatmentId) || 60; - if (existingBooking.bookedDurationMinutes) { - existingDuration = existingBooking.bookedDurationMinutes; + let existingDuration: number; + + // Handle both new bookings with treatments array and old bookings with treatmentId + if (existingBooking.treatments && existingBooking.treatments.length > 0) { + // New format: calculate duration from treatments array + existingDuration = existingBooking.treatments.reduce((sum, t) => sum + t.duration, 0); + } else if (existingBooking.treatmentId) { + // Old format: use cached duration or fallback to bookedDurationMinutes if available + existingDuration = treatmentDurationMap.get(existingBooking.treatmentId) || 60; + if (existingBooking.bookedDurationMinutes) { + existingDuration = existingBooking.bookedDurationMinutes; + } + } else { + // Fallback for bookings without treatment info + existingDuration = existingBooking.bookedDurationMinutes || 60; } const existingStartMinutes = parseTime(existingBooking.appointmentTime); @@ -128,8 +139,22 @@ async function checkBookingConflicts( } } +// Reusable treatments array schema with duplicate validation +const TreatmentsArraySchema = z.array(z.object({ + id: z.string(), + name: z.string(), + duration: z.number().positive(), + price: z.number().nonnegative(), +})) + .min(1, "Mindestens eine Behandlung muss ausgewählt werden") + .max(3, "Maximal 3 Behandlungen können ausgewählt werden") + .refine(list => { + const ids = list.map(t => t.id); + return ids.length === new Set(ids).size; + }, { message: "Doppelte Behandlungen sind nicht erlaubt" }); + const CreateBookingInputSchema = z.object({ - treatmentId: z.string(), + treatments: TreatmentsArraySchema, customerName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"), customerEmail: z.string().email("Ungültige E-Mail-Adresse"), customerPhone: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein").optional(), @@ -141,7 +166,12 @@ const CreateBookingInputSchema = z.object({ const BookingSchema = z.object({ id: z.string(), - treatmentId: z.string(), + treatments: z.array(z.object({ + id: z.string(), + name: z.string(), + duration: z.number().positive(), + price: z.number().nonnegative() + })), customerName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"), customerEmail: z.string().email("Ungültige E-Mail-Adresse").optional(), customerPhone: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein").optional(), @@ -150,10 +180,12 @@ const BookingSchema = z.object({ status: z.enum(["pending", "confirmed", "cancelled", "completed"]), notes: z.string().optional(), inspirationPhoto: z.string().optional(), // Base64 encoded image data - bookedDurationMinutes: z.number().optional(), // Snapshot of treatment duration at booking time createdAt: z.string(), // DEPRECATED: slotId is no longer used for validation, kept for backward compatibility slotId: z.string().optional(), + // DEPRECATED: treatmentId and bookedDurationMinutes kept for backward compatibility + treatmentId: z.string().optional(), + bookedDurationMinutes: z.number().optional(), }); type Booking = z.output; @@ -272,31 +304,46 @@ const create = os throw new Error("Du hast bereits eine Buchung für dieses Datum. Bitte wähle einen anderen Tag oder storniere zuerst."); } } - // Get treatment duration for validation - const treatment = await treatmentsKV.getItem(input.treatmentId); - if (!treatment) { - throw new Error("Behandlung nicht gefunden."); + // Validate all treatments exist and snapshot them from KV + const snapshottedTreatments = [] as Array<{id: string; name: string; duration: number; price: number}>; + for (const inputTreatment of input.treatments) { + const treatment = await treatmentsKV.getItem(inputTreatment.id); + if (!treatment) { + throw new Error(`Behandlung "${inputTreatment.name}" nicht gefunden.`); + } + // Verify snapshot data matches current treatment data + if (treatment.name !== inputTreatment.name || treatment.duration !== inputTreatment.duration || treatment.price !== inputTreatment.price) { + throw new Error(`Behandlungsdaten für "${inputTreatment.name}" stimmen nicht überein. Bitte lade die Seite neu.`); + } + snapshottedTreatments.push({ id: treatment.id, name: treatment.name, duration: treatment.duration, price: treatment.price }); } + const totalDuration = snapshottedTreatments.reduce((sum, t) => sum + t.duration, 0); // Validate booking time against recurring rules await validateBookingAgainstRules( input.appointmentDate, input.appointmentTime, - treatment.duration + totalDuration ); // Check for booking conflicts await checkBookingConflicts( input.appointmentDate, input.appointmentTime, - treatment.duration + totalDuration ); const id = randomUUID(); const booking = { id, - ...input, - bookedDurationMinutes: treatment.duration, // Snapshot treatment duration + treatments: snapshottedTreatments, + customerName: input.customerName, + customerEmail: input.customerEmail, + customerPhone: input.customerPhone, + appointmentDate: input.appointmentDate, + appointmentTime: input.appointmentTime, + notes: input.notes, + inspirationPhoto: input.inspirationPhoto, status: "pending" as const, createdAt: new Date().toISOString() }; @@ -330,16 +377,14 @@ const create = os 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"; + // 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: treatmentName, + treatment: treatmentsList, phone: input.customerPhone || "Nicht angegeben", notes: input.notes, hasInspirationPhoto: !!input.inspirationPhoto @@ -350,7 +395,7 @@ const create = os const adminText = `Neue Buchungsanfrage eingegangen:\n\n` + `Name: ${input.customerName}\n` + `Telefon: ${input.customerPhone || "Nicht angegeben"}\n` + - `Behandlung: ${treatmentName}\n` + + `Behandlungen: ${treatmentsList}\n` + `Datum: ${formatDateGerman(input.appointmentDate)}\n` + `Uhrzeit: ${input.appointmentTime}\n` + `${input.notes ? `Notizen: ${input.notes}\n` : ''}` + @@ -431,11 +476,12 @@ const updateStatus = os }); // Get treatment information for ICS file - const allTreatments = await treatmentsKV.getAllItems(); - const treatment = allTreatments.find(t => t.id === booking.treatmentId); - const treatmentName = treatment?.name || "Behandlung"; - // Use bookedDurationMinutes if available, otherwise fallback to treatment duration - const treatmentDuration = booking.bookedDurationMinutes || treatment?.duration || 60; + 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); if (booking.customerEmail) { await sendEmailWithAGBAndCalendar({ @@ -524,7 +570,7 @@ const remove = os const createManual = os .input(z.object({ sessionId: z.string(), - treatmentId: z.string(), + treatments: TreatmentsArraySchema, customerName: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein"), customerEmail: z.string().email("Ungültige E-Mail-Adresse").optional(), customerPhone: z.string().min(5, "Telefonnummer muss mindestens 5 Zeichen lang sein").optional(), @@ -557,37 +603,45 @@ const createManual = os } } - // Get treatment duration for validation - const treatment = await treatmentsKV.getItem(input.treatmentId); - if (!treatment) { - throw new Error("Behandlung nicht gefunden."); + // Validate all treatments exist and snapshot them from KV + const snapshottedTreatments = [] as Array<{id: string; name: string; duration: number; price: number}>; + for (const inputTreatment of input.treatments) { + const treatment = await treatmentsKV.getItem(inputTreatment.id); + if (!treatment) { + throw new Error(`Behandlung "${inputTreatment.name}" nicht gefunden.`); + } + // Verify snapshot data matches current treatment data + if (treatment.name !== inputTreatment.name || treatment.duration !== inputTreatment.duration || treatment.price !== inputTreatment.price) { + throw new Error(`Behandlungsdaten für "${inputTreatment.name}" stimmen nicht überein. Bitte lade die Seite neu.`); + } + snapshottedTreatments.push({ id: treatment.id, name: treatment.name, duration: treatment.duration, price: treatment.price }); } + const totalDuration = snapshottedTreatments.reduce((sum, t) => sum + t.duration, 0); // Validate booking time against recurring rules await validateBookingAgainstRules( input.appointmentDate, input.appointmentTime, - treatment.duration + totalDuration ); // Check for booking conflicts await checkBookingConflicts( input.appointmentDate, input.appointmentTime, - treatment.duration + totalDuration ); const id = randomUUID(); const booking = { id, - treatmentId: input.treatmentId, + treatments: snapshottedTreatments, customerName: input.customerName, customerEmail: input.customerEmail, customerPhone: input.customerPhone, appointmentDate: input.appointmentDate, appointmentTime: input.appointmentTime, notes: input.notes, - bookedDurationMinutes: treatment.duration, status: "confirmed" as const, createdAt: new Date().toISOString() } as Booking; @@ -622,9 +676,9 @@ const createManual = os }, { date: input.appointmentDate, time: input.appointmentTime, - durationMinutes: treatment.duration, + durationMinutes: totalDuration, customerName: input.customerName, - treatmentName: treatment.name + treatmentName: input.treatments.map(t => t.name).join(', ') }); } catch (e) { console.error("Email send failed for manual booking:", e); @@ -695,8 +749,10 @@ export const router = { if (!booking) throw new Error("Booking not found"); if (booking.status !== "confirmed") throw new Error("Nur bestätigte Termine können umgebucht werden."); - const treatment = await treatmentsKV.getItem(booking.treatmentId); - if (!treatment) throw new Error("Behandlung nicht gefunden."); + // Calculate total duration from treatments array + const totalDuration = booking.treatments && booking.treatments.length > 0 + ? booking.treatments.reduce((sum, t) => sum + t.duration, 0) + : (booking.bookedDurationMinutes || 60); // Validate grid and not in past const appointmentMinutes = parseTime(input.proposedTime); @@ -715,8 +771,8 @@ export const router = { } } - await validateBookingAgainstRules(input.proposedDate, input.proposedTime, booking.bookedDurationMinutes || treatment.duration); - await checkBookingConflicts(input.proposedDate, input.proposedTime, booking.bookedDurationMinutes || treatment.duration, booking.id); + await validateBookingAgainstRules(input.proposedDate, input.proposedTime, totalDuration); + await checkBookingConflicts(input.proposedDate, input.proposedTime, totalDuration, booking.id); // Invalidate and create new reschedule token via cancellation router const res = await queryClient.cancellation.createRescheduleToken({ @@ -729,13 +785,16 @@ export const router = { // Send proposal email to customer if (booking.customerEmail) { + const treatmentName = booking.treatments && booking.treatments.length > 0 + ? booking.treatments.map(t => t.name).join(', ') + : "Behandlung"; const html = await renderBookingRescheduleProposalHTML({ name: booking.customerName, originalDate: booking.appointmentDate, originalTime: booking.appointmentTime, proposedDate: input.proposedDate, proposedTime: input.proposedTime, - treatmentName: (await treatmentsKV.getItem(booking.treatmentId))?.name || "Behandlung", + treatmentName: treatmentName, acceptUrl, declineUrl, expiresAt: res.expiresAt, @@ -761,8 +820,9 @@ export const router = { if (!booking) throw new Error("Booking not found"); if (booking.status !== "confirmed") throw new Error("Buchung ist nicht mehr in bestätigtem Zustand."); - const treatment = await treatmentsKV.getItem(booking.treatmentId); - const duration = booking.bookedDurationMinutes || treatment?.duration || 60; + const duration = booking.treatments && booking.treatments.length > 0 + ? booking.treatments.reduce((sum, t) => sum + t.duration, 0) + : (booking.bookedDurationMinutes || 60); // Re-validate slot to ensure still available await validateBookingAgainstRules(proposal.proposed.date, proposal.proposed.time, duration); @@ -784,6 +844,9 @@ export const router = { cancellationUrl: generateUrl(`/booking/${bookingAccessToken.token}`), reviewUrl: generateUrl(`/review/${bookingAccessToken.token}`), }); + const treatmentName = updated.treatments && updated.treatments.length > 0 + ? updated.treatments.map(t => t.name).join(', ') + : "Behandlung"; await sendEmailWithAGBAndCalendar({ to: updated.customerEmail, subject: "Terminänderung bestätigt", @@ -794,18 +857,21 @@ export const router = { time: updated.appointmentTime, durationMinutes: duration, customerName: updated.customerName, - treatmentName: (await treatmentsKV.getItem(updated.treatmentId))?.name || "Behandlung", + treatmentName: treatmentName, }).catch(() => {}); } if (process.env.ADMIN_EMAIL) { + const treatmentName = updated.treatments && updated.treatments.length > 0 + ? updated.treatments.map(t => t.name).join(', ') + : "Behandlung"; const adminHtml = await renderAdminRescheduleAcceptedHTML({ customerName: updated.customerName, originalDate: proposal.original.date, originalTime: proposal.original.time, newDate: updated.appointmentDate, newTime: updated.appointmentTime, - treatmentName: (await treatmentsKV.getItem(updated.treatmentId))?.name || "Behandlung", + treatmentName: treatmentName, }); await sendEmail({ to: process.env.ADMIN_EMAIL, @@ -842,13 +908,16 @@ export const router = { // Notify admin if (process.env.ADMIN_EMAIL) { + const treatmentName = booking.treatments && booking.treatments.length > 0 + ? booking.treatments.map(t => t.name).join(', ') + : "Behandlung"; const html = await renderAdminRescheduleDeclinedHTML({ customerName: booking.customerName, originalDate: proposal.original.date, originalTime: proposal.original.time, proposedDate: proposal.proposed.date!, proposedTime: proposal.proposed.time!, - treatmentName: (await treatmentsKV.getItem(booking.treatmentId))?.name || "Behandlung", + treatmentName: treatmentName, customerEmail: booking.customerEmail, customerPhone: booking.customerPhone, }); @@ -938,8 +1007,9 @@ export const router = { } // Get treatment name for context - const treatment = await treatmentsKV.getItem(booking.treatmentId); - const treatmentName = treatment?.name || "Behandlung"; + const treatmentName = booking.treatments && booking.treatments.length > 0 + ? booking.treatments.map(t => t.name).join(', ') + : "Behandlung"; // Prepare email with Reply-To header const ownerName = process.env.OWNER_NAME || "Stargirlnails Kiel";