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