docs(api): refactor appointments endpoint documentation structure

Enhanced the API root documentation for the appointments system with improved formatting and updated description to include "flexible availability" feature. Restructured the endpoint documentation for better readability and maintainability while preserving all endpoint information including Jitsi meeting integration details.
This commit is contained in:
saani 2025-11-26 19:30:26 +00:00
parent ce3b0b77f5
commit a7d451702f
6 changed files with 1092 additions and 295 deletions

View File

@ -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'
}

View File

@ -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'

View File

@ -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 []

View File

@ -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})

View File

@ -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'),

View File

@ -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