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": {
"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'
}

View File

@ -1,33 +1,87 @@
from django import forms
from django.contrib import admin
from .models import AdminWeeklyAvailability, AppointmentRequest
class AdminWeeklyAvailabilityForm(forms.ModelForm):
class Meta:
model = AdminWeeklyAvailability
fields = '__all__'
widgets = {
'availability_schedule': forms.HiddenInput()
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
field_name = f'day_{day_num}'
self.fields[field_name] = forms.MultipleChoiceField(
choices=AdminWeeklyAvailability.TIME_SLOT_CHOICES,
required=False,
label=day_name,
widget=forms.CheckboxSelectMultiple
)
if self.instance.availability_schedule:
self.fields[field_name].initial = self.instance.availability_schedule.get(str(day_num), [])
def save(self, commit=True):
instance = super().save(commit=False)
availability_schedule = {}
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
field_name = f'day_{day_num}'
time_slots = self.cleaned_data.get(field_name, [])
if time_slots:
availability_schedule[str(day_num)] = time_slots
instance.availability_schedule = availability_schedule
if commit:
instance.save()
return instance
@admin.register(AdminWeeklyAvailability)
class AdminWeeklyAvailabilityAdmin(admin.ModelAdmin):
list_display = ['available_days_display', 'created_at']
form = AdminWeeklyAvailabilityForm
list_display = ['__str__', 'created_at', 'updated_at']
def available_days_display(self, obj):
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
return ', '.join([days_map[day] for day in obj.available_days])
available_days_display.short_description = 'Available Days'
def has_add_permission(self, request):
if self.model.objects.count() >= 1:
return False
return super().has_add_permission(request)
@admin.register(AppointmentRequest)
class AppointmentRequestAdmin(admin.ModelAdmin):
list_display = ['full_name', 'email', 'status', 'created_at', 'scheduled_datetime']
list_filter = ['status', 'created_at']
list_display = ['full_name', 'email', 'status', 'formatted_created_at', 'formatted_scheduled_datetime']
list_filter = ['status', 'created_at', 'scheduled_datetime']
search_fields = ['first_name', 'last_name', 'email']
readonly_fields = ['created_at', 'updated_at']
actions = ['mark_as_scheduled', 'mark_as_rejected']
readonly_fields = ['id', 'created_at', 'updated_at', 'formatted_created_at', 'formatted_scheduled_datetime']
def mark_as_scheduled(self, request, queryset):
for appointment in queryset:
if appointment.status == 'pending_review':
appointment.status = 'scheduled'
appointment.save()
mark_as_scheduled.short_description = "Mark selected as scheduled"
fieldsets = (
('Personal Information', {
'fields': ('first_name', 'last_name', 'email', 'phone', 'reason')
}),
('Appointment Preferences', {
'fields': ('preferred_dates', 'preferred_time_slots')
}),
('Scheduling', {
'fields': ('status', 'scheduled_datetime', 'scheduled_duration', 'rejection_reason')
}),
('Video Meeting', {
'fields': ('jitsi_meet_url', 'jitsi_room_id')
}),
('Metadata', {
'fields': ('id', 'created_at', 'updated_at', 'formatted_created_at', 'formatted_scheduled_datetime'),
'classes': ('collapse',)
}),
)
def mark_as_rejected(self, request, queryset):
for appointment in queryset:
if appointment.status == 'pending_review':
appointment.status = 'rejected'
appointment.save()
mark_as_rejected.short_description = "Mark selected as rejected"
def formatted_created_at(self, obj):
return obj.formatted_created_at
formatted_created_at.short_description = 'Created At'
def formatted_scheduled_datetime(self, obj):
return obj.formatted_scheduled_datetime
formatted_scheduled_datetime.short_description = 'Scheduled Date Time'

View File

@ -90,9 +90,15 @@ class AdminWeeklyAvailability(models.Model):
(6, 'Sunday'),
]
available_days = models.JSONField(
default=list,
help_text="List of weekdays (0-6) when appointments are accepted"
TIME_SLOT_CHOICES = [
('morning', 'Morning (9AM - 12PM)'),
('afternoon', 'Afternoon (1PM - 5PM)'),
('evening', 'Evening (6PM - 9PM)'),
]
availability_schedule = models.JSONField(
default=dict,
help_text="Dictionary with days as keys and lists of time slots as values"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -101,9 +107,59 @@ class AdminWeeklyAvailability(models.Model):
verbose_name = 'Admin Weekly Availability'
verbose_name_plural = 'Admin Weekly Availability'
def set_availability(self, day, time_slots):
if not self.availability_schedule:
self.availability_schedule = {}
if day not in [str(d[0]) for d in self.DAYS_OF_WEEK]:
raise ValueError(f"Invalid day: {day}")
valid_slots = [slot[0] for slot in self.TIME_SLOT_CHOICES]
for slot in time_slots:
if slot not in valid_slots:
raise ValueError(f"Invalid time slot: {slot}")
self.availability_schedule[str(day)] = time_slots
def get_availability_for_day(self, day):
return self.availability_schedule.get(str(day), [])
def is_available(self, day, time_slot):
return time_slot in self.get_availability_for_day(day)
def get_all_available_slots(self):
available_slots = []
for day_num, time_slots in self.availability_schedule.items():
day_name = dict(self.DAYS_OF_WEEK).get(int(day_num))
for time_slot in time_slots:
time_display = dict(self.TIME_SLOT_CHOICES).get(time_slot)
available_slots.append({
'day_num': int(day_num),
'day_name': day_name,
'time_slot': time_slot,
'time_display': time_display
})
return available_slots
def clear_availability(self, day=None):
if day is None:
self.availability_schedule = {}
else:
day_str = str(day)
if day_str in self.availability_schedule:
del self.availability_schedule[day_str]
def __str__(self):
days = [self.DAYS_OF_WEEK[day][1] for day in self.available_days]
return f"Available: {', '.join(days)}"
if not self.availability_schedule:
return "No availability set"
display_strings = []
for day_num, time_slots in sorted(self.availability_schedule.items()):
day_name = dict(self.DAYS_OF_WEEK).get(int(day_num))
slot_displays = [dict(self.TIME_SLOT_CHOICES).get(slot) for slot in time_slots]
display_strings.append(f"{day_name}: {', '.join(slot_displays)}")
return " | ".join(display_strings)
class AppointmentRequest(models.Model):
STATUS_CHOICES = [
@ -295,5 +351,101 @@ class AppointmentRequest(models.Model):
else:
return "Ended"
def get_available_time_slots_for_date(self, date_str):
try:
from datetime import datetime
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
day_of_week = date_obj.weekday()
availability = AdminWeeklyAvailability.objects.first()
if not availability:
return []
return availability.get_availability_for_day(day_of_week)
except Exception as e:
print(f"Error getting available slots: {e}")
return []
def are_preferences_available(self):
availability = AdminWeeklyAvailability.objects.first()
if not availability:
return False
for date_str in self.preferred_dates:
try:
from datetime import datetime
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
day_of_week = date_obj.weekday()
available_slots = availability.get_availability_for_day(day_of_week)
if any(slot in available_slots for slot in self.preferred_time_slots):
return True
except Exception as e:
print(f"Error checking availability for {date_str}: {e}")
continue
return False
def get_matching_availability(self):
availability = AdminWeeklyAvailability.objects.first()
if not availability:
return []
matching_slots = []
for date_str in self.preferred_dates:
try:
from datetime import datetime
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
day_of_week = date_obj.weekday()
day_name = dict(AdminWeeklyAvailability.DAYS_OF_WEEK).get(day_of_week)
available_slots = availability.get_availability_for_day(day_of_week)
matching_time_slots = [slot for slot in self.preferred_time_slots if slot in available_slots]
if matching_time_slots:
matching_slots.append({
'date': date_str,
'day_name': day_name,
'available_slots': matching_time_slots,
'date_obj': date_obj
})
except Exception as e:
print(f"Error processing {date_str}: {e}")
continue
return matching_slots
def __str__(self):
return f"{self.full_name} - {self.get_status_display()} - {self.created_at.strftime('%Y-%m-%d')}"
def get_admin_availability():
availability, created = AdminWeeklyAvailability.objects.get_or_create(
id=1
)
return availability
def set_admin_availability(availability_dict):
availability = get_admin_availability()
for day, time_slots in availability_dict.items():
availability.set_availability(day, time_slots)
availability.save()
return availability
def get_available_slots_for_week():
availability = get_admin_availability()
return availability.get_all_available_slots()
def check_date_availability(date_str):
try:
from datetime import datetime
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
day_of_week = date_obj.weekday()
availability = get_admin_availability()
return availability.get_availability_for_day(day_of_week)
except Exception as e:
print(f"Error checking date availability: {e}")
return []

View File

@ -1,19 +1,64 @@
from rest_framework import serializers
from .models import AdminWeeklyAvailability, AppointmentRequest
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, check_date_availability
from django.utils import timezone
from datetime import datetime, timedelta
import json
class AdminWeeklyAvailabilitySerializer(serializers.ModelSerializer):
available_days_display = serializers.SerializerMethodField()
availability_schedule_display = serializers.SerializerMethodField()
all_available_slots = serializers.SerializerMethodField()
class Meta:
model = AdminWeeklyAvailability
fields = ['id', 'available_days', 'available_days_display', 'created_at', 'updated_at']
fields = [
'id', 'availability_schedule', 'availability_schedule_display',
'all_available_slots', 'created_at', 'updated_at'
]
def get_available_days_display(self, obj):
def get_availability_schedule_display(self, obj):
if not obj.availability_schedule:
return "No availability set"
display = {}
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
return [days_map[day] for day in obj.available_days]
time_slots_map = dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES)
for day_num, time_slots in obj.availability_schedule.items():
day_name = days_map.get(int(day_num))
slot_names = [time_slots_map.get(slot, slot) for slot in time_slots]
display[day_name] = slot_names
return display
def get_all_available_slots(self, obj):
return obj.get_all_available_slots()
class AdminWeeklyAvailabilityUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = AdminWeeklyAvailability
fields = ['availability_schedule']
def validate_availability_schedule(self, value):
if not isinstance(value, dict):
raise serializers.ValidationError("Availability schedule must be a dictionary.")
valid_days = [str(day[0]) for day in AdminWeeklyAvailability.DAYS_OF_WEEK]
valid_slots = [slot[0] for slot in AdminWeeklyAvailability.TIME_SLOT_CHOICES]
for day, time_slots in value.items():
if day not in valid_days:
raise serializers.ValidationError(f"Invalid day: {day}. Must be one of {valid_days}.")
if not isinstance(time_slots, list):
raise serializers.ValidationError(f"Time slots for day {day} must be a list.")
for slot in time_slots:
if slot not in valid_slots:
raise serializers.ValidationError(
f"Invalid time slot: {slot} for day {day}. Must be one of {valid_slots}."
)
return value
class AppointmentRequestSerializer(serializers.ModelSerializer):
full_name = serializers.ReadOnlyField()
@ -26,6 +71,9 @@ class AppointmentRequestSerializer(serializers.ModelSerializer):
jitsi_room_id = serializers.ReadOnlyField()
can_join_meeting = serializers.ReadOnlyField()
meeting_status = serializers.ReadOnlyField()
meeting_duration_display = serializers.ReadOnlyField()
matching_availability = serializers.SerializerMethodField()
are_preferences_available = serializers.SerializerMethodField()
class Meta:
model = AppointmentRequest
@ -36,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})

