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
23 KiB
Schedule Management Guide
Overview
The schedule system in Attune Heart Therapy API allows administrators to create time slots that clients can book for therapy sessions. This guide explains how schedule creation works and how to implement it in your frontend application.
Understanding the Schedule System
Schedule Model Structure
type Schedule struct {
ID uint `json:"id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
IsAvailable bool `json:"is_available"`
MaxBookings int `json:"max_bookings"`
BookedCount int `json:"booked_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Key Concepts
- Time Slots: Each schedule represents a specific time period when therapy sessions can be booked
- Availability: Schedules can be enabled/disabled using the
is_availableflag - Capacity: Each schedule can handle multiple bookings (useful for group sessions)
- Booking Count: Tracks how many bookings have been made for each schedule
Backend Implementation
1. Creating Schedules (Admin Only)
Endpoint: POST /api/admin/schedules
Request Structure:
{
"start_time": "2024-12-15T10:00:00Z",
"end_time": "2024-12-15T11:00:00Z",
"max_bookings": 1
}
Validation Rules:
start_timemust be in the futureend_timemust be afterstart_timemax_bookingsmust be at least 1- No overlapping schedules allowed
Response:
{
"message": "Schedule created successfully",
"schedule": {
"id": 1,
"start_time": "2024-12-15T10:00:00Z",
"end_time": "2024-12-15T11:00:00Z",
"is_available": true,
"max_bookings": 1,
"booked_count": 0,
"created_at": "2024-12-06T10:00:00Z",
"updated_at": "2024-12-06T10:00:00Z"
}
}
2. Updating Schedules
Endpoint: PUT /api/admin/schedules/:id
Request Structure (all fields optional):
{
"start_time": "2024-12-15T14:00:00Z",
"end_time": "2024-12-15T15:00:00Z",
"max_bookings": 2,
"is_available": false
}
3. Getting Available Slots (Public)
Endpoint: GET /api/schedules?date=2024-12-15
Response:
{
"date": "2024-12-15",
"slots": [
{
"id": 1,
"start_time": "2024-12-15T10:00:00Z",
"end_time": "2024-12-15T11:00:00Z",
"is_available": true,
"max_bookings": 1,
"booked_count": 0,
"remaining_slots": 1
}
]
}
Frontend Implementation
1. Admin Schedule Management Interface
React Component Example
import React, { useState, useEffect } from 'react';
import { format, addDays, startOfWeek } from 'date-fns';
const ScheduleManager = () => {
const [schedules, setSchedules] = useState([]);
const [selectedDate, setSelectedDate] = useState(new Date());
const [isCreating, setIsCreating] = useState(false);
const [newSchedule, setNewSchedule] = useState({
start_time: '',
end_time: '',
max_bookings: 1
});
// API service functions
const apiService = {
async createSchedule(scheduleData) {
const response = await fetch('/api/admin/schedules', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
},
body: JSON.stringify(scheduleData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create schedule');
}
return response.json();
},
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 schedules');
}
return response.json();
},
async updateSchedule(scheduleId, updateData) {
const response = await fetch(`/api/admin/schedules/${scheduleId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
},
body: JSON.stringify(updateData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to update schedule');
}
return response.json();
}
};
// Load schedules for selected date
useEffect(() => {
loadSchedules();
}, [selectedDate]);
const loadSchedules = async () => {
try {
const data = await apiService.getAvailableSlots(selectedDate);
setSchedules(data.slots || []);
} catch (error) {
console.error('Error loading schedules:', error);
setSchedules([]);
}
};
const handleCreateSchedule = async (e) => {
e.preventDefault();
try {
// Convert local time to UTC
const startTime = new Date(newSchedule.start_time).toISOString();
const endTime = new Date(newSchedule.end_time).toISOString();
await apiService.createSchedule({
start_time: startTime,
end_time: endTime,
max_bookings: parseInt(newSchedule.max_bookings)
});
// Reset form and reload schedules
setNewSchedule({
start_time: '',
end_time: '',
max_bookings: 1
});
setIsCreating(false);
await loadSchedules();
alert('Schedule created successfully!');
} catch (error) {
alert(`Error creating schedule: ${error.message}`);
}
};
const handleToggleAvailability = async (scheduleId, currentAvailability) => {
try {
await apiService.updateSchedule(scheduleId, {
is_available: !currentAvailability
});
await loadSchedules();
} catch (error) {
alert(`Error updating schedule: ${error.message}`);
}
};
const generateTimeSlots = () => {
const slots = [];
for (let hour = 9; hour < 18; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
slots.push(time);
}
}
return slots;
};
const formatDateTime = (dateTime) => {
return format(new Date(dateTime), 'MMM dd, yyyy HH:mm');
};
return (
<div className="schedule-manager">
<div className="header">
<h2>Schedule Management</h2>
<button
onClick={() => setIsCreating(true)}
className="btn btn-primary"
>
Create New Schedule
</button>
</div>
{/* Date Selector */}
<div className="date-selector">
<label>Select Date:</label>
<input
type="date"
value={format(selectedDate, 'yyyy-MM-dd')}
onChange={(e) => setSelectedDate(new Date(e.target.value))}
className="form-control"
/>
</div>
{/* Create Schedule Form */}
{isCreating && (
<div className="create-schedule-form">
<h3>Create New Schedule</h3>
<form onSubmit={handleCreateSchedule}>
<div className="form-group">
<label>Start Time:</label>
<input
type="datetime-local"
value={newSchedule.start_time}
onChange={(e) => setNewSchedule({
...newSchedule,
start_time: e.target.value
})}
required
className="form-control"
/>
</div>
<div className="form-group">
<label>End Time:</label>
<input
type="datetime-local"
value={newSchedule.end_time}
onChange={(e) => setNewSchedule({
...newSchedule,
end_time: e.target.value
})}
required
className="form-control"
/>
</div>
<div className="form-group">
<label>Max Bookings:</label>
<input
type="number"
min="1"
max="10"
value={newSchedule.max_bookings}
onChange={(e) => setNewSchedule({
...newSchedule,
max_bookings: e.target.value
})}
required
className="form-control"
/>
</div>
<div className="form-actions">
<button type="submit" className="btn btn-success">
Create Schedule
</button>
<button
type="button"
onClick={() => setIsCreating(false)}
className="btn btn-secondary"
>
Cancel
</button>
</div>
</form>
</div>
)}
{/* Schedules List */}
<div className="schedules-list">
<h3>Schedules for {format(selectedDate, 'MMMM dd, yyyy')}</h3>
{schedules.length === 0 ? (
<p className="no-schedules">No schedules found for this date.</p>
) : (
<div className="schedules-grid">
{schedules.map((schedule) => (
<div key={schedule.id} className="schedule-card">
<div className="schedule-time">
{format(new Date(schedule.start_time), 'HH:mm')} -
{format(new Date(schedule.end_time), 'HH:mm')}
</div>
<div className="schedule-details">
<span className="capacity">
{schedule.booked_count}/{schedule.max_bookings} booked
</span>
<span className={`status ${schedule.is_available ? 'available' : 'unavailable'}`}>
{schedule.is_available ? 'Available' : 'Unavailable'}
</span>
</div>
<div className="schedule-actions">
<button
onClick={() => handleToggleAvailability(schedule.id, schedule.is_available)}
className={`btn btn-sm ${schedule.is_available ? 'btn-warning' : 'btn-success'}`}
>
{schedule.is_available ? 'Disable' : 'Enable'}
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default ScheduleManager;
CSS Styles
.schedule-manager {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.date-selector {
margin-bottom: 20px;
}
.date-selector label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.create-schedule-form {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.schedules-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.schedule-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
background: white;
}
.schedule-time {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
.schedule-details {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.capacity {
font-size: 14px;
color: #666;
}
.status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.status.available {
background: #d4edda;
color: #155724;
}
.status.unavailable {
background: #f8d7da;
color: #721c24;
}
.schedule-actions {
text-align: right;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.no-schedules {
text-align: center;
color: #666;
font-style: italic;
padding: 40px;
}
2. Client Booking Interface
React Component for Booking
import React, { useState, useEffect } from 'react';
import { format, addDays, startOfWeek } from 'date-fns';
const BookingInterface = () => {
const [selectedDate, setSelectedDate] = useState(new Date());
const [availableSlots, setAvailableSlots] = useState([]);
const [selectedSlot, setSelectedSlot] = useState(null);
const [bookingNotes, setBookingNotes] = useState('');
const [isLoading, setIsLoading] = useState(false);
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 createBooking(scheduleId, notes) {
const response = await fetch('/api/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
},
body: JSON.stringify({
schedule_id: scheduleId,
duration: 60,
notes: notes,
amount: 150.00 // $150 per session
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to create booking');
}
return response.json();
}
};
useEffect(() => {
loadAvailableSlots();
}, [selectedDate]);
const loadAvailableSlots = async () => {
setIsLoading(true);
try {
const data = await apiService.getAvailableSlots(selectedDate);
setAvailableSlots(data.slots || []);
} catch (error) {
console.error('Error loading available slots:', error);
setAvailableSlots([]);
} finally {
setIsLoading(false);
}
};
const handleBooking = async () => {
if (!selectedSlot) {
alert('Please select a time slot');
return;
}
try {
setIsLoading(true);
const result = await apiService.createBooking(selectedSlot.id, bookingNotes);
alert('Booking created successfully! You will receive payment instructions shortly.');
// Reset form
setSelectedSlot(null);
setBookingNotes('');
// Reload available slots
await loadAvailableSlots();
} catch (error) {
alert(`Error creating booking: ${error.message}`);
} finally {
setIsLoading(false);
}
};
const getNextSevenDays = () => {
const days = [];
for (let i = 0; i < 7; i++) {
days.push(addDays(new Date(), i));
}
return days;
};
return (
<div className="booking-interface">
<h2>Book Your Therapy Session</h2>
{/* Date Selection */}
<div className="date-selection">
<h3>Select Date</h3>
<div className="date-buttons">
{getNextSevenDays().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>
<div className="month-name">{format(date, 'MMM')}</div>
</button>
))}
</div>
</div>
{/* Time Slot Selection */}
<div className="time-selection">
<h3>Available Times for {format(selectedDate, 'MMMM dd, yyyy')}</h3>
{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' : ''}`}
disabled={slot.remaining_slots === 0}
>
<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 > 0
? `${slot.remaining_slots} slot${slot.remaining_slots > 1 ? 's' : ''} available`
: 'Fully booked'
}
</div>
</button>
))}
</div>
)}
</div>
{/* Booking Notes */}
{selectedSlot && (
<div className="booking-details">
<h3>Booking Details</h3>
<div className="selected-time">
<strong>Selected Time:</strong> {format(selectedDate, 'MMMM dd, yyyy')} at{' '}
{format(new Date(selectedSlot.start_time), 'HH:mm')} -
{format(new Date(selectedSlot.end_time), 'HH:mm')}
</div>
<div className="form-group">
<label htmlFor="notes">Session Notes (Optional):</label>
<textarea
id="notes"
value={bookingNotes}
onChange={(e) => setBookingNotes(e.target.value)}
placeholder="Any specific topics or concerns you'd like to discuss..."
rows="4"
className="form-control"
/>
</div>
<div className="booking-actions">
<button
onClick={handleBooking}
disabled={isLoading}
className="btn btn-primary btn-large"
>
{isLoading ? 'Creating Booking...' : 'Book Session ($150)'}
</button>
<button
onClick={() => setSelectedSlot(null)}
className="btn btn-secondary"
>
Cancel
</button>
</div>
</div>
)}
</div>
);
};
export default BookingInterface;
3. Bulk Schedule Creation
For creating multiple schedules efficiently:
const BulkScheduleCreator = () => {
const [scheduleTemplate, setScheduleTemplate] = useState({
startDate: '',
endDate: '',
startTime: '09:00',
endTime: '10:00',
maxBookings: 1,
daysOfWeek: [1, 2, 3, 4, 5] // Monday to Friday
});
const createBulkSchedules = async () => {
const schedules = [];
const startDate = new Date(scheduleTemplate.startDate);
const endDate = new Date(scheduleTemplate.endDate);
for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) {
const dayOfWeek = date.getDay();
if (scheduleTemplate.daysOfWeek.includes(dayOfWeek)) {
const startDateTime = new Date(date);
const [startHour, startMinute] = scheduleTemplate.startTime.split(':');
startDateTime.setHours(parseInt(startHour), parseInt(startMinute), 0, 0);
const endDateTime = new Date(date);
const [endHour, endMinute] = scheduleTemplate.endTime.split(':');
endDateTime.setHours(parseInt(endHour), parseInt(endMinute), 0, 0);
schedules.push({
start_time: startDateTime.toISOString(),
end_time: endDateTime.toISOString(),
max_bookings: scheduleTemplate.maxBookings
});
}
}
// Create schedules one by one
for (const schedule of schedules) {
try {
await apiService.createSchedule(schedule);
} catch (error) {
console.error('Error creating schedule:', error);
}
}
alert(`Created ${schedules.length} schedules successfully!`);
};
return (
<div className="bulk-schedule-creator">
<h3>Bulk Schedule Creation</h3>
{/* Form fields for bulk creation */}
<button onClick={createBulkSchedules} className="btn btn-primary">
Create Schedules
</button>
</div>
);
};
Common Issues and Solutions
1. Empty Slots Response
Problem: Getting {"date": "2025-11-07", "slots": null}
Causes:
- No schedules created for that date
- Database connection issues
- Repository method not implemented
Solution:
// First, create some schedules as admin
const createSampleSchedules = async () => {
const schedules = [
{
start_time: "2025-11-07T09:00:00Z",
end_time: "2025-11-07T10:00:00Z",
max_bookings: 1
},
{
start_time: "2025-11-07T14:00:00Z",
end_time: "2025-11-07T15:00:00Z",
max_bookings: 1
}
];
for (const schedule of schedules) {
await fetch('/api/admin/schedules', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${adminToken}`
},
body: JSON.stringify(schedule)
});
}
};
2. Time Zone Handling
Always use UTC times in the API and convert to local time in the frontend:
// Convert local time to UTC for API
const localToUTC = (localDateTime) => {
return new Date(localDateTime).toISOString();
};
// Convert UTC time from API to local time for display
const utcToLocal = (utcDateTime) => {
return new Date(utcDateTime);
};
3. Validation Errors
Handle common validation errors gracefully:
const handleScheduleError = (error) => {
switch (error.message) {
case 'end time must be after start time':
return 'Please ensure the end time is after the start time';
case 'cannot create schedule slots in the past':
return 'Cannot create schedules for past dates';
case 'max bookings must be at least 1':
return 'Maximum bookings must be at least 1';
default:
return `Error: ${error.message}`;
}
};
Best Practices
- Always validate dates on the frontend before sending to the API
- Use UTC times for all API communications
- Implement proper error handling for network failures
- Show loading states during API calls
- Refresh data after successful operations
- Use optimistic updates where appropriate
- Implement proper authentication checks before admin operations
This guide should help you implement a complete schedule management system for your therapy booking application.