# Booking Rescheduling Guide ## Overview The rescheduling system allows users to move their existing bookings to different time slots. This guide covers the backend implementation, frontend components, and best practices for handling booking rescheduling. ## Backend Implementation ### Rescheduling Endpoint **Endpoint**: `PUT /api/bookings/:id/reschedule` **Authentication**: Required (JWT Token) **Permission**: Users can only reschedule their own bookings ### Request Structure ```json { "new_schedule_id": 5 } ``` ### Business Rules 1. **Time Constraint**: Booking can only be rescheduled at least 2 hours before the original scheduled time 2. **Ownership**: Users can only reschedule their own bookings 3. **Availability**: New time slot must be available 4. **Status**: Only `scheduled` bookings can be rescheduled 5. **Automatic Cleanup**: Old Jitsi meeting is deleted, new one is created 6. **Notifications**: Reminder notifications are updated automatically ### Response Examples #### Success Response (200) ```json { "message": "Booking rescheduled successfully" } ``` #### Error Responses **Unauthorized (403)**: ```json { "error": "You can only reschedule your own bookings" } ``` **Time Constraint (400)**: ```json { "error": "This booking cannot be rescheduled (must be at least 2 hours before scheduled time)" } ``` **Slot Unavailable (409)**: ```json { "error": "The new time slot is not available" } ``` **Booking Not Found (404)**: ```json { "error": "Booking or new schedule not found" } ``` ## Frontend Implementation ### 1. Rescheduling Modal Component ```jsx import React, { useState, useEffect } from 'react'; import { format, addDays, isBefore, addHours } from 'date-fns'; const RescheduleModal = ({ booking, isOpen, onClose, onSuccess }) => { const [selectedDate, setSelectedDate] = useState(new Date()); const [availableSlots, setAvailableSlots] = useState([]); const [selectedSlot, setSelectedSlot] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); const apiService = { async getAvailableSlots(date) { const dateStr = format(date, 'yyyy-MM-dd'); const response = await fetch(`/api/schedules?date=${dateStr}`); if (!response.ok) { throw new Error('Failed to fetch available slots'); } return response.json(); }, async rescheduleBooking(bookingId, newScheduleId) { const response = await fetch(`/api/bookings/${bookingId}/reschedule`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('authToken')}` }, body: JSON.stringify({ new_schedule_id: newScheduleId }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to reschedule booking'); } return response.json(); } }; useEffect(() => { if (isOpen) { loadAvailableSlots(); } }, [selectedDate, isOpen]); const loadAvailableSlots = async () => { setIsLoading(true); setError(''); try { const data = await apiService.getAvailableSlots(selectedDate); // Filter out the current booking's slot and past slots const filteredSlots = (data.slots || []).filter(slot => { const slotTime = new Date(slot.start_time); const currentBookingTime = new Date(booking.scheduled_at); // Don't show the current booking's slot if (format(slotTime, 'yyyy-MM-dd HH:mm') === format(currentBookingTime, 'yyyy-MM-dd HH:mm')) { return false; } // Don't show past slots if (isBefore(slotTime, new Date())) { return false; } // Only show available slots return slot.is_available && slot.remaining_slots > 0; }); setAvailableSlots(filteredSlots); } catch (error) { setError('Failed to load available slots'); console.error('Error loading slots:', error); } finally { setIsLoading(false); } }; const handleReschedule = async () => { if (!selectedSlot) { setError('Please select a new time slot'); return; } setIsLoading(true); setError(''); try { await apiService.rescheduleBooking(booking.id, selectedSlot.id); onSuccess(); onClose(); } catch (error) { setError(error.message); } finally { setIsLoading(false); } }; const canReschedule = () => { const scheduledTime = new Date(booking.scheduled_at); const twoHoursFromNow = addHours(new Date(), 2); return scheduledTime > twoHoursFromNow; }; const getNextTwoWeeks = () => { const days = []; for (let i = 0; i < 14; i++) { days.push(addDays(new Date(), i)); } return days; }; if (!isOpen) return null; return (

