From 04f2d02afc6e795027d4cdc07644fae16f42c053 Mon Sep 17 00:00:00 2001 From: ats-tech25 Date: Fri, 7 Nov 2025 19:22:46 +0000 Subject: [PATCH] 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 --- .env.example | 10 +- .vscode/settings.json | 2 + docs/API.md | 647 +++++++++++++ docs/API_FLOW.md | 652 +++++++++++++ docs/JITSI_JWT_AUTHENTICATION.md | 372 ++++++++ docs/MEETING_JOIN_GUIDE.md | 217 +++++ docs/PERSONALIZED_MEETING_LINKS.md | 323 +++++++ docs/RESCHEDULING_GUIDE.md | 1030 +++++++++++++++++++++ docs/SCHEDULE_MANAGEMENT.md | 891 ++++++++++++++++++ internal/container/container.go | 10 +- internal/handlers/admin.go | 80 +- internal/handlers/booking.go | 72 +- internal/handlers/meeting.go | 130 +++ internal/server/server.go | 6 + internal/services/interfaces.go | 1 + internal/services/jitsi_service.go | 68 +- internal/services/notification_service.go | 47 +- internal/templates/email_templates.go | 4 +- 18 files changed, 4548 insertions(+), 14 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 docs/API.md create mode 100644 docs/API_FLOW.md create mode 100644 docs/JITSI_JWT_AUTHENTICATION.md create mode 100644 docs/MEETING_JOIN_GUIDE.md create mode 100644 docs/PERSONALIZED_MEETING_LINKS.md create mode 100644 docs/RESCHEDULING_GUIDE.md create mode 100644 docs/SCHEDULE_MANAGEMENT.md create mode 100644 internal/handlers/meeting.go diff --git a/.env.example b/.env.example index 85aee0d..18c1e1d 100644 --- a/.env.example +++ b/.env.example @@ -27,7 +27,13 @@ SMTP_PASSWORD=your_app_password SMTP_FROM=your_email@gmail.com # Jitsi Configuration +# For public Jitsi (meet.jit.si) - JWT not required, leave API_KEY and APP_ID empty JITSI_BASE_URL=https://meet.jit.si -JITSI_API_KEY=your_jitsi_api_key -JITSI_APP_ID=your_jitsi_app_id +JITSI_API_KEY= +JITSI_APP_ID= + +# For self-hosted Jitsi with JWT authentication - fill in these values +# JITSI_BASE_URL=https://meet.yourdomain.com +# JITSI_API_KEY=your_jwt_secret_key +# JITSI_APP_ID=your_jitsi_app_id JITSI_PRIVATE_KEY=your_jitsi_private_key \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..86485df --- /dev/null +++ b/docs/API.md @@ -0,0 +1,647 @@ +# Attune Heart Therapy API Documentation + +## Overview + +The Attune Heart Therapy API is a comprehensive video conference booking system that enables users to schedule therapy sessions, process payments, and manage appointments. The API provides endpoints for user authentication, booking management, payment processing, and administrative functions. + +**Base URL:** `http://localhost:8080` +**API Version:** v1 +**Authentication:** JWT Bearer Token + +## Table of Contents + +1. [Authentication Flow](#authentication-flow) +2. [API Usage Flow](#api-usage-flow) +3. [Endpoints](#endpoints) +4. [Data Models](#data-models) +5. [Error Handling](#error-handling) +6. [Rate Limiting](#rate-limiting) + +## Authentication Flow + +The API uses JWT (JSON Web Token) for authentication. Here's the authentication flow: + +1. **Register** a new user account or **Login** with existing credentials +2. Receive a JWT token in the response +3. Include the token in the `Authorization` header for protected endpoints: `Bearer ` +4. Token expires after 24 hours (configurable) + +### Authentication Headers +``` +Authorization: Bearer +Content-Type: application/json +``` + +## API Usage Flow + +### For Regular Users (Clients) + +```mermaid +graph TD + A[Register/Login] --> B[Get Available Slots] + B --> C[Create Booking] + C --> D[Create Payment Intent] + D --> E[Confirm Payment] + E --> F[Receive Jitsi Room Details] + F --> G[Join Video Session] + + C --> H[View My Bookings] + H --> I[Cancel/Reschedule Booking] +``` + +### For Administrators + +```mermaid +graph TD + A[Admin Login] --> B[View Dashboard] + B --> C[Create Schedule Slots] + C --> D[Manage Users] + D --> E[View All Bookings] + E --> F[Generate Reports] +``` + +### Typical User Journey + +1. **User Registration/Login** + - New users register with personal details + - Existing users login with email/password + - System returns JWT token for subsequent requests + +2. **Browse Available Slots** + - User queries available time slots for specific dates + - System returns available schedule slots + +3. **Create Booking** + - User selects a time slot and creates a booking + - System reserves the slot and returns booking details + +4. **Payment Processing** + - User initiates payment through Stripe integration + - System creates payment intent and processes payment + - Upon successful payment, booking is confirmed + +5. **Session Management** + - System generates Jitsi room details + - User receives video conference link + - User can view, cancel, or reschedule bookings + +## Endpoints + +### Health & Monitoring + +#### GET /health +Basic health check endpoint. + +**Response:** +```json +{ + "status": "ok", + "message": "Video Conference Booking System API", + "timestamp": "2024-12-06T10:00:00Z" +} +``` + +#### GET /health/detailed +Detailed system health check. + +**Response:** +```json +{ + "status": "healthy", + "checks": { + "database": "healthy", + "job_manager": "healthy", + "memory": "healthy" + }, + "timestamp": "2024-12-06T10:00:00Z" +} +``` + +#### GET /metrics +System metrics and monitoring data. + +### Authentication Endpoints + +#### POST /api/auth/register +Register a new user account. + +**Request Body:** +```json +{ + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "location": "New York, NY", + "password": "securepassword123" +} +``` + +**Response (201):** +```json +{ + "message": "User registered successfully", + "user": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "location": "New York, NY", + "is_admin": false, + "created_at": "2024-12-06T10:00:00Z" + } +} +``` + +#### POST /api/auth/login +Authenticate user and receive JWT token. + +**Request Body:** +```json +{ + "email": "john.doe@example.com", + "password": "securepassword123" +} +``` + +**Response (200):** +```json +{ + "message": "Login successful", + "user": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "is_admin": false + }, + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +#### GET /api/auth/profile +Get current user profile (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Response (200):** +```json +{ + "user": { + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "location": "New York, NY", + "is_admin": false + } +} +``` + +#### PUT /api/auth/profile +Update user profile (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Request Body:** +```json +{ + "first_name": "John", + "last_name": "Smith", + "phone": "+1234567891", + "location": "Los Angeles, CA" +} +``` + +#### POST /api/auth/logout +Logout user (requires authentication). + +**Headers:** `Authorization: Bearer ` + +### Schedule & Booking Endpoints + +#### GET /api/schedules +Get available time slots for a specific date. + +**Query Parameters:** +- `date` (required): Date in YYYY-MM-DD format + +**Example:** `/api/schedules?date=2024-12-15` + +**Response (200):** +```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 + } + ] +} +``` + +#### POST /api/bookings +Create a new booking (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Request Body:** +```json +{ + "schedule_id": 1, + "duration": 60, + "notes": "Initial consultation session" +} +``` + +**Response (201):** +```json +{ + "message": "Booking created successfully", + "booking": { + "id": 1, + "user_id": 1, + "scheduled_at": "2024-12-15T10:00:00Z", + "duration": 60, + "status": "scheduled", + "jitsi_room_id": "room_abc123", + "jitsi_room_url": "https://meet.jit.si/room_abc123", + "amount": 100.00, + "payment_status": "pending", + "notes": "Initial consultation session" + } +} +``` + +#### GET /api/bookings +Get all bookings for the current user (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Response (200):** +```json +{ + "bookings": [ + { + "id": 1, + "scheduled_at": "2024-12-15T10:00:00Z", + "duration": 60, + "status": "scheduled", + "jitsi_room_url": "https://meet.jit.si/room_abc123", + "amount": 100.00, + "payment_status": "succeeded" + } + ] +} +``` + +#### PUT /api/bookings/:id/cancel +Cancel a booking (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Requirements:** Booking must be at least 24 hours before scheduled time. + +**Response (200):** +```json +{ + "message": "Booking cancelled successfully" +} +``` + +#### PUT /api/bookings/:id/reschedule +Reschedule a booking to a new time slot (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Request Body:** +```json +{ + "new_schedule_id": 2 +} +``` + +**Requirements:** Booking must be at least 2 hours before scheduled time. + +### Payment Endpoints + +#### POST /api/payments/intent +Create a payment intent for Stripe (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Request Body:** +```json +{ + "amount": 10000, + "currency": "usd" +} +``` + +**Note:** Amount is in cents (10000 = $100.00) + +**Response (201):** +```json +{ + "message": "Payment intent created successfully", + "client_secret": "pi_1234567890_secret_abc123", + "payment_intent": "pi_1234567890", + "amount": 10000, + "currency": "usd", + "status": "requires_payment_method" +} +``` + +#### POST /api/payments/confirm +Confirm a payment intent (requires authentication). + +**Headers:** `Authorization: Bearer ` + +**Request Body:** +```json +{ + "payment_intent_id": "pi_1234567890" +} +``` + +**Response (200):** +```json +{ + "message": "Payment confirmed successfully", + "payment_intent": "pi_1234567890", + "status": "succeeded", + "amount": 10000, + "currency": "usd" +} +``` + +#### POST /api/payments/webhook +Stripe webhook endpoint (no authentication required). + +**Headers:** +- `Content-Type: application/json` +- `Stripe-Signature: ` + +### Admin Endpoints + +All admin endpoints require authentication with admin privileges. + +#### GET /api/admin/dashboard +Get dashboard statistics (admin only). + +**Headers:** `Authorization: Bearer ` + +**Response (200):** +```json +{ + "stats": { + "total_users": 150, + "total_bookings": 89, + "pending_bookings": 12, + "completed_bookings": 67, + "cancelled_bookings": 10, + "total_revenue": 8900.00, + "monthly_revenue": 2100.00 + } +} +``` + +#### POST /api/admin/schedules +Create a new schedule slot (admin only). + +**Headers:** `Authorization: Bearer ` + +**Request Body:** +```json +{ + "start_time": "2024-12-15T10:00:00Z", + "end_time": "2024-12-15T11:00:00Z", + "max_bookings": 1, + "is_available": true +} +``` + +#### PUT /api/admin/schedules/:id +Update an existing schedule slot (admin only). + +#### GET /api/admin/users +Get all users with pagination (admin only). + +**Query Parameters:** +- `limit` (optional): Number of users to return (default: 50) +- `offset` (optional): Number of users to skip (default: 0) + +#### GET /api/admin/bookings +Get all bookings with pagination (admin only). + +#### GET /api/admin/reports/financial +Get financial reports for a date range (admin only). + +**Query Parameters:** +- `start_date` (required): Start date in YYYY-MM-DD format +- `end_date` (required): End date in YYYY-MM-DD format + +## Data Models + +### User +```json +{ + "id": 1, + "first_name": "John", + "last_name": "Doe", + "email": "john.doe@example.com", + "phone": "+1234567890", + "location": "New York, NY", + "date_of_birth": "1990-01-01T00:00:00Z", + "is_admin": false, + "created_at": "2024-12-06T10:00:00Z", + "updated_at": "2024-12-06T10:00:00Z" +} +``` + +### Booking +```json +{ + "id": 1, + "user_id": 1, + "scheduled_at": "2024-12-15T10:00:00Z", + "duration": 60, + "status": "scheduled", + "jitsi_room_id": "room_abc123", + "jitsi_room_url": "https://meet.jit.si/room_abc123", + "payment_id": "pi_1234567890", + "payment_status": "succeeded", + "amount": 100.00, + "notes": "Initial consultation session", + "created_at": "2024-12-06T10:00:00Z", + "updated_at": "2024-12-06T10:00:00Z" +} +``` + +### Schedule +```json +{ + "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" +} +``` + +## Status Codes + +### Booking Status +- `scheduled`: Booking is confirmed and scheduled +- `completed`: Session has been completed +- `cancelled`: Booking has been cancelled + +### Payment Status +- `pending`: Payment is pending +- `succeeded`: Payment was successful +- `failed`: Payment failed +- `refunded`: Payment was refunded + +## Error Handling + +The API returns standard HTTP status codes and error messages in JSON format. + +### Error Response Format +```json +{ + "error": "Error message", + "details": "Detailed error information" +} +``` + +### Common HTTP Status Codes +- `200 OK`: Request successful +- `201 Created`: Resource created successfully +- `400 Bad Request`: Invalid request format or parameters +- `401 Unauthorized`: Authentication required or invalid token +- `403 Forbidden`: Insufficient permissions +- `404 Not Found`: Resource not found +- `409 Conflict`: Resource conflict (e.g., time slot already booked) +- `422 Payment Required`: Payment failed or required +- `500 Internal Server Error`: Server error + +### Common Error Scenarios + +#### Authentication Errors +```json +{ + "error": "User not authenticated" +} +``` + +#### Validation Errors +```json +{ + "error": "Invalid request format", + "details": "Email is required" +} +``` + +#### Business Logic Errors +```json +{ + "error": "The selected time slot is no longer available" +} +``` + +## Rate Limiting + +The API implements rate limiting to prevent abuse: + +- **General endpoints**: 100 requests per minute per IP +- **Authentication endpoints**: 5 requests per minute per IP (strict rate limiting) +- **Admin endpoints**: 200 requests per minute per authenticated admin + +Rate limit headers are included in responses: +- `X-RateLimit-Limit`: Request limit per window +- `X-RateLimit-Remaining`: Remaining requests in current window +- `X-RateLimit-Reset`: Time when the rate limit resets + +## Security Features + +- **JWT Authentication**: Secure token-based authentication +- **Password Hashing**: Bcrypt hashing for user passwords +- **CORS Protection**: Cross-origin request protection +- **Rate Limiting**: Protection against abuse and DDoS +- **Input Validation**: Comprehensive request validation +- **SQL Injection Protection**: Parameterized queries with GORM +- **Security Headers**: Standard security headers included + +## Integration Examples + +### Frontend Integration (JavaScript) +```javascript +// Login and store token +const loginResponse = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'user@example.com', + password: 'password123' + }) +}); + +const { token } = await loginResponse.json(); +localStorage.setItem('authToken', token); + +// Make authenticated request +const bookingsResponse = await fetch('/api/bookings', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('authToken')}` + } +}); +``` + +### cURL Examples +```bash +# Register user +curl -X POST http://localhost:8080/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com", + "password": "password123" + }' + +# Login +curl -X POST http://localhost:8080/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "password": "password123" + }' + +# Get available slots +curl -X GET "http://localhost:8080/api/schedules?date=2024-12-15" + +# Create booking (with auth token) +curl -X POST http://localhost:8080/api/bookings \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "schedule_id": 1, + "duration": 60, + "notes": "Initial consultation" + }' +``` + +## Support + +For API support and questions, please contact the development team or refer to the project documentation. \ No newline at end of file diff --git a/docs/API_FLOW.md b/docs/API_FLOW.md new file mode 100644 index 0000000..4d6ba15 --- /dev/null +++ b/docs/API_FLOW.md @@ -0,0 +1,652 @@ +# API Usage Flow Guide + +## Overview + +This document provides detailed workflows for different user types and common scenarios in the Attune Heart Therapy API system. + +## User Types and Flows + +### 1. Client User Flow (Regular Users) + +#### A. New User Registration & First Booking + +```mermaid +sequenceDiagram + participant C as Client + participant API as API Server + participant DB as Database + participant Stripe as Stripe + participant Jitsi as Jitsi + + C->>API: POST /api/auth/register + API->>DB: Create user record + API->>C: User created successfully + + C->>API: POST /api/auth/login + API->>DB: Validate credentials + API->>C: JWT token + user info + + C->>API: GET /api/schedules?date=2024-12-15 + API->>DB: Query available slots + API->>C: Available time slots + + C->>API: POST /api/bookings (with JWT) + API->>DB: Create booking record + API->>Jitsi: Generate room details + API->>C: Booking created + room info + + C->>API: POST /api/payments/intent (with JWT) + API->>Stripe: Create payment intent + API->>C: Payment intent + client secret + + C->>Stripe: Process payment (frontend) + Stripe->>API: POST /api/payments/webhook + API->>DB: Update booking payment status + + C->>API: POST /api/payments/confirm (with JWT) + API->>Stripe: Confirm payment + API->>DB: Update booking status + API->>C: Payment confirmed +``` + +**Step-by-step breakdown:** + +1. **Registration** (`POST /api/auth/register`) + ```json + { + "first_name": "Sarah", + "last_name": "Johnson", + "email": "sarah.johnson@email.com", + "phone": "+1234567890", + "location": "New York, NY", + "password": "securePassword123" + } + ``` + +2. **Login** (`POST /api/auth/login`) + ```json + { + "email": "sarah.johnson@email.com", + "password": "securePassword123" + } + ``` + *Store the returned JWT token for subsequent requests* + +3. **Browse Available Slots** (`GET /api/schedules?date=2024-12-15`) + *No authentication required - public endpoint* + +4. **Create Booking** (`POST /api/bookings`) + ```json + { + "schedule_id": 5, + "duration": 60, + "notes": "First therapy session - anxiety management" + } + ``` + *Requires JWT token in Authorization header* + +5. **Process Payment** (`POST /api/payments/intent`) + ```json + { + "amount": 15000, + "currency": "usd" + } + ``` + *Amount in cents ($150.00)* + +6. **Confirm Payment** (`POST /api/payments/confirm`) + ```json + { + "payment_intent_id": "pi_1234567890abcdef" + } + ``` + +#### B. Returning User Flow + +```mermaid +sequenceDiagram + participant C as Client + participant API as API Server + participant DB as Database + + C->>API: POST /api/auth/login + API->>C: JWT token + + C->>API: GET /api/bookings (with JWT) + API->>DB: Get user's bookings + API->>C: List of bookings + + C->>API: GET /api/auth/profile (with JWT) + API->>C: User profile data + + alt Update Profile + C->>API: PUT /api/auth/profile (with JWT) + API->>DB: Update user data + API->>C: Updated profile + end + + alt Cancel Booking + C->>API: PUT /api/bookings/123/cancel (with JWT) + API->>DB: Update booking status + API->>C: Booking cancelled + end + + alt Reschedule Booking + C->>API: PUT /api/bookings/123/reschedule (with JWT) + API->>DB: Update booking schedule + API->>C: Booking rescheduled + end +``` + +### 2. Admin User Flow + +#### A. Admin Dashboard & Management + +```mermaid +sequenceDiagram + participant A as Admin + participant API as API Server + participant DB as Database + + A->>API: POST /api/auth/login (admin credentials) + API->>A: JWT token (with admin privileges) + + A->>API: GET /api/admin/dashboard (with admin JWT) + API->>DB: Aggregate statistics + API->>A: Dashboard stats + + A->>API: POST /api/admin/schedules (with admin JWT) + API->>DB: Create schedule slots + API->>A: Schedule created + + A->>API: GET /api/admin/users?limit=50&offset=0 + API->>DB: Query users with pagination + API->>A: User list + + A->>API: GET /api/admin/bookings?limit=50&offset=0 + API->>DB: Query all bookings + API->>A: Booking list + + A->>API: GET /api/admin/reports/financial?start_date=2024-01-01&end_date=2024-12-31 + API->>DB: Generate financial report + API->>A: Financial data +``` + +#### B. Schedule Management Flow + +```mermaid +sequenceDiagram + participant A as Admin + participant API as API Server + participant DB as Database + + Note over A,DB: Creating Weekly Schedule + + loop For each day of the week + A->>API: POST /api/admin/schedules + Note right of A: Create morning slot (9:00-10:00) + API->>DB: Insert schedule record + + A->>API: POST /api/admin/schedules + Note right of A: Create afternoon slot (14:00-15:00) + API->>DB: Insert schedule record + + A->>API: POST /api/admin/schedules + Note right of A: Create evening slot (18:00-19:00) + API->>DB: Insert schedule record + end + + A->>API: GET /api/schedules?date=2024-12-15 + API->>A: Verify created slots are available +``` + +## Common Integration Patterns + +### 1. Frontend Application Integration + +#### React/Vue.js Example Flow + +```javascript +// 1. Authentication Service +class AuthService { + async login(email, password) { + const response = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) + }); + + const data = await response.json(); + if (data.token) { + localStorage.setItem('authToken', data.token); + localStorage.setItem('user', JSON.stringify(data.user)); + } + return data; + } + + getAuthHeader() { + const token = localStorage.getItem('authToken'); + return token ? { 'Authorization': `Bearer ${token}` } : {}; + } +} + +// 2. Booking Service +class BookingService { + constructor(authService) { + this.authService = authService; + } + + async getAvailableSlots(date) { + const response = await fetch(`/api/schedules?date=${date}`); + return response.json(); + } + + async createBooking(scheduleId, duration, notes) { + const response = await fetch('/api/bookings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.authService.getAuthHeader() + }, + body: JSON.stringify({ + schedule_id: scheduleId, + duration, + notes + }) + }); + return response.json(); + } + + async getUserBookings() { + const response = await fetch('/api/bookings', { + headers: this.authService.getAuthHeader() + }); + return response.json(); + } +} + +// 3. Payment Service +class PaymentService { + constructor(authService) { + this.authService = authService; + } + + async createPaymentIntent(amount, currency = 'usd') { + const response = await fetch('/api/payments/intent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.authService.getAuthHeader() + }, + body: JSON.stringify({ amount, currency }) + }); + return response.json(); + } + + async confirmPayment(paymentIntentId) { + const response = await fetch('/api/payments/confirm', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.authService.getAuthHeader() + }, + body: JSON.stringify({ payment_intent_id: paymentIntentId }) + }); + return response.json(); + } +} +``` + +### 2. Mobile Application Integration + +#### React Native Example + +```javascript +// API Client with automatic token management +class APIClient { + constructor() { + this.baseURL = 'http://localhost:8080'; + this.token = null; + } + + async setToken(token) { + this.token = token; + await AsyncStorage.setItem('authToken', token); + } + + async getToken() { + if (!this.token) { + this.token = await AsyncStorage.getItem('authToken'); + } + return this.token; + } + + async request(endpoint, options = {}) { + const token = await this.getToken(); + const headers = { + 'Content-Type': 'application/json', + ...(token && { 'Authorization': `Bearer ${token}` }), + ...options.headers + }; + + const response = await fetch(`${this.baseURL}${endpoint}`, { + ...options, + headers + }); + + if (response.status === 401) { + // Token expired, redirect to login + await AsyncStorage.removeItem('authToken'); + this.token = null; + // Navigate to login screen + } + + return response.json(); + } +} +``` + +## Error Handling Patterns + +### 1. Client-Side Error Handling + +```javascript +class APIErrorHandler { + static handle(error, response) { + switch (response.status) { + case 400: + return { type: 'VALIDATION_ERROR', message: error.details || error.error }; + case 401: + return { type: 'AUTH_ERROR', message: 'Please login again' }; + case 403: + return { type: 'PERMISSION_ERROR', message: 'Access denied' }; + case 409: + return { type: 'CONFLICT_ERROR', message: error.error }; + case 422: + return { type: 'PAYMENT_ERROR', message: error.error }; + case 500: + return { type: 'SERVER_ERROR', message: 'Server error, please try again' }; + default: + return { type: 'UNKNOWN_ERROR', message: 'An unexpected error occurred' }; + } + } +} + +// Usage in service methods +async createBooking(data) { + try { + const response = await fetch('/api/bookings', { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify(data) + }); + + const result = await response.json(); + + if (!response.ok) { + const error = APIErrorHandler.handle(result, response); + throw error; + } + + return result; + } catch (error) { + if (error.type) { + // Handled API error + throw error; + } else { + // Network or other error + throw { type: 'NETWORK_ERROR', message: 'Network error, please check your connection' }; + } + } +} +``` + +### 2. Retry Logic for Failed Requests + +```javascript +class RetryableAPIClient { + async requestWithRetry(endpoint, options = {}, maxRetries = 3) { + let lastError; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const response = await fetch(`${this.baseURL}${endpoint}`, options); + + if (response.ok) { + return response.json(); + } + + // Don't retry client errors (4xx) + if (response.status >= 400 && response.status < 500) { + throw await response.json(); + } + + lastError = await response.json(); + + if (attempt < maxRetries) { + // Exponential backoff + await new Promise(resolve => + setTimeout(resolve, Math.pow(2, attempt) * 1000) + ); + } + } catch (error) { + lastError = error; + + if (attempt < maxRetries && this.isRetryableError(error)) { + await new Promise(resolve => + setTimeout(resolve, Math.pow(2, attempt) * 1000) + ); + } else { + break; + } + } + } + + throw lastError; + } + + isRetryableError(error) { + return error.code === 'NETWORK_ERROR' || + error.code === 'TIMEOUT' || + (error.status >= 500); + } +} +``` + +## Testing Workflows + +### 1. Postman Collection Testing Flow + +```javascript +// Collection-level pre-request script +pm.collectionVariables.set("baseUrl", "http://localhost:8080"); + +// Test script for login request +if (pm.response.code === 200) { + const response = pm.response.json(); + pm.collectionVariables.set("authToken", response.token); + pm.collectionVariables.set("userId", response.user.id); + + pm.test("Login successful", function () { + pm.expect(response.token).to.not.be.empty; + pm.expect(response.user.email).to.not.be.empty; + }); +} + +// Test script for booking creation +if (pm.response.code === 201) { + const response = pm.response.json(); + pm.collectionVariables.set("bookingId", response.booking.id); + + pm.test("Booking created successfully", function () { + pm.expect(response.booking.id).to.be.a('number'); + pm.expect(response.booking.jitsi_room_url).to.not.be.empty; + }); +} +``` + +### 2. Automated Testing Sequence + +```bash +# Run the complete user journey +newman run "Attune Heart Therapy API.postman_collection.json" \ + -e "Local Environment.postman_environment.json" \ + --folder "Authentication" \ + --folder "Schedules & Bookings" \ + --folder "Payments" \ + --reporters cli,json \ + --reporter-json-export results.json +``` + +## Performance Considerations + +### 1. Caching Strategy + +```javascript +// Client-side caching for available slots +class CachedBookingService { + constructor() { + this.cache = new Map(); + this.cacheTimeout = 5 * 60 * 1000; // 5 minutes + } + + async getAvailableSlots(date) { + const cacheKey = `slots_${date}`; + const cached = this.cache.get(cacheKey); + + if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { + return cached.data; + } + + const data = await this.fetchAvailableSlots(date); + this.cache.set(cacheKey, { + data, + timestamp: Date.now() + }); + + return data; + } +} +``` + +### 2. Pagination Handling + +```javascript +// Efficient pagination for admin endpoints +class PaginatedDataService { + async getAllUsers(pageSize = 50) { + let allUsers = []; + let offset = 0; + let hasMore = true; + + while (hasMore) { + const response = await fetch( + `/api/admin/users?limit=${pageSize}&offset=${offset}`, + { headers: this.getAuthHeaders() } + ); + + const data = await response.json(); + allUsers = [...allUsers, ...data.users]; + + hasMore = data.users.length === pageSize; + offset += pageSize; + } + + return allUsers; + } +} +``` + +## Security Best Practices + +### 1. Token Management + +```javascript +// Secure token storage and refresh +class SecureAuthService { + constructor() { + this.tokenRefreshThreshold = 5 * 60 * 1000; // 5 minutes before expiry + } + + async getValidToken() { + const token = localStorage.getItem('authToken'); + const tokenExpiry = localStorage.getItem('tokenExpiry'); + + if (!token || !tokenExpiry) { + throw new Error('No valid token found'); + } + + const expiryTime = new Date(tokenExpiry).getTime(); + const now = Date.now(); + + if (now >= expiryTime - this.tokenRefreshThreshold) { + // Token is about to expire, refresh it + await this.refreshToken(); + return localStorage.getItem('authToken'); + } + + return token; + } + + async refreshToken() { + // Implement token refresh logic + // This would require a refresh token endpoint + } +} +``` + +### 2. Input Validation + +```javascript +// Client-side validation before API calls +class ValidationService { + static validateBookingData(data) { + const errors = []; + + if (!data.schedule_id || !Number.isInteger(data.schedule_id)) { + errors.push('Valid schedule ID is required'); + } + + if (!data.duration || data.duration < 15 || data.duration > 480) { + errors.push('Duration must be between 15 and 480 minutes'); + } + + if (data.notes && data.notes.length > 1000) { + errors.push('Notes cannot exceed 1000 characters'); + } + + return { + isValid: errors.length === 0, + errors + }; + } + + static validateUserRegistration(data) { + const errors = []; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!data.first_name || data.first_name.length < 2) { + errors.push('First name must be at least 2 characters'); + } + + if (!data.last_name || data.last_name.length < 2) { + errors.push('Last name must be at least 2 characters'); + } + + if (!data.email || !emailRegex.test(data.email)) { + errors.push('Valid email address is required'); + } + + if (!data.password || data.password.length < 8) { + errors.push('Password must be at least 8 characters'); + } + + return { + isValid: errors.length === 0, + errors + }; + } +} +``` + +This comprehensive flow guide should help developers understand how to integrate with your API effectively and handle various scenarios that may arise during implementation. \ No newline at end of file diff --git a/docs/JITSI_JWT_AUTHENTICATION.md b/docs/JITSI_JWT_AUTHENTICATION.md new file mode 100644 index 0000000..ee2dc2e --- /dev/null +++ b/docs/JITSI_JWT_AUTHENTICATION.md @@ -0,0 +1,372 @@ +# Jitsi JWT Authentication Implementation + +## Overview + +The system now uses JWT (JSON Web Token) authentication for Jitsi meetings, providing proper moderator privileges and secure meeting access. This replaces the previous URL parameter approach with industry-standard JWT tokens. + +## How It Works + +### JWT Token Structure + +Each meeting link includes a JWT token that contains: + +```json +{ + "context": { + "user": { + "name": "John Doe", + "email": "john@example.com", + "moderator": "true" // "true" for admin, "false" for regular users + } + }, + "room": "booking-123-1234567890-abc123", + "aud": "jitsi", + "iss": "your-app-id", + "sub": "your-jitsi-domain.com", + "exp": 1234567890 // 24 hour expiry +} +``` + +### Meeting URL Format + +**With JWT (Recommended):** +``` +https://meet.jit.si/booking-123-abc?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +**Fallback (No JWT configured):** +``` +https://meet.jit.si/booking-123-abc#userInfo.displayName="John Doe" +``` + +## Configuration + +### Option 1: Public Jitsi (meet.jit.si) + +For testing or using public Jitsi servers, leave JWT configuration empty: + +```env +JITSI_BASE_URL=https://meet.jit.si +JITSI_API_KEY= +JITSI_APP_ID= +``` + +**Note:** Public Jitsi doesn't enforce JWT authentication, so the system will fall back to URL parameters. Moderator privileges won't work properly on public servers. + +### Option 2: Self-Hosted Jitsi with JWT + +For production with proper moderator control, configure your self-hosted Jitsi: + +```env +JITSI_BASE_URL=https://meet.yourdomain.com +JITSI_API_KEY=your_jwt_secret_key_here +JITSI_APP_ID=your_app_id_here +``` + +## Setting Up Self-Hosted Jitsi with JWT + +### 1. Install Jitsi Meet + +Follow the official guide: https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-quickstart + +### 2. Enable JWT Authentication + +Edit `/etc/prosody/conf.avail/yourdomain.com.cfg.lua`: + +```lua +VirtualHost "yourdomain.com" + authentication = "token" + app_id = "your_app_id" + app_secret = "your_jwt_secret_key" + allow_empty_token = false +``` + +### 3. Configure Jicofo + +Edit `/etc/jitsi/jicofo/sip-communicator.properties`: + +```properties +org.jitsi.jicofo.auth.URL=XMPP:yourdomain.com +``` + +### 4. Restart Services + +```bash +systemctl restart prosody +systemctl restart jicofo +systemctl restart jitsi-videobridge2 +``` + +### 5. Update Your .env File + +```env +JITSI_BASE_URL=https://meet.yourdomain.com +JITSI_API_KEY=your_jwt_secret_key +JITSI_APP_ID=your_app_id +``` + +## API Response Examples + +### GET /api/bookings + +```json +{ + "bookings": [ + { + "id": 123, + "jitsi_room_id": "booking-123-1234567890-abc123", + "jitsi_room_url": "https://meet.jit.si/booking-123-1234567890-abc123", + "personalized_meeting_url": "https://meet.jit.si/booking-123-1234567890-abc123?jwt=eyJhbGc..." + } + ] +} +``` + +### GET /api/admin/bookings + +```json +{ + "bookings": [ + { + "id": 123, + "jitsi_room_id": "booking-123-1234567890-abc123", + "jitsi_room_url": "https://meet.jit.si/booking-123-1234567890-abc123", + "admin_meeting_url": "https://meet.jit.si/booking-123-1234567890-abc123?jwt=eyJhbGc..." + } + ] +} +``` + +### GET /api/meetings/:id/link + +```json +{ + "booking_id": 123, + "meeting_url": "https://meet.jit.si/booking-123-1234567890-abc123?jwt=eyJhbGc...", + "display_name": "John Doe", + "is_admin": false +} +``` + +## User vs Admin Differences + +### Regular User Token + +```json +{ + "context": { + "user": { + "name": "John Doe", + "email": "john@example.com", + "moderator": "false" + } + } +} +``` + +**Permissions:** +- Can join meeting +- Can share screen +- Can mute/unmute themselves +- Cannot kick participants +- Cannot end meeting for all + +### Admin User Token + +```json +{ + "context": { + "user": { + "name": "Dr. Smith", + "email": "dr.smith@example.com", + "moderator": "true" + } + } +} +``` + +**Permissions:** +- All regular user permissions +- Can kick participants +- Can mute other participants +- Can end meeting for all +- Can start/stop recording (if configured) +- Can enable/disable lobby + +## Email Notifications + +All email notifications now include JWT-authenticated links: + +### Meeting Info Email +```html +Join Meeting +``` + +### Reminder Email +```html +Join Meeting +``` + +## Security Features + +### Token Expiration +- Tokens expire after 24 hours +- Users must request a new link after expiration +- Prevents unauthorized access to old meetings + +### Moderator Control +- Only users with `is_admin: true` get moderator tokens +- Moderator status is cryptographically signed in JWT +- Cannot be tampered with by users + +### Room Isolation +- Each booking gets a unique room ID +- Room name is embedded in JWT +- Token only works for the specified room + +## Testing + +### Test JWT Generation + +```bash +# Start the server +go run cmd/server/main.go + +# Get a meeting link as regular user +curl -H "Authorization: Bearer " \ + http://localhost:8080/api/meetings/123/link + +# Get a meeting link as admin +curl -H "Authorization: Bearer " \ + http://localhost:8080/api/admin/bookings +``` + +### Verify JWT Token + +Use https://jwt.io to decode and verify your tokens: + +1. Copy the JWT from the meeting URL +2. Paste into jwt.io debugger +3. Verify the payload contains correct user info +4. Check moderator field is "true" for admins + +### Test Moderator Privileges + +1. Create two bookings +2. Join one as regular user +3. Join another as admin +4. Verify admin can: + - See moderator controls + - Kick participants + - End meeting for all + +## Troubleshooting + +### Issue: "Room locked" or "Authentication failed" + +**Cause:** JWT not configured or invalid + +**Solution:** +1. Check `JITSI_API_KEY` and `JITSI_APP_ID` are set +2. Verify they match your Jitsi server configuration +3. Ensure Jitsi server has JWT authentication enabled + +### Issue: Admin doesn't have moderator privileges + +**Cause:** JWT not being used or moderator claim not set + +**Solution:** +1. Verify `JITSI_API_KEY` is configured +2. Check admin user has `is_admin: true` in database +3. Decode JWT token and verify `moderator: "true"` + +### Issue: Token expired error + +**Cause:** JWT token older than 24 hours + +**Solution:** +1. Request a new meeting link +2. Tokens are generated fresh on each API call +3. Consider reducing expiry time if needed + +### Issue: Fallback to URL parameters + +**Cause:** JWT configuration missing + +**Solution:** +1. This is expected behavior when JWT not configured +2. For production, configure JWT authentication +3. URL parameters work but don't enforce moderator privileges + +## Migration from URL Parameters + +### Before (URL Parameters) +``` +https://meet.jit.si/room#userInfo.displayName="John"&config.startWithAudioMuted=false +``` + +**Problems:** +- No real moderator enforcement +- Anyone can modify URL parameters +- No security + +### After (JWT Authentication) +``` +https://meet.jit.si/room?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +**Benefits:** +- Cryptographically signed tokens +- True moderator enforcement +- Secure and tamper-proof +- Industry standard + +## Best Practices + +1. **Use Self-Hosted Jitsi**: For production, always use self-hosted Jitsi with JWT +2. **Rotate Secrets**: Regularly rotate your `JITSI_API_KEY` +3. **Monitor Token Usage**: Log token generation for audit purposes +4. **Set Appropriate Expiry**: 24 hours is reasonable, adjust based on needs +5. **Test Moderator Controls**: Regularly verify admin privileges work correctly + +## Advanced Configuration + +### Custom Token Expiry + +Edit `internal/services/jitsi_service.go`: + +```go +ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), // 2 hour expiry +``` + +### Additional JWT Claims + +Add custom claims to the JWT: + +```go +type JitsiClaims struct { + Context JitsiContext `json:"context"` + Room string `json:"room"` + Avatar string `json:"avatar,omitempty"` // User avatar URL + jwt.RegisteredClaims +} +``` + +### Multiple Jitsi Servers + +Support multiple Jitsi servers by environment: + +```go +func (j *jitsiService) getJitsiURL() string { + if os.Getenv("ENVIRONMENT") == "production" { + return "https://meet.production.com" + } + return "https://meet.staging.com" +} +``` + +## References + +- [Jitsi JWT Documentation](https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker#authentication) +- [JWT.io](https://jwt.io) +- [Jitsi Meet API](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-iframe) diff --git a/docs/MEETING_JOIN_GUIDE.md b/docs/MEETING_JOIN_GUIDE.md new file mode 100644 index 0000000..fb421bd --- /dev/null +++ b/docs/MEETING_JOIN_GUIDE.md @@ -0,0 +1,217 @@ +# Meeting Join Implementation Guide + +## Overview + +This guide explains how to properly implement the meeting join functionality with personalized user information and admin differentiation. + +## Issues Addressed + +1. **Name Prepopulation**: Users now join meetings with their actual name instead of "Booking ID" +2. **Admin Differentiation**: Admin users get special moderator privileges in the meeting +3. **Personalized Links**: Each user gets a personalized meeting URL with their display name + +## API Endpoint + +### GET /api/meetings/:id/link + +Returns a personalized meeting link for the authenticated user. + +**Authentication**: Required (JWT token) + +**Parameters**: +- `id` (path parameter): The booking ID + +**Response** (200 OK): +```json +{ + "booking_id": 123, + "meeting_url": "https://meet.jit.si/booking-123-1234567890-abc123#userInfo.displayName=\"John Doe\"", + "display_name": "John Doe", + "is_admin": false, + "scheduled_at": "2024-01-15T10:00:00Z", + "duration": 60, + "status": "scheduled" +} +``` + +**Error Responses**: +- `400 Bad Request`: Invalid booking ID +- `401 Unauthorized`: User not authenticated +- `403 Forbidden`: User doesn't have permission to access this meeting +- `404 Not Found`: Booking not found or meeting link not available + +## Frontend Implementation + +### Step 1: Fetch Personalized Meeting Link + +Instead of using the `jitsi_room_url` directly from the booking response, call the new endpoint: + +```javascript +// When user clicks "Join Meeting" button +async function joinMeeting(bookingId) { + try { + const response = await fetch(`/api/meetings/${bookingId}/link`, { + headers: { + 'Authorization': `Bearer ${userToken}` + } + }); + + if (!response.ok) { + throw new Error('Failed to get meeting link'); + } + + const data = await response.json(); + + // Open the personalized meeting URL + window.open(data.meeting_url, '_blank'); + + // Optionally show user info + console.log(`Joining as: ${data.display_name}`); + console.log(`Admin privileges: ${data.is_admin}`); + + } catch (error) { + console.error('Error joining meeting:', error); + alert('Failed to join meeting. Please try again.'); + } +} +``` + +### Step 2: Update Booking Display Component + +```javascript +function BookingCard({ booking }) { + const handleJoinMeeting = async () => { + try { + const response = await fetch(`/api/meetings/${booking.id}/link`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + + const data = await response.json(); + window.open(data.meeting_url, '_blank'); + } catch (error) { + console.error('Error:', error); + } + }; + + return ( +
+