View File

@ -1,28 +1,39 @@
from django.urls import path
from .views import (
AdminAvailabilityView,
AdminAvailabilityConfigView,
AppointmentRequestListView,
AppointmentRequestCreateView,
AppointmentRequestDetailView,
ScheduleAppointmentView,
RejectAppointmentView,
AvailableDatesView,
CheckDateAvailabilityView,
WeeklyAvailabilityView,
UserAppointmentsView,
AppointmentStatsView,
UserAppointmentStatsView
UserAppointmentStatsView,
MatchingAvailabilityView,
availability_overview
)
urlpatterns = [
path('admin/availability/', AdminAvailabilityView.as_view(), name='admin-availability'),
path('availability/config/', AdminAvailabilityConfigView.as_view(), name='availability-config'),
path('availability/check/', CheckDateAvailabilityView.as_view(), name='check-availability'),
path('availability/weekly/', WeeklyAvailabilityView.as_view(), name='weekly-availability'),
path('availability/overview/', availability_overview, name='availability-overview'),
path('appointments/', AppointmentRequestListView.as_view(), name='appointment-list'),
path('appointments/create/', AppointmentRequestCreateView.as_view(), name='appointment-create'),
path('appointments/<uuid:pk>/', AppointmentRequestDetailView.as_view(), name='appointment-detail'),
path('appointments/<uuid:pk>/matching-availability/', MatchingAvailabilityView.as_view(), name='matching-availability'),
path('appointments/<uuid:pk>/schedule/', ScheduleAppointmentView.as_view(), name='appointment-schedule'),
path('appointments/<uuid:pk>/reject/', RejectAppointmentView.as_view(), name='appointment-reject'),
path('appointments/available-dates/', AvailableDatesView.as_view(), name='available-dates'),
path('user/appointments/', UserAppointmentsView.as_view(), name='user-appointments'),
path('appointments/stats/', AppointmentStatsView.as_view(), name='appointment-stats'),

View File

@ -1,16 +1,21 @@
from rest_framework import generics, status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny,IsAdminUser
from rest_framework.permissions import IsAuthenticated, AllowAny, IsAdminUser
from django.utils import timezone
from datetime import datetime, timedelta
from .models import AdminWeeklyAvailability, AppointmentRequest
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, set_admin_availability, get_available_slots_for_week, check_date_availability
from .serializers import (
AdminWeeklyAvailabilitySerializer,
AdminWeeklyAvailabilityUpdateSerializer,
AppointmentRequestSerializer,
AppointmentRequestCreateSerializer,
AppointmentScheduleSerializer,
AppointmentRejectSerializer
AppointmentRejectSerializer,
AvailabilityCheckSerializer,
AvailabilityResponseSerializer,
WeeklyAvailabilitySerializer,
AdminAvailabilityConfigSerializer
)
from .email_service import EmailService
from users.models import CustomUser
@ -19,13 +24,30 @@ from django.db.models import Count, Q
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
permission_classes = [IsAuthenticated, IsAdminUser]
serializer_class = AdminWeeklyAvailabilitySerializer
def get_serializer_class(self):
if self.request.method == 'GET':
return AdminWeeklyAvailabilitySerializer
return AdminWeeklyAvailabilityUpdateSerializer
def get_object(self):
obj, created = AdminWeeklyAvailability.objects.get_or_create(
defaults={'available_days': []}
)
return obj
return get_admin_availability()
def update(self, request, *args, **kwargs):
response = super().update(request, *args, **kwargs)
availability = self.get_object()
full_serializer = AdminWeeklyAvailabilitySerializer(availability)
return Response(full_serializer.data)
class AdminAvailabilityConfigView(generics.GenericAPIView):
permission_classes = [AllowAny]
def get(self, request):
"""Get availability configuration"""
config = AdminAvailabilityConfigSerializer.get_default_config()
return Response(config.data)
class AppointmentRequestListView(generics.ListAPIView):
serializer_class = AppointmentRequestSerializer
@ -39,25 +61,20 @@ class AppointmentRequestListView(generics.ListAPIView):
return queryset.filter(email=self.request.user.email)
class AppointmentRequestCreateView(generics.CreateAPIView):
permission_classes = [IsAuthenticated]
queryset = AppointmentRequest.objects.all()
serializer_class = AppointmentRequestCreateSerializer
def perform_create(self, serializer):
availability = AdminWeeklyAvailability.objects.first()
if availability:
available_days = availability.available_days
preferred_dates = serializer.validated_data['preferred_dates']
for date_str in preferred_dates:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
if date_obj.weekday() not in available_days:
from rest_framework.exceptions import ValidationError
raise ValidationError(f'Date {date_str} is not available for appointments.')
appointment = serializer.save()
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