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:
parent
ce3b0b77f5
commit
a7d451702f
@ -148,64 +148,124 @@ def api_root(request, format=None):
|
||||
}
|
||||
},
|
||||
"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/",
|
||||
"endpoints": {
|
||||
"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/"),
|
||||
"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"
|
||||
"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": {
|
||||
"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": {
|
||||
"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/"),
|
||||
"methods": ["GET"],
|
||||
"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": {
|
||||
"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/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (User only)",
|
||||
"required_fields": [
|
||||
"first_name", "last_name", "email",
|
||||
"preferred_dates", "preferred_time_slots"
|
||||
"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": "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"]
|
||||
"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"}
|
||||
]
|
||||
},
|
||||
"validation": "Preferred dates must be within admin available days"
|
||||
"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",
|
||||
"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"
|
||||
"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": {
|
||||
@ -214,14 +274,26 @@ def api_root(request, format=None):
|
||||
"methods": ["GET"],
|
||||
"authentication": "Required",
|
||||
"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": {
|
||||
"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"
|
||||
"response": "List of user's appointment requests with enhanced availability data"
|
||||
},
|
||||
"schedule_appointment": {
|
||||
"description": "Schedule an appointment and automatically create Jitsi meeting (Admin only)",
|
||||
@ -229,12 +301,17 @@ def api_root(request, format=None):
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required (Staff users only)",
|
||||
"required_fields": ["scheduled_datetime"],
|
||||
"optional_fields": ["scheduled_duration"],
|
||||
"optional_fields": ["scheduled_duration", "date_str", "time_slot"],
|
||||
"prerequisites": "Appointment must be in 'pending_review' status",
|
||||
"example_request": {
|
||||
"scheduled_datetime": "2024-01-15T10:00:00Z",
|
||||
"scheduled_duration": 60
|
||||
"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",
|
||||
@ -265,24 +342,9 @@ def api_root(request, format=None):
|
||||
"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)",
|
||||
"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)",
|
||||
@ -291,11 +353,14 @@ def api_root(request, format=None):
|
||||
"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"
|
||||
"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 and analytics for the authenticated user",
|
||||
"description": "Get appointment statistics for a specific user",
|
||||
"url": request.build_absolute_uri("/api/meetings/user/appointments/stats/"),
|
||||
"methods": ["POST"],
|
||||
"authentication": "Required",
|
||||
@ -306,11 +371,36 @@ def api_root(request, format=None):
|
||||
"scheduled": "Number of scheduled appointments",
|
||||
"rejected": "Number of rejected requests",
|
||||
"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": {
|
||||
"description": "Automatic Jitsi video meeting integration",
|
||||
"features": [
|
||||
@ -332,51 +422,75 @@ def api_root(request, format=None):
|
||||
"Both client and therapist can join using the same URL"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response({
|
||||
'message': 'Therapy Appointment API',
|
||||
'version': '1.0.0',
|
||||
'message': 'Therapy Appointment API with Enhanced Availability System',
|
||||
'version': '2.0.0',
|
||||
'base_url': base_url,
|
||||
'new_features': [
|
||||
'Flexible day-time availability management',
|
||||
'Real-time availability validation',
|
||||
'Matching preference detection',
|
||||
'Enhanced scheduling options',
|
||||
'Availability statistics and coverage metrics'
|
||||
],
|
||||
'project_structure': {
|
||||
'admin': '/admin/ - Django admin interface',
|
||||
'authentication': '/api/auth/ - User authentication and management',
|
||||
'appointments': '/api/meetings/ - Appointment booking system'
|
||||
'appointments': '/api/meetings/ - Enhanced appointment booking system'
|
||||
},
|
||||
'endpoints': endpoints,
|
||||
|
||||
'appointment_workflows': {
|
||||
'client_booking_flow': [
|
||||
'1. GET /api/meetings/appointments/available-dates/ - Check available dates',
|
||||
'2. POST /api/meetings/appointments/create/ - Submit appointment request',
|
||||
'3. GET /api/meetings/user/appointments/ - Track request status',
|
||||
'4. Receive email notification when scheduled/rejected'
|
||||
'1. GET /api/meetings/availability/weekly/ - Check weekly availability',
|
||||
'2. POST /api/meetings/availability/check/ - Check specific date availability',
|
||||
'3. GET /api/meetings/appointments/available-dates/ - See next available dates',
|
||||
'4. POST /api/meetings/appointments/create/ - Submit appointment request',
|
||||
'5. GET /api/meetings/user/appointments/ - Track request status',
|
||||
'6. GET /api/meetings/appointments/{id}/matching-availability/ - See matching options',
|
||||
'7. Receive email notification when scheduled/rejected'
|
||||
],
|
||||
'admin_management_flow': [
|
||||
'1. PUT /api/meetings/admin/availability/ - Set weekly availability',
|
||||
'1. PUT /api/meetings/admin/availability/ - Set flexible day-time availability',
|
||||
'2. GET /api/meetings/appointments/ - Review pending requests',
|
||||
'3. POST /api/meetings/appointments/{id}/schedule/ - Schedule appointment OR',
|
||||
'4. POST /api/meetings/appointments/{id}/reject/ - Reject with reason',
|
||||
'5. GET /api/meetings/appointments/stats/ - Monitor performance'
|
||||
'3. GET /api/meetings/appointments/stats/ - Check availability coverage',
|
||||
'4. POST /api/meetings/appointments/{id}/schedule/ - Schedule with date+slot OR direct datetime',
|
||||
'5. POST /api/meetings/appointments/{id}/reject/ - Reject with reason'
|
||||
],
|
||||
'status_lifecycle': [
|
||||
'pending_review → scheduled (with datetime)',
|
||||
'pending_review → rejected (with optional reason)'
|
||||
'pending_review → rejected (with optional reason)',
|
||||
'scheduled → completed (after meeting)',
|
||||
'scheduled → cancelled (if needed)'
|
||||
]
|
||||
},
|
||||
|
||||
'authentication_flows': {
|
||||
'registration_flow': [
|
||||
'1. POST /api/auth/register/ - Register user and send OTP',
|
||||
'2. POST /api/auth/verify-otp/ - Verify email with OTP',
|
||||
'3. POST /api/auth/login/ - Login with credentials'
|
||||
],
|
||||
'password_reset_flow': [
|
||||
'1. POST /api/auth/forgot-password/ - Request password reset OTP',
|
||||
'2. POST /api/auth/verify-password-reset-otp/ - Verify OTP',
|
||||
'3. POST /api/auth/reset-password/ - Set new password'
|
||||
]
|
||||
'availability_examples': {
|
||||
'monday_evening_only': {
|
||||
"availability_schedule": {
|
||||
"0": ["evening"]
|
||||
}
|
||||
},
|
||||
'weekday_mornings_afternoons': {
|
||||
"availability_schedule": {
|
||||
"0": ["morning", "afternoon"],
|
||||
"1": ["morning", "afternoon"],
|
||||
"2": ["morning", "afternoon"],
|
||||
"3": ["morning", "afternoon"],
|
||||
"4": ["morning", "afternoon"]
|
||||
}
|
||||
},
|
||||
'flexible_schedule': {
|
||||
"availability_schedule": {
|
||||
"0": ["morning", "evening"],
|
||||
"1": ["afternoon"],
|
||||
"3": ["morning", "afternoon"],
|
||||
"5": ["morning"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'quick_start': {
|
||||
@ -384,14 +498,16 @@ def api_root(request, format=None):
|
||||
'1. Register: POST /api/auth/register/',
|
||||
'2. Verify email: POST /api/auth/verify-otp/',
|
||||
'3. Login: POST /api/auth/login/',
|
||||
'4. Check availability: GET /api/meetings/appointments/available-dates/',
|
||||
'5. Book appointment: POST /api/meetings/appointments/create/'
|
||||
'4. Check weekly availability: GET /api/meetings/availability/weekly/',
|
||||
'5. Check specific date: POST /api/meetings/availability/check/',
|
||||
'6. Book appointment: POST /api/meetings/appointments/create/'
|
||||
],
|
||||
'for_admins': [
|
||||
'1. Login to Django admin: /admin/',
|
||||
'2. Set availability: PUT /api/meetings/admin/availability/',
|
||||
'3. Manage appointments: GET /api/meetings/appointments/',
|
||||
'4. Schedule/Reject: Use specific appointment endpoints'
|
||||
'2. Set flexible availability: PUT /api/meetings/admin/availability/',
|
||||
'3. Check availability coverage: GET /api/meetings/appointments/stats/',
|
||||
'4. Manage appointments: GET /api/meetings/appointments/',
|
||||
'5. Schedule/Reject: Use specific appointment endpoints'
|
||||
]
|
||||
},
|
||||
|
||||
@ -400,7 +516,9 @@ def api_root(request, format=None):
|
||||
'status_choices': [
|
||||
'pending_review - Initial state, awaiting admin action',
|
||||
'scheduled - Approved with specific date/time',
|
||||
'rejected - Not accepted, with optional reason'
|
||||
'rejected - Not accepted, with optional reason',
|
||||
'completed - Meeting has been completed',
|
||||
'cancelled - Appointment was cancelled'
|
||||
],
|
||||
'time_slot_choices': [
|
||||
'morning - 9AM to 12PM',
|
||||
@ -415,7 +533,8 @@ def api_root(request, format=None):
|
||||
},
|
||||
'availability': {
|
||||
'day_format': '0=Monday, 1=Tuesday, ..., 6=Sunday',
|
||||
'example': '[0, 1, 2, 3, 4] for Monday-Friday'
|
||||
'time_slot_format': 'morning, afternoon, evening',
|
||||
'schedule_format': 'Dictionary: {"0": ["morning", "evening"], "1": ["afternoon"]}'
|
||||
}
|
||||
},
|
||||
|
||||
@ -423,7 +542,7 @@ def api_root(request, format=None):
|
||||
'token_usage': 'Include JWT token in Authorization header: Bearer <token>',
|
||||
'token_refresh': 'Use refresh token to get new access token when expired',
|
||||
'permissions': {
|
||||
'public_endpoints': 'No authentication required',
|
||||
'public_endpoints': 'No authentication required (availability checks, overview)',
|
||||
'user_endpoints': 'Valid JWT token required',
|
||||
'admin_endpoints': 'Staff user with valid JWT token required'
|
||||
}
|
||||
|
||||
@ -1,33 +1,87 @@
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest
|
||||
|
||||
class AdminWeeklyAvailabilityForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = AdminWeeklyAvailability
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
'availability_schedule': forms.HiddenInput()
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
|
||||
field_name = f'day_{day_num}'
|
||||
self.fields[field_name] = forms.MultipleChoiceField(
|
||||
choices=AdminWeeklyAvailability.TIME_SLOT_CHOICES,
|
||||
required=False,
|
||||
label=day_name,
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
if self.instance.availability_schedule:
|
||||
self.fields[field_name].initial = self.instance.availability_schedule.get(str(day_num), [])
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
|
||||
availability_schedule = {}
|
||||
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
|
||||
field_name = f'day_{day_num}'
|
||||
time_slots = self.cleaned_data.get(field_name, [])
|
||||
if time_slots:
|
||||
availability_schedule[str(day_num)] = time_slots
|
||||
|
||||
instance.availability_schedule = availability_schedule
|
||||
|
||||
if commit:
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
@admin.register(AdminWeeklyAvailability)
|
||||
class AdminWeeklyAvailabilityAdmin(admin.ModelAdmin):
|
||||
list_display = ['available_days_display', 'created_at']
|
||||
form = AdminWeeklyAvailabilityForm
|
||||
list_display = ['__str__', 'created_at', 'updated_at']
|
||||
|
||||
def available_days_display(self, obj):
|
||||
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
|
||||
return ', '.join([days_map[day] for day in obj.available_days])
|
||||
available_days_display.short_description = 'Available Days'
|
||||
def has_add_permission(self, request):
|
||||
if self.model.objects.count() >= 1:
|
||||
return False
|
||||
return super().has_add_permission(request)
|
||||
|
||||
@admin.register(AppointmentRequest)
|
||||
class AppointmentRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ['full_name', 'email', 'status', 'created_at', 'scheduled_datetime']
|
||||
list_filter = ['status', 'created_at']
|
||||
list_display = ['full_name', 'email', 'status', 'formatted_created_at', 'formatted_scheduled_datetime']
|
||||
list_filter = ['status', 'created_at', 'scheduled_datetime']
|
||||
search_fields = ['first_name', 'last_name', 'email']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['mark_as_scheduled', 'mark_as_rejected']
|
||||
readonly_fields = ['id', 'created_at', 'updated_at', 'formatted_created_at', 'formatted_scheduled_datetime']
|
||||
|
||||
def mark_as_scheduled(self, request, queryset):
|
||||
for appointment in queryset:
|
||||
if appointment.status == 'pending_review':
|
||||
appointment.status = 'scheduled'
|
||||
appointment.save()
|
||||
mark_as_scheduled.short_description = "Mark selected as scheduled"
|
||||
fieldsets = (
|
||||
('Personal Information', {
|
||||
'fields': ('first_name', 'last_name', 'email', 'phone', 'reason')
|
||||
}),
|
||||
('Appointment Preferences', {
|
||||
'fields': ('preferred_dates', 'preferred_time_slots')
|
||||
}),
|
||||
('Scheduling', {
|
||||
'fields': ('status', 'scheduled_datetime', 'scheduled_duration', 'rejection_reason')
|
||||
}),
|
||||
('Video Meeting', {
|
||||
'fields': ('jitsi_meet_url', 'jitsi_room_id')
|
||||
}),
|
||||
('Metadata', {
|
||||
'fields': ('id', 'created_at', 'updated_at', 'formatted_created_at', 'formatted_scheduled_datetime'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def mark_as_rejected(self, request, queryset):
|
||||
for appointment in queryset:
|
||||
if appointment.status == 'pending_review':
|
||||
appointment.status = 'rejected'
|
||||
appointment.save()
|
||||
mark_as_rejected.short_description = "Mark selected as rejected"
|
||||
def formatted_created_at(self, obj):
|
||||
return obj.formatted_created_at
|
||||
formatted_created_at.short_description = 'Created At'
|
||||
|
||||
def formatted_scheduled_datetime(self, obj):
|
||||
return obj.formatted_scheduled_datetime
|
||||
formatted_scheduled_datetime.short_description = 'Scheduled Date Time'
|
||||
@ -90,9 +90,15 @@ class AdminWeeklyAvailability(models.Model):
|
||||
(6, 'Sunday'),
|
||||
]
|
||||
|
||||
available_days = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of weekdays (0-6) when appointments are accepted"
|
||||
TIME_SLOT_CHOICES = [
|
||||
('morning', 'Morning (9AM - 12PM)'),
|
||||
('afternoon', 'Afternoon (1PM - 5PM)'),
|
||||
('evening', 'Evening (6PM - 9PM)'),
|
||||
]
|
||||
|
||||
availability_schedule = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Dictionary with days as keys and lists of time slots as values"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@ -101,9 +107,59 @@ class AdminWeeklyAvailability(models.Model):
|
||||
verbose_name = 'Admin Weekly Availability'
|
||||
verbose_name_plural = 'Admin Weekly Availability'
|
||||
|
||||
def set_availability(self, day, time_slots):
|
||||
if not self.availability_schedule:
|
||||
self.availability_schedule = {}
|
||||
|
||||
if day not in [str(d[0]) for d in self.DAYS_OF_WEEK]:
|
||||
raise ValueError(f"Invalid day: {day}")
|
||||
|
||||
valid_slots = [slot[0] for slot in self.TIME_SLOT_CHOICES]
|
||||
for slot in time_slots:
|
||||
if slot not in valid_slots:
|
||||
raise ValueError(f"Invalid time slot: {slot}")
|
||||
|
||||
self.availability_schedule[str(day)] = time_slots
|
||||
|
||||
def get_availability_for_day(self, day):
|
||||
return self.availability_schedule.get(str(day), [])
|
||||
|
||||
def is_available(self, day, time_slot):
|
||||
return time_slot in self.get_availability_for_day(day)
|
||||
|
||||
def get_all_available_slots(self):
|
||||
available_slots = []
|
||||
for day_num, time_slots in self.availability_schedule.items():
|
||||
day_name = dict(self.DAYS_OF_WEEK).get(int(day_num))
|
||||
for time_slot in time_slots:
|
||||
time_display = dict(self.TIME_SLOT_CHOICES).get(time_slot)
|
||||
available_slots.append({
|
||||
'day_num': int(day_num),
|
||||
'day_name': day_name,
|
||||
'time_slot': time_slot,
|
||||
'time_display': time_display
|
||||
})
|
||||
return available_slots
|
||||
|
||||
def clear_availability(self, day=None):
|
||||
if day is None:
|
||||
self.availability_schedule = {}
|
||||
else:
|
||||
day_str = str(day)
|
||||
if day_str in self.availability_schedule:
|
||||
del self.availability_schedule[day_str]
|
||||
|
||||
def __str__(self):
|
||||
days = [self.DAYS_OF_WEEK[day][1] for day in self.available_days]
|
||||
return f"Available: {', '.join(days)}"
|
||||
if not self.availability_schedule:
|
||||
return "No availability set"
|
||||
|
||||
display_strings = []
|
||||
for day_num, time_slots in sorted(self.availability_schedule.items()):
|
||||
day_name = dict(self.DAYS_OF_WEEK).get(int(day_num))
|
||||
slot_displays = [dict(self.TIME_SLOT_CHOICES).get(slot) for slot in time_slots]
|
||||
display_strings.append(f"{day_name}: {', '.join(slot_displays)}")
|
||||
|
||||
return " | ".join(display_strings)
|
||||
|
||||
class AppointmentRequest(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
@ -295,5 +351,101 @@ class AppointmentRequest(models.Model):
|
||||
else:
|
||||
return "Ended"
|
||||
|
||||
def get_available_time_slots_for_date(self, date_str):
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if not availability:
|
||||
return []
|
||||
|
||||
return availability.get_availability_for_day(day_of_week)
|
||||
except Exception as e:
|
||||
print(f"Error getting available slots: {e}")
|
||||
return []
|
||||
|
||||
def are_preferences_available(self):
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if not availability:
|
||||
return False
|
||||
|
||||
for date_str in self.preferred_dates:
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
|
||||
available_slots = availability.get_availability_for_day(day_of_week)
|
||||
if any(slot in available_slots for slot in self.preferred_time_slots):
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error checking availability for {date_str}: {e}")
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def get_matching_availability(self):
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if not availability:
|
||||
return []
|
||||
|
||||
matching_slots = []
|
||||
for date_str in self.preferred_dates:
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
day_name = dict(AdminWeeklyAvailability.DAYS_OF_WEEK).get(day_of_week)
|
||||
|
||||
available_slots = availability.get_availability_for_day(day_of_week)
|
||||
matching_time_slots = [slot for slot in self.preferred_time_slots if slot in available_slots]
|
||||
|
||||
if matching_time_slots:
|
||||
matching_slots.append({
|
||||
'date': date_str,
|
||||
'day_name': day_name,
|
||||
'available_slots': matching_time_slots,
|
||||
'date_obj': date_obj
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error processing {date_str}: {e}")
|
||||
continue
|
||||
|
||||
return matching_slots
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.full_name} - {self.get_status_display()} - {self.created_at.strftime('%Y-%m-%d')}"
|
||||
|
||||
|
||||
def get_admin_availability():
|
||||
availability, created = AdminWeeklyAvailability.objects.get_or_create(
|
||||
id=1
|
||||
)
|
||||
return availability
|
||||
|
||||
def set_admin_availability(availability_dict):
|
||||
availability = get_admin_availability()
|
||||
|
||||
for day, time_slots in availability_dict.items():
|
||||
availability.set_availability(day, time_slots)
|
||||
|
||||
availability.save()
|
||||
return availability
|
||||
|
||||
def get_available_slots_for_week():
|
||||
availability = get_admin_availability()
|
||||
return availability.get_all_available_slots()
|
||||
|
||||
def check_date_availability(date_str):
|
||||
try:
|
||||
from datetime import datetime
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
day_of_week = date_obj.weekday()
|
||||
|
||||
availability = get_admin_availability()
|
||||
return availability.get_availability_for_day(day_of_week)
|
||||
except Exception as e:
|
||||
print(f"Error checking date availability: {e}")
|
||||
return []
|
||||
@ -1,19 +1,64 @@
|
||||
from rest_framework import serializers
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, check_date_availability
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
|
||||
class AdminWeeklyAvailabilitySerializer(serializers.ModelSerializer):
|
||||
available_days_display = serializers.SerializerMethodField()
|
||||
availability_schedule_display = serializers.SerializerMethodField()
|
||||
all_available_slots = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdminWeeklyAvailability
|
||||
fields = ['id', 'available_days', 'available_days_display', 'created_at', 'updated_at']
|
||||
fields = [
|
||||
'id', 'availability_schedule', 'availability_schedule_display',
|
||||
'all_available_slots', 'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
def get_available_days_display(self, obj):
|
||||
def get_availability_schedule_display(self, obj):
|
||||
if not obj.availability_schedule:
|
||||
return "No availability set"
|
||||
|
||||
display = {}
|
||||
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
|
||||
return [days_map[day] for day in obj.available_days]
|
||||
time_slots_map = dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES)
|
||||
|
||||
for day_num, time_slots in obj.availability_schedule.items():
|
||||
day_name = days_map.get(int(day_num))
|
||||
slot_names = [time_slots_map.get(slot, slot) for slot in time_slots]
|
||||
display[day_name] = slot_names
|
||||
|
||||
return display
|
||||
|
||||
def get_all_available_slots(self, obj):
|
||||
return obj.get_all_available_slots()
|
||||
|
||||
class AdminWeeklyAvailabilityUpdateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AdminWeeklyAvailability
|
||||
fields = ['availability_schedule']
|
||||
|
||||
def validate_availability_schedule(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise serializers.ValidationError("Availability schedule must be a dictionary.")
|
||||
|
||||
valid_days = [str(day[0]) for day in AdminWeeklyAvailability.DAYS_OF_WEEK]
|
||||
valid_slots = [slot[0] for slot in AdminWeeklyAvailability.TIME_SLOT_CHOICES]
|
||||
|
||||
for day, time_slots in value.items():
|
||||
if day not in valid_days:
|
||||
raise serializers.ValidationError(f"Invalid day: {day}. Must be one of {valid_days}.")
|
||||
|
||||
if not isinstance(time_slots, list):
|
||||
raise serializers.ValidationError(f"Time slots for day {day} must be a list.")
|
||||
|
||||
for slot in time_slots:
|
||||
if slot not in valid_slots:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid time slot: {slot} for day {day}. Must be one of {valid_slots}."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
class AppointmentRequestSerializer(serializers.ModelSerializer):
|
||||
full_name = serializers.ReadOnlyField()
|
||||
@ -26,6 +71,9 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
|
||||
jitsi_room_id = serializers.ReadOnlyField()
|
||||
can_join_meeting = serializers.ReadOnlyField()
|
||||
meeting_status = serializers.ReadOnlyField()
|
||||
meeting_duration_display = serializers.ReadOnlyField()
|
||||
matching_availability = serializers.SerializerMethodField()
|
||||
are_preferences_available = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AppointmentRequest
|
||||
@ -36,7 +84,8 @@ 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',
|
||||
@ -44,48 +93,214 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
|
||||
'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:
|
||||
@ -96,3 +311,70 @@ class AppointmentScheduleSerializer(serializers.Serializer):
|
||||
|
||||
class AppointmentRejectSerializer(serializers.Serializer):
|
||||
rejection_reason = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
class AvailabilityCheckSerializer(serializers.Serializer):
|
||||
date = serializers.CharField(required=True)
|
||||
|
||||
def validate_date(self, value):
|
||||
try:
|
||||
date_obj = datetime.strptime(value, '%Y-%m-%d').date()
|
||||
if date_obj < timezone.now().date():
|
||||
raise serializers.ValidationError("Date cannot be in the past.")
|
||||
return value
|
||||
except ValueError:
|
||||
raise serializers.ValidationError("Invalid date format. Use YYYY-MM-DD.")
|
||||
|
||||
class AvailabilityResponseSerializer(serializers.Serializer):
|
||||
date = serializers.CharField()
|
||||
day_name = serializers.CharField()
|
||||
available_slots = serializers.ListField(child=serializers.CharField())
|
||||
available_slots_display = serializers.ListField(child=serializers.CharField())
|
||||
is_available = serializers.BooleanField()
|
||||
|
||||
class WeeklyAvailabilitySerializer(serializers.Serializer):
|
||||
day_number = serializers.IntegerField()
|
||||
day_name = serializers.CharField()
|
||||
available_slots = serializers.ListField(child=serializers.CharField())
|
||||
available_slots_display = serializers.ListField(child=serializers.CharField())
|
||||
|
||||
class TimeSlotSerializer(serializers.Serializer):
|
||||
value = serializers.CharField()
|
||||
display = serializers.CharField()
|
||||
disabled = serializers.BooleanField(default=False)
|
||||
|
||||
class DayAvailabilitySerializer(serializers.Serializer):
|
||||
day_number = serializers.IntegerField()
|
||||
day_name = serializers.CharField()
|
||||
time_slots = TimeSlotSerializer(many=True)
|
||||
is_available = serializers.BooleanField()
|
||||
|
||||
class AdminAvailabilityConfigSerializer(serializers.Serializer):
|
||||
days = DayAvailabilitySerializer(many=True)
|
||||
|
||||
@classmethod
|
||||
def get_default_config(cls):
|
||||
days_config = []
|
||||
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
|
||||
days_config.append({
|
||||
'day_number': day_num,
|
||||
'day_name': day_name,
|
||||
'time_slots': [
|
||||
{
|
||||
'value': 'morning',
|
||||
'display': 'Morning (9AM - 12PM)',
|
||||
'disabled': False
|
||||
},
|
||||
{
|
||||
'value': 'afternoon',
|
||||
'display': 'Afternoon (1PM - 5PM)',
|
||||
'disabled': False
|
||||
},
|
||||
{
|
||||
'value': 'evening',
|
||||
'display': 'Evening (6PM - 9PM)',
|
||||
'disabled': False
|
||||
}
|
||||
],
|
||||
'is_available': False
|
||||
})
|
||||
return cls({'days': days_config})
|
||||
@ -1,28 +1,39 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
AdminAvailabilityView,
|
||||
AdminAvailabilityConfigView,
|
||||
AppointmentRequestListView,
|
||||
AppointmentRequestCreateView,
|
||||
AppointmentRequestDetailView,
|
||||
ScheduleAppointmentView,
|
||||
RejectAppointmentView,
|
||||
AvailableDatesView,
|
||||
CheckDateAvailabilityView,
|
||||
WeeklyAvailabilityView,
|
||||
UserAppointmentsView,
|
||||
AppointmentStatsView,
|
||||
UserAppointmentStatsView
|
||||
UserAppointmentStatsView,
|
||||
MatchingAvailabilityView,
|
||||
availability_overview
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/availability/', AdminAvailabilityView.as_view(), name='admin-availability'),
|
||||
path('availability/config/', AdminAvailabilityConfigView.as_view(), name='availability-config'),
|
||||
path('availability/check/', CheckDateAvailabilityView.as_view(), name='check-availability'),
|
||||
path('availability/weekly/', WeeklyAvailabilityView.as_view(), name='weekly-availability'),
|
||||
path('availability/overview/', availability_overview, name='availability-overview'),
|
||||
|
||||
path('appointments/', AppointmentRequestListView.as_view(), name='appointment-list'),
|
||||
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
|
||||
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
|
||||
path('appointments/<uuid:pk>/matching-availability/', MatchingAvailabilityView.as_view(), name='matching-availability'),
|
||||
|
||||
path('appointments/<uuid:pk>/schedule/', ScheduleAppointmentView.as_view(), name='appointment-schedule'),
|
||||
path('appointments/<uuid:pk>/reject/', RejectAppointmentView.as_view(), name='appointment-reject'),
|
||||
|
||||
path('appointments/available-dates/', AvailableDatesView.as_view(), name='available-dates'),
|
||||
|
||||
path('user/appointments/', UserAppointmentsView.as_view(), name='user-appointments'),
|
||||
|
||||
path('appointments/stats/', AppointmentStatsView.as_view(), name='appointment-stats'),
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny,IsAdminUser
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, timedelta
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest
|
||||
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, set_admin_availability, get_available_slots_for_week, check_date_availability
|
||||
from .serializers import (
|
||||
AdminWeeklyAvailabilitySerializer,
|
||||
AdminWeeklyAvailabilityUpdateSerializer,
|
||||
AppointmentRequestSerializer,
|
||||
AppointmentRequestCreateSerializer,
|
||||
AppointmentScheduleSerializer,
|
||||
AppointmentRejectSerializer
|
||||
AppointmentRejectSerializer,
|
||||
AvailabilityCheckSerializer,
|
||||
AvailabilityResponseSerializer,
|
||||
WeeklyAvailabilitySerializer,
|
||||
AdminAvailabilityConfigSerializer
|
||||
)
|
||||
from .email_service import EmailService
|
||||
from users.models import CustomUser
|
||||
@ -19,13 +24,30 @@ from django.db.models import Count, Q
|
||||
|
||||
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
|
||||
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||
serializer_class = AdminWeeklyAvailabilitySerializer
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return AdminWeeklyAvailabilitySerializer
|
||||
return AdminWeeklyAvailabilityUpdateSerializer
|
||||
|
||||
def get_object(self):
|
||||
obj, created = AdminWeeklyAvailability.objects.get_or_create(
|
||||
defaults={'available_days': []}
|
||||
)
|
||||
return obj
|
||||
return get_admin_availability()
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
response = super().update(request, *args, **kwargs)
|
||||
availability = self.get_object()
|
||||
full_serializer = AdminWeeklyAvailabilitySerializer(availability)
|
||||
return Response(full_serializer.data)
|
||||
|
||||
|
||||
class AdminAvailabilityConfigView(generics.GenericAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
"""Get availability configuration"""
|
||||
config = AdminAvailabilityConfigSerializer.get_default_config()
|
||||
return Response(config.data)
|
||||
|
||||
|
||||
class AppointmentRequestListView(generics.ListAPIView):
|
||||
serializer_class = AppointmentRequestSerializer
|
||||
@ -39,25 +61,20 @@ class AppointmentRequestListView(generics.ListAPIView):
|
||||
|
||||
return queryset.filter(email=self.request.user.email)
|
||||
|
||||
|
||||
class AppointmentRequestCreateView(generics.CreateAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
queryset = AppointmentRequest.objects.all()
|
||||
serializer_class = AppointmentRequestCreateSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
availability = AdminWeeklyAvailability.objects.first()
|
||||
if availability:
|
||||
available_days = availability.available_days
|
||||
preferred_dates = serializer.validated_data['preferred_dates']
|
||||
|
||||
for date_str in preferred_dates:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
if date_obj.weekday() not in available_days:
|
||||
from rest_framework.exceptions import ValidationError
|
||||
raise ValidationError(f'Date {date_str} is not available for appointments.')
|
||||
|
||||
appointment = serializer.save()
|
||||
|
||||
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
|
||||
@ -129,21 +147,92 @@ 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):
|
||||
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
|
||||
@ -163,21 +252,39 @@ class AppointmentStatsView(generics.GenericAPIView):
|
||||
pending = AppointmentRequest.objects.filter(status='pending_review').count()
|
||||
scheduled = AppointmentRequest.objects.filter(status='scheduled').count()
|
||||
rejected = AppointmentRequest.objects.filter(status='rejected').count()
|
||||
completed = AppointmentRequest.objects.filter(status='completed').count()
|
||||
|
||||
availability = get_admin_availability()
|
||||
availability_coverage = 0
|
||||
if availability and availability.availability_schedule:
|
||||
days_with_availability = len(availability.availability_schedule)
|
||||
availability_coverage = round((days_with_availability / 7) * 100, 2)
|
||||
|
||||
return Response({
|
||||
'total_requests': total,
|
||||
'pending_review': pending,
|
||||
'scheduled': scheduled,
|
||||
'rejected': rejected,
|
||||
'completed': completed,
|
||||
'users': users,
|
||||
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0
|
||||
'completion_rate': round((scheduled / total * 100), 2) if total > 0 else 0,
|
||||
'availability_coverage': availability_coverage,
|
||||
'available_days_count': days_with_availability if availability else 0
|
||||
})
|
||||
|
||||
|
||||
class UserAppointmentStatsView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
email = request.data.get('email')
|
||||
email = request.data.get('email', self.request.user.email)
|
||||
|
||||
if not self.request.user.is_staff and email != self.request.user.email:
|
||||
return Response(
|
||||
{'error': 'You can only view your own statistics'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
stats = AppointmentRequest.objects.filter(
|
||||
email=email
|
||||
).aggregate(
|
||||
@ -198,5 +305,77 @@ class UserAppointmentStatsView(generics.GenericAPIView):
|
||||
'scheduled': scheduled,
|
||||
'rejected': stats['rejected'],
|
||||
'completed': stats['completed'],
|
||||
'completion_rate': completion_rate
|
||||
'completion_rate': completion_rate,
|
||||
'email': email
|
||||
})
|
||||
|
||||
|
||||
class MatchingAvailabilityView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
try:
|
||||
appointment = AppointmentRequest.objects.get(pk=pk)
|
||||
|
||||
if not request.user.is_staff and appointment.email != request.user.email:
|
||||
return Response(
|
||||
{'error': 'You can only view your own appointments'},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
matching_slots = appointment.get_matching_availability()
|
||||
return Response({
|
||||
'appointment_id': str(appointment.id),
|
||||
'preferences_match_availability': appointment.are_preferences_available(),
|
||||
'matching_slots': matching_slots,
|
||||
'total_matching_slots': len(matching_slots)
|
||||
})
|
||||
|
||||
except AppointmentRequest.DoesNotExist:
|
||||
return Response(
|
||||
{'error': 'Appointment not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
def availability_overview(request):
|
||||
availability = get_admin_availability()
|
||||
if not availability:
|
||||
return Response({
|
||||
'available': False,
|
||||
'message': 'No availability set'
|
||||
})
|
||||
|
||||
all_slots = availability.get_all_available_slots()
|
||||
|
||||
return Response({
|
||||
'available': len(all_slots) > 0,
|
||||
'total_available_slots': len(all_slots),
|
||||
'available_days': list(set(slot['day_name'] for slot in all_slots)),
|
||||
'next_available_dates': get_next_available_dates(7)
|
||||
})
|
||||
|
||||
|
||||
def get_next_available_dates(days_count=7):
|
||||
availability = get_admin_availability()
|
||||
if not availability:
|
||||
return []
|
||||
|
||||
today = timezone.now().date()
|
||||
next_dates = []
|
||||
|
||||
for i in range(1, days_count + 1):
|
||||
date = today + timedelta(days=i)
|
||||
day_of_week = date.weekday()
|
||||
available_slots = availability.get_availability_for_day(day_of_week)
|
||||
|
||||
if available_slots:
|
||||
next_dates.append({
|
||||
'date': date.strftime('%Y-%m-%d'),
|
||||
'day_name': date.strftime('%A'),
|
||||
'available_slots': available_slots
|
||||
})
|
||||
|
||||
return next_dates
|
||||
Loading…
Reference in New Issue
Block a user