diff --git a/.gitignore b/.gitignore index 8a43bb6..45c8b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,13 +7,16 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* -node_modules -dist -dist-ssr +node_modules/ +dist/ +dist-ssr/ +.vite/ +coverage/ # Editor directories and files .idea .DS_Store +Thumbs.db *.suo *.ntvs* *.njsproj @@ -37,8 +40,10 @@ dist-ssr etilqs_* # Environment variables -.env* +.env +.env.* !.env.example +.env.local # TypeScript cache *.tsbuildinfo \ No newline at end of file diff --git a/assets/stargilnails_logo_transparent_112.png b/assets/stargilnails_logo_transparent_112.png new file mode 100644 index 0000000..8492cc0 Binary files /dev/null and b/assets/stargilnails_logo_transparent_112.png differ diff --git a/public/assets/stargilnails_logo_transparent_112.png b/public/assets/stargilnails_logo_transparent_112.png new file mode 100644 index 0000000..8492cc0 Binary files /dev/null and b/public/assets/stargilnails_logo_transparent_112.png differ diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..8492cc0 Binary files /dev/null and b/public/favicon.png differ diff --git a/src/client/app.tsx b/src/client/app.tsx index 3cea443..83d18fe 100644 --- a/src/client/app.tsx +++ b/src/client/app.tsx @@ -17,7 +17,11 @@ function App() { return (
-
đź’…
+ Stargil Nails Logo
Lade...
@@ -45,9 +49,13 @@ function App() { {/* Header */}
-
+
-
đź’…
+ Stargil Nails Logo

Stargirlnails Kiel

Professional Nail Design & Care

diff --git a/src/client/components/admin-availability.tsx b/src/client/components/admin-availability.tsx index 19aea43..ddec3c5 100644 --- a/src/client/components/admin-availability.tsx +++ b/src/client/components/admin-availability.tsx @@ -8,10 +8,13 @@ export function AdminAvailability() { const [time, setTime] = useState("09:00"); const [duration, setDuration] = useState(30); - const { data: slots } = useQuery( - queryClient.availability.live.byDate.experimental_liveOptions(selectedDate) + const { data: allSlots } = useQuery( + queryClient.availability.live.list.experimental_liveOptions() ); + const [errorMsg, setErrorMsg] = useState(""); + const [successMsg, setSuccessMsg] = useState(""); + const { mutate: createSlot, isPending: isCreating } = useMutation( queryClient.availability.create.mutationOptions() ); @@ -20,8 +23,38 @@ export function AdminAvailability() { ); const addSlot = () => { - if (!selectedDate || !time || !duration) return; - createSlot({ sessionId: localStorage.getItem("sessionId") || "", date: selectedDate, time, durationMinutes: duration }); + setErrorMsg(""); + setSuccessMsg(""); + if (!selectedDate || !time || !duration) { + setErrorMsg("Bitte Datum, Uhrzeit und Dauer angeben."); + return; + } + const sessionId = localStorage.getItem("sessionId") || ""; + if (!sessionId) { + setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden."); + return; + } + createSlot( + { sessionId, date: selectedDate, time, durationMinutes: duration }, + { + onSuccess: () => { + setSuccessMsg("Slot angelegt."); + // advance time to next 30-minute step + const [hStr, mStr] = time.split(":"); + let h = parseInt(hStr, 10); + let m = parseInt(mStr, 10); + m += 30; + if (m >= 60) { h += 1; m -= 60; } + if (h >= 24) { h = 0; } + const next = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; + setTime(next); + }, + onError: (err: any) => { + const msg = (err && (err.message || (err as any).toString())) || "Fehler beim Anlegen."; + setErrorMsg(msg); + }, + } + ); }; return ( @@ -58,32 +91,43 @@ export function AdminAvailability() {
+ {(errorMsg || successMsg) && ( +
+ {errorMsg &&
{errorMsg}
} + {successMsg &&
{successMsg}
} +
+ )} +
-

Slots am {selectedDate}

+

Alle freien Slots

- {slots?.sort((a, b) => a.time.localeCompare(b.time)).map((slot) => ( -
-
- {slot.time} - {slot.durationMinutes} Min - - {slot.status === "free" ? "frei" : "reserviert"} - + {allSlots + ?.filter((s) => s.status === "free") + .sort((a, b) => (a.date === b.date ? a.time.localeCompare(b.time) : a.date.localeCompare(b.date))) + .map((slot) => ( +
+
+ {slot.date} + {slot.time} + {slot.durationMinutes} Min + + {slot.status === "free" ? "frei" : "reserviert"} + +
+
+ +
-
- -
-
- ))} - {slots?.length === 0 && ( -
Keine Slots vorhanden.
+ ))} + {allSlots?.filter((s) => s.status === "free").length === 0 && ( +
Keine freien Slots vorhanden.
)}
diff --git a/src/client/components/booking-form.tsx b/src/client/components/booking-form.tsx index 4397641..b1d22c7 100644 --- a/src/client/components/booking-form.tsx +++ b/src/client/components/booking-form.tsx @@ -14,18 +14,23 @@ export function BookingForm() { const { data: treatments } = useQuery( queryClient.treatments.live.list.experimental_liveOptions() ); - const { data: slotsByDate } = useQuery( - appointmentDate - ? queryClient.availability.live.byDate.experimental_liveOptions(appointmentDate) - : queryClient.availability.live.byDate.experimental_liveOptions("") + + // Lade alle Slots live und filtere freie Slots + const { data: allSlots } = useQuery( + queryClient.availability.live.list.experimental_liveOptions() ); + const freeSlots = (allSlots || []).filter((s) => s.status === "free"); + 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 || []).filter(s => s.status === "free"); + const selectedTreatmentData = treatments?.find((t) => t.id === selectedTreatment); + const availableSlots = (slotsByDate || []).filter((s) => s.status === "free"); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -33,33 +38,36 @@ export function BookingForm() { alert("Bitte fülle alle erforderlichen Felder aus"); return; } - const slot = availableSlots.find(s => s.id === selectedSlotId); + const slot = availableSlots.find((s) => s.id === selectedSlotId); const appointmentTime = slot?.time || ""; - createBooking({ - treatmentId: selectedTreatment, - customerName, - customerEmail, - customerPhone, - appointmentDate, - appointmentTime, - notes, - slotId: selectedSlotId, - }, { - onSuccess: () => { - setSelectedTreatment(""); - setCustomerName(""); - setCustomerEmail(""); - setCustomerPhone(""); - setAppointmentDate(""); - setSelectedSlotId(""); - setNotes(""); - alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen."); + createBooking( + { + treatmentId: selectedTreatment, + customerName, + customerEmail, + customerPhone, + appointmentDate, + appointmentTime, + notes, + slotId: selectedSlotId, + }, + { + onSuccess: () => { + setSelectedTreatment(""); + setCustomerName(""); + setCustomerEmail(""); + setCustomerPhone(""); + setAppointmentDate(""); + setSelectedSlotId(""); + setNotes(""); + alert("Buchung erfolgreich erstellt! Wir werden dich kontaktieren, um deinen Termin zu bestätigen."); + }, } - }); + ); }; - // Get minimum date (today) - const today = new Date().toISOString().split('T')[0]; + // Get minimum date (today) – nicht mehr genutzt, Datumsauswahl erfolgt aus freien Slots + const today = new Date().toISOString().split("T")[0]; return (
@@ -134,16 +142,22 @@ export function BookingForm() {
- setAppointmentDate(e.target.value)} - min={today} + onChange={(e) => { setAppointmentDate(e.target.value); setSelectedSlotId(""); }} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" required - /> + > + + {availableDates.map((d) => ( + + ))} + + {availableDates.length === 0 && ( +

Aktuell keine freien Termine verfĂĽgbar.

+ )}