Verbessere Booking-Form UX: Reset selectedTime bei Treatment-Wechsel, bessere Loading-States und lokale Datumsvalidierung
This commit is contained in:
@@ -9,6 +9,22 @@ export function AdminAvailability() {
|
||||
const [duration, setDuration] = useState<number>(30);
|
||||
const [selectedTreatmentId, setSelectedTreatmentId] = useState<string>("");
|
||||
const [slotType, setSlotType] = useState<"treatment" | "manual">("treatment");
|
||||
|
||||
// Neue State-Variablen für Tab-Navigation
|
||||
const [activeSubTab, setActiveSubTab] = useState<"slots" | "recurring" | "timeoff">("slots");
|
||||
|
||||
// States für Recurring Rules
|
||||
const [selectedDayOfWeek, setSelectedDayOfWeek] = useState<number>(1); // 1=Montag
|
||||
const [ruleStartTime, setRuleStartTime] = useState<string>("13:00");
|
||||
const [ruleEndTime, setRuleEndTime] = useState<string>("18:00");
|
||||
const [editingRuleId, setEditingRuleId] = useState<string>("");
|
||||
|
||||
// States für Time-Off
|
||||
const [timeOffStartDate, setTimeOffStartDate] = useState<string>("");
|
||||
const [timeOffEndDate, setTimeOffEndDate] = useState<string>("");
|
||||
const [timeOffReason, setTimeOffReason] = useState<string>("");
|
||||
const [editingTimeOffId, setEditingTimeOffId] = useState<string>("");
|
||||
|
||||
|
||||
const { data: allSlots, refetch: refetchSlots } = useQuery(
|
||||
queryClient.availability.live.list.experimental_liveOptions()
|
||||
@@ -17,6 +33,18 @@ export function AdminAvailability() {
|
||||
const { data: treatments } = useQuery(
|
||||
queryClient.treatments.live.list.experimental_liveOptions()
|
||||
);
|
||||
|
||||
// Neue Queries für wiederkehrende Verfügbarkeiten (mit Authentifizierung)
|
||||
const { data: recurringRules } = useQuery(
|
||||
queryClient.recurringAvailability.live.adminListRules.experimental_liveOptions({
|
||||
input: { sessionId: localStorage.getItem("sessionId") || "" }
|
||||
})
|
||||
);
|
||||
const { data: timeOffPeriods } = useQuery(
|
||||
queryClient.recurringAvailability.live.adminListTimeOff.experimental_liveOptions({
|
||||
input: { sessionId: localStorage.getItem("sessionId") || "" }
|
||||
})
|
||||
);
|
||||
|
||||
const [errorMsg, setErrorMsg] = useState<string>("");
|
||||
const [successMsg, setSuccessMsg] = useState<string>("");
|
||||
@@ -45,6 +73,29 @@ export function AdminAvailability() {
|
||||
const { mutate: cleanupPastSlots } = useMutation(
|
||||
queryClient.availability.cleanupPastSlots.mutationOptions()
|
||||
);
|
||||
|
||||
// Neue Mutations für wiederkehrende Verfügbarkeiten
|
||||
const { mutate: createRule } = useMutation(
|
||||
queryClient.recurringAvailability.createRule.mutationOptions()
|
||||
);
|
||||
const { mutate: updateRule } = useMutation(
|
||||
queryClient.recurringAvailability.updateRule.mutationOptions()
|
||||
);
|
||||
const { mutate: deleteRule } = useMutation(
|
||||
queryClient.recurringAvailability.deleteRule.mutationOptions()
|
||||
);
|
||||
const { mutate: toggleRuleActive } = useMutation(
|
||||
queryClient.recurringAvailability.toggleRuleActive.mutationOptions()
|
||||
);
|
||||
const { mutate: createTimeOff } = useMutation(
|
||||
queryClient.recurringAvailability.createTimeOff.mutationOptions()
|
||||
);
|
||||
const { mutate: updateTimeOff } = useMutation(
|
||||
queryClient.recurringAvailability.updateTimeOff.mutationOptions()
|
||||
);
|
||||
const { mutate: deleteTimeOff } = useMutation(
|
||||
queryClient.recurringAvailability.deleteTimeOff.mutationOptions()
|
||||
);
|
||||
|
||||
// Auto-update duration when treatment is selected
|
||||
useEffect(() => {
|
||||
@@ -63,6 +114,12 @@ export function AdminAvailability() {
|
||||
const getTreatmentName = (treatmentId: string) => {
|
||||
return treatments?.find(t => t.id === treatmentId)?.name || "Unbekannte Behandlung";
|
||||
};
|
||||
|
||||
// Helper-Funktion für Wochentag-Namen
|
||||
const getDayName = (dayOfWeek: number): string => {
|
||||
const days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
||||
return days[dayOfWeek];
|
||||
};
|
||||
|
||||
const addSlot = () => {
|
||||
setErrorMsg("");
|
||||
@@ -116,7 +173,48 @@ export function AdminAvailability() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Slot Type Selection */}
|
||||
{/* Tab-Navigation */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="-mb-px flex space-x-8 px-6">
|
||||
<button
|
||||
onClick={() => setActiveSubTab("slots")}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeSubTab === "slots"
|
||||
? "border-pink-500 text-pink-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
📅 Slots
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSubTab("recurring")}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeSubTab === "recurring"
|
||||
? "border-pink-500 text-pink-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
🔄 Wiederkehrende Zeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSubTab("timeoff")}
|
||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeSubTab === "timeoff"
|
||||
? "border-pink-500 text-pink-600"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
🏖️ Urlaubszeiten
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Inhalt */}
|
||||
{activeSubTab === "slots" && (
|
||||
<>
|
||||
{/* Slot Type Selection */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">Neuen Slot anlegen</h3>
|
||||
|
||||
@@ -421,6 +519,338 @@ export function AdminAvailability() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tab "Wiederkehrende Zeiten" */}
|
||||
{activeSubTab === "recurring" && (
|
||||
<div className="space-y-6">
|
||||
{/* Neue Regel erstellen */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">Neue wiederkehrende Regel erstellen</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Wochentag
|
||||
</label>
|
||||
<select
|
||||
value={selectedDayOfWeek}
|
||||
onChange={(e) => setSelectedDayOfWeek(Number(e.target.value))}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
>
|
||||
<option value={1}>Montag</option>
|
||||
<option value={2}>Dienstag</option>
|
||||
<option value={3}>Mittwoch</option>
|
||||
<option value={4}>Donnerstag</option>
|
||||
<option value={5}>Freitag</option>
|
||||
<option value={6}>Samstag</option>
|
||||
<option value={0}>Sonntag</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Startzeit
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={ruleStartTime}
|
||||
onChange={(e) => setRuleStartTime(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Endzeit
|
||||
</label>
|
||||
<input
|
||||
type="time"
|
||||
value={ruleEndTime}
|
||||
onChange={(e) => setRuleEndTime(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setErrorMsg("");
|
||||
setSuccessMsg("");
|
||||
|
||||
if (ruleStartTime >= ruleEndTime) {
|
||||
setErrorMsg("Startzeit muss vor der Endzeit liegen.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = localStorage.getItem("sessionId") || "";
|
||||
if (!sessionId) {
|
||||
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
|
||||
return;
|
||||
}
|
||||
|
||||
createRule(
|
||||
{ sessionId, dayOfWeek: selectedDayOfWeek, startTime: ruleStartTime, endTime: ruleEndTime },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSuccessMsg(`Regel für ${getDayName(selectedDayOfWeek)} erstellt.`);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorMsg(err?.message || "Fehler beim Erstellen der Regel.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 font-medium transition-colors"
|
||||
>
|
||||
Regel hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bestehende Regeln */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b">
|
||||
<h3 className="text-lg font-semibold">Bestehende Regeln</h3>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{recurringRules?.length === 0 && (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<div className="text-lg font-medium mb-2">Noch keine wiederkehrenden Regeln definiert</div>
|
||||
<div className="text-sm">Erstellen Sie Ihre erste Regel, um automatisch Slots zu generieren.</div>
|
||||
</div>
|
||||
)}
|
||||
{recurringRules?.map((rule) => (
|
||||
<div key={rule.id} className="p-4 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="font-medium">{getDayName(rule.dayOfWeek)}</div>
|
||||
<div className="text-gray-600">{rule.startTime} - {rule.endTime} Uhr</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
rule.isActive
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
}`}>
|
||||
{rule.isActive ? "Aktiv" : "Inaktiv"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
if (!sessionId) {
|
||||
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
|
||||
return;
|
||||
}
|
||||
toggleRuleActive(
|
||||
{ sessionId, id: rule.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSuccessMsg(`Regel ${rule.isActive ? "deaktiviert" : "aktiviert"}.`);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorMsg(err?.message || "Fehler beim Umschalten der Regel.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
className="px-3 py-1 text-blue-600 hover:bg-blue-50 rounded-md transition-colors text-sm"
|
||||
>
|
||||
{rule.isActive ? "Deaktivieren" : "Aktivieren"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
if (!sessionId) {
|
||||
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
|
||||
return;
|
||||
}
|
||||
deleteRule(
|
||||
{ sessionId, id: rule.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSuccessMsg("Regel gelöscht.");
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorMsg(err?.message || "Fehler beim Löschen der Regel.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
className="px-3 py-1 text-red-600 hover:bg-red-50 rounded-md transition-colors text-sm"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab "Urlaubszeiten" */}
|
||||
{activeSubTab === "timeoff" && (
|
||||
<div className="space-y-6">
|
||||
{/* Neue Urlaubszeit erstellen */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">Neue Urlaubszeit erstellen</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Von Datum
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={timeOffStartDate}
|
||||
onChange={(e) => setTimeOffStartDate(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bis Datum
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={timeOffEndDate}
|
||||
onChange={(e) => setTimeOffEndDate(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Grund/Notiz
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="z.B. Sommerurlaub, Feiertag"
|
||||
value={timeOffReason}
|
||||
onChange={(e) => setTimeOffReason(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setErrorMsg("");
|
||||
setSuccessMsg("");
|
||||
|
||||
if (!timeOffStartDate || !timeOffEndDate || !timeOffReason) {
|
||||
setErrorMsg("Bitte alle Felder ausfüllen.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeOffStartDate > timeOffEndDate) {
|
||||
setErrorMsg("Startdatum muss vor dem Enddatum liegen.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = localStorage.getItem("sessionId") || "";
|
||||
if (!sessionId) {
|
||||
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
|
||||
return;
|
||||
}
|
||||
|
||||
createTimeOff(
|
||||
{ sessionId, startDate: timeOffStartDate, endDate: timeOffEndDate, reason: timeOffReason },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSuccessMsg("Urlaubszeit hinzugefügt.");
|
||||
setTimeOffStartDate("");
|
||||
setTimeOffEndDate("");
|
||||
setTimeOffReason("");
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorMsg(err?.message || "Fehler beim Hinzufügen der Urlaubszeit.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
className="bg-pink-600 text-white px-4 py-2 rounded-md hover:bg-pink-700 font-medium transition-colors"
|
||||
>
|
||||
Urlaubszeit hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bestehende Urlaubszeiten */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b">
|
||||
<h3 className="text-lg font-semibold">Bestehende Urlaubszeiten</h3>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{timeOffPeriods?.length === 0 && (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<div className="text-lg font-medium mb-2">Keine Urlaubszeiten eingetragen</div>
|
||||
<div className="text-sm">Fügen Sie Urlaubszeiten hinzu, um automatisch Slots zu blockieren.</div>
|
||||
</div>
|
||||
)}
|
||||
{timeOffPeriods?.map((period) => {
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
const isPast = period.endDate < today;
|
||||
const isCurrent = period.startDate <= today && period.endDate >= today;
|
||||
const isFuture = period.startDate > today;
|
||||
|
||||
return (
|
||||
<div key={period.id} className="p-4 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="font-medium">
|
||||
{new Date(period.startDate).toLocaleDateString('de-DE')} - {new Date(period.endDate).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
<div className="text-gray-600">{period.reason}</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
|
||||
isPast
|
||||
? "bg-gray-100 text-gray-800"
|
||||
: isCurrent
|
||||
? "bg-red-100 text-red-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}`}>
|
||||
{isPast ? "Vergangen" : isCurrent ? "Aktuell" : "Geplant"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const sessionId = localStorage.getItem("sessionId");
|
||||
if (!sessionId) {
|
||||
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
|
||||
return;
|
||||
}
|
||||
deleteTimeOff(
|
||||
{ sessionId, id: period.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSuccessMsg("Urlaubszeit gelöscht.");
|
||||
},
|
||||
onError: (err: any) => {
|
||||
setErrorMsg(err?.message || "Fehler beim Löschen der Urlaubszeit.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
className="px-3 py-1 text-red-600 hover:bg-red-50 rounded-md transition-colors text-sm"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { queryClient } from "@/client/rpc-client";
|
||||
|
||||
@@ -8,7 +8,7 @@ export function BookingForm() {
|
||||
const [customerEmail, setCustomerEmail] = useState("");
|
||||
const [customerPhone, setCustomerPhone] = useState("");
|
||||
const [appointmentDate, setAppointmentDate] = useState("");
|
||||
const [selectedSlotId, setSelectedSlotId] = useState<string>("");
|
||||
const [selectedTime, setSelectedTime] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [agbAccepted, setAgbAccepted] = useState(false);
|
||||
const [inspirationPhoto, setInspirationPhoto] = useState<string>("");
|
||||
@@ -19,58 +19,44 @@ export function BookingForm() {
|
||||
queryClient.treatments.live.list.experimental_liveOptions()
|
||||
);
|
||||
|
||||
// Lade alle Slots live und filtere freie Slots
|
||||
const { data: allSlots } = useQuery(
|
||||
queryClient.availability.live.list.experimental_liveOptions()
|
||||
);
|
||||
|
||||
// Filtere freie Slots und entferne vergangene Termine
|
||||
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const freeSlots = (allSlots || []).filter((s) => {
|
||||
// Nur freie Slots
|
||||
if (s.status !== "free") return false;
|
||||
|
||||
// Nur zukünftige oder heutige Termine
|
||||
if (s.date < today) return false;
|
||||
|
||||
// Für heute: nur zukünftige Uhrzeiten
|
||||
if (s.date === today) {
|
||||
const now = new Date();
|
||||
const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
if (s.time <= currentTime) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
// Dynamische Verfügbarkeitsabfrage für das gewählte Datum und die Behandlung
|
||||
const { data: availableTimes, isLoading, isFetching, error } = useQuery({
|
||||
...queryClient.recurringAvailability.getAvailableTimes.queryOptions({
|
||||
input: {
|
||||
date: appointmentDate,
|
||||
treatmentId: selectedTreatment
|
||||
}
|
||||
}),
|
||||
enabled: !!appointmentDate && !!selectedTreatment
|
||||
});
|
||||
|
||||
const availableDates = Array.from(new Set(freeSlots.map((s) => s.date))).sort();
|
||||
const slotsByDate = appointmentDate
|
||||
? freeSlots.filter((s) => s.date === appointmentDate)
|
||||
: [];
|
||||
|
||||
const { mutate: createBooking, isPending } = useMutation(
|
||||
queryClient.bookings.create.mutationOptions()
|
||||
);
|
||||
|
||||
const selectedTreatmentData = treatments?.find((t) => t.id === selectedTreatment);
|
||||
const availableSlots = slotsByDate || []; // Slots sind bereits gefiltert
|
||||
|
||||
// Debug logging (commented out - uncomment if needed)
|
||||
// console.log("Debug - All slots:", allSlots);
|
||||
// console.log("Debug - Free slots:", freeSlots);
|
||||
// console.log("Debug - Available dates:", availableDates);
|
||||
// console.log("Debug - Selected date:", appointmentDate);
|
||||
// console.log("Debug - Slots by date:", slotsByDate);
|
||||
// console.log("Debug - Available slots:", availableSlots);
|
||||
|
||||
// Additional debugging for slot status
|
||||
// if (allSlots && allSlots.length > 0) {
|
||||
// const statusCounts = allSlots.reduce((acc, slot) => {
|
||||
// acc[slot.status] = (acc[slot.status] || 0) + 1;
|
||||
// return acc;
|
||||
// }, {} as Record<string, number>);
|
||||
// console.log("Debug - Slot status counts:", statusCounts);
|
||||
// }
|
||||
|
||||
// Clear selectedTime when treatment changes
|
||||
const handleTreatmentChange = (treatmentId: string) => {
|
||||
setSelectedTreatment(treatmentId);
|
||||
setSelectedTime("");
|
||||
};
|
||||
|
||||
// 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<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -153,7 +139,7 @@ export function BookingForm() {
|
||||
// agbAccepted
|
||||
// });
|
||||
|
||||
if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !selectedSlotId) {
|
||||
if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !selectedTime) {
|
||||
setErrorMessage("Bitte fülle alle erforderlichen Felder aus.");
|
||||
return;
|
||||
}
|
||||
@@ -162,9 +148,8 @@ export function BookingForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Email validation now handled in backend before slot reservation
|
||||
const slot = availableSlots.find((s) => s.id === selectedSlotId);
|
||||
const appointmentTime = slot?.time || "";
|
||||
// Email validation now handled in backend before booking creation
|
||||
const appointmentTime = selectedTime;
|
||||
// console.log("Creating booking with data:", {
|
||||
// treatmentId: selectedTreatment,
|
||||
// customerName,
|
||||
@@ -173,8 +158,7 @@ export function BookingForm() {
|
||||
// appointmentDate,
|
||||
// appointmentTime,
|
||||
// notes,
|
||||
// inspirationPhoto,
|
||||
// slotId: selectedSlotId,
|
||||
// inspirationPhoto
|
||||
// });
|
||||
createBooking(
|
||||
{
|
||||
@@ -186,7 +170,6 @@ export function BookingForm() {
|
||||
appointmentTime,
|
||||
notes,
|
||||
inspirationPhoto,
|
||||
slotId: selectedSlotId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -195,7 +178,7 @@ export function BookingForm() {
|
||||
setCustomerEmail("");
|
||||
setCustomerPhone("");
|
||||
setAppointmentDate("");
|
||||
setSelectedSlotId("");
|
||||
setSelectedTime("");
|
||||
setNotes("");
|
||||
setAgbAccepted(false);
|
||||
setInspirationPhoto("");
|
||||
@@ -224,7 +207,8 @@ export function BookingForm() {
|
||||
);
|
||||
};
|
||||
|
||||
// Get minimum date (today) – nicht mehr genutzt, Datumsauswahl erfolgt aus freien Slots
|
||||
// Dynamische Zeitauswahl: Kunde wählt beliebiges zukünftiges Datum,
|
||||
// System berechnet verfügbare Zeiten in 15-Minuten-Intervallen basierend auf wiederkehrenden Regeln
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
|
||||
@@ -238,7 +222,7 @@ export function BookingForm() {
|
||||
</label>
|
||||
<select
|
||||
value={selectedTreatment}
|
||||
onChange={(e) => setSelectedTreatment(e.target.value)}
|
||||
onChange={(e) => handleTreatmentChange(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
|
||||
>
|
||||
@@ -299,48 +283,53 @@ export function BookingForm() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Datum (nur freie Termine) *
|
||||
Wunschdatum *
|
||||
</label>
|
||||
<select
|
||||
<input
|
||||
type="date"
|
||||
value={appointmentDate}
|
||||
onChange={(e) => { setAppointmentDate(e.target.value); setSelectedSlotId(""); }}
|
||||
onChange={(e) => { 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
|
||||
>
|
||||
<option value="">Datum auswählen</option>
|
||||
{availableDates.map((d) => (
|
||||
<option key={d} value={d}>{d}</option>
|
||||
))}
|
||||
</select>
|
||||
{availableDates.length === 0 && (
|
||||
<p className="mt-2 text-sm text-gray-500">Aktuell keine freien Termine verfügbar.</p>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Verfügbare Uhrzeit *
|
||||
Verfügbare Uhrzeit (15-Min-Raster) *
|
||||
</label>
|
||||
<select
|
||||
value={selectedSlotId}
|
||||
onChange={(e) => setSelectedSlotId(e.target.value)}
|
||||
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}
|
||||
disabled={!appointmentDate || !selectedTreatment || isLoading || isFetching}
|
||||
required
|
||||
>
|
||||
<option value="">Zeit auswählen</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>
|
||||
))}
|
||||
{availableTimes?.map((time) => (
|
||||
<option key={time} value={time}>
|
||||
{time}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{appointmentDate && availableSlots.length === 0 && (
|
||||
{appointmentDate && selectedTreatment && isLoading && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Keine freien Zeitslots für {appointmentDate} verfügbar.
|
||||
Lade verfügbare Zeiten...
|
||||
</p>
|
||||
)}
|
||||
{appointmentDate && selectedTreatment && error && (
|
||||
<p className="mt-2 text-sm text-red-500">
|
||||
Fehler beim Laden der verfügbaren Zeiten. Bitte versuche es erneut.
|
||||
</p>
|
||||
)}
|
||||
{appointmentDate && selectedTreatment && !isLoading && !isFetching && !error && (!availableTimes || availableTimes.length === 0) && (
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Keine verfügbaren Zeiten für dieses Datum. Bitte wähle ein anderes Datum.
|
||||
</p>
|
||||
)}
|
||||
{selectedTreatmentData && (
|
||||
<p className="mt-1 text-xs text-gray-500">Dauer: {selectedTreatmentData.duration} Minuten</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
Reference in New Issue
Block a user