backend-service/docs/RESCHEDULING_GUIDE.md
ats-tech25 04f2d02afc docs(api comprehensive API documentation for attune Heart Therapy
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
2025-11-07 19:22:46 +00:00

1030 lines
25 KiB
Markdown

# 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 (
<div className="modal-overlay">
<div className="modal-content reschedule-modal">
<div className="modal-header">
<h3>Reschedule Appointment</h3>
<button onClick={onClose} className="close-btn">&times;</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
```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 <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
```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 (
<div className="bulk-reschedule">
{/* Implementation for bulk operations */}
</div>
);
};
```
### 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 (
<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
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.