backend-service/docs/SCHEDULE_MANAGEMENT.md

891 lines
23 KiB
Markdown
Raw Permalink Normal View History

# 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
```go
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
1. **Time Slots**: Each schedule represents a specific time period when therapy sessions can be booked
2. **Availability**: Schedules can be enabled/disabled using the `is_available` flag
3. **Capacity**: Each schedule can handle multiple bookings (useful for group sessions)
4. **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**:
```json
{
"start_time": "2024-12-15T10:00:00Z",
"end_time": "2024-12-15T11:00:00Z",
"max_bookings": 1
}
```
**Validation Rules**:
- `start_time` must be in the future
- `end_time` must be after `start_time`
- `max_bookings` must be at least 1
- No overlapping schedules allowed
**Response**:
```json
{
"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):
```json
{
"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**:
```json
{
"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
```jsx
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
```css
.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
```jsx
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:
```jsx
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**:
```javascript
// 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:
```javascript
// 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:
```javascript
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
1. **Always validate dates on the frontend** before sending to the API
2. **Use UTC times** for all API communications
3. **Implement proper error handling** for network failures
4. **Show loading states** during API calls
5. **Refresh data** after successful operations
6. **Use optimistic updates** where appropriate
7. **Implement proper authentication** checks before admin operations
This guide should help you implement a complete schedule management system for your therapy booking application.