from rest_framework import serializers 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): availability_schedule_display = serializers.SerializerMethodField() all_available_slots = serializers.SerializerMethodField() class Meta: model = AdminWeeklyAvailability fields = [ 'id', 'availability_schedule', 'availability_schedule_display', 'all_available_slots', 'created_at', 'updated_at' ] def get_availability_schedule_display(self, obj): if not obj.availability_schedule: return "No availability set" display = {} days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK) 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() formatted_created_at = serializers.ReadOnlyField() formatted_scheduled_datetime = serializers.ReadOnlyField() preferred_dates_display = serializers.ReadOnlyField() preferred_time_slots_display = serializers.ReadOnlyField() has_jitsi_meeting = serializers.ReadOnlyField() jitsi_meet_url = serializers.ReadOnlyField() 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 fields = [ 'id', 'first_name', 'last_name', 'email', 'phone', 'reason', 'preferred_dates', 'preferred_time_slots', 'status', 'scheduled_datetime', 'scheduled_duration', 'rejection_reason', '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', 'meeting_duration_display', 'matching_availability', 'are_preferences_available' ] read_only_fields = [ 'id', 'status', 'scheduled_datetime', 'scheduled_duration', 'rejection_reason', 'jitsi_meet_url', 'jitsi_room_id', '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', 'selected_slots', 'available_slots_info' ] extra_kwargs = { 'preferred_dates': {'required': False}, 'preferred_time_slots': {'required': False} } 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 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.") 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 preferred_time_slots: if slot not in valid_slots: raise serializers.ValidationError(f"Invalid time slot: {slot}. Must be one of {valid_slots}.") 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(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 data def _convert_to_datetime(self, date_obj, time_slot): 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: raise serializers.ValidationError("Duration must be at least 30 minutes.") if value > 240: raise serializers.ValidationError("Duration cannot exceed 4 hours.") return value 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})