Reschedule Appointment

{/* Current Booking Info */}

Current Appointment

Date: {format(new Date(booking.scheduled_at), 'MMMM dd, yyyy')}
Time: {format(new Date(booking.scheduled_at), 'HH:mm')}
Duration: {booking.duration} minutes
{!canReschedule() ? (

⚠️ This booking cannot be rescheduled because it's less than 2 hours away. Please contact support if you need to make changes.

) : ( <> {/* Date Selection */}

Select New Date

{getNextTwoWeeks().map((date) => ( ))}
{/* Time Slot Selection */}

Available Times for {format(selectedDate, 'MMMM dd, yyyy')}

{error && (
{error}
)} {isLoading ? (
Loading available times...
) : availableSlots.length === 0 ? (
No available time slots for this date. Please select another date.
) : (
{availableSlots.map((slot) => ( ))}
)}
)}
{canReschedule() && ( )}
); }; export default RescheduleModal; ``` ### 2. Booking Management Component ```jsx import React, { useState, useEffect } from 'react'; import { format, isBefore, addHours } from 'date-fns'; import RescheduleModal from './RescheduleModal'; const MyBookings = () => { const [bookings, setBookings] = useState([]); const [isLoading, setIsLoading] = useState(true); const [selectedBooking, setSelectedBooking] = useState(null); const [showRescheduleModal, setShowRescheduleModal] = useState(false); const apiService = { async getUserBookings() { const response = await fetch('/api/bookings', { headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` } }); if (!response.ok) { throw new Error('Failed to fetch bookings'); } return response.json(); }, async cancelBooking(bookingId) { const response = await fetch(`/api/bookings/${bookingId}/cancel`, { method: 'PUT', headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` } }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to cancel booking'); } return response.json(); } }; useEffect(() => { loadBookings(); }, []); const loadBookings = async () => { setIsLoading(true); try { const data = await apiService.getUserBookings(); setBookings(data.bookings || []); } catch (error) { console.error('Error loading bookings:', error); } finally { setIsLoading(false); } }; const handleReschedule = (booking) => { setSelectedBooking(booking); setShowRescheduleModal(true); }; const handleCancelBooking = async (bookingId) => { if (!confirm('Are you sure you want to cancel this booking?')) { return; } try { await apiService.cancelBooking(bookingId); await loadBookings(); // Refresh the list alert('Booking cancelled successfully'); } catch (error) { alert(`Error cancelling booking: ${error.message}`); } }; const canReschedule = (booking) => { const scheduledTime = new Date(booking.scheduled_at); const twoHoursFromNow = addHours(new Date(), 2); return booking.status === 'scheduled' && scheduledTime > twoHoursFromNow; }; const canCancel = (booking) => { const scheduledTime = new Date(booking.scheduled_at); const twentyFourHoursFromNow = addHours(new Date(), 24); return booking.status === 'scheduled' && scheduledTime > twentyFourHoursFromNow; }; const getStatusColor = (status) => { switch (status) { case 'scheduled': return 'status-scheduled'; case 'completed': return 'status-completed'; case 'cancelled': return 'status-cancelled'; default: return 'status-default'; } }; const getPaymentStatusColor = (status) => { switch (status) { case 'succeeded': return 'payment-success'; case 'pending': return 'payment-pending'; case 'failed': return 'payment-failed'; default: return 'payment-default'; } }; if (isLoading) { return
Loading your bookings...
; } return (

My Appointments

{bookings.length === 0 ? (

You don't have any bookings yet.

) : (
{bookings.map((booking) => (
{format(new Date(booking.scheduled_at), 'MMM dd, yyyy')}
{format(new Date(booking.scheduled_at), 'HH:mm')}
{booking.status.charAt(0).toUpperCase() + booking.status.slice(1)} Payment: {booking.payment_status}
Duration: {booking.duration} minutes
Amount: ${booking.amount?.toFixed(2) || '0.00'}
{booking.notes && (
Notes: {booking.notes}
)} {booking.jitsi_room_url && booking.status === 'scheduled' && (
Meeting Link:{' '} Join Video Session
)}
{canReschedule(booking) && ( )} {canCancel(booking) && ( )} {booking.status === 'scheduled' && !canReschedule(booking) && !canCancel(booking) && ( Too close to appointment time for changes )}
))}
)} {/* Reschedule Modal */} { setShowRescheduleModal(false); setSelectedBooking(null); }} onSuccess={() => { loadBookings(); // Refresh bookings after successful reschedule alert('Appointment rescheduled successfully!'); }} />
); }; export default MyBookings; ``` ### 3. CSS Styles for Rescheduling Components ```css /* Reschedule Modal Styles */ .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .reschedule-modal { background: white; border-radius: 8px; width: 90%; max-width: 600px; max-height: 90vh; overflow-y: auto; } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid #eee; } .modal-header h3 { margin: 0; color: #333; } .close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; } .modal-body { padding: 20px; } .current-booking { background: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px; } .current-booking h4 { margin: 0 0 10px 0; color: #333; } .booking-info { color: #666; line-height: 1.5; } .reschedule-restriction { text-align: center; padding: 20px; } .warning { color: #856404; background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 6px; margin: 0; } .date-selection h4, .time-selection h4 { margin: 20px 0 15px 0; color: #333; } .date-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); gap: 10px; margin-bottom: 20px; } .date-btn { padding: 10px 5px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; text-align: center; transition: all 0.2s; } .date-btn:hover { border-color: #007bff; background: #f8f9ff; } .date-btn.active { background: #007bff; color: white; border-color: #007bff; } .day-name { font-size: 12px; font-weight: bold; margin-bottom: 2px; } .day-number { font-size: 16px; font-weight: bold; } .time-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; } .time-slot { padding: 15px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; text-align: left; transition: all 0.2s; } .time-slot:hover { border-color: #007bff; background: #f8f9ff; } .time-slot.selected { background: #007bff; color: white; border-color: #007bff; } .time-slot .time { font-weight: bold; margin-bottom: 5px; } .time-slot .availability { font-size: 12px; opacity: 0.8; } .modal-footer { padding: 20px; border-top: 1px solid #eee; display: flex; gap: 10px; justify-content: flex-end; } /* Booking Cards Styles */ .my-bookings { max-width: 800px; margin: 0 auto; padding: 20px; } .bookings-list { display: flex; flex-direction: column; gap: 20px; } .booking-card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .booking-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; } .booking-date .date { font-size: 18px; font-weight: bold; color: #333; } .booking-date .time { font-size: 16px; color: #666; } .booking-status { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; } .status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; text-transform: uppercase; } .status-scheduled { background: #d4edda; color: #155724; } .status-completed { background: #cce5ff; color: #004085; } .status-cancelled { background: #f8d7da; color: #721c24; } .payment-status { padding: 2px 6px; border-radius: 3px; font-size: 11px; } .payment-success { background: #d4edda; color: #155724; } .payment-pending { background: #fff3cd; color: #856404; } .payment-failed { background: #f8d7da; color: #721c24; } .booking-details { margin-bottom: 15px; } .detail-item { margin-bottom: 8px; color: #666; } .meeting-link { color: #007bff; text-decoration: none; font-weight: bold; } .meeting-link:hover { text-decoration: underline; } .booking-actions { display: flex; gap: 10px; align-items: center; } .no-actions { color: #666; font-style: italic; font-size: 14px; } .btn-outline-primary { background: transparent; color: #007bff; border: 1px solid #007bff; } .btn-outline-primary:hover { background: #007bff; color: white; } .btn-outline-danger { background: transparent; color: #dc3545; border: 1px solid #dc3545; } .btn-outline-danger:hover { background: #dc3545; color: white; } .btn-sm { padding: 6px 12px; font-size: 14px; } .error-message { background: #f8d7da; color: #721c24; padding: 10px; border-radius: 4px; margin-bottom: 15px; } .loading { text-align: center; padding: 20px; color: #666; } .no-slots { text-align: center; color: #666; font-style: italic; padding: 20px; } .no-bookings { text-align: center; padding: 40px; } .no-bookings p { color: #666; margin-bottom: 20px; } /* Responsive Design */ @media (max-width: 768px) { .reschedule-modal { width: 95%; margin: 10px; } .booking-header { flex-direction: column; gap: 10px; } .booking-status { align-items: flex-start; } .date-grid { grid-template-columns: repeat(auto-fill, minmax(50px, 1fr)); } .time-slots { grid-template-columns: 1fr; } .booking-actions { flex-direction: column; align-items: stretch; } } ``` ## Advanced Features ### 1. Bulk Rescheduling (Admin Feature) ```jsx const BulkReschedule = () => { const [selectedBookings, setSelectedBookings] = useState([]); const [newScheduleId, setNewScheduleId] = useState(''); const handleBulkReschedule = async () => { const results = await Promise.allSettled( selectedBookings.map(bookingId => apiService.rescheduleBooking(bookingId, newScheduleId) ) ); const successful = results.filter(r => r.status === 'fulfilled').length; const failed = results.filter(r => r.status === 'rejected').length; alert(`Rescheduled ${successful} bookings successfully. ${failed} failed.`); }; return (
{/* Implementation for bulk operations */}
); }; ``` ### 2. Rescheduling with Conflict Detection ```jsx const detectScheduleConflicts = (bookings, newScheduleTime) => { return bookings.filter(booking => { const bookingTime = new Date(booking.scheduled_at); const newTime = new Date(newScheduleTime); // Check for time conflicts (within 1 hour) const timeDiff = Math.abs(bookingTime - newTime); return timeDiff < 60 * 60 * 1000; // 1 hour in milliseconds }); }; ``` ### 3. Rescheduling History Tracking ```jsx const ReschedulingHistory = ({ bookingId }) => { const [history, setHistory] = useState([]); useEffect(() => { // Fetch rescheduling history for the booking fetchReschedulingHistory(bookingId); }, [bookingId]); return (

Rescheduling History

{history.map((entry, index) => (
From: {format(new Date(entry.old_time), 'MMM dd, yyyy HH:mm')}
To: {format(new Date(entry.new_time), 'MMM dd, yyyy HH:mm')}
Changed: {format(new Date(entry.changed_at), 'MMM dd, yyyy HH:mm')}
))}
); }; ``` ## Testing Rescheduling ### Postman Test Scenarios 1. **Successful Rescheduling**: ```javascript // Test script for successful reschedule pm.test("Booking rescheduled successfully", function () { pm.response.to.have.status(200); const response = pm.response.json(); pm.expect(response.message).to.include("rescheduled successfully"); }); ``` 2. **Time Constraint Violation**: ```javascript // Test rescheduling too close to appointment time pm.test("Cannot reschedule within 2 hours", function () { pm.response.to.have.status(400); const response = pm.response.json(); pm.expect(response.error).to.include("2 hours before"); }); ``` 3. **Unauthorized Rescheduling**: ```javascript // Test rescheduling someone else's booking pm.test("Cannot reschedule other user's booking", function () { pm.response.to.have.status(403); const response = pm.response.json(); pm.expect(response.error).to.include("your own bookings"); }); ``` ## Best Practices 1. **Always validate time constraints** before allowing rescheduling 2. **Show clear error messages** for different failure scenarios 3. **Provide visual feedback** during the rescheduling process 4. **Automatically refresh** booking lists after successful operations 5. **Handle edge cases** like fully booked slots gracefully 6. **Implement optimistic updates** where appropriate 7. **Track rescheduling history** for audit purposes 8. **Send notifications** for successful rescheduling 9. **Clean up resources** (Jitsi rooms, reminders) properly 10. **Test thoroughly** with different user scenarios This comprehensive rescheduling system provides a smooth user experience while maintaining business rules and data integrity.