Marwan Alwali 263292f6be update
2025-11-04 00:50:06 +03:00

208 lines
6.8 KiB
Python

"""
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]