2025-11-23 00:19:26 +00:00
|
|
|
from rest_framework import serializers
|
2025-11-26 19:30:26 +00:00
|
|
|
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, check_date_availability
|
2025-11-23 00:19:26 +00:00
|
|
|
from django.utils import timezone
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
class AdminWeeklyAvailabilitySerializer(serializers.ModelSerializer):
|
2025-11-26 19:30:26 +00:00
|
|
|
availability_schedule_display = serializers.SerializerMethodField()
|
|
|
|
|
all_available_slots = serializers.SerializerMethodField()
|
2025-11-23 00:19:26 +00:00
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
|
model = AdminWeeklyAvailability
|
2025-11-26 19:30:26 +00:00
|
|
|
fields = [
|
|
|
|
|
'id', 'availability_schedule', 'availability_schedule_display',
|
|
|
|
|
'all_available_slots', 'created_at', 'updated_at'
|
|
|
|
|
]
|
2025-11-23 00:19:26 +00:00
|
|
|
|
2025-11-26 19:30:26 +00:00
|
|
|
def get_availability_schedule_display(self, obj):
|
|
|
|
|
if not obj.availability_schedule:
|
|
|
|
|
return "No availability set"
|
|
|
|
|
|
|
|
|
|
display = {}
|
2025-11-23 00:19:26 +00:00
|
|
|
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
|
2025-11-26 19:30:26 +00:00
|
|
|
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
|
2025-11-23 00:19:26 +00:00
|
|
|
|
|
|
|
|
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()
|
2025-11-26 19:30:26 +00:00
|
|
|
meeting_duration_display = serializers.ReadOnlyField()
|
|
|
|
|
matching_availability = serializers.SerializerMethodField()
|
|
|
|
|
are_preferences_available = serializers.SerializerMethodField()
|
2025-11-23 00:19:26 +00:00
|
|
|
|
|
|
|
|
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',
|
2025-11-26 19:30:26 +00:00
|
|
|
'has_jitsi_meeting', 'can_join_meeting', 'meeting_status',
|
|
|
|
|
'meeting_duration_display', 'matching_availability', 'are_preferences_available'
|
2025-11-23 00:19:26 +00:00
|
|
|
]
|
|
|
|
|
read_only_fields = [
|
|
|
|
|
'id', 'status', 'scheduled_datetime', 'scheduled_duration',
|
|
|
|
|
'rejection_reason', 'jitsi_meet_url', 'jitsi_room_id',
|
|
|
|
|
'created_at', 'updated_at'
|
|
|
|
|
]
|
2025-11-26 19:30:26 +00:00
|
|
|
|
|
|
|
|
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()
|
2025-11-23 00:19:26 +00:00
|
|
|
|
|
|
|
|
class AppointmentRequestCreateSerializer(serializers.ModelSerializer):
|
2025-11-26 19:30:26 +00:00
|
|
|
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)
|
|
|
|
|
|
2025-11-23 00:19:26 +00:00
|
|
|
class Meta:
|
|
|
|
|
model = AppointmentRequest
|
|
|
|
|
fields = [
|
|
|
|
|
'first_name', 'last_name', 'email', 'phone', 'reason',
|
2025-11-26 19:30:26 +00:00
|
|
|
'preferred_dates', 'preferred_time_slots', 'selected_slots',
|
|
|
|
|
'available_slots_info'
|
2025-11-23 00:19:26 +00:00
|
|
|
]
|
2025-11-26 19:30:26 +00:00
|
|
|
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'."
|
|
|
|
|
)
|
2025-11-23 00:19:26 +00:00
|
|
|
|
2025-11-26 19:30:26 +00:00
|
|
|
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:
|
2025-11-23 00:19:26 +00:00
|
|
|
raise serializers.ValidationError("At least one preferred date is required.")
|
|
|
|
|
|
|
|
|
|
today = timezone.now().date()
|
2025-11-26 19:30:26 +00:00
|
|
|
for date_str in preferred_dates:
|
2025-11-23 00:19:26 +00:00
|
|
|
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.")
|
2025-11-26 19:30:26 +00:00
|
|
|
|
|
|
|
|
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')}"
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-23 00:19:26 +00:00
|
|
|
except ValueError:
|
|
|
|
|
raise serializers.ValidationError(f"Invalid date format: {date_str}. Use YYYY-MM-DD.")
|
|
|
|
|
|
2025-11-26 19:30:26 +00:00
|
|
|
if not preferred_time_slots or len(preferred_time_slots) == 0:
|
2025-11-23 00:19:26 +00:00
|
|
|
raise serializers.ValidationError("At least one time slot is required.")
|
|
|
|
|
|
|
|
|
|
valid_slots = ['morning', 'afternoon', 'evening']
|
2025-11-26 19:30:26 +00:00
|
|
|
for slot in preferred_time_slots:
|
2025-11-23 00:19:26 +00:00
|
|
|
if slot not in valid_slots:
|
|
|
|
|
raise serializers.ValidationError(f"Invalid time slot: {slot}. Must be one of {valid_slots}.")
|
|
|
|
|
|
2025-11-26 19:30:26 +00:00
|
|
|
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)
|
|
|
|
|
|
2025-11-23 00:19:26 +00:00
|
|
|
class AppointmentScheduleSerializer(serializers.Serializer):
|
|
|
|
|
scheduled_datetime = serializers.DateTimeField()
|
|
|
|
|
scheduled_duration = serializers.IntegerField(default=60, min_value=30, max_value=240)
|
2025-11-26 19:30:26 +00:00
|
|
|
date_str = serializers.CharField(required=False, write_only=True)
|
|
|
|
|
time_slot = serializers.CharField(required=False, write_only=True)
|
2025-11-23 00:19:26 +00:00
|
|
|
|
2025-11-26 19:30:26 +00:00
|
|
|
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():
|
2025-11-23 00:19:26 +00:00
|
|
|
raise serializers.ValidationError("Scheduled datetime must be in the future.")
|
2025-11-26 19:30:26 +00:00
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
)
|
2025-11-23 00:19:26 +00:00
|
|
|
|
|
|
|
|
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):
|
2025-11-26 19:30:26 +00:00
|
|
|
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})
|