Booking #{booking.id}

+

Scheduled: {new Date(booking.scheduled_at).toLocaleString()}

+

Duration: {booking.duration} minutes

+ + {booking.status === 'scheduled' && booking.jitsi_room_url && ( + + )} +
+ ); +} +``` + +## How It Works + +### For Regular Users + +1. User calls `/api/meetings/:id/link` with their JWT token +2. Backend verifies the user owns the booking +3. Backend generates a personalized Jitsi URL with the user's name: + ``` + https://meet.jit.si/room-id#userInfo.displayName="John Doe" + ``` +4. User joins the meeting with their name pre-filled + +### For Admin Users + +1. Admin calls `/api/meetings/:id/link` with their JWT token +2. Backend verifies admin status (can access any booking) +3. Backend generates a personalized Jitsi URL with admin privileges: + ``` + https://meet.jit.si/room-id#userInfo.displayName="Dr. Smith"&config.startWithAudioMuted=false&config.startWithVideoMuted=false + ``` +4. Admin joins with moderator privileges and audio/video enabled by default + +## Display Name Logic + +The system determines the display name in this order: + +1. **First Name + Last Name**: If both are available +2. **Email**: If name fields are empty +3. **Fallback**: Uses email as last resort + +## Security Considerations + +1. **Authentication Required**: All meeting link requests require valid JWT token +2. **Authorization Check**: + - Regular users can only access their own bookings + - Admin users can access any booking +3. **No Direct URL Sharing**: The personalized URL includes user-specific information +4. **Token Expiration**: JWT tokens expire, requiring re-authentication + +## Migration Notes + +### Existing Code + +If your frontend currently uses: +```javascript +// OLD WAY - Don't use this anymore +Join Meeting +``` + +### Updated Code + +Change to: +```javascript +// NEW WAY - Use the API endpoint + +``` + +## Testing + +### Test as Regular User + +1. Login as a regular user +2. Create a booking +3. Call `/api/meetings/{booking_id}/link` +4. Verify the response includes your name in the URL +5. Open the URL and confirm your name appears in Jitsi + +### Test as Admin + +1. Login as admin +2. Get any booking ID +3. Call `/api/meetings/{booking_id}/link` +4. Verify `is_admin: true` in response +5. Open the URL and confirm moderator privileges + +### Test Authorization + +1. Try to access another user's booking as a regular user +2. Should receive `403 Forbidden` error +3. Admin should be able to access any booking + +## Troubleshooting + +### Issue: Name shows as "Booking ID" +**Solution**: Ensure you're using the new `/api/meetings/:id/link` endpoint instead of the raw `jitsi_room_url` + +### Issue: Admin doesn't have moderator privileges +**Solution**: Verify the user's `is_admin` field is set to `true` in the database + +### Issue: 403 Forbidden error +**Solution**: Regular users can only access their own bookings. Verify the booking belongs to the authenticated user. + +### Issue: Meeting URL not available +**Solution**: The booking might not have a Jitsi room created yet. This can happen if payment hasn't been completed or if there was an error during booking creation. + +## Future Enhancements + +Potential improvements for the future: + +1. **Waiting Room**: Add a waiting room for participants before admin joins +2. **Recording Control**: Give admin ability to start/stop recording +3. **Participant Limits**: Enforce maximum participant limits +4. **Custom Branding**: Add custom Jitsi branding for the therapy practice +5. **Meeting Analytics**: Track join times, duration, and participant count diff --git a/docs/PERSONALIZED_MEETING_LINKS.md b/docs/PERSONALIZED_MEETING_LINKS.md new file mode 100644 index 0000000..9649c36 --- /dev/null +++ b/docs/PERSONALIZED_MEETING_LINKS.md @@ -0,0 +1,323 @@ +# Personalized Meeting Links Implementation + +## Overview + +All meeting links are now personalized with the user's name and role-specific settings. This ensures users join meetings with their actual names instead of generic identifiers, and admins get appropriate moderator privileges. + +## Changes Made + +### 1. API Endpoints with Personalized Links + +#### GET /api/bookings +Returns the authenticated user's bookings with personalized meeting links. + +**Response Example:** +```json +{ + "bookings": [ + { + "id": 123, + "user_id": 45, + "scheduled_at": "2024-01-15T10:00:00Z", + "duration": 60, + "status": "scheduled", + "jitsi_room_id": "booking-123-1234567890-abc123", + "jitsi_room_url": "https://meet.jit.si/booking-123-1234567890-abc123", + "personalized_meeting_url": "https://meet.jit.si/booking-123-1234567890-abc123#userInfo.displayName=\"John Doe\"", + "amount": 100.00, + "payment_status": "succeeded" + } + ] +} +``` + +**Key Fields:** +- `jitsi_room_url`: Original Jitsi room URL (kept for backward compatibility) +- `personalized_meeting_url`: User-specific URL with their name pre-filled + +#### GET /api/admin/bookings +Returns all bookings with admin's personalized meeting links. + +**Response Example:** +```json +{ + "bookings": [ + { + "id": 123, + "user_id": 45, + "scheduled_at": "2024-01-15T10:00:00Z", + "duration": 60, + "status": "scheduled", + "jitsi_room_id": "booking-123-1234567890-abc123", + "jitsi_room_url": "https://meet.jit.si/booking-123-1234567890-abc123", + "admin_meeting_url": "https://meet.jit.si/booking-123-1234567890-abc123#userInfo.displayName=\"Dr. Smith\"&config.startWithAudioMuted=false&config.startWithVideoMuted=false", + "amount": 100.00, + "payment_status": "succeeded" + } + ], + "total": 1, + "limit": 50, + "offset": 0 +} +``` + +**Key Fields:** +- `admin_meeting_url`: Admin-specific URL with moderator privileges + +#### GET /api/meetings/:id/link +Returns a personalized meeting link for a specific booking. + +**Response Example:** +```json +{ + "booking_id": 123, + "meeting_url": "https://meet.jit.si/booking-123-1234567890-abc123#userInfo.displayName=\"John Doe\"", + "display_name": "John Doe", + "is_admin": false, + "scheduled_at": "2024-01-15T10:00:00Z", + "duration": 60, + "status": "scheduled" +} +``` + +### 2. Email Notifications + +All email notifications now include personalized meeting links: + +#### Meeting Info Email (After Booking) +- Sent to: **User** +- Link includes: User's name +- Example: `https://meet.jit.si/room-id#userInfo.displayName="John Doe"` + +#### Reminder Email (Before Meeting) +- Sent to: **User** +- Link includes: User's name +- Example: `https://meet.jit.si/room-id#userInfo.displayName="John Doe"` + +**Note:** Admin users receive emails with admin privileges in the link if they book for themselves. + +### 3. Display Name Logic + +The system determines display names in this order: + +1. **First Name + Last Name**: `"John Doe"` +2. **Email**: If name fields are empty, uses email address +3. **Fallback**: Email as last resort + +### 4. Admin vs Regular User Differences + +#### Regular User Link +``` +https://meet.jit.si/room-id#userInfo.displayName="John Doe" +``` + +**Features:** +- Name pre-filled +- Standard participant permissions +- Default audio/video settings + +#### Admin User Link +``` +https://meet.jit.si/room-id#userInfo.displayName="Dr. Smith"&config.startWithAudioMuted=false&config.startWithVideoMuted=false +``` + +**Features:** +- Name pre-filled +- Moderator privileges +- Audio and video enabled by default +- Can manage participants +- Can start/stop recording (if configured) + +## Frontend Implementation Guide + +### Using Personalized Links from /api/bookings + +```javascript +// Fetch user's bookings +async function fetchMyBookings() { + const response = await fetch('/api/bookings', { + headers: { + 'Authorization': `Bearer ${userToken}` + } + }); + + const data = await response.json(); + + // Use personalized_meeting_url instead of jitsi_room_url + data.bookings.forEach(booking => { + if (booking.personalized_meeting_url) { + console.log(`Join link: ${booking.personalized_meeting_url}`); + } + }); +} +``` + +### Using Admin Links from /api/admin/bookings + +```javascript +// Fetch all bookings as admin +async function fetchAllBookings() { + const response = await fetch('/api/admin/bookings', { + headers: { + 'Authorization': `Bearer ${adminToken}` + } + }); + + const data = await response.json(); + + // Use admin_meeting_url for admin access + data.bookings.forEach(booking => { + if (booking.admin_meeting_url) { + console.log(`Admin join link: ${booking.admin_meeting_url}`); + } + }); +} +``` + +### React Component Example + +```jsx +function BookingCard({ booking, isAdmin }) { + const getMeetingLink = () => { + if (isAdmin && booking.admin_meeting_url) { + return booking.admin_meeting_url; + } + return booking.personalized_meeting_url || booking.jitsi_room_url; + }; + + const handleJoinMeeting = () => { + const meetingLink = getMeetingLink(); + window.open(meetingLink, '_blank'); + }; + + return ( +
+

