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
This commit is contained in:
2025-10-02 14:27:24 +02:00
parent 0b4e7e725f
commit 6502f0d416
3 changed files with 194 additions and 22 deletions

View File

@@ -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() {
</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 */}
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b">

View File

@@ -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<string>("");
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()
);
@@ -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 (
<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 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-lg shadow p-4">
@@ -193,7 +261,7 @@ export function AdminBookings() {
Confirm
</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"
>
Cancel
@@ -209,7 +277,7 @@ export function AdminBookings() {
Complete
</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"
>
Cancel
@@ -272,6 +340,35 @@ export function AdminBookings() {
</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>
);
}

View File

@@ -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<string, number>);
// 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,
};