diff --git a/src/client/components/admin-bookings.tsx b/src/client/components/admin-bookings.tsx index 522c9e4..610db15 100644 --- a/src/client/components/admin-bookings.tsx +++ b/src/client/components/admin-bookings.tsx @@ -212,8 +212,8 @@ export function AdminBookings() {
{booking.customerName}
-
{booking.customerEmail}
-
{booking.customerPhone}
+
{booking.customerEmail || '—'}
+
{booking.customerPhone || '—'}
diff --git a/src/client/components/admin-calendar.tsx b/src/client/components/admin-calendar.tsx index 7250f2c..4a88e5d 100644 --- a/src/client/components/admin-calendar.tsx +++ b/src/client/components/admin-calendar.tsx @@ -5,6 +5,30 @@ import { queryClient } from "@/client/rpc-client"; export function AdminCalendar() { const [currentMonth, setCurrentMonth] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(null); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(null); + const [sendDeleteEmail, setSendDeleteEmail] = useState(false); + const [deleteActionType, setDeleteActionType] = useState<'delete' | 'cancel'>('delete'); + + // Manual booking modal state + const [showCreateModal, setShowCreateModal] = useState(false); + const [createFormData, setCreateFormData] = useState({ + customerName: '', + treatmentId: '', + appointmentDate: '', + appointmentTime: '', + customerEmail: '', + customerPhone: '', + notes: '' + }); + const [createError, setCreateError] = useState(''); + + // Reschedule modal state + const [showRescheduleModal, setShowRescheduleModal] = useState(null); + const [rescheduleFormData, setRescheduleFormData] = useState({ + appointmentDate: '', + appointmentTime: '' + }); + const [rescheduleError, setRescheduleError] = useState(''); const { data: bookings } = useQuery( queryClient.bookings.live.list.experimental_liveOptions() @@ -14,10 +38,45 @@ export function AdminCalendar() { queryClient.treatments.live.list.experimental_liveOptions() ); + // Optional query for available times when treatment and date are selected + const { data: availableTimes } = useQuery({ + ...queryClient.recurringAvailability.getAvailableTimes.queryOptions({ + input: { + date: createFormData.appointmentDate, + treatmentId: createFormData.treatmentId + } + }), + enabled: !!createFormData.appointmentDate && !!createFormData.treatmentId + }); + + // Available times for reschedule modal + const { data: rescheduleAvailableTimes } = useQuery({ + ...queryClient.recurringAvailability.getAvailableTimes.queryOptions({ + input: { + date: rescheduleFormData.appointmentDate, + treatmentId: (showRescheduleModal ? bookings?.find(b => b.id === showRescheduleModal)?.treatmentId : '') || '' + } + }), + enabled: !!showRescheduleModal && !!rescheduleFormData.appointmentDate + }); + const { mutate: updateBookingStatus } = useMutation( queryClient.bookings.updateStatus.mutationOptions() ); + const { mutate: removeBooking } = useMutation( + queryClient.bookings.remove.mutationOptions() + ); + + const { mutate: createManualBooking } = useMutation( + queryClient.bookings.createManual.mutationOptions() + ); + + // Propose reschedule mutation + const { mutate: proposeReschedule, isLoading: isProposingReschedule } = useMutation( + queryClient.bookings.proposeReschedule.mutationOptions() + ); + const getTreatmentName = (treatmentId: string) => { return treatments?.find(t => t.id === treatmentId)?.name || "Unbekannte Behandlung"; }; @@ -106,8 +165,116 @@ export function AdminCalendar() { }); }; + const handleDeleteBooking = () => { + const sessionId = localStorage.getItem('sessionId'); + if (!sessionId || !showDeleteConfirm) return; + + if (deleteActionType === 'cancel') { + // For cancel action, use updateStatus instead of remove + updateBookingStatus({ + sessionId, + id: showDeleteConfirm, + status: "cancelled" + }, { + onSuccess: () => { + setShowDeleteConfirm(null); + setSendDeleteEmail(false); + setDeleteActionType('delete'); + }, + onError: () => { + // no-op; errors can be surfaced via existing patterns/toasts later + } + }); + } else { + // For delete action, use remove with email option + removeBooking({ + sessionId, + id: showDeleteConfirm, + sendEmail: sendDeleteEmail, + }, { + onSuccess: () => { + setShowDeleteConfirm(null); + setSendDeleteEmail(false); + setDeleteActionType('delete'); + }, + onError: () => { + // no-op; errors can be surfaced via existing patterns/toasts later + } + }); + } + }; + const today = new Date().toISOString().split('T')[0]; + const handleCreateBooking = () => { + const sessionId = localStorage.getItem('sessionId'); + if (!sessionId) return; + + createManualBooking({ + sessionId, + ...createFormData + }, { + onSuccess: () => { + setShowCreateModal(false); + setCreateFormData({ + customerName: '', + treatmentId: '', + appointmentDate: '', + appointmentTime: '', + customerEmail: '', + customerPhone: '', + notes: '' + }); + setCreateError(''); + }, + onError: (error: any) => { + setCreateError(error?.message || 'Fehler beim Erstellen der Buchung'); + } + }); + }; + + const handleFormChange = (field: string, value: string) => { + setCreateFormData(prev => ({ + ...prev, + [field]: value, + // Reset time when treatment or date changes + ...(field === 'treatmentId' || field === 'appointmentDate' ? { appointmentTime: '' } : {}) + })); + setCreateError(''); + }; + + const handleRescheduleFormChange = (field: string, value: string) => { + setRescheduleFormData(prev => ({ + ...prev, + [field]: value, + ...(field === 'appointmentDate' ? { appointmentTime: '' } : {}) + })); + setRescheduleError(''); + }; + + const handleRescheduleBooking = () => { + const sessionId = localStorage.getItem('sessionId'); + if (!sessionId || !showRescheduleModal) return; + const booking = bookings?.find(b => b.id === showRescheduleModal); + if (!booking) return; + + proposeReschedule({ + sessionId, + bookingId: booking.id, + proposedDate: rescheduleFormData.appointmentDate, + proposedTime: rescheduleFormData.appointmentTime, + }, { + onSuccess: () => { + setShowRescheduleModal(null); + setRescheduleFormData({ appointmentDate: '', appointmentTime: '' }); + setRescheduleError(''); + }, + onError: (error: any) => { + setRescheduleError(error?.message || 'Fehler beim Senden des Vorschlags'); + } + }); + }; + return (

Kalender - Bevorstehende Buchungen

@@ -153,9 +320,17 @@ export function AdminCalendar() { -

- {monthNames[month]} {year} -

+
+

+ {monthNames[month]} {year} +

+ +
- E-Mail: {booking.customerEmail} + E-Mail: {booking.customerEmail || '—'}
- Telefon: {booking.customerPhone} + Telefon: {booking.customerPhone || '—'}
@@ -293,7 +468,11 @@ export function AdminCalendar() { Bestätigen + <> + + + )} + {(() => { + const isPastDate = booking.appointmentDate < today; + const isCompleted = booking.status === 'completed'; + const shouldDisableDelete = isPastDate || isCompleted; + + return ( + + ); + })()} @@ -317,6 +534,289 @@ export function AdminCalendar() { )} )} + {showDeleteConfirm !== null && ( +
+
+

+ {deleteActionType === 'cancel' ? 'Termin stornieren?' : 'Termin löschen?'} +

+

+ {deleteActionType === 'cancel' + ? 'Dieser Termin wird als storniert markiert. Der Zeitslot wird wieder freigegeben.' + : 'Dieser Termin wird als storniert markiert. Der Zeitslot wird wieder freigegeben.' + } +

+ {deleteActionType === 'delete' && ( + + )} +
+ + +
+
+
+ )} + + {/* Create Manual Booking Modal */} + {showCreateModal && ( +
+
+

Termin erstellen

+ + {createError && ( +
+ {createError} +
+ )} + +
+ {/* Customer Name */} +
+ + handleFormChange('customerName', e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" + required + /> +
+ + {/* Treatment */} +
+ + +
+ + {/* Date */} +
+ + handleFormChange('appointmentDate', e.target.value)} + min={today} + className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" + required + /> +
+ + {/* Time */} +
+ + + {(!createFormData.treatmentId || !createFormData.appointmentDate) && ( +

+ Wähle zuerst Behandlung und Datum +

+ )} +
+ + {/* Email */} +
+ + handleFormChange('customerEmail', e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" + /> +
+ + {/* Phone */} +
+ + handleFormChange('customerPhone', e.target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500" + /> +
+ + {/* Notes */} +
+ +