Merge pull request 'docs(api): refactor appointments endpoint documentation structure' (#40) from feature/meetings into main
Reviewed-on: https://gitea.blackbusinesslabs.com/ATTUNE-HEART-THERAPY/alternative-backend-service/pulls/40
This commit is contained in:
commit
ab61b35913
@ -148,235 +148,349 @@ def api_root(request, format=None):
|
||||
}
|
||||
},
|
||||
"appointments": {
|
||||
"description": "Appointment request and management system with Jitsi video meetings",
|
||||
"base_path": "/api/meetings/",
|
||||
"endpoints": {
|
||||
"admin_availability": {
|
||||
"description": "Get or update admin weekly availability (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/admin/availability/"),
|
||||
"methods": ["GET", "PUT", "PATCH"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"response_fields": {
|
||||
"available_days": "List of weekday numbers (0-6) when appointments are accepted",
|
||||
"available_days_display": "Human-readable day names"
|
||||
"description": "Appointment request and management system with Jitsi video meetings and flexible availability",
|
||||
"base_path": "/api/meetings/",
|
||||
"endpoints": {
|
||||
"admin_availability": {
|
||||
"description": "Get or update admin weekly availability with day-time combinations (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/admin/availability/"),
|
||||
"methods": ["GET", "PUT", "PATCH"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"response_fields": {
|
||||
"availability_schedule": "Dictionary with days as keys and time slots as values",
|
||||
"availability_schedule_display": "Human-readable availability schedule",
|
||||
"all_available_slots": "All available day-time combinations"
|
||||
},
|
||||
"example_request": {
|
||||
"availability_schedule": {
|
||||
"0": ["morning", "evening"],
|
||||
"1": ["morning", "afternoon"],
|
||||
"3": ["afternoon", "evening"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"availability_config": {
|
||||
"description": "Get availability configuration for frontend (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/availability/config/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "None required",
|
||||
"response": "Default availability configuration with all days and time slots"
|
||||
},
|
||||
"check_date_availability": {
|
||||
"description": "Check available time slots for a specific date (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/availability/check/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "None required",
|
||||
"required_fields": ["date (YYYY-MM-DD)"],
|
||||
"example_request": {
|
||||
"date": "2024-01-15"
|
||||
},
|
||||
"response_fields": {
|
||||
"date": "The checked date",
|
||||
"day_name": "Day of the week",
|
||||
"available_slots": "List of available time slots",
|
||||
"available_slots_display": "Human-readable time slots",
|
||||
"is_available": "Boolean indicating if any slots are available"
|
||||
}
|
||||
},
|
||||
"weekly_availability": {
|
||||
"description": "Get complete weekly availability overview (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/availability/weekly/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "None required",
|
||||
"response": "Array of days with their available time slots for the entire week"
|
||||
},
|
||||
"availability_overview": {
|
||||
"description": "Get public availability overview and next available dates (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/availability/overview/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "None required",
|
||||
"response_fields": {
|
||||
"available": "Boolean indicating if admin has any availability",
|
||||
"total_available_slots": "Total number of available day-time slots",
|
||||
"available_days": "List of days with availability",
|
||||
"next_available_dates": "Next 7 days with availability information"
|
||||
}
|
||||
},
|
||||
|
||||
"available_dates": {
|
||||
"description": "Get available appointment dates with time slots for the next 30 days (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/available-dates/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "None required",
|
||||
"response": "List of available dates with their available time slots"
|
||||
},
|
||||
"create_appointment": {
|
||||
"description": "Create a new appointment request with availability validation (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/create/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (User only)",
|
||||
"required_fields": [
|
||||
"first_name", "last_name", "email",
|
||||
"selected_slots"
|
||||
],
|
||||
"optional_fields": ["phone", "reason"],
|
||||
"validation": [
|
||||
"Selected slots must match admin availability",
|
||||
"At least one time slot must be selected"
|
||||
],
|
||||
"example_request": {
|
||||
"first_name": "Shani",
|
||||
"last_name": "Iddi",
|
||||
"email": "saanii929@gmail.com",
|
||||
"phone": "+233552732025",
|
||||
"reason": "Therapy session",
|
||||
"selected_slots": [
|
||||
{"day": 1, "time_slot": "morning"},
|
||||
{"day": 1, "time_slot": "afternoon"},
|
||||
{"day": 3, "time_slot": "afternoon"},
|
||||
{"day": 3, "time_slot": "evening"},
|
||||
{"day": 4, "time_slot": "evening"}
|
||||
]
|
||||
},
|
||||
"response_includes": {
|
||||
"appointment_id": "UUID of the created appointment",
|
||||
"message": "Success message"
|
||||
}
|
||||
},
|
||||
"list_appointments": {
|
||||
"description": "List appointment requests (Admin sees all, users see their own)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"response_fields": {
|
||||
"jitsi_meet_url": "Jitsi meeting URL (only for scheduled appointments)",
|
||||
"jitsi_room_id": "Jitsi room ID",
|
||||
"has_jitsi_meeting": "Boolean indicating if meeting is created",
|
||||
"can_join_meeting": "Boolean indicating if meeting can be joined now",
|
||||
"meeting_status": "Current meeting status",
|
||||
"matching_availability": "Date-time combinations that match admin availability",
|
||||
"are_preferences_available": "Boolean indicating if preferences match availability"
|
||||
}
|
||||
},
|
||||
"appointment_detail": {
|
||||
"description": "Get detailed information about a specific appointment",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"url_parameter": "pk (UUID of the appointment)",
|
||||
"response_includes": "Jitsi meeting information and availability matching data"
|
||||
},
|
||||
"matching_availability": {
|
||||
"description": "Get matching availability for a specific appointment request",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/matching-availability/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"response_fields": {
|
||||
"appointment_id": "UUID of the appointment",
|
||||
"preferences_match_availability": "Boolean indicating if preferences match",
|
||||
"matching_slots": "List of date-time combinations that match",
|
||||
"total_matching_slots": "Number of matching combinations"
|
||||
}
|
||||
},
|
||||
"user_appointments": {
|
||||
"description": "Get appointments for the authenticated user",
|
||||
"url": request.build_absolute_uri("/api/meetings/user/appointments/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"response": "List of user's appointment requests with enhanced availability data"
|
||||
},
|
||||
"schedule_appointment": {
|
||||
"description": "Schedule an appointment and automatically create Jitsi meeting (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/schedule/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"required_fields": ["scheduled_datetime"],
|
||||
"optional_fields": ["scheduled_duration", "date_str", "time_slot"],
|
||||
"prerequisites": "Appointment must be in 'pending_review' status",
|
||||
"scheduling_options": {
|
||||
"direct_datetime": {
|
||||
"example": {"scheduled_datetime": "2024-01-15T10:00:00Z", "scheduled_duration": 60}
|
||||
},
|
||||
"date_and_slot": {
|
||||
"example": {"date_str": "2024-01-15", "time_slot": "morning", "scheduled_duration": 60}
|
||||
}
|
||||
},
|
||||
"validation": "Validates against admin availability when using date_str + time_slot",
|
||||
"side_effects": [
|
||||
"Updates status to 'scheduled'",
|
||||
"Automatically generates Jitsi meeting room",
|
||||
"Creates unique Jitsi room ID and URL",
|
||||
"Sends confirmation email to user with meeting link",
|
||||
"Clears rejection reason if any"
|
||||
],
|
||||
"response_includes": {
|
||||
"jitsi_meet_url": "Generated Jitsi meeting URL",
|
||||
"jitsi_room_id": "Unique Jitsi room ID",
|
||||
"has_jitsi_meeting": "true"
|
||||
}
|
||||
},
|
||||
"reject_appointment": {
|
||||
"description": "Reject an appointment request (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/reject/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"optional_fields": ["rejection_reason"],
|
||||
"prerequisites": "Appointment must be in 'pending_review' status",
|
||||
"example_request": {
|
||||
"rejection_reason": "No availability for preferred dates"
|
||||
},
|
||||
"side_effects": [
|
||||
"Updates status to 'rejected'",
|
||||
"Clears Jitsi meeting information",
|
||||
"Sends rejection email to user",
|
||||
"Clears scheduled datetime if any"
|
||||
]
|
||||
},
|
||||
|
||||
"appointment_stats": {
|
||||
"description": "Get appointment statistics and analytics with availability metrics (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/stats/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"response_fields": {
|
||||
"total_requests": "Total number of appointment requests",
|
||||
"pending_review": "Number of pending review requests",
|
||||
"scheduled": "Number of scheduled appointments",
|
||||
"rejected": "Number of rejected requests",
|
||||
"completed": "Number of completed appointments",
|
||||
"completion_rate": "Percentage of requests that were scheduled",
|
||||
"availability_coverage": "Percentage of week covered by availability",
|
||||
"available_days_count": "Number of days with availability set"
|
||||
}
|
||||
},
|
||||
"user_appointment_stats": {
|
||||
"description": "Get appointment statistics for a specific user",
|
||||
"url": request.build_absolute_uri("/api/meetings/user/appointments/stats/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required",
|
||||
"required_fields": ["email"],
|
||||
"response_fields": {
|
||||
"total_requests": "Total number of appointment requests",
|
||||
"pending_review": "Number of pending review requests",
|
||||
"scheduled": "Number of scheduled appointments",
|
||||
"rejected": "Number of rejected requests",
|
||||
"completed": "Number of completed appointments",
|
||||
"completion_rate": "Percentage of requests that were scheduled",
|
||||
"email": "User email address"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example_request": {
|
||||
"available_days": [0, 1, 2, 3, 4]
|
||||
}
|
||||
},
|
||||
"available_dates": {
|
||||
"description": "Get available appointment dates for the next 30 days (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/available-dates/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "None required",
|
||||
"response": "List of available dates in YYYY-MM-DD format"
|
||||
},
|
||||
"create_appointment": {
|
||||
"description": "Create a new appointment request (Public)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/create/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (User only)",
|
||||
"required_fields": [
|
||||
"first_name", "last_name", "email",
|
||||
"preferred_dates", "preferred_time_slots"
|
||||
],
|
||||
"optional_fields": ["phone", "reason"],
|
||||
"example_request": {
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"email": "john@example.com",
|
||||
"phone": "+1234567890",
|
||||
"reason": "Initial consultation for anxiety",
|
||||
"preferred_dates": ["2024-01-15", "2024-01-16"],
|
||||
"preferred_time_slots": ["morning", "afternoon"]
|
||||
|
||||
"availability_system": {
|
||||
"description": "Flexible day-time availability management",
|
||||
"features": [
|
||||
"Different time slots for each day of the week",
|
||||
"Real-time availability validation",
|
||||
"Matching preference detection",
|
||||
"Weekly availability overview"
|
||||
],
|
||||
"time_slots": {
|
||||
"morning": "Morning (9AM - 12PM)",
|
||||
"afternoon": "Afternoon (1PM - 5PM)",
|
||||
"evening": "Evening (6PM - 9PM)"
|
||||
},
|
||||
"days_of_week": {
|
||||
"0": "Monday",
|
||||
"1": "Tuesday",
|
||||
"2": "Wednesday",
|
||||
"3": "Thursday",
|
||||
"4": "Friday",
|
||||
"5": "Saturday",
|
||||
"6": "Sunday"
|
||||
}
|
||||
},
|
||||
"validation": "Preferred dates must be within admin available days"
|
||||
},
|
||||
"list_appointments": {
|
||||
"description": "List appointment requests (Admin sees all, users see their own)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"query_parameters": {
|
||||
"email": "For non-authenticated user lookup (simplified approach)"
|
||||
},
|
||||
"response_fields": {
|
||||
"jitsi_meet_url": "Jitsi meeting URL (only for scheduled appointments)",
|
||||
"jitsi_room_id": "Jitsi room ID",
|
||||
"has_jitsi_meeting": "Boolean indicating if meeting is created",
|
||||
"can_join_meeting": "Boolean indicating if meeting can be joined now",
|
||||
"meeting_status": "Current meeting status"
|
||||
|
||||
"jitsi_integration": {
|
||||
"description": "Automatic Jitsi video meeting integration",
|
||||
"features": [
|
||||
"Automatic meeting room generation when appointment is scheduled",
|
||||
"Unique room IDs for each therapy session",
|
||||
"No setup required for clients - just click and join",
|
||||
"Meeting availability based on scheduled time",
|
||||
"Secure, encrypted video sessions"
|
||||
],
|
||||
"meeting_lifecycle": {
|
||||
"pending": "No Jitsi meeting created",
|
||||
"scheduled": "Jitsi meeting automatically generated with unique URL",
|
||||
"active": "Meeting can be joined 10 minutes before scheduled time",
|
||||
"completed": "Meeting ends 15 minutes after scheduled duration"
|
||||
},
|
||||
"join_conditions": [
|
||||
"Appointment must be in 'scheduled' status",
|
||||
"Current time must be within 10 minutes before to 15 minutes after scheduled end",
|
||||
"Both client and therapist can join using the same URL"
|
||||
]
|
||||
}
|
||||
},
|
||||
"appointment_detail": {
|
||||
"description": "Get detailed information about a specific appointment",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"url_parameter": "pk (UUID of the appointment)",
|
||||
"response_includes": "Jitsi meeting information for scheduled appointments"
|
||||
},
|
||||
"user_appointments": {
|
||||
"description": "Get appointments for the authenticated user",
|
||||
"url": request.build_absolute_uri("/api/meetings/user/appointments/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"response": "List of user's appointment requests with Jitsi meeting details"
|
||||
},
|
||||
"schedule_appointment": {
|
||||
"description": "Schedule an appointment and automatically create Jitsi meeting (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/schedule/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"required_fields": ["scheduled_datetime"],
|
||||
"optional_fields": ["scheduled_duration"],
|
||||
"prerequisites": "Appointment must be in 'pending_review' status",
|
||||
"example_request": {
|
||||
"scheduled_datetime": "2024-01-15T10:00:00Z",
|
||||
"scheduled_duration": 60
|
||||
},
|
||||
"side_effects": [
|
||||
"Updates status to 'scheduled'",
|
||||
"Automatically generates Jitsi meeting room",
|
||||
"Creates unique Jitsi room ID and URL",
|
||||
"Sends confirmation email to user with meeting link",
|
||||
"Clears rejection reason if any"
|
||||
],
|
||||
"response_includes": {
|
||||
"jitsi_meet_url": "Generated Jitsi meeting URL",
|
||||
"jitsi_room_id": "Unique Jitsi room ID",
|
||||
"has_jitsi_meeting": "true"
|
||||
}
|
||||
},
|
||||
"reject_appointment": {
|
||||
"description": "Reject an appointment request (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/reject/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"optional_fields": ["rejection_reason"],
|
||||
"prerequisites": "Appointment must be in 'pending_review' status",
|
||||
"example_request": {
|
||||
"rejection_reason": "No availability for preferred dates"
|
||||
},
|
||||
"side_effects": [
|
||||
"Updates status to 'rejected'",
|
||||
"Clears Jitsi meeting information",
|
||||
"Sends rejection email to user",
|
||||
"Clears scheduled datetime if any"
|
||||
]
|
||||
},
|
||||
"jitsi_meeting_info": {
|
||||
"description": "Get Jitsi meeting information for a scheduled appointment",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/<uuid:pk>/jitsi-meeting/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"prerequisites": "Appointment must be in 'scheduled' status",
|
||||
"response_fields": {
|
||||
"meeting_url": "Jitsi meeting URL",
|
||||
"room_id": "Jitsi room ID",
|
||||
"scheduled_time": "Formatted scheduled datetime",
|
||||
"duration": "Meeting duration display",
|
||||
"can_join": "Boolean indicating if meeting can be joined now",
|
||||
"meeting_status": "Current meeting status",
|
||||
"join_instructions": "Instructions for joining the meeting"
|
||||
}
|
||||
},
|
||||
"appointment_stats": {
|
||||
"description": "Get appointment statistics and analytics (Admin only)",
|
||||
"url": request.build_absolute_uri("/api/meetings/appointments/stats/"),
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"response_fields": {
|
||||
"total_requests": "Total number of appointment requests",
|
||||
"pending_review": "Number of pending review requests",
|
||||
"scheduled": "Number of scheduled appointments",
|
||||
"rejected": "Number of rejected requests",
|
||||
"completion_rate": "Percentage of requests that were scheduled"
|
||||
}
|
||||
},
|
||||
"user_appointment_stats": {
|
||||
"description": "Get appointment statistics and analytics for the authenticated user",
|
||||
"url": request.build_absolute_uri("/api/meetings/user/appointments/stats/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required",
|
||||
"required_fields": ["email"],
|
||||
"response_fields": {
|
||||
"total_requests": "Total number of appointment requests",
|
||||
"pending_review": "Number of pending review requests",
|
||||
"scheduled": "Number of scheduled appointments",
|
||||
"rejected": "Number of rejected requests",
|
||||
"completed": "Number of completed appointments",
|
||||
"completion_rate": "Percentage of requests that were scheduled"
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
"jitsi_integration": {
|
||||
"description": "Automatic Jitsi video meeting integration",
|
||||
"features": [
|
||||
"Automatic meeting room generation when appointment is scheduled",
|
||||
"Unique room IDs for each therapy session",
|
||||
"No setup required for clients - just click and join",
|
||||
"Meeting availability based on scheduled time",
|
||||
"Secure, encrypted video sessions"
|
||||
],
|
||||
"meeting_lifecycle": {
|
||||
"pending": "No Jitsi meeting created",
|
||||
"scheduled": "Jitsi meeting automatically generated with unique URL",
|
||||
"active": "Meeting can be joined 10 minutes before scheduled time",
|
||||
"completed": "Meeting ends 15 minutes after scheduled duration"
|
||||
},
|
||||
"join_conditions": [
|
||||
"Appointment must be in 'scheduled' status",
|
||||
"Current time must be within 10 minutes before to 15 minutes after scheduled end",
|
||||
"Both client and therapist can join using the same URL"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response({
|
||||
'message': 'Therapy Appointment API',
|
||||
'version': '1.0.0',
|
||||
'message': 'Therapy Appointment API with Enhanced Availability System',
|
||||
'version': '2.0.0',
|
||||
'base_url': base_url,
|
||||
'new_features': [
|
||||
'Flexible day-time availability management',
|
||||
'Real-time availability validation',
|
||||
'Matching preference detection',
|
||||
'Enhanced scheduling options',
|
||||
'Availability statistics and coverage metrics'
|
||||
],
|
||||
'project_structure': {
|
||||
'admin': '/admin/ - Django admin interface',
|
||||
'authentication': '/api/auth/ - User authentication and management',
|
||||
'appointments': '/api/meetings/ - Appointment booking system'
|
||||
'appointments': '/api/meetings/ - Enhanced appointment booking system'
|
||||
},
|
||||
'endpoints': endpoints,
|
||||
|
||||
'appointment_workflows': {
|
||||
'client_booking_flow': [
|
||||
'1. GET /api/meetings/appointments/available-dates/ - Check available dates',
|
||||
'2. POST /api/meetings/appointments/create/ - Submit appointment request',
|
||||
'3. GET /api/meetings/user/appointments/ - Track request status',
|
||||
'4. Receive email notification when scheduled/rejected'
|
||||
'1. GET /api/meetings/availability/weekly/ - Check weekly availability',
|
||||
'2. POST /api/meetings/availability/check/ - Check specific date availability',
|
||||
'3. GET /api/meetings/appointments/available-dates/ - See next available dates',
|
||||
'4. POST /api/meetings/appointments/create/ - Submit appointment request',
|
||||
'5. GET /api/meetings/user/appointments/ - Track request status',
|
||||
'6. GET /api/meetings/appointments/{id}/matching-availability/ - See matching options',
|
||||
'7. Receive email notification when scheduled/rejected'
|
||||
],
|
||||
'admin_management_flow': [
|
||||
'1. PUT /api/meetings/admin/availability/ - Set weekly availability',
|
||||
'1. PUT /api/meetings/admin/availability/ - Set flexible day-time availability',
|
||||
'2. GET /api/meetings/appointments/ - Review pending requests',
|
||||
'3. POST /api/meetings/appointments/{id}/schedule/ - Schedule appointment OR',
|
||||
'4. POST /api/meetings/appointments/{id}/reject/ - Reject with reason',
|
||||
'5. GET /api/meetings/appointments/stats/ - Monitor performance'
|
||||
'3. GET /api/meetings/appointments/stats/ - Check availability coverage',
|
||||
'4. POST /api/meetings/appointments/{id}/schedule/ - Schedule with date+slot OR direct datetime',
|
||||
'5. POST /api/meetings/appointments/{id}/reject/ - Reject with reason'
|
||||
],
|
||||
'status_lifecycle': [
|
||||
'pending_review → scheduled (with datetime)',
|
||||
'pending_review → rejected (with optional reason)'
|
||||
'pending_review → rejected (with optional reason)',
|
||||
'scheduled → completed (after meeting)',
|
||||
'scheduled → cancelled (if needed)'
|
||||
]
|
||||
},
|
||||
|
||||
'authentication_flows': {
|
||||
'registration_flow': [
|
||||
'1. POST /api/auth/register/ - Register user and send OTP',
|
||||
'2. POST /api/auth/verify-otp/ - Verify email with OTP',
|
||||
'3. POST /api/auth/login/ - Login with credentials'
|
||||
],
|
||||
'password_reset_flow': [
|
||||
'1. POST /api/auth/forgot-password/ - Request password reset OTP',
|
||||
'2. POST /api/auth/verify-password-reset-otp/ - Verify OTP',
|
||||
'3. POST /api/auth/reset-password/ - Set new password'
|
||||
]
|
||||
'availability_examples': {
|
||||
'monday_evening_only': {
|
||||
"availability_schedule": {
|
||||
"0": ["evening"]
|
||||
}
|
||||
},
|
||||
'weekday_mornings_afternoons': {
|
||||
"availability_schedule": {
|
||||
"0": ["morning", "afternoon"],
|
||||
"1": ["morning", "afternoon"],
|
||||
"2": ["morning", "afternoon"],
|
||||
"3": ["morning", "afternoon"],
|
||||
"4": ["morning", "afternoon"]
|
||||
}
|
||||
},
|
||||
'flexible_schedule': {
|
||||
"availability_schedule": {
|
||||
"0": ["morning", "evening"],
|
||||
"1": ["afternoon"],
|
||||
"3": ["morning", "afternoon"],
|
||||
"5": ["morning"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'quick_start': {
|
||||
@ -384,14 +498,16 @@ def api_root(request, format=None):
|
||||
'1. Register: POST /api/auth/register/',
|
||||
'2. Verify email: POST /api/auth/verify-otp/',
|
||||
'3. Login: POST /api/auth/login/',
|
||||
'4. Check availability: GET /api/meetings/appointments/available-dates/',
|
||||
'5. Book appointment: POST /api/meetings/appointments/create/'
|
||||
'4. Check weekly availability: GET /api/meetings/availability/weekly/',
|
||||
'5. Check specific date: POST /api/meetings/availability/check/',
|
||||
'6. Book appointment: POST /api/meetings/appointments/create/'
|
||||
],
|
||||
'for_admins': [
|
||||
'1. Login to Django admin: /admin/',
|
||||
'2. Set availability: PUT /api/meetings/admin/availability/',
|
||||
'3. Manage appointments: GET /api/meetings/appointments/',
|
||||
'4. Schedule/Reject: Use specific appointment endpoints'
|
||||
'2. Set flexible availability: PUT /api/meetings/admin/availability/',
|
||||
'3. Check availability coverage: GET /api/meetings/appointments/stats/',
|
||||
'4. Manage appointments: GET /api/meetings/appointments/',
|
||||
'5. Schedule/Reject: Use specific appointment endpoints'
|
||||
]
|
||||
},
|
||||
|
||||
@ -400,7 +516,9 @@ def api_root(request, format=None):
|
||||
'status_choices': [
|
||||
'pending_review - Initial state, awaiting admin action',
|
||||
'scheduled - Approved with specific date/time',
|
||||
'rejected - Not accepted, with optional reason'
|
||||
'rejected - Not accepted, with optional reason',
|
||||
'completed - Meeting has been completed',
|
||||
'cancelled - Appointment was cancelled'
|
||||
],
|
||||
'time_slot_choices': [
|
||||
'morning - 9AM to 12PM',
|
||||
@ -415,7 +533,8 @@ def api_root(request, format=None):
|
||||
},
|
||||
'availability': {
|
||||
'day_format': '0=Monday, 1=Tuesday, ..., 6=Sunday',
|
||||
'example': '[0, 1, 2, 3, 4] for Monday-Friday'
|
||||
'time_slot_format': 'morning, afternoon, evening',
|
||||
'schedule_format': 'Dictionary: {"0": ["morning", "evening"], "1": ["afternoon"]}'
|
||||
}
|
||||
},
|
||||
|
||||
@ -423,7 +542,7 @@ def api_root(request, format=None):
|
||||
'token_usage': 'Include JWT token in Authorization header: Bearer <token>',
|
||||
'token_refresh': 'Use refresh token to get new access token when expired',
|
||||
'permissions': {
|
||||
'public_endpoints': 'No authentication required',
|
||||
'public_endpoints': 'No authentication required (availability checks, overview)',
|
||||
'user_endpoints': 'Valid JWT token required',
|
||||
'admin_endpoints': 'Staff user with valid JWT token required'
|
||||
}
|
||||
|
||||
@ -1,33 +1,87 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest
|
||||
|
||||
class AdminWeeklyAvailabilityForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = AdminWeeklyAvailability
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'availability_schedule': forms.HiddenInput()
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
|
||||
field_name = f'day_{day_num}'
|
||||
self.fields[field_name] = forms.MultipleChoiceField(
|
||||
choices=AdminWeeklyAvailability.TIME_SLOT_CHOICES,
|
||||
required=False,
|
||||
label=day_name,
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
if self.instance.availability_schedule:
|
||||
self.fields[field_name].initial = self.instance.availability_schedule.get(str(day_num), [])
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
|
||||
availability_schedule = {}
|
||||
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
|
||||
field_name = f'day_{day_num}'
|
||||
time_slots = self.cleaned_data.get(field_name, [])
|
||||
if time_slots:
|
||||
availability_schedule[str(day_num)] = time_slots
|
||||
|
||||
instance.availability_schedule = availability_schedule
|
||||
|
||||
if commit:
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
@admin.register(AdminWeeklyAvailability)
|
||||
class AdminWeeklyAvailabilityAdmin(admin.ModelAdmin):
|
||||
list_display = ['available_days_display', 'created_at']
|
||||
form = AdminWeeklyAvailabilityForm
|
||||
list_display = ['__str__', 'created_at', 'updated_at']
|
||||
|
||||
def available_days_display(self, obj):
|
||||
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
|
||||
return ', '.join([days_map[day] for day in obj.available_days])
|
||||
available_days_display.short_description = 'Available Days'
|
||||
def has_add_permission(self, request):
|
||||
if self.model.objects.count() >= 1:
|
||||
return False
|
||||
return super().has_add_permission(request)
|
||||
|
||||
@admin.register(AppointmentRequest)
|
||||
class AppointmentRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ['full_name', 'email', 'status', 'created_at', 'scheduled_datetime']
|
||||
list_filter = ['status', 'created_at']
|
||||
list_display = ['full_name', 'email', 'status', 'formatted_created_at', 'formatted_scheduled_datetime']
|
||||
list_filter = ['status', 'created_at', 'scheduled_datetime']
|
||||
search_fields = ['first_name', 'last_name', 'email']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['mark_as_scheduled', 'mark_as_rejected']
|
||||
readonly_fields = ['id', 'created_at', 'updated_at', 'formatted_created_at', 'formatted_scheduled_datetime']
|
||||
|
||||
def mark_as_scheduled(self, request, queryset):
|
||||
for appointment in queryset:
|
||||
if appointment.status == 'pending_review':
|
||||
appointment.status = 'scheduled'
|
||||
appointment.save()
|
||||
mark_as_scheduled.short_description = "Mark selected as scheduled"
|
||||
fieldsets = (
|
||||
('Personal Information', {
|
||||
'fields': ('first_name', 'last_name', 'email', 'phone', 'reason')
|
||||
}),
|
||||
('Appointment Preferences', {
|
||||
'fields': ('preferred_dates', 'preferred_time_slots')
|
||||
}),
|
||||
('Scheduling', {
|
||||
'fields': ('status', 'scheduled_datetime', 'scheduled_duration', 'rejection_reason')
|
||||
}),
|
||||
('Video Meeting', {
|
||||
'fields': ('jitsi_meet_url', 'jitsi_room_id')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('id', 'created_at', 'updated_at', 'formatted_created_at', 'formatted_scheduled_datetime'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def mark_as_rejected(self, request, queryset):
|
||||
for appointment in queryset:
|
||||
if appointment.status == 'pending_review':
|
||||
appointment.status = 'rejected'
|
||||
appointment.save()
|
||||
mark_as_rejected.short_description = "Mark selected as rejected"
|
||||
def formatted_created_at(self, obj):
|
||||
return obj.formatted_created_at
|
||||
formatted_created_at.short_description = 'Created At'
|
||||
|
||||
def formatted_scheduled_datetime(self, obj):
|
||||
return obj.formatted_scheduled_datetime
|
||||
formatted_scheduled_datetime.short_description = 'Scheduled Date Time'
|
||||
@ -90,9 +90,15 @@ class AdminWeeklyAvailability(models.Model):
|
||||
(6, 'Sunday'),
|
||||
]
|
||||
|
||||
available_days = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of weekdays (0-6) when appointments are accepted"
|
||||
TIME_SLOT_CHOICES = [
|
||||
('morning', 'Morning (9AM - 12PM)'),
|
||||
('afternoon', 'Afternoon (1PM - 5PM)'),
|
||||
('evening', 'Evening (6PM - 9PM)'),
|
||||
]
|
||||
|
||||
availability_schedule = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Dictionary with days as keys and lists of time slots as values"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@ -101,9 +107,59 @@ class AdminWeeklyAvailability(models.Model):
|
||||
verbose_name = 'Admin Weekly Availability'
|
||||
verbose_name_plural = 'Admin Weekly Availability'
|
||||
|
||||
def set_availability(self, day, time_slots):
|
||||
if not self.availability_schedule:
|
||||
self.availability_schedule = {}
|
||||
|
||||
if day not in [str(d[0]) for d in self.DAYS_OF_WEEK]:
|
||||
raise ValueError(f"Invalid day: {day}")
|
||||
|
||||
valid_slots = [slot[0] for slot in self.TIME_SLOT_CHOICES]
|
||||
for slot in time_slots:
|
||||
if slot not in valid_slots:
|
||||
raise ValueError(f"Invalid time slot: {slot}")
|
||||
|
||||
self.availability_schedule[str(day)] = time_slots
|
||||
|
||||
def get_availability_for_day(self, day):
|
||||
return self.availability_schedule.get(str(day), [])
|
||||
|
||||
def is_available(self, day, time_slot):
|
||||
return time_slot in self.get_availability_for_day(day)
|
||||
|
||||
def get_all_available_slots(self):
|
||||
available_slots = []
|
||||
for day_num, time_slots in self.availability_schedule.items():
|
||||
day_name = dict(self.DAYS_OF_WEEK).get(int(day_num))
|
||||
for time_slot in time_slots:
|
||||
time_display = dict(self.TIME_SLOT_CHOICES).get(time_slot)
|
||||
available_slots.append({
|
||||
'day_num': int(day_num),
|
||||
'day_name': day_name,
|
||||
'time_slot': time_slot,
|
||||
'time_display': time_display
|
||||
})
|
||||
return available_slots
|
||||
|
||||
def clear_availability(self, day=None):
|
||||
if day is None:
|
||||
self.availability_schedule = {}
|
||||
else:
|
||||
day_str = str(day)
|
||||
if day_str in self.availability_schedule:
|
||||
del self.availability_schedule[day_str]
|
||||
|
||||
def __str__(self):
|
||||
days = [self.DAYS_OF_WEEK[day][1] for day in self.available_days]
|
||||
return f"Available: {', '.join(days)}"
|
||||
if not self.availability_schedule:
|
||||
return "No availability set"
|
||||
|
||||
display_strings = []
|
||||
for day_num, time_slots in sorted(self.availability_schedule.items()):
|
||||
day_name = dict(self.DAYS_OF_WEEK).get(int(day_num))
|
||||
slot_displays = [dict(self.TIME_SLOT_CHOICES).get(slot) for slot in time_slots]
|
||||
display_strings.append(f"{day_name}: {', '.join(slot_displays)}")
|
||||
|
||||
return " | ".join(display_strings)
|
||||
|
||||
class AppointmentRequest(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
@ -295,5 +351,101 @@ class AppointmentRequest(models.Model):
|
||||
else:
|
||||
return "Ended"
|
||||
|
||||
def get_available_time_slots_for_date(self, date_str):
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if not availability:
|
||||
return []
|
||||
|
||||
return availability.get_availability_for_day(day_of_week)
|
||||
except Exception as e:
|
||||
print(f"Error getting available slots: {e}")
|
||||
return []
|
||||
|
||||
def are_preferences_available(self):
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if not availability:
|
||||
return False
|
||||
|
||||
for date_str in self.preferred_dates:
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
|
||||
available_slots = availability.get_availability_for_day(day_of_week)
|
||||
if any(slot in available_slots for slot in self.preferred_time_slots):
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error checking availability for {date_str}: {e}")
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def get_matching_availability(self):
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if not availability:
|
||||
return []
|
||||
|
||||
matching_slots = []
|
||||
for date_str in self.preferred_dates:
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
day_name = dict(AdminWeeklyAvailability.DAYS_OF_WEEK).get(day_of_week)
|
||||
|
||||
available_slots = availability.get_availability_for_day(day_of_week)
|
||||
matching_time_slots = [slot for slot in self.preferred_time_slots if slot in available_slots]
|
||||
|
||||
if matching_time_slots:
|
||||
matching_slots.append({
|
||||
'date': date_str,
|
||||
'day_name': day_name,
|
||||
'available_slots': matching_time_slots,
|
||||
'date_obj': date_obj
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error processing {date_str}: {e}")
|
||||
continue
|
||||
|
||||
return matching_slots
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.full_name} - {self.get_status_display()} - {self.created_at.strftime('%Y-%m-%d')}"
|
||||
return f"{self.full_name} - {self.get_status_display()} - {self.created_at.strftime('%Y-%m-%d')}"
|
||||
|
||||
|
||||
def get_admin_availability():
|
||||
availability, created = AdminWeeklyAvailability.objects.get_or_create(
|
||||
id=1
|
||||
)
|
||||
return availability
|
||||
|
||||
def set_admin_availability(availability_dict):
|
||||
availability = get_admin_availability()
|
||||
|
||||
for day, time_slots in availability_dict.items():
|
||||
availability.set_availability(day, time_slots)
|
||||
|
||||
availability.save()
|
||||
return availability
|
||||
|
||||
def get_available_slots_for_week():
|
||||
availability = get_admin_availability()
|
||||
return availability.get_all_available_slots()
|
||||
|
||||
def check_date_availability(date_str):
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
|
||||
availability = get_admin_availability()
|
||||
return availability.get_availability_for_day(day_of_week)
|
||||
except Exception as e:
|
||||
print(f"Error checking date availability: {e}")
|
||||
return []
|
||||
@ -1,19 +1,64 @@
|
||||
from rest_framework import serializers
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, check_date_availability
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
class AdminWeeklyAvailabilitySerializer(serializers.ModelSerializer):
|
||||
available_days_display = serializers.SerializerMethodField()
|
||||
availability_schedule_display = serializers.SerializerMethodField()
|
||||
all_available_slots = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdminWeeklyAvailability
|
||||
fields = ['id', 'available_days', 'available_days_display', 'created_at', 'updated_at']
|
||||
fields = [
|
||||
'id', 'availability_schedule', 'availability_schedule_display',
|
||||
'all_available_slots', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
def get_available_days_display(self, obj):
|
||||
def get_availability_schedule_display(self, obj):
|
||||
if not obj.availability_schedule:
|
||||
return "No availability set"
|
||||
|
||||
display = {}
|
||||
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
|
||||
return [days_map[day] for day in obj.available_days]
|
||||
time_slots_map = dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES)
|
||||
|
||||
for day_num, time_slots in obj.availability_schedule.items():
|
||||
day_name = days_map.get(int(day_num))
|
||||
slot_names = [time_slots_map.get(slot, slot) for slot in time_slots]
|
||||
display[day_name] = slot_names
|
||||
|
||||
return display
|
||||
|
||||
def get_all_available_slots(self, obj):
|
||||
return obj.get_all_available_slots()
|
||||
|
||||
class AdminWeeklyAvailabilityUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AdminWeeklyAvailability
|
||||
fields = ['availability_schedule']
|
||||
|
||||
def validate_availability_schedule(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise serializers.ValidationError("Availability schedule must be a dictionary.")
|
||||
|
||||
valid_days = [str(day[0]) for day in AdminWeeklyAvailability.DAYS_OF_WEEK]
|
||||
valid_slots = [slot[0] for slot in AdminWeeklyAvailability.TIME_SLOT_CHOICES]
|
||||
|
||||
for day, time_slots in value.items():
|
||||
if day not in valid_days:
|
||||
raise serializers.ValidationError(f"Invalid day: {day}. Must be one of {valid_days}.")
|
||||
|
||||
if not isinstance(time_slots, list):
|
||||
raise serializers.ValidationError(f"Time slots for day {day} must be a list.")
|
||||
|
||||
for slot in time_slots:
|
||||
if slot not in valid_slots:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid time slot: {slot} for day {day}. Must be one of {valid_slots}."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
class AppointmentRequestSerializer(serializers.ModelSerializer):
|
||||
full_name = serializers.ReadOnlyField()
|
||||
@ -26,6 +71,9 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
|
||||
jitsi_room_id = serializers.ReadOnlyField()
|
||||
can_join_meeting = serializers.ReadOnlyField()
|
||||
meeting_status = serializers.ReadOnlyField()
|
||||
meeting_duration_display = serializers.ReadOnlyField()
|
||||
matching_availability = serializers.SerializerMethodField()
|
||||
are_preferences_available = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AppointmentRequest
|
||||
@ -36,56 +84,223 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
|
||||
'jitsi_meet_url', 'jitsi_room_id', 'created_at', 'updated_at',
|
||||
'full_name', 'formatted_created_at', 'formatted_scheduled_datetime',
|
||||
'preferred_dates_display', 'preferred_time_slots_display',
|
||||
'has_jitsi_meeting', 'can_join_meeting', 'meeting_status'
|
||||
'has_jitsi_meeting', 'can_join_meeting', 'meeting_status',
|
||||
'meeting_duration_display', 'matching_availability', 'are_preferences_available'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'status', 'scheduled_datetime', 'scheduled_duration',
|
||||
'rejection_reason', 'jitsi_meet_url', 'jitsi_room_id',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
def get_matching_availability(self, obj):
|
||||
"""Get matching availability for this appointment request"""
|
||||
return obj.get_matching_availability()
|
||||
|
||||
def get_are_preferences_available(self, obj):
|
||||
"""Check if preferences match admin availability"""
|
||||
return obj.are_preferences_available()
|
||||
|
||||
class AppointmentRequestCreateSerializer(serializers.ModelSerializer):
|
||||
selected_slots = serializers.ListField(
|
||||
child=serializers.DictField(),
|
||||
write_only=True,
|
||||
required=False,
|
||||
help_text="List of selected day-time combinations: [{'day': 0, 'time_slot': 'morning'}]"
|
||||
)
|
||||
available_slots_info = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AppointmentRequest
|
||||
fields = [
|
||||
'first_name', 'last_name', 'email', 'phone', 'reason',
|
||||
'preferred_dates', 'preferred_time_slots'
|
||||
'preferred_dates', 'preferred_time_slots', 'selected_slots',
|
||||
'available_slots_info'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'preferred_dates': {'required': False},
|
||||
'preferred_time_slots': {'required': False}
|
||||
}
|
||||
|
||||
def validate_preferred_dates(self, value):
|
||||
if not value or len(value) == 0:
|
||||
def get_available_slots_info(self, obj):
|
||||
if not hasattr(obj, 'preferred_dates') or not obj.preferred_dates:
|
||||
return {}
|
||||
|
||||
availability_info = {}
|
||||
for date_str in obj.preferred_dates:
|
||||
available_slots = check_date_availability(date_str)
|
||||
availability_info[date_str] = {
|
||||
'available_slots': available_slots,
|
||||
'is_available': any(slot in available_slots for slot in obj.preferred_time_slots)
|
||||
}
|
||||
|
||||
return availability_info
|
||||
|
||||
def validate(self, data):
|
||||
selected_slots = data.get('selected_slots')
|
||||
preferred_dates = data.get('preferred_dates')
|
||||
preferred_time_slots = data.get('preferred_time_slots')
|
||||
|
||||
if selected_slots:
|
||||
return self._validate_selected_slots(data, selected_slots)
|
||||
elif preferred_dates and preferred_time_slots:
|
||||
return self._validate_old_format(data, preferred_dates, preferred_time_slots)
|
||||
else:
|
||||
raise serializers.ValidationError(
|
||||
"Either provide 'selected_slots' or both 'preferred_dates' and 'preferred_time_slots'."
|
||||
)
|
||||
|
||||
def _validate_selected_slots(self, data, selected_slots):
|
||||
if not selected_slots:
|
||||
raise serializers.ValidationError("At least one time slot must be selected.")
|
||||
|
||||
availability = get_admin_availability()
|
||||
if not availability:
|
||||
raise serializers.ValidationError("No admin availability set.")
|
||||
|
||||
for i, slot in enumerate(selected_slots):
|
||||
day = slot.get('day')
|
||||
time_slot = slot.get('time_slot')
|
||||
|
||||
if day is None or time_slot is None:
|
||||
raise serializers.ValidationError(f"Slot {i+1}: Must have 'day' and 'time_slot'.")
|
||||
|
||||
if not availability.is_available(day, time_slot):
|
||||
day_name = dict(AdminWeeklyAvailability.DAYS_OF_WEEK).get(day, 'Unknown day')
|
||||
raise serializers.ValidationError(
|
||||
f"Slot {i+1}: '{time_slot}' on {day_name} is not available."
|
||||
)
|
||||
|
||||
data['preferred_dates'] = self._convert_slots_to_dates(selected_slots)
|
||||
data['preferred_time_slots'] = self._extract_time_slots(selected_slots)
|
||||
|
||||
del data['selected_slots']
|
||||
|
||||
return data
|
||||
|
||||
def _validate_old_format(self, data, preferred_dates, preferred_time_slots):
|
||||
if not preferred_dates or len(preferred_dates) == 0:
|
||||
raise serializers.ValidationError("At least one preferred date is required.")
|
||||
|
||||
today = timezone.now().date()
|
||||
for date_str in value:
|
||||
for date_str in preferred_dates:
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
if date_obj < today:
|
||||
raise serializers.ValidationError("Preferred dates cannot be in the past.")
|
||||
|
||||
available_slots = check_date_availability(date_str)
|
||||
if not available_slots:
|
||||
raise serializers.ValidationError(
|
||||
f"No admin availability on {date_obj.strftime('%A, %B %d, %Y')}"
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
raise serializers.ValidationError(f"Invalid date format: {date_str}. Use YYYY-MM-DD.")
|
||||
|
||||
return value
|
||||
|
||||
def validate_preferred_time_slots(self, value):
|
||||
if not value or len(value) == 0:
|
||||
if not preferred_time_slots or len(preferred_time_slots) == 0:
|
||||
raise serializers.ValidationError("At least one time slot is required.")
|
||||
|
||||
valid_slots = ['morning', 'afternoon', 'evening']
|
||||
for slot in value:
|
||||
for slot in preferred_time_slots:
|
||||
if slot not in valid_slots:
|
||||
raise serializers.ValidationError(f"Invalid time slot: {slot}. Must be one of {valid_slots}.")
|
||||
|
||||
return value
|
||||
|
||||
has_available_slot = False
|
||||
for date_str in preferred_dates:
|
||||
available_slots = check_date_availability(date_str)
|
||||
if any(slot in available_slots for slot in preferred_time_slots):
|
||||
has_available_slot = True
|
||||
break
|
||||
|
||||
if not has_available_slot:
|
||||
raise serializers.ValidationError(
|
||||
"None of your preferred date and time combinations match the admin's availability. "
|
||||
"Please check the admin's schedule and adjust your preferences."
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def _convert_slots_to_dates(self, selected_slots):
|
||||
from datetime import timedelta
|
||||
|
||||
today = timezone.now().date()
|
||||
preferred_dates = []
|
||||
|
||||
for slot in selected_slots:
|
||||
target_weekday = slot['day']
|
||||
|
||||
found_date = None
|
||||
for days_ahead in range(1, 15):
|
||||
check_date = today + timedelta(days=days_ahead)
|
||||
if check_date.weekday() == target_weekday:
|
||||
found_date = check_date
|
||||
break
|
||||
|
||||
if found_date:
|
||||
date_str = found_date.strftime('%Y-%m-%d')
|
||||
if date_str not in preferred_dates:
|
||||
preferred_dates.append(date_str)
|
||||
|
||||
return preferred_dates
|
||||
|
||||
def _extract_time_slots(self, selected_slots):
|
||||
time_slots = []
|
||||
for slot in selected_slots:
|
||||
if slot['time_slot'] not in time_slots:
|
||||
time_slots.append(slot['time_slot'])
|
||||
return time_slots
|
||||
|
||||
def create(self, validated_data):
|
||||
return super().create(validated_data)
|
||||
|
||||
class AppointmentScheduleSerializer(serializers.Serializer):
|
||||
scheduled_datetime = serializers.DateTimeField()
|
||||
scheduled_duration = serializers.IntegerField(default=60, min_value=30, max_value=240)
|
||||
date_str = serializers.CharField(required=False, write_only=True)
|
||||
time_slot = serializers.CharField(required=False, write_only=True)
|
||||
|
||||
def validate_scheduled_datetime(self, value):
|
||||
if value <= timezone.now():
|
||||
def validate(self, data):
|
||||
scheduled_datetime = data.get('scheduled_datetime')
|
||||
date_str = data.get('date_str')
|
||||
time_slot = data.get('time_slot')
|
||||
|
||||
if date_str and time_slot:
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
|
||||
availability = get_admin_availability()
|
||||
if not availability.is_available(day_of_week, time_slot):
|
||||
raise serializers.ValidationError(
|
||||
f"The admin is not available on {date_obj.strftime('%A')} during the {time_slot} time slot."
|
||||
)
|
||||
datetime_obj = self._convert_to_datetime(date_obj, time_slot)
|
||||
data['scheduled_datetime'] = datetime_obj
|
||||
|
||||
except ValueError as e:
|
||||
raise serializers.ValidationError(f"Invalid date format: {e}")
|
||||
|
||||
if scheduled_datetime and scheduled_datetime <= timezone.now():
|
||||
raise serializers.ValidationError("Scheduled datetime must be in the future.")
|
||||
return value
|
||||
|
||||
return data
|
||||
|
||||
def _convert_to_datetime(self, date_obj, time_slot):
|
||||
"""Convert date and time slot to actual datetime"""
|
||||
time_mapping = {
|
||||
'morning': (9, 0),
|
||||
'afternoon': (13, 0),
|
||||
'evening': (18, 0)
|
||||
}
|
||||
|
||||
if time_slot not in time_mapping:
|
||||
raise serializers.ValidationError(f"Invalid time slot: {time_slot}")
|
||||
|
||||
hour, minute = time_mapping[time_slot]
|
||||
return timezone.make_aware(
|
||||
datetime.combine(date_obj, datetime.min.time().replace(hour=hour, minute=minute))
|
||||
)
|
||||
|
||||
def validate_scheduled_duration(self, value):
|
||||
if value < 30:
|
||||
@ -95,4 +310,71 @@ class AppointmentScheduleSerializer(serializers.Serializer):
|
||||
return value
|
||||
|
||||
class AppointmentRejectSerializer(serializers.Serializer):
|
||||
rejection_reason = serializers.CharField(required=False, allow_blank=True)
|
||||
rejection_reason = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
class AvailabilityCheckSerializer(serializers.Serializer):
|
||||
date = serializers.CharField(required=True)
|
||||
|
||||
def validate_date(self, value):
|
||||
try:
|
||||
date_obj = datetime.strptime(value, '%Y-%m-%d').date()
|
||||
if date_obj < timezone.now().date():
|
||||
raise serializers.ValidationError("Date cannot be in the past.")
|
||||
return value
|
||||
except ValueError:
|
||||
raise serializers.ValidationError("Invalid date format. Use YYYY-MM-DD.")
|
||||
|
||||
class AvailabilityResponseSerializer(serializers.Serializer):
|
||||
date = serializers.CharField()
|
||||
day_name = serializers.CharField()
|
||||
available_slots = serializers.ListField(child=serializers.CharField())
|
||||
available_slots_display = serializers.ListField(child=serializers.CharField())
|
||||
is_available = serializers.BooleanField()
|
||||
|
||||
class WeeklyAvailabilitySerializer(serializers.Serializer):
|
||||
day_number = serializers.IntegerField()
|
||||
day_name = serializers.CharField()
|
||||
available_slots = serializers.ListField(child=serializers.CharField())
|
||||
available_slots_display = serializers.ListField(child=serializers.CharField())
|
||||
|
||||
class TimeSlotSerializer(serializers.Serializer):
|
||||
value = serializers.CharField()
|
||||
display = serializers.CharField()
|
||||
disabled = serializers.BooleanField(default=False)
|
||||
|
||||
class DayAvailabilitySerializer(serializers.Serializer):
|
||||
day_number = serializers.IntegerField()
|
||||
day_name = serializers.CharField()
|
||||
time_slots = TimeSlotSerializer(many=True)
|
||||
is_available = serializers.BooleanField()
|
||||
|
||||
class AdminAvailabilityConfigSerializer(serializers.Serializer):
|
||||
days = DayAvailabilitySerializer(many=True)
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls):
|
||||
days_config = []
|
||||
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
|
||||
days_config.append({
|
||||
'day_number': day_num,
|
||||
'day_name': day_name,
|
||||
'time_slots': [
|
||||
{
|
||||
'value': 'morning',
|
||||
'display': 'Morning (9AM - 12PM)',
|
||||
'disabled': False
|
||||
},
|
||||
{
|
||||
'value': 'afternoon',
|
||||
'display': 'Afternoon (1PM - 5PM)',
|
||||
'disabled': False
|
||||
},
|
||||
{
|
||||
'value': 'evening',
|
||||
'display': 'Evening (6PM - 9PM)',
|
||||
'disabled': False
|
||||
}
|
||||
],
|
||||
'is_available': False
|
||||
})
|
||||
return cls({'days': days_config})
|
||||
@ -1,28 +1,39 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
AdminAvailabilityView,
|
||||
AdminAvailabilityConfigView,
|
||||
AppointmentRequestListView,
|
||||
AppointmentRequestCreateView,
|
||||
AppointmentRequestDetailView,
|
||||
ScheduleAppointmentView,
|
||||
RejectAppointmentView,
|
||||
AvailableDatesView,
|
||||
CheckDateAvailabilityView,
|
||||
WeeklyAvailabilityView,
|
||||
UserAppointmentsView,
|
||||
AppointmentStatsView,
|
||||
UserAppointmentStatsView
|
||||
UserAppointmentStatsView,
|
||||
MatchingAvailabilityView,
|
||||
availability_overview
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/availability/', AdminAvailabilityView.as_view(), name='admin-availability'),
|
||||
path('availability/config/', AdminAvailabilityConfigView.as_view(), name='availability-config'),
|
||||
path('availability/check/', CheckDateAvailabilityView.as_view(), name='check-availability'),
|
||||
path('availability/weekly/', WeeklyAvailabilityView.as_view(), name='weekly-availability'),
|
||||
path('availability/overview/', availability_overview, name='availability-overview'),
|
||||
|
||||
path('appointments/', AppointmentRequestListView.as_view(), name='appointment-list'),
|
||||
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
|
||||
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
|
||||
path('appointments/<uuid:pk>/matching-availability/', MatchingAvailabilityView.as_view(), name='matching-availability'),
|
||||
|
||||
path('appointments/<uuid:pk>/schedule/', ScheduleAppointmentView.as_view(), name='appointment-schedule'),
|
||||
path('appointments/<uuid:pk>/reject/', RejectAppointmentView.as_view(), name='appointment-reject'),
|
||||
|
||||
path('appointments/available-dates/', AvailableDatesView.as_view(), name='available-dates'),
|
||||
|
||||
path('user/appointments/', UserAppointmentsView.as_view(), name='user-appointments'),
|
||||
|
||||
path('appointments/stats/', AppointmentStatsView.as_view(), name='appointment-stats'),
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny,IsAdminUser
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, set_admin_availability, get_available_slots_for_week, check_date_availability
|
||||
from .serializers import (
|
||||
AdminWeeklyAvailabilitySerializer,
|
||||
AdminWeeklyAvailabilityUpdateSerializer,
|
||||
AppointmentRequestSerializer,
|
||||
AppointmentRequestCreateSerializer,
|
||||
AppointmentScheduleSerializer,
|
||||
AppointmentRejectSerializer
|
||||
AppointmentRejectSerializer,
|
||||
AvailabilityCheckSerializer,
|
||||
AvailabilityResponseSerializer,
|
||||
WeeklyAvailabilitySerializer,
|
||||
AdminAvailabilityConfigSerializer
|
||||
)
|
||||
from .email_service import EmailService
|
||||
from users.models import CustomUser
|
||||
@ -19,13 +24,30 @@ from django.db.models import Count, Q
|
||||
|
||||
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
|
||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||
serializer_class = AdminWeeklyAvailabilitySerializer
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return AdminWeeklyAvailabilitySerializer
|
||||
return AdminWeeklyAvailabilityUpdateSerializer
|
||||
|
||||
def get_object(self):
|
||||
obj, created = AdminWeeklyAvailability.objects.get_or_create(
|
||||
defaults={'available_days': []}
|
||||
)
|
||||
return obj
|
||||
return get_admin_availability()
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
response = super().update(request, *args, **kwargs)
|
||||
availability = self.get_object()
|
||||
full_serializer = AdminWeeklyAvailabilitySerializer(availability)
|
||||
return Response(full_serializer.data)
|
||||
|
||||
|
||||
class AdminAvailabilityConfigView(generics.GenericAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
"""Get availability configuration"""
|
||||
config = AdminAvailabilityConfigSerializer.get_default_config()
|
||||
return Response(config.data)
|
||||
|
||||
|
||||
class AppointmentRequestListView(generics.ListAPIView):
|
||||
serializer_class = AppointmentRequestSerializer
|
||||
@ -39,25 +61,20 @@ class AppointmentRequestListView(generics.ListAPIView):
|
||||
|
||||
return queryset.filter(email=self.request.user.email)
|
||||
|
||||
|
||||
class AppointmentRequestCreateView(generics.CreateAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
queryset = AppointmentRequest.objects.all()
|
||||
serializer_class = AppointmentRequestCreateSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if availability:
|
||||
available_days = availability.available_days
|
||||
preferred_dates = serializer.validated_data['preferred_dates']
|
||||
|
||||
for date_str in preferred_dates:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
if date_obj.weekday() not in available_days:
|
||||
from rest_framework.exceptions import ValidationError
|
||||
raise ValidationError(f'Date {date_str} is not available for appointments.')
|
||||
|
||||
appointment = serializer.save()
|
||||
EmailService.send_admin_notification(appointment)
|
||||
|
||||
if appointment.are_preferences_available():
|
||||
EmailService.send_admin_notification(appointment)
|
||||
else:
|
||||
EmailService.send_admin_notification(appointment, availability_mismatch=True)
|
||||
|
||||
|
||||
class AppointmentRequestDetailView(generics.RetrieveAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
@ -65,6 +82,7 @@ class AppointmentRequestDetailView(generics.RetrieveAPIView):
|
||||
serializer_class = AppointmentRequestSerializer
|
||||
lookup_field = 'pk'
|
||||
|
||||
|
||||
class ScheduleAppointmentView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||
serializer_class = AppointmentScheduleSerializer
|
||||
@ -123,27 +141,98 @@ class RejectAppointmentView(generics.GenericAPIView):
|
||||
|
||||
response_serializer = AppointmentRequestSerializer(appointment)
|
||||
return Response(response_serializer.data)
|
||||
|
||||
|
||||
|
||||
class AvailableDatesView(generics.GenericAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if not availability:
|
||||
availability = get_admin_availability()
|
||||
if not availability or not availability.availability_schedule:
|
||||
return Response([])
|
||||
|
||||
available_days = availability.available_days
|
||||
today = timezone.now().date()
|
||||
available_dates = []
|
||||
|
||||
for i in range(1, 31):
|
||||
for i in range(1, 31):
|
||||
date = today + timedelta(days=i)
|
||||
if date.weekday() in available_days:
|
||||
available_dates.append(date.strftime('%Y-%m-%d'))
|
||||
day_of_week = date.weekday()
|
||||
|
||||
available_slots = availability.get_availability_for_day(day_of_week)
|
||||
if available_slots:
|
||||
available_dates.append({
|
||||
'date': date.strftime('%Y-%m-%d'),
|
||||
'day_name': date.strftime('%A'),
|
||||
'available_slots': available_slots,
|
||||
'available_slots_display': [
|
||||
dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES).get(slot, slot)
|
||||
for slot in available_slots
|
||||
]
|
||||
})
|
||||
|
||||
return Response(available_dates)
|
||||
|
||||
|
||||
class CheckDateAvailabilityView(generics.GenericAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
serializer_class = AvailabilityCheckSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
date_str = serializer.validated_data['date']
|
||||
available_slots = check_date_availability(date_str)
|
||||
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_name = date_obj.strftime('%A')
|
||||
|
||||
response_data = {
|
||||
'date': date_str,
|
||||
'day_name': day_name,
|
||||
'available_slots': available_slots,
|
||||
'available_slots_display': [
|
||||
dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES).get(slot, slot)
|
||||
for slot in available_slots
|
||||
],
|
||||
'is_available': len(available_slots) > 0
|
||||
}
|
||||
|
||||
return Response(response_data)
|
||||
|
||||
except ValueError:
|
||||
return Response(
|
||||
{'error': 'Invalid date format'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class WeeklyAvailabilityView(generics.GenericAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
availability = get_admin_availability()
|
||||
if not availability:
|
||||
return Response([])
|
||||
|
||||
weekly_availability = []
|
||||
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
|
||||
available_slots = availability.get_availability_for_day(day_num)
|
||||
weekly_availability.append({
|
||||
'day_number': day_num,
|
||||
'day_name': day_name,
|
||||
'available_slots': available_slots,
|
||||
'available_slots_display': [
|
||||
dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES).get(slot, slot)
|
||||
for slot in available_slots
|
||||
],
|
||||
'is_available': len(available_slots) > 0
|
||||
})
|
||||
|
||||
return Response(weekly_availability)
|
||||
|
||||
|
||||
class UserAppointmentsView(generics.ListAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = AppointmentRequestSerializer
|
||||
@ -152,7 +241,7 @@ class UserAppointmentsView(generics.ListAPIView):
|
||||
return AppointmentRequest.objects.filter(
|
||||
email=self.request.user.email
|
||||
).order_by('-created_at')
|
||||
|
||||
|
||||
|
||||
class AppointmentStatsView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||
@ -163,21 +252,39 @@ class AppointmentStatsView(generics.GenericAPIView):
|
||||
pending = AppointmentRequest.objects.filter(status='pending_review').count()
|
||||
scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
|
||||
rejected = AppointmentRequest.objects.filter(status='rejected').count()
|
||||
completed = AppointmentRequest.objects.filter(status='completed').count()
|
||||
|
||||
availability = get_admin_availability()
|
||||
availability_coverage = 0
|
||||
if availability and availability.availability_schedule:
|
||||
days_with_availability = len(availability.availability_schedule)
|
||||
availability_coverage = round((days_with_availability / 7) * 100, 2)
|
||||
|
||||
return Response({
|
||||
'total_requests': total,
|
||||
'pending_review': pending,
|
||||
'scheduled': scheduled,
|
||||
'rejected': rejected,
|
||||
'completed': completed,
|
||||
'users': users,
|
||||
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0
|
||||
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0,
|
||||
'availability_coverage': availability_coverage,
|
||||
'available_days_count': days_with_availability if availability else 0
|
||||
})
|
||||
|
||||
|
||||
|
||||
class UserAppointmentStatsView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get('email')
|
||||
email = request.data.get('email', self.request.user.email)
|
||||
|
||||
if not self.request.user.is_staff and email != self.request.user.email:
|
||||
return Response(
|
||||
{'error': 'You can only view your own statistics'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
stats = AppointmentRequest.objects.filter(
|
||||
email=email
|
||||
).aggregate(
|
||||
@ -198,5 +305,77 @@ class UserAppointmentStatsView(generics.GenericAPIView):
|
||||
'scheduled': scheduled,
|
||||
'rejected': stats['rejected'],
|
||||
'completed': stats['completed'],
|
||||
'completion_rate': completion_rate
|
||||
})
|
||||
'completion_rate': completion_rate,
|
||||
'email': email
|
||||
})
|
||||
|
||||
|
||||
class MatchingAvailabilityView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
try:
|
||||
appointment = AppointmentRequest.objects.get(pk=pk)
|
||||
|
||||
if not request.user.is_staff and appointment.email != request.user.email:
|
||||
return Response(
|
||||
{'error': 'You can only view your own appointments'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
matching_slots = appointment.get_matching_availability()
|
||||
return Response({
|
||||
'appointment_id': str(appointment.id),
|
||||
'preferences_match_availability': appointment.are_preferences_available(),
|
||||
'matching_slots': matching_slots,
|
||||
'total_matching_slots': len(matching_slots)
|
||||
})
|
||||
|
||||
except AppointmentRequest.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'Appointment not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def availability_overview(request):
|
||||
availability = get_admin_availability()
|
||||
if not availability:
|
||||
return Response({
|
||||
'available': False,
|
||||
'message': 'No availability set'
|
||||
})
|
||||
|
||||
all_slots = availability.get_all_available_slots()
|
||||
|
||||
return Response({
|
||||
'available': len(all_slots) > 0,
|
||||
'total_available_slots': len(all_slots),
|
||||
'available_days': list(set(slot['day_name'] for slot in all_slots)),
|
||||
'next_available_dates': get_next_available_dates(7)
|
||||
})
|
||||
|
||||
|
||||
def get_next_available_dates(days_count=7):
|
||||
availability = get_admin_availability()
|
||||
if not availability:
|
||||
return []
|
||||
|
||||
today = timezone.now().date()
|
||||
next_dates = []
|
||||
|
||||
for i in range(1, days_count + 1):
|
||||
date = today + timedelta(days=i)
|
||||
day_of_week = date.weekday()
|
||||
available_slots = availability.get_availability_for_day(day_of_week)
|
||||
|
||||
if available_slots:
|
||||
next_dates.append({
|
||||
'date': date.strftime('%Y-%m-%d'),
|
||||
'day_name': date.strftime('%A'),
|
||||
'available_slots': available_slots
|
||||
})
|
||||
|
||||
return next_dates
|
||||
Loading…
Reference in New Issue
Block a user