From 6502f0d416a875280319efbb85189f45b7eaf5f3 Mon Sep 17 00:00:00 2001 From: elpatron Date: Thu, 2 Oct 2025 14:27:24 +0200 Subject: [PATCH] Fix: Cancel button functionality and live updates in booking management - Add confirmation modal for booking cancellations - Implement proper error handling and success messages - Fix live updates for booking status changes - Add manual refetch to ensure immediate UI updates - Auto-delete past availability slots on list access - Add manual cleanup function for past slots - Improve user experience with instant feedback --- src/client/components/admin-availability.tsx | 38 +++++++ src/client/components/admin-bookings.tsx | 107 ++++++++++++++++++- src/server/rpc/availability.ts | 71 +++++++++--- 3 files changed, 194 insertions(+), 22 deletions(-) diff --git a/src/client/components/admin-availability.tsx b/src/client/components/admin-availability.tsx index d010588..c0d0b78 100644 --- a/src/client/components/admin-availability.tsx +++ b/src/client/components/admin-availability.tsx @@ -42,6 +42,9 @@ export function AdminAvailability() { const { mutate: removeSlot } = useMutation( queryClient.availability.remove.mutationOptions() ); + const { mutate: cleanupPastSlots } = useMutation( + queryClient.availability.cleanupPastSlots.mutationOptions() + ); // Auto-update duration when treatment is selected useEffect(() => { @@ -280,6 +283,41 @@ export function AdminAvailability() { + {/* Cleanup Button */} +
+
+
+

Bereinigung

+

Vergangene Slots automatisch löschen

+
+ +
+
+ {/* All Slots List */}
diff --git a/src/client/components/admin-bookings.tsx b/src/client/components/admin-bookings.tsx index ebad65e..522c9e4 100644 --- a/src/client/components/admin-bookings.tsx +++ b/src/client/components/admin-bookings.tsx @@ -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"; @@ -6,8 +6,26 @@ export function AdminBookings() { const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [selectedPhoto, setSelectedPhoto] = useState(""); const [showPhotoModal, setShowPhotoModal] = useState(false); + const [showCancelConfirm, setShowCancelConfirm] = useState(null); + const [successMsg, setSuccessMsg] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); - const { data: bookings } = useQuery( + // Auto-clear messages after 5 seconds + useEffect(() => { + if (errorMsg) { + const timer = setTimeout(() => setErrorMsg(""), 5000); + return () => clearTimeout(timer); + } + }, [errorMsg]); + + useEffect(() => { + if (successMsg) { + const timer = setTimeout(() => setSuccessMsg(""), 5000); + return () => clearTimeout(timer); + } + }, [successMsg]); + + const { data: bookings, refetch: refetchBookings } = useQuery( queryClient.bookings.live.list.experimental_liveOptions() ); @@ -16,7 +34,19 @@ export function AdminBookings() { ); const { mutate: updateBookingStatus } = useMutation( - queryClient.bookings.updateStatus.mutationOptions() + queryClient.bookings.updateStatus.mutationOptions({ + onSuccess: (data, variables) => { + const statusText = getStatusText(variables.status); + setSuccessMsg(`Buchung wurde erfolgreich auf "${statusText}" gesetzt.`); + setShowCancelConfirm(null); + // Manually refetch bookings to ensure live updates + refetchBookings(); + }, + onError: (error: any) => { + setErrorMsg(error?.message || "Fehler beim Aktualisieren der Buchung."); + setShowCancelConfirm(null); + } + }) ); const getTreatmentName = (treatmentId: string) => { @@ -33,6 +63,16 @@ export function AdminBookings() { } }; + const getStatusText = (status: string) => { + switch (status) { + case "pending": return "Ausstehend"; + case "confirmed": return "Bestätigt"; + case "cancelled": return "Storniert"; + case "completed": return "Abgeschlossen"; + default: return status; + } + }; + const openPhotoModal = (photoData: string) => { setSelectedPhoto(photoData); setShowPhotoModal(true); @@ -66,6 +106,34 @@ export function AdminBookings() { return (
+ {/* Success/Error Messages */} + {(successMsg || errorMsg) && ( +
+ {errorMsg && ( +
+
+ + + + Fehler: + {errorMsg} +
+
+ )} + {successMsg && ( +
+
+ + + + Erfolg: + {successMsg} +
+
+ )} +
+ )} + {/* Quick Stats */}
@@ -193,7 +261,7 @@ export function AdminBookings() { Confirm
)} + + {/* Cancel Confirmation Modal */} + {showCancelConfirm && ( +
+
+

Buchung stornieren

+

+ Bist du sicher, dass du diese Buchung stornieren möchtest? Diese Aktion kann nicht rückgängig gemacht werden. +

+
+ + +
+
+
+ )}
); } \ No newline at end of file diff --git a/src/server/rpc/availability.ts b/src/server/rpc/availability.ts index be23bae..d695f27 100644 --- a/src/server/rpc/availability.ts +++ b/src/server/rpc/availability.ts @@ -87,32 +87,41 @@ const remove = os const list = os.handler(async () => { const allSlots = await kv.getAllItems(); - // Filter out past slots automatically + // Auto-delete past slots const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD const now = new Date(); const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; - const filteredSlots = allSlots.filter(slot => { - // Keep slots for future dates - if (slot.date > today) return true; + let deletedCount = 0; + const slotsToDelete: string[] = []; + + // Identify past slots for deletion + allSlots.forEach(slot => { + const isPastDate = slot.date < today; + const isPastTime = slot.date === today && slot.time <= currentTime; - // For today: only keep future time slots - if (slot.date === today) { - return slot.time > currentTime; + if (isPastDate || isPastTime) { + slotsToDelete.push(slot.id); } - - // Remove past slots - return false; }); - // Debug logging (commented out - uncomment if needed) - // const statusCounts = filteredSlots.reduce((acc, slot) => { - // acc[slot.status] = (acc[slot.status] || 0) + 1; - // return acc; - // }, {} as Record); - // console.log(`Availability list: ${filteredSlots.length} slots (${JSON.stringify(statusCounts)})`); + // Delete past slots (only if not reserved) + for (const slotId of slotsToDelete) { + const slot = await kv.getItem(slotId); + if (slot && slot.status !== "reserved") { + await kv.removeItem(slotId); + deletedCount++; + } + } - return filteredSlots; + if (deletedCount > 0) { + console.log(`Auto-deleted ${deletedCount} past availability slots`); + } + + // Return remaining slots (all are now current/future) + const remainingSlots = allSlots.filter(slot => !slotsToDelete.includes(slot.id)); + + return remainingSlots; }); const get = os.input(z.string()).handler(async ({ input }) => { @@ -126,6 +135,33 @@ const getByDate = os return all.filter((s) => s.date === input); }); +// Cleanup function to manually delete past slots +const cleanupPastSlots = os + .input(z.object({ sessionId: z.string() })) + .handler(async ({ input }) => { + await assertOwner(input.sessionId); + + const allSlots = await kv.getAllItems(); + const today = new Date().toISOString().split("T")[0]; + const now = new Date(); + const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; + + let deletedCount = 0; + + for (const slot of allSlots) { + const isPastDate = slot.date < today; + const isPastTime = slot.date === today && slot.time <= currentTime; + + if ((isPastDate || isPastTime) && slot.status !== "reserved") { + await kv.removeItem(slot.id); + deletedCount++; + } + } + + console.log(`Manual cleanup: deleted ${deletedCount} past availability slots`); + return { deletedCount, message: `${deletedCount} vergangene Slots wurden gelöscht.` }; + }); + const live = { list: os.handler(async function* ({ signal }) { yield call(list, {}, { signal }); @@ -150,6 +186,7 @@ export const router = { list, get, getByDate, + cleanupPastSlots, live, };