docs(api): refactor appointments endpoint documentation structure #40

Merged
Saani merged 1 commits from feature/meetings into main 2025-11-26 19:41:57 +00:00
6 changed files with 1092 additions and 295 deletions
Showing only changes of commit a7d451702f - Show all commits

View File

@ -148,64 +148,124 @@ def api_root(request, format=None):
} }
}, },
"appointments": { "appointments": {
"description": "Appointment request and management system with Jitsi video meetings", "description": "Appointment request and management system with Jitsi video meetings and flexible availability",
"base_path": "/api/meetings/", "base_path": "/api/meetings/",
"endpoints": { "endpoints": {
"admin_availability": { "admin_availability": {
"description": "Get or update admin weekly availability (Admin only)", "description": "Get or update admin weekly availability with day-time combinations (Admin only)",
"url": request.build_absolute_uri("/api/meetings/admin/availability/"), "url": request.build_absolute_uri("/api/meetings/admin/availability/"),
"methods": ["GET", "PUT", "PATCH"], "methods": ["GET", "PUT", "PATCH"],
"authentication": "Required (Staff users only)", "authentication": "Required (Staff users only)",
"response_fields": { "response_fields": {
"available_days": "List of weekday numbers (0-6) when appointments are accepted", "availability_schedule": "Dictionary with days as keys and time slots as values",
"available_days_display": "Human-readable day names" "availability_schedule_display": "Human-readable availability schedule",
"all_available_slots": "All available day-time combinations"
}, },
"example_request": { "example_request": {
"available_days": [0, 1, 2, 3, 4] "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": { "available_dates": {
"description": "Get available appointment dates for the next 30 days (Public)", "description": "Get available appointment dates with time slots for the next 30 days (Public)",
"url": request.build_absolute_uri("/api/meetings/appointments/available-dates/"), "url": request.build_absolute_uri("/api/meetings/appointments/available-dates/"),
"methods": ["GET"], "methods": ["GET"],
"authentication": "None required", "authentication": "None required",
"response": "List of available dates in YYYY-MM-DD format" "response": "List of available dates with their available time slots"
}, },
"create_appointment": { "create_appointment": {
"description": "Create a new appointment request (Public)", "description": "Create a new appointment request with availability validation (Public)",
"url": request.build_absolute_uri("/api/meetings/appointments/create/"), "url": request.build_absolute_uri("/api/meetings/appointments/create/"),
"methods": ["POST"], "methods": ["POST"],
"authentication": "Required (User only)", "authentication": "Required (User only)",
"required_fields": [ "required_fields": [
"first_name", "last_name", "email", "first_name", "last_name", "email",
"preferred_dates", "preferred_time_slots" "selected_slots"
], ],
"optional_fields": ["phone", "reason"], "optional_fields": ["phone", "reason"],
"validation": [
"Selected slots must match admin availability",
"At least one time slot must be selected"
],
"example_request": { "example_request": {
"first_name": "John", "first_name": "Shani",
"last_name": "Doe", "last_name": "Iddi",
"email": "john@example.com", "email": "saanii929@gmail.com",
"phone": "+1234567890", "phone": "+233552732025",
"reason": "Initial consultation for anxiety", "reason": "Therapy session",
"preferred_dates": ["2024-01-15", "2024-01-16"], "selected_slots": [
"preferred_time_slots": ["morning", "afternoon"] {"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"}
]
}, },
"validation": "Preferred dates must be within admin available days" "response_includes": {
"appointment_id": "UUID of the created appointment",
"message": "Success message"
}
}, },
"list_appointments": { "list_appointments": {
"description": "List appointment requests (Admin sees all, users see their own)", "description": "List appointment requests (Admin sees all, users see their own)",
"url": request.build_absolute_uri("/api/meetings/appointments/"), "url": request.build_absolute_uri("/api/meetings/appointments/"),
"methods": ["GET"], "methods": ["GET"],
"authentication": "Required", "authentication": "Required",
"query_parameters": {
"email": "For non-authenticated user lookup (simplified approach)"
},
"response_fields": { "response_fields": {
"jitsi_meet_url": "Jitsi meeting URL (only for scheduled appointments)", "jitsi_meet_url": "Jitsi meeting URL (only for scheduled appointments)",
"jitsi_room_id": "Jitsi room ID", "jitsi_room_id": "Jitsi room ID",
"has_jitsi_meeting": "Boolean indicating if meeting is created", "has_jitsi_meeting": "Boolean indicating if meeting is created",
"can_join_meeting": "Boolean indicating if meeting can be joined now", "can_join_meeting": "Boolean indicating if meeting can be joined now",
"meeting_status": "Current meeting status" "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": { "appointment_detail": {
@ -214,14 +274,26 @@ def api_root(request, format=None):
"methods": ["GET"], "methods": ["GET"],
"authentication": "Required", "authentication": "Required",
"url_parameter": "pk (UUID of the appointment)", "url_parameter": "pk (UUID of the appointment)",
"response_includes": "Jitsi meeting information for scheduled appointments" "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": { "user_appointments": {
"description": "Get appointments for the authenticated user", "description": "Get appointments for the authenticated user",
"url": request.build_absolute_uri("/api/meetings/user/appointments/"), "url": request.build_absolute_uri("/api/meetings/user/appointments/"),
"methods": ["GET"], "methods": ["GET"],
"authentication": "Required", "authentication": "Required",
"response": "List of user's appointment requests with Jitsi meeting details" "response": "List of user's appointment requests with enhanced availability data"
}, },
"schedule_appointment": { "schedule_appointment": {
"description": "Schedule an appointment and automatically create Jitsi meeting (Admin only)", "description": "Schedule an appointment and automatically create Jitsi meeting (Admin only)",
@ -229,12 +301,17 @@ def api_root(request, format=None):
"methods": ["POST"], "methods": ["POST"],
"authentication": "Required (Staff users only)", "authentication": "Required (Staff users only)",
"required_fields": ["scheduled_datetime"], "required_fields": ["scheduled_datetime"],
"optional_fields": ["scheduled_duration"], "optional_fields": ["scheduled_duration", "date_str", "time_slot"],
"prerequisites": "Appointment must be in 'pending_review' status", "prerequisites": "Appointment must be in 'pending_review' status",
"example_request": { "scheduling_options": {
"scheduled_datetime": "2024-01-15T10:00:00Z", "direct_datetime": {
"scheduled_duration": 60 "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": [ "side_effects": [
"Updates status to 'scheduled'", "Updates status to 'scheduled'",
"Automatically generates Jitsi meeting room", "Automatically generates Jitsi meeting room",
@ -265,24 +342,9 @@ def api_root(request, format=None):
"Clears scheduled datetime if any" "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": { "appointment_stats": {
"description": "Get appointment statistics and analytics (Admin only)", "description": "Get appointment statistics and analytics with availability metrics (Admin only)",
"url": request.build_absolute_uri("/api/meetings/appointments/stats/"), "url": request.build_absolute_uri("/api/meetings/appointments/stats/"),
"methods": ["GET"], "methods": ["GET"],
"authentication": "Required (Staff users only)", "authentication": "Required (Staff users only)",
@ -291,11 +353,14 @@ def api_root(request, format=None):
"pending_review": "Number of pending review requests", "pending_review": "Number of pending review requests",
"scheduled": "Number of scheduled appointments", "scheduled": "Number of scheduled appointments",
"rejected": "Number of rejected requests", "rejected": "Number of rejected requests",
"completion_rate": "Percentage of requests that were scheduled" "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": { "user_appointment_stats": {
"description": "Get appointment statistics and analytics for the authenticated user", "description": "Get appointment statistics for a specific user",
"url": request.build_absolute_uri("/api/meetings/user/appointments/stats/"), "url": request.build_absolute_uri("/api/meetings/user/appointments/stats/"),
"methods": ["POST"], "methods": ["POST"],
"authentication": "Required", "authentication": "Required",
@ -306,11 +371,36 @@ def api_root(request, format=None):
"scheduled": "Number of scheduled appointments", "scheduled": "Number of scheduled appointments",
"rejected": "Number of rejected requests", "rejected": "Number of rejected requests",
"completed": "Number of completed appointments", "completed": "Number of completed appointments",
"completion_rate": "Percentage of requests that were scheduled" "completion_rate": "Percentage of requests that were scheduled",
"email": "User email address"
}
} }
}, },
"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"
}
},
"jitsi_integration": { "jitsi_integration": {
"description": "Automatic Jitsi video meeting integration", "description": "Automatic Jitsi video meeting integration",
"features": [ "features": [
@ -332,51 +422,75 @@ def api_root(request, format=None):
"Both client and therapist can join using the same URL" "Both client and therapist can join using the same URL"
] ]
} }
} }
} }
return Response({ return Response({
'message': 'Therapy Appointment API', 'message': 'Therapy Appointment API with Enhanced Availability System',
'version': '1.0.0', 'version': '2.0.0',
'base_url': base_url, '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': { 'project_structure': {
'admin': '/admin/ - Django admin interface', 'admin': '/admin/ - Django admin interface',
'authentication': '/api/auth/ - User authentication and management', 'authentication': '/api/auth/ - User authentication and management',
'appointments': '/api/meetings/ - Appointment booking system' 'appointments': '/api/meetings/ - Enhanced appointment booking system'
}, },
'endpoints': endpoints, 'endpoints': endpoints,
'appointment_workflows': { 'appointment_workflows': {
'client_booking_flow': [ 'client_booking_flow': [
'1. GET /api/meetings/appointments/available-dates/ - Check available dates', '1. GET /api/meetings/availability/weekly/ - Check weekly availability',
'2. POST /api/meetings/appointments/create/ - Submit appointment request', '2. POST /api/meetings/availability/check/ - Check specific date availability',
'3. GET /api/meetings/user/appointments/ - Track request status', '3. GET /api/meetings/appointments/available-dates/ - See next available dates',
'4. Receive email notification when scheduled/rejected' '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': [ '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', '2. GET /api/meetings/appointments/ - Review pending requests',
'3. POST /api/meetings/appointments/{id}/schedule/ - Schedule appointment OR', '3. GET /api/meetings/appointments/stats/ - Check availability coverage',
'4. POST /api/meetings/appointments/{id}/reject/ - Reject with reason', '4. POST /api/meetings/appointments/{id}/schedule/ - Schedule with date+slot OR direct datetime',
'5. GET /api/meetings/appointments/stats/ - Monitor performance' '5. POST /api/meetings/appointments/{id}/reject/ - Reject with reason'
], ],
'status_lifecycle': [ 'status_lifecycle': [
'pending_review → scheduled (with datetime)', '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': { 'availability_examples': {
'registration_flow': [ 'monday_evening_only': {
'1. POST /api/auth/register/ - Register user and send OTP', "availability_schedule": {
'2. POST /api/auth/verify-otp/ - Verify email with OTP', "0": ["evening"]
'3. POST /api/auth/login/ - Login with credentials' }
], },
'password_reset_flow': [ 'weekday_mornings_afternoons': {
'1. POST /api/auth/forgot-password/ - Request password reset OTP', "availability_schedule": {
'2. POST /api/auth/verify-password-reset-otp/ - Verify OTP', "0": ["morning", "afternoon"],
'3. POST /api/auth/reset-password/ - Set new password' "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': { 'quick_start': {
@ -384,14 +498,16 @@ def api_root(request, format=None):
'1. Register: POST /api/auth/register/', '1. Register: POST /api/auth/register/',
'2. Verify email: POST /api/auth/verify-otp/', '2. Verify email: POST /api/auth/verify-otp/',
'3. Login: POST /api/auth/login/', '3. Login: POST /api/auth/login/',
'4. Check availability: GET /api/meetings/appointments/available-dates/', '4. Check weekly availability: GET /api/meetings/availability/weekly/',
'5. Book appointment: POST /api/meetings/appointments/create/' '5. Check specific date: POST /api/meetings/availability/check/',
'6. Book appointment: POST /api/meetings/appointments/create/'
], ],
'for_admins': [ 'for_admins': [
'1. Login to Django admin: /admin/', '1. Login to Django admin: /admin/',
'2. Set availability: PUT /api/meetings/admin/availability/', '2. Set flexible availability: PUT /api/meetings/admin/availability/',
'3. Manage appointments: GET /api/meetings/appointments/', '3. Check availability coverage: GET /api/meetings/appointments/stats/',
'4. Schedule/Reject: Use specific appointment endpoints' '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': [ 'status_choices': [
'pending_review - Initial state, awaiting admin action', 'pending_review - Initial state, awaiting admin action',
'scheduled - Approved with specific date/time', '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': [ 'time_slot_choices': [
'morning - 9AM to 12PM', 'morning - 9AM to 12PM',
@ -415,7 +533,8 @@ def api_root(request, format=None):
}, },
'availability': { 'availability': {
'day_format': '0=Monday, 1=Tuesday, ..., 6=Sunday', '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_usage': 'Include JWT token in Authorization header: Bearer <token>',
'token_refresh': 'Use refresh token to get new access token when expired', 'token_refresh': 'Use refresh token to get new access token when expired',
'permissions': { 'permissions': {
'public_endpoints': 'No authentication required', 'public_endpoints': 'No authentication required (availability checks, overview)',
'user_endpoints': 'Valid JWT token required', 'user_endpoints': 'Valid JWT token required',
'admin_endpoints': 'Staff user with 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 django.contrib import admin
from .models import AdminWeeklyAvailability, AppointmentRequest 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) @admin.register(AdminWeeklyAvailability)
class AdminWeeklyAvailabilityAdmin(admin.ModelAdmin): 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): def has_add_permission(self, request):
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK) if self.model.objects.count() >= 1:
return ', '.join([days_map[day] for day in obj.available_days]) return False
available_days_display.short_description = 'Available Days' return super().has_add_permission(request)
@admin.register(AppointmentRequest) @admin.register(AppointmentRequest)
class AppointmentRequestAdmin(admin.ModelAdmin): class AppointmentRequestAdmin(admin.ModelAdmin):
list_display = ['full_name', 'email', 'status', 'created_at', 'scheduled_datetime'] list_display = ['full_name', 'email', 'status', 'formatted_created_at', 'formatted_scheduled_datetime']
list_filter = ['status', 'created_at'] list_filter = ['status', 'created_at', 'scheduled_datetime']
search_fields = ['first_name', 'last_name', 'email'] search_fields = ['first_name', 'last_name', 'email']
readonly_fields = ['created_at', 'updated_at'] readonly_fields = ['id', 'created_at', 'updated_at', 'formatted_created_at', 'formatted_scheduled_datetime']
actions = ['mark_as_scheduled', 'mark_as_rejected']
def mark_as_scheduled(self, request, queryset): fieldsets = (
for appointment in queryset: ('Personal Information', {
if appointment.status == 'pending_review': 'fields': ('first_name', 'last_name', 'email', 'phone', 'reason')
appointment.status = 'scheduled' }),
appointment.save() ('Appointment Preferences', {
mark_as_scheduled.short_description = "Mark selected as scheduled" '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): def formatted_created_at(self, obj):
for appointment in queryset: return obj.formatted_created_at
if appointment.status == 'pending_review': formatted_created_at.short_description = 'Created At'
appointment.status = 'rejected'
appointment.save() def formatted_scheduled_datetime(self, obj):
mark_as_rejected.short_description = "Mark selected as rejected" 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'), (6, 'Sunday'),
] ]
available_days = models.JSONField( TIME_SLOT_CHOICES = [
default=list, ('morning', 'Morning (9AM - 12PM)'),
help_text="List of weekdays (0-6) when appointments are accepted" ('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) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -101,9 +107,59 @@ class AdminWeeklyAvailability(models.Model):
verbose_name = 'Admin Weekly Availability' verbose_name = 'Admin Weekly Availability'
verbose_name_plural = '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): def __str__(self):
days = [self.DAYS_OF_WEEK[day][1] for day in self.available_days] if not self.availability_schedule:
return f"Available: {', '.join(days)}" 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): class AppointmentRequest(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
@ -295,5 +351,101 @@ class AppointmentRequest(models.Model):
else: else:
return "Ended" 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): 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 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 django.utils import timezone
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json import json
class AdminWeeklyAvailabilitySerializer(serializers.ModelSerializer): class AdminWeeklyAvailabilitySerializer(serializers.ModelSerializer):
available_days_display = serializers.SerializerMethodField() availability_schedule_display = serializers.SerializerMethodField()
all_available_slots = serializers.SerializerMethodField()
class Meta: class Meta:
model = AdminWeeklyAvailability 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) 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): class AppointmentRequestSerializer(serializers.ModelSerializer):
full_name = serializers.ReadOnlyField() full_name = serializers.ReadOnlyField()
@ -26,6 +71,9 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
jitsi_room_id = serializers.ReadOnlyField() jitsi_room_id = serializers.ReadOnlyField()
can_join_meeting = serializers.ReadOnlyField() can_join_meeting = serializers.ReadOnlyField()
meeting_status = serializers.ReadOnlyField() meeting_status = serializers.ReadOnlyField()
meeting_duration_display = serializers.ReadOnlyField()
matching_availability = serializers.SerializerMethodField()
are_preferences_available = serializers.SerializerMethodField()
class Meta: class Meta:
model = AppointmentRequest model = AppointmentRequest
@ -36,7 +84,8 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
'jitsi_meet_url', 'jitsi_room_id', 'created_at', 'updated_at', 'jitsi_meet_url', 'jitsi_room_id', 'created_at', 'updated_at',
'full_name', 'formatted_created_at', 'formatted_scheduled_datetime', 'full_name', 'formatted_created_at', 'formatted_scheduled_datetime',
'preferred_dates_display', 'preferred_time_slots_display', '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 = [ read_only_fields = [
'id', 'status', 'scheduled_datetime', 'scheduled_duration', 'id', 'status', 'scheduled_datetime', 'scheduled_duration',
@ -44,48 +93,214 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
'created_at', 'updated_at' '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): 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: class Meta:
model = AppointmentRequest model = AppointmentRequest
fields = [ fields = [
'first_name', 'last_name', 'email', 'phone', 'reason', '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): def get_available_slots_info(self, obj):
if not value or len(value) == 0: 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.") raise serializers.ValidationError("At least one preferred date is required.")
today = timezone.now().date() today = timezone.now().date()
for date_str in value: for date_str in preferred_dates:
try: try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
if date_obj < today: if date_obj < today:
raise serializers.ValidationError("Preferred dates cannot be in the past.") 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: except ValueError:
raise serializers.ValidationError(f"Invalid date format: {date_str}. Use YYYY-MM-DD.") raise serializers.ValidationError(f"Invalid date format: {date_str}. Use YYYY-MM-DD.")
return value if not preferred_time_slots or len(preferred_time_slots) == 0:
def validate_preferred_time_slots(self, value):
if not value or len(value) == 0:
raise serializers.ValidationError("At least one time slot is required.") raise serializers.ValidationError("At least one time slot is required.")
valid_slots = ['morning', 'afternoon', 'evening'] valid_slots = ['morning', 'afternoon', 'evening']
for slot in value: for slot in preferred_time_slots:
if slot not in valid_slots: if slot not in valid_slots:
raise serializers.ValidationError(f"Invalid time slot: {slot}. Must be one of {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): class AppointmentScheduleSerializer(serializers.Serializer):
scheduled_datetime = serializers.DateTimeField() scheduled_datetime = serializers.DateTimeField()
scheduled_duration = serializers.IntegerField(default=60, min_value=30, max_value=240) 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): def validate(self, data):
if value <= timezone.now(): 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.") 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): def validate_scheduled_duration(self, value):
if value < 30: if value < 30:
@ -96,3 +311,70 @@ class AppointmentScheduleSerializer(serializers.Serializer):
class AppointmentRejectSerializer(serializers.Serializer): 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 django.urls import path
from .views import ( from .views import (
AdminAvailabilityView, AdminAvailabilityView,
AdminAvailabilityConfigView,
AppointmentRequestListView, AppointmentRequestListView,
AppointmentRequestCreateView, AppointmentRequestCreateView,
AppointmentRequestDetailView, AppointmentRequestDetailView,
ScheduleAppointmentView, ScheduleAppointmentView,
RejectAppointmentView, RejectAppointmentView,
AvailableDatesView, AvailableDatesView,
CheckDateAvailabilityView,
WeeklyAvailabilityView,
UserAppointmentsView, UserAppointmentsView,
AppointmentStatsView, AppointmentStatsView,
UserAppointmentStatsView UserAppointmentStatsView,
MatchingAvailabilityView,
availability_overview
) )
urlpatterns = [ urlpatterns = [
path('admin/availability/', AdminAvailabilityView.as_view(), name='admin-availability'), 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/', AppointmentRequestListView.as_view(), name='appointment-list'),
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'), path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'), 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>/schedule/', ScheduleAppointmentView.as_view(), name='appointment-schedule'),
path('appointments/<uuid:pk>/reject/', RejectAppointmentView.as_view(), name='appointment-reject'), path('appointments/<uuid:pk>/reject/', RejectAppointmentView.as_view(), name='appointment-reject'),
path('appointments/available-dates/', AvailableDatesView.as_view(), name='available-dates'), path('appointments/available-dates/', AvailableDatesView.as_view(), name='available-dates'),
path('user/appointments/', UserAppointmentsView.as_view(), name='user-appointments'), path('user/appointments/', UserAppointmentsView.as_view(), name='user-appointments'),
path('appointments/stats/', AppointmentStatsView.as_view(), name='appointment-stats'), path('appointments/stats/', AppointmentStatsView.as_view(), name='appointment-stats'),

View File

@ -1,16 +1,21 @@
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response 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 django.utils import timezone
from datetime import datetime, timedelta 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 ( from .serializers import (
AdminWeeklyAvailabilitySerializer, AdminWeeklyAvailabilitySerializer,
AdminWeeklyAvailabilityUpdateSerializer,
AppointmentRequestSerializer, AppointmentRequestSerializer,
AppointmentRequestCreateSerializer, AppointmentRequestCreateSerializer,
AppointmentScheduleSerializer, AppointmentScheduleSerializer,
AppointmentRejectSerializer AppointmentRejectSerializer,
AvailabilityCheckSerializer,
AvailabilityResponseSerializer,
WeeklyAvailabilitySerializer,
AdminAvailabilityConfigSerializer
) )
from .email_service import EmailService from .email_service import EmailService
from users.models import CustomUser from users.models import CustomUser
@ -19,13 +24,30 @@ from django.db.models import Count, Q
class AdminAvailabilityView(generics.RetrieveUpdateAPIView): class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
permission_classes = [IsAuthenticated, IsAdminUser] 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): def get_object(self):
obj, created = AdminWeeklyAvailability.objects.get_or_create( return get_admin_availability()
defaults={'available_days': []}
) def update(self, request, *args, **kwargs):
return obj 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): class AppointmentRequestListView(generics.ListAPIView):
serializer_class = AppointmentRequestSerializer serializer_class = AppointmentRequestSerializer
@ -39,25 +61,20 @@ class AppointmentRequestListView(generics.ListAPIView):
return queryset.filter(email=self.request.user.email) return queryset.filter(email=self.request.user.email)
class AppointmentRequestCreateView(generics.CreateAPIView): class AppointmentRequestCreateView(generics.CreateAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
queryset = AppointmentRequest.objects.all() queryset = AppointmentRequest.objects.all()
serializer_class = AppointmentRequestCreateSerializer serializer_class = AppointmentRequestCreateSerializer
def perform_create(self, serializer): 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() appointment = serializer.save()
if appointment.are_preferences_available():
EmailService.send_admin_notification(appointment) EmailService.send_admin_notification(appointment)
else:
EmailService.send_admin_notification(appointment, availability_mismatch=True)
class AppointmentRequestDetailView(generics.RetrieveAPIView): class AppointmentRequestDetailView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@ -65,6 +82,7 @@ class AppointmentRequestDetailView(generics.RetrieveAPIView):
serializer_class = AppointmentRequestSerializer serializer_class = AppointmentRequestSerializer
lookup_field = 'pk' lookup_field = 'pk'
class ScheduleAppointmentView(generics.GenericAPIView): class ScheduleAppointmentView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, IsAdminUser] permission_classes = [IsAuthenticated, IsAdminUser]
serializer_class = AppointmentScheduleSerializer serializer_class = AppointmentScheduleSerializer
@ -129,21 +147,92 @@ class AvailableDatesView(generics.GenericAPIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
def get(self, request): def get(self, request):
availability = AdminWeeklyAvailability.objects.first() availability = get_admin_availability()
if not availability: if not availability or not availability.availability_schedule:
return Response([]) return Response([])
available_days = availability.available_days
today = timezone.now().date() today = timezone.now().date()
available_dates = [] available_dates = []
for i in range(1, 31): for i in range(1, 31):
date = today + timedelta(days=i) date = today + timedelta(days=i)
if date.weekday() in available_days: day_of_week = date.weekday()
available_dates.append(date.strftime('%Y-%m-%d'))
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) 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): class UserAppointmentsView(generics.ListAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = AppointmentRequestSerializer serializer_class = AppointmentRequestSerializer
@ -163,21 +252,39 @@ class AppointmentStatsView(generics.GenericAPIView):
pending = AppointmentRequest.objects.filter(status='pending_review').count() pending = AppointmentRequest.objects.filter(status='pending_review').count()
scheduled = AppointmentRequest.objects.filter(status='scheduled').count() scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
rejected = AppointmentRequest.objects.filter(status='rejected').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({ return Response({
'total_requests': total, 'total_requests': total,
'pending_review': pending, 'pending_review': pending,
'scheduled': scheduled, 'scheduled': scheduled,
'rejected': rejected, 'rejected': rejected,
'completed': completed,
'users': users, '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): class UserAppointmentStatsView(generics.GenericAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request): 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( stats = AppointmentRequest.objects.filter(
email=email email=email
).aggregate( ).aggregate(
@ -198,5 +305,77 @@ class UserAppointmentStatsView(generics.GenericAPIView):
'scheduled': scheduled, 'scheduled': scheduled,
'rejected': stats['rejected'], 'rejected': stats['rejected'],
'completed': stats['completed'], '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