Booking #{booking.id}

+

Scheduled: {new Date(booking.scheduled_at).toLocaleString()}

+ + {booking.status === 'scheduled' && ( + + )} +
+ ); +} +``` + +## Migration Guide + +### Before (Old Implementation) +```javascript +// Direct use of jitsi_room_url + + Join Meeting + +``` + +**Problems:** +- Users join as "Booking ID" or generic name +- No admin differentiation +- Same link for everyone + +### After (New Implementation) +```javascript +// Use personalized_meeting_url for users + + +// Use admin_meeting_url for admins + +``` + +**Benefits:** +- Users join with their actual names +- Admins get moderator privileges +- Better user experience + +## Testing Checklist + +### Test as Regular User + +1. ✅ Login as regular user +2. ✅ Create a booking +3. ✅ Call `GET /api/bookings` +4. ✅ Verify `personalized_meeting_url` contains your name +5. ✅ Open the URL and confirm name appears in Jitsi +6. ✅ Check email notification has personalized link + +### Test as Admin + +1. ✅ Login as admin +2. ✅ Call `GET /api/admin/bookings` +3. ✅ Verify `admin_meeting_url` contains admin name and moderator config +4. ✅ Open the URL and confirm moderator privileges +5. ✅ Verify audio/video enabled by default +6. ✅ Check email notification has admin link (if admin books for themselves) + +### Test Email Notifications + +1. ✅ Create a booking +2. ✅ Check "Meeting Info" email +3. ✅ Verify "Join Meeting" button uses personalized link +4. ✅ Click link and confirm name is pre-filled +5. ✅ Wait for reminder email +6. ✅ Verify reminder also has personalized link + +## Security Considerations + +### Authorization +- Regular users can only access their own bookings via `/api/bookings` +- Admins can access all bookings via `/api/admin/bookings` +- `/api/meetings/:id/link` enforces ownership or admin check + +### Link Sharing +- Personalized links include user-specific information +- Links are generated on-demand, not stored +- Original `jitsi_room_url` remains unchanged in database + +### Token Expiration +- All endpoints require valid JWT authentication +- Expired tokens require re-authentication +- Meeting links remain valid regardless of token expiration + +## Backward Compatibility + +The `jitsi_room_url` field is still present in all responses for backward compatibility. However, frontends should migrate to using: + +- `personalized_meeting_url` for regular users +- `admin_meeting_url` for admins +- `/api/meetings/:id/link` endpoint for dynamic link generation + +## Troubleshooting + +### Issue: Name still shows as "Booking ID" +**Solution:** Ensure you're using `personalized_meeting_url` or `admin_meeting_url` instead of `jitsi_room_url` + +### Issue: Admin doesn't have moderator privileges +**Solution:** +1. Verify user's `is_admin` field is `true` in database +2. Check that `admin_meeting_url` includes moderator config parameters +3. Ensure using `/api/admin/bookings` endpoint, not `/api/bookings` + +### Issue: Email links don't have personalized URLs +**Solution:** +1. Verify notification service is generating personalized URLs +2. Check email template uses `{{.JoinURL}}` field +3. Ensure user has valid first/last name or email + +### Issue: Display name is email instead of name +**Solution:** Update user's `first_name` and `last_name` fields in the database + +## Future Enhancements + +Potential improvements: + +1. **Custom Display Names**: Allow users to set custom display names +2. **Meeting Roles**: Support different roles beyond admin/user (e.g., observer, presenter) +3. **Link Expiration**: Add time-based expiration to meeting links +4. **Waiting Room**: Implement waiting room for participants before admin joins +5. **Custom Branding**: Add organization-specific branding to Jitsi interface diff --git a/docs/RESCHEDULING_GUIDE.md b/docs/RESCHEDULING_GUIDE.md new file mode 100644 index 0000000..8a8bc77 --- /dev/null +++ b/docs/RESCHEDULING_GUIDE.md @@ -0,0 +1,1030 @@ +# 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. \ No newline at end of file diff --git a/docs/SCHEDULE_MANAGEMENT.md b/docs/SCHEDULE_MANAGEMENT.md new file mode 100644 index 0000000..5731ef2 --- /dev/null +++ b/docs/SCHEDULE_MANAGEMENT.md @@ -0,0 +1,891 @@ +# 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 ( +
+
+

