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.
381 lines
14 KiB
Python
381 lines
14 KiB
Python
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 django.utils import timezone
|
|
from datetime import datetime, timedelta
|
|
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,
|
|
AvailabilityCheckSerializer,
|
|
AvailabilityResponseSerializer,
|
|
WeeklyAvailabilitySerializer,
|
|
AdminAvailabilityConfigSerializer
|
|
)
|
|
from .email_service import EmailService
|
|
from users.models import CustomUser
|
|
from django.db.models import Count, Q
|
|
|
|
|
|
class AdminAvailabilityView(generics.RetrieveUpdateAPIView):
|
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
|
|
|
def get_serializer_class(self):
|
|
if self.request.method == 'GET':
|
|
return AdminWeeklyAvailabilitySerializer
|
|
return AdminWeeklyAvailabilityUpdateSerializer
|
|
|
|
def get_object(self):
|
|
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
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
queryset = AppointmentRequest.objects.all()
|
|
|
|
if self.request.user.is_staff:
|
|
return queryset
|
|
|
|
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):
|
|
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]
|
|
queryset = AppointmentRequest.objects.all()
|
|
serializer_class = AppointmentRequestSerializer
|
|
lookup_field = 'pk'
|
|
|
|
|
|
class ScheduleAppointmentView(generics.GenericAPIView):
|
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
|
serializer_class = AppointmentScheduleSerializer
|
|
queryset = AppointmentRequest.objects.all()
|
|
lookup_field = 'pk'
|
|
|
|
def post(self, request, pk):
|
|
appointment = self.get_object()
|
|
|
|
if appointment.status != 'pending_review':
|
|
return Response(
|
|
{'error': 'Only pending review appointments can be scheduled.'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
appointment.schedule_appointment(
|
|
datetime_obj=serializer.validated_data['scheduled_datetime'],
|
|
duration=serializer.validated_data['scheduled_duration']
|
|
)
|
|
|
|
EmailService.send_appointment_scheduled(appointment)
|
|
|
|
response_serializer = AppointmentRequestSerializer(appointment)
|
|
return Response({
|
|
**response_serializer.data,
|
|
'message': 'Appointment scheduled successfully. Jitsi meeting created.',
|
|
'jitsi_meeting_created': appointment.has_jitsi_meeting
|
|
})
|
|
|
|
|
|
class RejectAppointmentView(generics.GenericAPIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = AppointmentRejectSerializer
|
|
queryset = AppointmentRequest.objects.all()
|
|
lookup_field = 'pk'
|
|
|
|
def post(self, request, pk):
|
|
appointment = self.get_object()
|
|
|
|
if appointment.status != 'pending_review':
|
|
return Response(
|
|
{'error': 'Only pending appointments can be rejected'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
appointment.reject_appointment(
|
|
serializer.validated_data.get('rejection_reason', '')
|
|
)
|
|
EmailService.send_appointment_rejected(appointment)
|
|
|
|
response_serializer = AppointmentRequestSerializer(appointment)
|
|
return Response(response_serializer.data)
|
|
|
|
|
|
class AvailableDatesView(generics.GenericAPIView):
|
|
permission_classes = [AllowAny]
|
|
|
|
def get(self, request):
|
|
availability = get_admin_availability()
|
|
if not availability or not availability.availability_schedule:
|
|
return Response([])
|
|
|
|
today = timezone.now().date()
|
|
available_dates = []
|
|
|
|
for i in range(1, 31):
|
|
date = today + timedelta(days=i)
|
|
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
|
|
|
|
def get_queryset(self):
|
|
return AppointmentRequest.objects.filter(
|
|
email=self.request.user.email
|
|
).order_by('-created_at')
|
|
|
|
|
|
class AppointmentStatsView(generics.GenericAPIView):
|
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
|
|
|
def get(self, request):
|
|
total = AppointmentRequest.objects.count()
|
|
users = CustomUser.objects.filter(is_staff=False).count()
|
|
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,
|
|
'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', 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(
|
|
total=Count('id'),
|
|
pending=Count('id', filter=Q(status='pending_review')),
|
|
scheduled=Count('id', filter=Q(status='scheduled')),
|
|
rejected=Count('id', filter=Q(status='rejected')),
|
|
completed=Count('id', filter=Q(status='completed'))
|
|
)
|
|
|
|
total = stats['total']
|
|
scheduled = stats['scheduled']
|
|
completion_rate = round((scheduled / total * 100), 2) if total > 0 else 0
|
|
|
|
return Response({
|
|
'total_requests': total,
|
|
'pending_review': stats['pending'],
|
|
'scheduled': scheduled,
|
|
'rejected': stats['rejected'],
|
|
'completed': stats['completed'],
|
|
'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 |