Add detailed API: - Complete API documentation for In Format Usage flow diagrams for authentication and booking processes - Comprehensive endpoint descriptions with request/response examples - Detailed authentication and booking flow explanations - Structured documentation for health checks, authentication, and booking endpoints -: - Includes JWT authentication details usage - Provides clear API usage patterns for users and clients and administrators system interactions - Enhances project documentation with provides clear, structured API reference - Improves developer and user understanding of system capabilities
25 KiB
25 KiB
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
{
"new_schedule_id": 5
}
Business Rules
- Time Constraint: Booking can only be rescheduled at least 2 hours before the original scheduled time
- Ownership: Users can only reschedule their own bookings
- Availability: New time slot must be available
- Status: Only
scheduledbookings can be rescheduled - Automatic Cleanup: Old Jitsi meeting is deleted, new one is created
- Notifications: Reminder notifications are updated automatically
Response Examples
Success Response (200)
{
"message": "Booking rescheduled successfully"
}
Error Responses
Unauthorized (403):
{
"error": "You can only reschedule your own bookings"
}
Time Constraint (400):
{
"error": "This booking cannot be rescheduled (must be at least 2 hours before scheduled time)"
}
Slot Unavailable (409):
{
"error": "The new time slot is not available"
}
Booking Not Found (404):
{
"error": "Booking or new schedule not found"
}
Frontend Implementation
1. Rescheduling Modal Component
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 (
<div className="modal-overlay">
<div className="modal-content reschedule-modal">
<div className="modal-header">
<h3>Reschedule Appointment</h3>
<button onClick={onClose} className="close-btn">×</button>
</div>
<div className="modal-body">
{/* Current Booking Info */}
<div className="current-booking">
<h4>Current Appointment</h4>
<div className="booking-info">
<strong>Date:</strong> {format(new Date(booking.scheduled_at), 'MMMM dd, yyyy')}
<br />
<strong>Time:</strong> {format(new Date(booking.scheduled_at), 'HH:mm')}
<br />
<strong>Duration:</strong> {booking.duration} minutes
</div>
</div>
{!canReschedule() ? (
<div className="reschedule-restriction">
<p className="warning">
⚠️ This booking cannot be rescheduled because it's less than 2 hours away.
Please contact support if you need to make changes.
</p>
</div>
) : (
<>
{/* Date Selection */}
<div className="date-selection">
<h4>Select New Date</h4>
<div className="date-grid">
{getNextTwoWeeks().map((date) => (
<button
key={date.toISOString()}
onClick={() => setSelectedDate(date)}
className={`date-btn ${
format(date, 'yyyy-MM-dd') === format(selectedDate, 'yyyy-MM-dd')
? 'active'
: ''
}`}
>
<div className="day-name">{format(date, 'EEE')}</div>
<div className="day-number">{format(date, 'dd')}</div>
</button>
))}
</div>
</div>
{/* Time Slot Selection */}
<div className="time-selection">
<h4>Available Times for {format(selectedDate, 'MMMM dd, yyyy')}</h4>
{error && (
<div className="error-message">{error}</div>
)}
{isLoading ? (
<div className="loading">Loading available times...</div>
) : availableSlots.length === 0 ? (
<div className="no-slots">
No available time slots for this date. Please select another date.
</div>
) : (
<div className="time-slots">
{availableSlots.map((slot) => (
<button
key={slot.id}
onClick={() => setSelectedSlot(slot)}
className={`time-slot ${selectedSlot?.id === slot.id ? 'selected' : ''}`}
>
<div className="time">
{format(new Date(slot.start_time), 'HH:mm')} -
{format(new Date(slot.end_time), 'HH:mm')}
</div>
<div className="availability">
{slot.remaining_slots} slot{slot.remaining_slots > 1 ? 's' : ''} available
</div>
</button>
))}
</div>
)}
</div>
</>
)}
</div>
<div className="modal-footer">
{canReschedule() && (
<button
onClick={handleReschedule}
disabled={!selectedSlot || isLoading}
className="btn btn-primary"
>
{isLoading ? 'Rescheduling...' : 'Confirm Reschedule'}
</button>
)}
<button onClick={onClose} className="btn btn-secondary">
Cancel
</button>
</div>
</div>
</div>
);
};
export default RescheduleModal;
2. Booking Management Component
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 <div className="loading">Loading your bookings...</div>;
}
return (
<div className="my-bookings">
<h2>My Appointments</h2>
{bookings.length === 0 ? (
<div className="no-bookings">
<p>You don't have any bookings yet.</p>
<button className="btn btn-primary">Book Your First Session</button>
</div>
) : (
<div className="bookings-list">
{bookings.map((booking) => (
<div key={booking.id} className="booking-card">
<div className="booking-header">
<div className="booking-date">
<div className="date">
{format(new Date(booking.scheduled_at), 'MMM dd, yyyy')}
</div>
<div className="time">
{format(new Date(booking.scheduled_at), 'HH:mm')}
</div>
</div>
<div className="booking-status">
<span className={`status ${getStatusColor(booking.status)}`}>
{booking.status.charAt(0).toUpperCase() + booking.status.slice(1)}
</span>
<span className={`payment-status ${getPaymentStatusColor(booking.payment_status)}`}>
Payment: {booking.payment_status}
</span>
</div>
</div>
<div className="booking-details">
<div className="detail-item">
<strong>Duration:</strong> {booking.duration} minutes
</div>
<div className="detail-item">
<strong>Amount:</strong> ${booking.amount?.toFixed(2) || '0.00'}
</div>
{booking.notes && (
<div className="detail-item">
<strong>Notes:</strong> {booking.notes}
</div>
)}
{booking.jitsi_room_url && booking.status === 'scheduled' && (
<div className="detail-item">
<strong>Meeting Link:</strong>{' '}
<a
href={booking.jitsi_room_url}
target="_blank"
rel="noopener noreferrer"
className="meeting-link"
>
Join Video Session
</a>
</div>
)}
</div>
<div className="booking-actions">
{canReschedule(booking) && (
<button
onClick={() => handleReschedule(booking)}
className="btn btn-outline-primary btn-sm"
>
Reschedule
</button>
)}
{canCancel(booking) && (
<button
onClick={() => handleCancelBooking(booking.id)}
className="btn btn-outline-danger btn-sm"
>
Cancel
</button>
)}
{booking.status === 'scheduled' && !canReschedule(booking) && !canCancel(booking) && (
<span className="no-actions">
Too close to appointment time for changes
</span>
)}
</div>
</div>
))}
</div>
)}
{/* Reschedule Modal */}
<RescheduleModal
booking={selectedBooking}
isOpen={showRescheduleModal}
onClose={() => {
setShowRescheduleModal(false);
setSelectedBooking(null);
}}
onSuccess={() => {
loadBookings(); // Refresh bookings after successful reschedule
alert('Appointment rescheduled successfully!');
}}
/>
</div>
);
};
export default MyBookings;
3. CSS Styles for Rescheduling Components
/* 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)
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 (
<div className="bulk-reschedule">
{/* Implementation for bulk operations */}
</div>
);
};
2. Rescheduling with Conflict Detection
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
const ReschedulingHistory = ({ bookingId }) => {
const [history, setHistory] = useState([]);
useEffect(() => {
// Fetch rescheduling history for the booking
fetchReschedulingHistory(bookingId);
}, [bookingId]);
return (
<div className="rescheduling-history">
<h4>Rescheduling History</h4>
{history.map((entry, index) => (
<div key={index} className="history-entry">
<div>From: {format(new Date(entry.old_time), 'MMM dd, yyyy HH:mm')}</div>
<div>To: {format(new Date(entry.new_time), 'MMM dd, yyyy HH:mm')}</div>
<div>Changed: {format(new Date(entry.changed_at), 'MMM dd, yyyy HH:mm')}</div>
</div>
))}
</div>
);
};
Testing Rescheduling
Postman Test Scenarios
- Successful Rescheduling:
// 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");
});
- Time Constraint Violation:
// 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");
});
- Unauthorized Rescheduling:
// 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
- Always validate time constraints before allowing rescheduling
- Show clear error messages for different failure scenarios
- Provide visual feedback during the rescheduling process
- Automatically refresh booking lists after successful operations
- Handle edge cases like fully booked slots gracefully
- Implement optimistic updates where appropriate
- Track rescheduling history for audit purposes
- Send notifications for successful rescheduling
- Clean up resources (Jitsi rooms, reminders) properly
- Test thoroughly with different user scenarios
This comprehensive rescheduling system provides a smooth user experience while maintaining business rules and data integrity.