""" Scheduling utilities for analytics and conflict detection. """ from datetime import datetime, timedelta from typing import List, Dict from django.db.models import Avg, Count, Q from django.utils import timezone class SchedulingAnalytics: """Analytics and reporting for scheduling performance.""" @staticmethod def calculate_provider_utilization(provider, date_range_start, date_range_end): """ Calculate provider utilization rate. Args: provider: User instance (healthcare provider) date_range_start: Start date for calculation date_range_end: End date for calculation Returns: float: Utilization rate percentage """ from appointments.models import SlotAvailability, AppointmentRequest # Total available slots total_slots = SlotAvailability.objects.filter( provider=provider, date__gte=date_range_start, date__lte=date_range_end, is_active=True ).aggregate( total=Count('id') )['total'] or 0 # Booked appointments booked = AppointmentRequest.objects.filter( provider=provider, scheduled_datetime__date__gte=date_range_start, scheduled_datetime__date__lte=date_range_end, status__in=['CONFIRMED', 'COMPLETED', 'IN_PROGRESS'] ).count() if total_slots == 0: return 0.0 return round((booked / total_slots) * 100, 2) @staticmethod def calculate_no_show_rate(provider, date_range_start, date_range_end): """ Calculate no-show rate for provider. Args: provider: User instance (healthcare provider) date_range_start: Start date for calculation date_range_end: End date for calculation Returns: float: No-show rate percentage """ from appointments.models import AppointmentRequest total = AppointmentRequest.objects.filter( provider=provider, scheduled_datetime__date__gte=date_range_start, scheduled_datetime__date__lte=date_range_end ).count() no_shows = AppointmentRequest.objects.filter( provider=provider, scheduled_datetime__date__gte=date_range_start, scheduled_datetime__date__lte=date_range_end, status='NO_SHOW' ).count() if total == 0: return 0.0 return round((no_shows / total) * 100, 2) @staticmethod def update_patient_scheduling_preferences(patient): """ Update patient preferences based on historical data. Args: patient: PatientProfile instance Returns: SchedulingPreference instance or None """ from appointments.models import AppointmentRequest, SchedulingPreference # Get patient's appointment history appointments = AppointmentRequest.objects.filter( patient=patient ).order_by('-scheduled_datetime') if not appointments.exists(): return None # Calculate metrics total = appointments.count() completed = appointments.filter(status='COMPLETED').count() no_shows = appointments.filter(status='NO_SHOW').count() # Extract patterns preferred_days = list(set([ appt.scheduled_datetime.strftime('%A') for appt in appointments.filter(status='COMPLETED')[:20] ])) preferred_times = [] for appt in appointments.filter(status='COMPLETED')[:20]: hour = appt.scheduled_datetime.hour if hour < 12: preferred_times.append('morning') elif hour < 17: preferred_times.append('afternoon') else: preferred_times.append('evening') preferred_times = list(set(preferred_times)) # Update or create preferences prefs, created = SchedulingPreference.objects.update_or_create( tenant=patient.tenant, patient=patient, defaults={ 'preferred_days': preferred_days, 'preferred_times': preferred_times, 'total_appointments': total, 'completed_appointments': completed, 'average_no_show_rate': round((no_shows / total) * 100, 2) if total > 0 else 0 } ) return prefs class ConflictDetector: """Detect and resolve scheduling conflicts.""" @staticmethod def check_conflicts(provider, start_time, end_time, exclude_appointment_id=None): """ Check for scheduling conflicts. Args: provider: User instance (healthcare provider) start_time: Proposed start datetime end_time: Proposed end datetime exclude_appointment_id: Appointment ID to exclude from check Returns: list: List of conflicting AppointmentRequest instances """ from appointments.models import AppointmentRequest conflicts = AppointmentRequest.objects.filter( provider=provider, scheduled_datetime__lt=end_time, scheduled_end_datetime__gt=start_time, status__in=['PENDING', 'CONFIRMED', 'CHECKED_IN', 'IN_PROGRESS'] ) if exclude_appointment_id: conflicts = conflicts.exclude(id=exclude_appointment_id) return list(conflicts) @staticmethod def suggest_alternative_slots(provider, original_time, duration_minutes=30, count=5): """ Suggest alternative slots when conflict exists. Args: provider: User instance (healthcare provider) original_time: Original requested datetime duration_minutes: Appointment duration in minutes count: Number of alternatives to suggest Returns: list: List of alternative slot dictionaries """ from appointments.scheduling.smart_scheduler import SmartScheduler scheduler = SmartScheduler(provider.tenant) # Generate date range (next 7 days) date_range = [ original_time + timedelta(days=i) for i in range(7) ] # Find available slots slots = scheduler._get_available_slots( provider, date_range, duration_minutes ) # Return closest alternatives slots.sort(key=lambda x: abs((x['start'] - original_time).total_seconds())) return slots[:count]