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 [selectedTreatments, setSelectedTreatments] = useState>([]); const [customerName, setCustomerName] = useState(""); const [customerEmail, setCustomerEmail] = useState(""); const [customerPhone, setCustomerPhone] = useState(""); const [appointmentDate, setAppointmentDate] = useState(""); const [selectedTime, setSelectedTime] = useState(""); const [notes, setNotes] = useState(""); const [agbAccepted, setAgbAccepted] = useState(false); const [ageConfirmed, setAgeConfirmed] = useState(false); const [inspirationPhoto, setInspirationPhoto] = useState(""); const [photoPreview, setPhotoPreview] = useState(""); const [errorMessage, setErrorMessage] = useState(""); const [isInitialized, setIsInitialized] = useState(false); // Load saved customer data from localStorage on mount useEffect(() => { const savedName = localStorage.getItem("bookingForm_customerName"); const savedEmail = localStorage.getItem("bookingForm_customerEmail"); const savedPhone = localStorage.getItem("bookingForm_customerPhone"); if (savedName) setCustomerName(savedName); if (savedEmail) setCustomerEmail(savedEmail); if (savedPhone) setCustomerPhone(savedPhone); setIsInitialized(true); }, []); // Save customer data to localStorage when it changes (after initial load) useEffect(() => { if (!isInitialized) return; if (customerName) { localStorage.setItem("bookingForm_customerName", customerName); } else { localStorage.removeItem("bookingForm_customerName"); } }, [customerName, isInitialized]); useEffect(() => { if (!isInitialized) return; if (customerEmail) { localStorage.setItem("bookingForm_customerEmail", customerEmail); } else { localStorage.removeItem("bookingForm_customerEmail"); } }, [customerEmail, isInitialized]); useEffect(() => { if (!isInitialized) return; if (customerPhone) { localStorage.setItem("bookingForm_customerPhone", customerPhone); } else { localStorage.removeItem("bookingForm_customerPhone"); } }, [customerPhone, isInitialized]); const { data: treatments } = useQuery( queryClient.treatments.live.list.experimental_liveOptions() ); // 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: availabilityQueryInput as any }), enabled: availabilityQueryEnabled }); const { mutate: createBooking, isPending } = useMutation( queryClient.bookings.create.mutationOptions() ); // 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)) { setSelectedTime(""); } }, [availableTimes, selectedTime]); // Helper function for local date in YYYY-MM-DD format const getLocalYmd = () => { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const handlePhotoUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // Check file size (max 2MB for better performance) if (file.size > 2 * 1024 * 1024) { alert("Das Foto ist zu groß. Bitte wähle ein Bild unter 2MB."); return; } // Check file type if (!file.type.startsWith('image/')) { alert("Bitte wähle nur Bilddateien aus."); return; } // Compress the image before converting to base64 const compressImage = (file: File, maxWidth: number = 800, quality: number = 0.8): Promise => { return new Promise((resolve, reject) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = () => { // Calculate new dimensions let { width, height } = img; if (width > maxWidth) { height = (height * maxWidth) / width; width = maxWidth; } canvas.width = width; canvas.height = height; // Draw and compress ctx?.drawImage(img, 0, 0, width, height); const compressedDataUrl = canvas.toDataURL('image/jpeg', quality); resolve(compressedDataUrl); }; img.onerror = reject; img.src = URL.createObjectURL(file); }); }; try { const compressedDataUrl = await compressImage(file); setInspirationPhoto(compressedDataUrl); setPhotoPreview(compressedDataUrl); // console.log(`Photo compressed: ${file.size} bytes → ${compressedDataUrl.length} chars`); } catch (error) { console.error('Photo compression failed:', error); alert('Fehler beim Verarbeiten des Bildes. Bitte versuche es mit einem anderen Bild.'); return; } }; const removePhoto = () => { setInspirationPhoto(""); setPhotoPreview(""); // Reset file input const fileInput = document.getElementById('photo-upload') as HTMLInputElement; if (fileInput) fileInput.value = ''; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrorMessage(""); // Clear any previous error messages // console.log("Form submitted with data:", { // selectedTreatments, // customerName, // customerEmail, // customerPhone, // appointmentDate, // selectedSlotId, // agbAccepted // }); 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) { setErrorMessage("Bitte bestätige die Kenntnisnahme der Allgemeinen Geschäftsbedingungen."); return; } if (!ageConfirmed) { setErrorMessage("Bitte bestätige, dass du mindestens 16 Jahre alt bist."); return; } // Email validation now handled in backend before booking creation const appointmentTime = selectedTime; // console.log("Creating booking with data:", { // treatments: selectedTreatments, // customerName, // customerEmail, // customerPhone, // appointmentDate, // appointmentTime, // notes, // inspirationPhoto // }); createBooking( { treatments: selectedTreatments, customerName, customerEmail, customerPhone, appointmentDate, appointmentTime, notes, inspirationPhoto, }, { onSuccess: () => { setSelectedTreatments([]); setCustomerName(""); setCustomerEmail(""); setCustomerPhone(""); setAppointmentDate(""); setSelectedTime(""); setNotes(""); setAgbAccepted(false); setAgeConfirmed(false); setInspirationPhoto(""); setPhotoPreview(""); setErrorMessage(""); // Reset file input const fileInput = document.getElementById('photo-upload') as HTMLInputElement; if (fileInput) fileInput.value = ''; alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen."); }, onError: (error: any) => { console.error("Booking error:", error); // Simple error handling for oRPC errors let errorText = "Ein unbekannter Fehler ist aufgetreten."; if (error?.cause?.message) { errorText = error.cause.message; } else if (error?.message && error.message !== "Internal server error") { errorText = error.message; } setErrorMessage(errorText); }, } ); }; // Dynamische Zeitauswahl: Kunde wählt beliebiges zukünftiges Datum, // System berechnet verfügbare Zeiten in 15-Minuten-Intervallen basierend auf wiederkehrenden Regeln return (

Buche deine Nagelbehandlung

{/* Treatment Selection */}
{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)} €

)}
{/* Customer Information */}
setCustomerName(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" required />
setCustomerEmail(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" required />
setCustomerPhone(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" required />
{/* Date and Time Selection */}
{ setAppointmentDate(e.target.value); setSelectedTime(""); }} min={getLocalYmd()} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" required />
{appointmentDate && selectedTreatments.length > 0 && isLoading && (

Lade verfügbare Zeiten...

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

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

)} {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.

)} {selectedTreatments.length > 0 && (

Gesamtdauer: {totalDuration} Minuten

)}
{/* Notes */}