2 Commits

Author SHA1 Message Date
6502f0d416 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
2025-10-02 14:27:24 +02:00
0b4e7e725f Fix: Live updates in availability management
- Add manual refetch calls after slot creation/deletion
- Ensure availability list updates immediately after changes
- Fix issue where slots didn't appear in list after adding
- Improve user experience with real-time updates
2025-10-02 13:55:24 +02:00
3 changed files with 200 additions and 23 deletions

View File

@@ -10,7 +10,7 @@ export function AdminAvailability() {
const [selectedTreatmentId, setSelectedTreatmentId] = useState<string>(""); const [selectedTreatmentId, setSelectedTreatmentId] = useState<string>("");
const [slotType, setSlotType] = useState<"treatment" | "manual">("treatment"); const [slotType, setSlotType] = useState<"treatment" | "manual">("treatment");
const { data: allSlots } = useQuery( const { data: allSlots, refetch: refetchSlots } = useQuery(
queryClient.availability.live.list.experimental_liveOptions() queryClient.availability.live.list.experimental_liveOptions()
); );
@@ -42,6 +42,9 @@ export function AdminAvailability() {
const { mutate: removeSlot } = useMutation( const { mutate: removeSlot } = useMutation(
queryClient.availability.remove.mutationOptions() queryClient.availability.remove.mutationOptions()
); );
const { mutate: cleanupPastSlots } = useMutation(
queryClient.availability.cleanupPastSlots.mutationOptions()
);
// Auto-update duration when treatment is selected // Auto-update duration when treatment is selected
useEffect(() => { useEffect(() => {
@@ -90,6 +93,9 @@ export function AdminAvailability() {
: `Manueller Slot (${duration} Min)`; : `Manueller Slot (${duration} Min)`;
setSuccessMsg(`${slotDescription} angelegt.`); setSuccessMsg(`${slotDescription} angelegt.`);
// Manually refetch slots to ensure live updates
refetchSlots();
// advance time by the duration of the slot // advance time by the duration of the slot
const [hStr, mStr] = time.split(":"); const [hStr, mStr] = time.split(":");
let h = parseInt(hStr, 10); let h = parseInt(hStr, 10);
@@ -277,6 +283,41 @@ export function AdminAvailability() {
</div> </div>
</div> </div>
{/* Cleanup Button */}
<div className="bg-white rounded-lg shadow p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900">Bereinigung</h3>
<p className="text-sm text-gray-600">Vergangene Slots automatisch löschen</p>
</div>
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId") || "";
if (!sessionId) {
setErrorMsg("Nicht eingeloggt. Bitte als Inhaber anmelden.");
return;
}
cleanupPastSlots(
{ sessionId },
{
onSuccess: (result: any) => {
setSuccessMsg(result.message || "Bereinigung abgeschlossen.");
refetchSlots();
},
onError: (err: any) => {
setErrorMsg(err?.message || "Fehler bei der Bereinigung.");
}
}
);
}}
className="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors"
>
Vergangene Slots löschen
</button>
</div>
</div>
{/* All Slots List */} {/* All Slots List */}
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-lg shadow">
<div className="p-4 border-b"> <div className="p-4 border-b">
@@ -337,6 +378,8 @@ export function AdminAvailability() {
{ {
onSuccess: () => { onSuccess: () => {
setSuccessMsg("Slot erfolgreich gelöscht."); setSuccessMsg("Slot erfolgreich gelöscht.");
// Manually refetch slots to ensure live updates
refetchSlots();
}, },
onError: (err: any) => { onError: (err: any) => {
const msg = (err && (err.message || (err as any).toString())) || "Fehler beim Löschen des Slots."; const msg = (err && (err.message || (err as any).toString())) || "Fehler beim Löschen des Slots.";

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { queryClient } from "@/client/rpc-client"; import { queryClient } from "@/client/rpc-client";
@@ -6,8 +6,26 @@ export function AdminBookings() {
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
const [selectedPhoto, setSelectedPhoto] = useState<string>(""); const [selectedPhoto, setSelectedPhoto] = useState<string>("");
const [showPhotoModal, setShowPhotoModal] = useState(false); const [showPhotoModal, setShowPhotoModal] = useState(false);
const [showCancelConfirm, setShowCancelConfirm] = useState<string | null>(null);
const [successMsg, setSuccessMsg] = useState<string>("");
const [errorMsg, setErrorMsg] = useState<string>("");
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() queryClient.bookings.live.list.experimental_liveOptions()
); );
@@ -16,7 +34,19 @@ export function AdminBookings() {
); );
const { mutate: updateBookingStatus } = useMutation( 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) => { 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) => { const openPhotoModal = (photoData: string) => {
setSelectedPhoto(photoData); setSelectedPhoto(photoData);
setShowPhotoModal(true); setShowPhotoModal(true);
@@ -66,6 +106,34 @@ export function AdminBookings() {
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
{/* Success/Error Messages */}
{(successMsg || errorMsg) && (
<div className="mb-4">
{errorMsg && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md">
<div className="flex items-center">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
<span className="font-medium">Fehler:</span>
<span className="ml-1">{errorMsg}</span>
</div>
</div>
)}
{successMsg && (
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md">
<div className="flex items-center">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
<span className="font-medium">Erfolg:</span>
<span className="ml-1">{successMsg}</span>
</div>
</div>
)}
</div>
)}
{/* Quick Stats */} {/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-lg shadow p-4"> <div className="bg-white rounded-lg shadow p-4">
@@ -193,7 +261,7 @@ export function AdminBookings() {
Confirm Confirm
</button> </button>
<button <button
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "cancelled" })} onClick={() => setShowCancelConfirm(booking.id)}
className="text-red-600 hover:text-red-900" className="text-red-600 hover:text-red-900"
> >
Cancel Cancel
@@ -209,7 +277,7 @@ export function AdminBookings() {
Complete Complete
</button> </button>
<button <button
onClick={() => updateBookingStatus({ sessionId: localStorage.getItem("sessionId") || "", id: booking.id, status: "cancelled" })} onClick={() => setShowCancelConfirm(booking.id)}
className="text-red-600 hover:text-red-900" className="text-red-600 hover:text-red-900"
> >
Cancel Cancel
@@ -272,6 +340,35 @@ export function AdminBookings() {
</div> </div>
</div> </div>
)} )}
{/* Cancel Confirmation Modal */}
{showCancelConfirm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-medium text-gray-900 mb-4">Buchung stornieren</h3>
<p className="text-gray-600 mb-6">
Bist du sicher, dass du diese Buchung stornieren möchtest? Diese Aktion kann nicht rückgängig gemacht werden.
</p>
<div className="flex space-x-3">
<button
onClick={() => {
const sessionId = localStorage.getItem("sessionId") || "";
updateBookingStatus({ sessionId, id: showCancelConfirm, status: "cancelled" });
}}
className="flex-1 bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
>
Ja, stornieren
</button>
<button
onClick={() => setShowCancelConfirm(null)}
className="flex-1 bg-gray-200 text-gray-800 py-2 px-4 rounded-md hover:bg-gray-300 transition-colors"
>
Abbrechen
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@@ -87,32 +87,41 @@ const remove = os
const list = os.handler(async () => { const list = os.handler(async () => {
const allSlots = await kv.getAllItems(); 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 today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const now = new Date(); const now = new Date();
const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
const filteredSlots = allSlots.filter(slot => { let deletedCount = 0;
// Keep slots for future dates const slotsToDelete: string[] = [];
if (slot.date > today) return true;
// 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 (isPastDate || isPastTime) {
if (slot.date === today) { slotsToDelete.push(slot.id);
return slot.time > currentTime;
} }
// Remove past slots
return false;
}); });
// Debug logging (commented out - uncomment if needed) // Delete past slots (only if not reserved)
// const statusCounts = filteredSlots.reduce((acc, slot) => { for (const slotId of slotsToDelete) {
// acc[slot.status] = (acc[slot.status] || 0) + 1; const slot = await kv.getItem(slotId);
// return acc; if (slot && slot.status !== "reserved") {
// }, {} as Record<string, number>); await kv.removeItem(slotId);
// console.log(`Availability list: ${filteredSlots.length} slots (${JSON.stringify(statusCounts)})`); 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 }) => { const get = os.input(z.string()).handler(async ({ input }) => {
@@ -126,6 +135,33 @@ const getByDate = os
return all.filter((s) => s.date === input); 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 = { const live = {
list: os.handler(async function* ({ signal }) { list: os.handler(async function* ({ signal }) {
yield call(list, {}, { signal }); yield call(list, {}, { signal });
@@ -150,6 +186,7 @@ export const router = {
list, list,
get, get,
getByDate, getByDate,
cleanupPastSlots,
live, live,
}; };