Schedule Management

+ +
+ + {/* Date Selector */} +
+ + setSelectedDate(new Date(e.target.value))} + className="form-control" + /> +
+ + {/* Create Schedule Form */} + {isCreating && ( +
+

Create New Schedule

+
+
+ + setNewSchedule({ + ...newSchedule, + start_time: e.target.value + })} + required + className="form-control" + /> +
+ +
+ + setNewSchedule({ + ...newSchedule, + end_time: e.target.value + })} + required + className="form-control" + /> +
+ +
+ + setNewSchedule({ + ...newSchedule, + max_bookings: e.target.value + })} + required + className="form-control" + /> +
+ +
+ + +
+
+
+ )} + + {/* Schedules List */} +
+

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

+ + {schedules.length === 0 ? ( +

No schedules found for this date.

+ ) : ( +
+ {schedules.map((schedule) => ( +
+
+ {format(new Date(schedule.start_time), 'HH:mm')} - + {format(new Date(schedule.end_time), 'HH:mm')} +
+ +
+ + {schedule.booked_count}/{schedule.max_bookings} booked + + + + {schedule.is_available ? 'Available' : 'Unavailable'} + +
+ +
+ +
+
+ ))} +
+ )} +
+
+ ); +}; + +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 ( +
+

Book Your Therapy Session

+ + {/* Date Selection */} +
+

Select Date

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

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

+ + {isLoading ? ( +
Loading available times...
+ ) : availableSlots.length === 0 ? ( +
+ No available time slots for this date. Please select another date. +
+ ) : ( +
+ {availableSlots.map((slot) => ( + + ))} +
+ )} +
+ + {/* Booking Notes */} + {selectedSlot && ( +
+

Booking Details

+
+ Selected Time: {format(selectedDate, 'MMMM dd, yyyy')} at{' '} + {format(new Date(selectedSlot.start_time), 'HH:mm')} - + {format(new Date(selectedSlot.end_time), 'HH:mm')} +
+ +
+ +