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') def post(self, request, *args, **kwargs): email = request.data.get('email') if not email: return Response( {"error": "Email is required"}, status=status.HTTP_400_BAD_REQUEST ) if email != request.user.email: return Response( {"error": "You can only view your own appointments"}, status=status.HTTP_403_FORBID_REQUEST ) appointments = AppointmentRequest.objects.filter(email__iexact=email).order_by('-created_at') serializer = self.get_serializer(appointments, many=True) return Response(serializer.data) 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 ) appointments = AppointmentRequest.objects.filter(email__iexact=email) stats = { 'total': appointments.count(), 'pending': appointments.filter(status='pending_review').count(), 'scheduled': appointments.filter(status='scheduled').count(), 'rejected': appointments.filter(status='rejected').count(), 'completed': appointments.filter(status='completed').count(), } 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