208 lines
6.8 KiB
Python
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]
|