Initial commit: Kalender, Buchungen mit Slot-Reservierung, Resend-E-Mails, Admin-UI, Startscript

This commit is contained in:
2025-09-29 19:10:42 +02:00
parent a3d032af9f
commit b33036300f
13 changed files with 571 additions and 58 deletions

View File

@@ -8,66 +8,33 @@ export function BookingForm() {
const [customerEmail, setCustomerEmail] = useState("");
const [customerPhone, setCustomerPhone] = useState("");
const [appointmentDate, setAppointmentDate] = useState("");
const [appointmentTime, setAppointmentTime] = useState("");
const [selectedSlotId, setSelectedSlotId] = useState<string>("");
const [notes, setNotes] = useState("");
const { data: treatments } = useQuery(
queryClient.treatments.live.list.experimental_liveOptions()
);
const { data: allBookings } = useQuery(
queryClient.bookings.live.list.experimental_liveOptions()
const { data: slotsByDate } = useQuery(
appointmentDate
? queryClient.availability.live.byDate.experimental_liveOptions(appointmentDate)
: queryClient.availability.live.byDate.experimental_liveOptions("")
);
const selectedDateBookings = allBookings?.filter(booking =>
booking.appointmentDate === appointmentDate
) || [];
const { mutate: createBooking, isPending } = useMutation(
queryClient.bookings.create.mutationOptions()
);
const selectedTreatmentData = treatments?.find(t => t.id === selectedTreatment);
// Generate available time slots (9 AM to 6 PM, 30-minute intervals)
const generateTimeSlots = () => {
const slots: string[] = [];
for (let hour = 9; hour < 18; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
slots.push(time);
}
}
return slots;
};
const availableTimeSlots = generateTimeSlots().filter(time => {
if (!selectedDateBookings || !selectedTreatmentData) return true;
// Check if this time slot conflicts with existing bookings
const timeSlotStart = new Date(`${appointmentDate}T${time}:00`);
const timeSlotEnd = new Date(timeSlotStart.getTime() + selectedTreatmentData.duration * 60000);
return !selectedDateBookings.some(booking => {
if (booking.status === 'cancelled') return false;
const bookingStart = new Date(`${booking.appointmentDate}T${booking.appointmentTime}:00`);
const treatment = treatments?.find(t => t.id === booking.treatmentId);
if (!treatment) return false;
const bookingEnd = new Date(bookingStart.getTime() + treatment.duration * 60000);
return (timeSlotStart < bookingEnd && timeSlotEnd > bookingStart);
});
});
const availableSlots = (slotsByDate || []).filter(s => s.status === "free");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !appointmentTime) {
if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !selectedSlotId) {
alert("Bitte fülle alle erforderlichen Felder aus");
return;
}
const slot = availableSlots.find(s => s.id === selectedSlotId);
const appointmentTime = slot?.time || "";
createBooking({
treatmentId: selectedTreatment,
customerName,
@@ -76,6 +43,7 @@ export function BookingForm() {
appointmentDate,
appointmentTime,
notes,
slotId: selectedSlotId,
}, {
onSuccess: () => {
setSelectedTreatment("");
@@ -83,7 +51,7 @@ export function BookingForm() {
setCustomerEmail("");
setCustomerPhone("");
setAppointmentDate("");
setAppointmentTime("");
setSelectedSlotId("");
setNotes("");
alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen.");
}
@@ -179,21 +147,23 @@ export function BookingForm() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Gewünschte Uhrzeit *
Verfügbare Uhrzeit *
</label>
<select
value={appointmentTime}
onChange={(e) => setAppointmentTime(e.target.value)}
value={selectedSlotId}
onChange={(e) => setSelectedSlotId(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}
required
>
<option value="">Zeit auswählen</option>
{availableTimeSlots.map((time) => (
<option key={time} value={time}>
{time}
</option>
))}
{availableSlots
.sort((a, b) => a.time.localeCompare(b.time))
.map((slot) => (
<option key={slot.id} value={slot.id}>
{slot.time} ({slot.durationMinutes} min)
</option>
))}
</select>
</div>
</div>