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:
@@ -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">
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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,
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user