diff --git a/src/client/components/admin-bookings.tsx b/src/client/components/admin-bookings.tsx index 52c4942..fbd3bd1 100644 --- a/src/client/components/admin-bookings.tsx +++ b/src/client/components/admin-bookings.tsx @@ -4,6 +4,8 @@ import { queryClient } from "@/client/rpc-client"; export function AdminBookings() { const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); + const [selectedPhoto, setSelectedPhoto] = useState(""); + const [showPhotoModal, setShowPhotoModal] = useState(false); const { data: bookings } = useQuery( queryClient.bookings.live.list.experimental_liveOptions() @@ -31,6 +33,16 @@ export function AdminBookings() { } }; + const openPhotoModal = (photoData: string) => { + setSelectedPhoto(photoData); + setShowPhotoModal(true); + }; + + const closePhotoModal = () => { + setShowPhotoModal(false); + setSelectedPhoto(""); + }; + const filteredBookings = bookings?.filter(booking => selectedDate ? booking.appointmentDate === selectedDate : true ).sort((a, b) => { @@ -117,6 +129,9 @@ export function AdminBookings() { Date & Time + + Inspiration + Status @@ -148,6 +163,22 @@ export function AdminBookings() {
Slot-ID: {booking.slotId}
)} + + {booking.inspirationPhoto ? ( + + ) : ( + Kein Foto + )} + {booking.status} @@ -211,6 +242,38 @@ export function AdminBookings() { )} + + {/* Photo Modal */} + {showPhotoModal && ( +
+
+
+

Inspiration Foto

+ +
+
+ Inspiration Foto +
+
+ +
+
+
+ )} ); } \ No newline at end of file diff --git a/src/client/components/booking-form.tsx b/src/client/components/booking-form.tsx index 71225a2..cc5aed1 100644 --- a/src/client/components/booking-form.tsx +++ b/src/client/components/booking-form.tsx @@ -11,6 +11,8 @@ export function BookingForm() { const [selectedSlotId, setSelectedSlotId] = useState(""); const [notes, setNotes] = useState(""); const [agbAccepted, setAgbAccepted] = useState(false); + const [inspirationPhoto, setInspirationPhoto] = useState(""); + const [photoPreview, setPhotoPreview] = useState(""); const { data: treatments } = useQuery( queryClient.treatments.live.list.experimental_liveOptions() @@ -33,6 +35,39 @@ export function BookingForm() { const selectedTreatmentData = treatments?.find((t) => t.id === selectedTreatment); const availableSlots = (slotsByDate || []).filter((s) => s.status === "free"); + const handlePhotoUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Check file size (max 5MB) + if (file.size > 5 * 1024 * 1024) { + alert("Das Foto ist zu groß. Bitte wähle ein Bild unter 5MB."); + return; + } + + // Check file type + if (!file.type.startsWith('image/')) { + alert("Bitte wähle nur Bilddateien aus."); + return; + } + + const reader = new FileReader(); + reader.onload = (event) => { + const result = event.target?.result as string; + setInspirationPhoto(result); + setPhotoPreview(result); + }; + reader.readAsDataURL(file); + }; + + const removePhoto = () => { + setInspirationPhoto(""); + setPhotoPreview(""); + // Reset file input + const fileInput = document.getElementById('photo-upload') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + }; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !selectedSlotId) { @@ -54,6 +89,7 @@ export function BookingForm() { appointmentDate, appointmentTime, notes, + inspirationPhoto, slotId: selectedSlotId, }, { @@ -66,6 +102,11 @@ export function BookingForm() { setSelectedSlotId(""); setNotes(""); setAgbAccepted(false); + setInspirationPhoto(""); + setPhotoPreview(""); + // 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."); }, } @@ -202,6 +243,41 @@ export function BookingForm() { /> + {/* Inspiration Photo Upload */} +
+ +
+ + {photoPreview && ( +
+ Inspiration Preview + +
+ )} +

+ 📸 Lade ein Foto hoch, das als Inspiration für deine Nagelbehandlung dienen soll. Max. 5MB, alle Bildformate erlaubt. +

+
+
+ {/* AGB Acceptance */}
diff --git a/src/server/rpc/bookings.ts b/src/server/rpc/bookings.ts index 1598673..4a5e019 100644 --- a/src/server/rpc/bookings.ts +++ b/src/server/rpc/bookings.ts @@ -22,6 +22,7 @@ const BookingSchema = z.object({ appointmentTime: z.string(), // HH:MM format status: z.enum(["pending", "confirmed", "cancelled", "completed"]), notes: z.string().optional(), + inspirationPhoto: z.string().optional(), // Base64 encoded image data createdAt: z.string(), slotId: z.string().optional(), });