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

281 lines
9.5 KiB
Python

"""
Queue Management Signals
Handles automated queue operations and metrics updates
Part of Phase 11: Advanced Queue Management
"""
from django.db.models.signals import post_save, pre_save, post_delete
from django.dispatch import receiver
from django.utils import timezone
from django.db.models import Avg, Count, Max, F
from datetime import timedelta
from ..models import QueueEntry, QueueConfiguration, QueueMetrics, WaitingQueue
@receiver(post_save, sender=QueueEntry)
def handle_queue_entry_created(sender, instance, created, **kwargs):
"""
Handle new queue entry creation.
- Auto-position patient based on configuration
- Update queue metrics
- Send notifications if configured
"""
if created:
# Get queue configuration
try:
config = QueueConfiguration.objects.get(queue=instance.queue)
# Auto-position if dynamic positioning is enabled
if config.use_dynamic_positioning:
from .queue_engine import AdvancedQueueEngine
engine = AdvancedQueueEngine(instance.queue)
# Calculate optimal position for the new entry
optimal_position = engine.calculate_optimal_position(
patient=instance.patient,
appointment=instance.appointment,
priority_score=instance.priority_score
)
instance.queue_position = optimal_position
instance.save(update_fields=['queue_position'])
except QueueConfiguration.DoesNotExist:
# Create default configuration if it doesn't exist
config = QueueConfiguration.objects.create(queue=instance.queue)
# Update queue size
update_queue_size(instance.queue)
@receiver(pre_save, sender=QueueEntry)
def handle_queue_entry_status_change(sender, instance, **kwargs):
"""
Handle queue entry status changes.
- Track timing for metrics
- Update queue statistics
"""
if instance.pk:
try:
old_instance = QueueEntry.objects.get(pk=instance.pk)
# Track when patient is called
if old_instance.status != 'CALLED' and instance.status == 'CALLED':
instance.called_at = timezone.now()
# Track when patient is served
if old_instance.status != 'SERVED' and instance.status == 'SERVED':
instance.served_at = timezone.now()
except QueueEntry.DoesNotExist:
pass
@receiver(post_save, sender=QueueEntry)
def update_queue_metrics_on_entry_change(sender, instance, **kwargs):
"""
Update queue metrics when entry changes.
"""
# Update hourly metrics
update_hourly_metrics(instance.queue)
@receiver(post_delete, sender=QueueEntry)
def handle_queue_entry_deleted(sender, instance, **kwargs):
"""
Handle queue entry deletion.
- Update queue size
- Reposition remaining patients
"""
# Update queue size
update_queue_size(instance.queue)
# Reposition remaining patients if dynamic positioning is enabled
try:
config = QueueConfiguration.objects.get(queue=instance.queue)
if config.use_dynamic_positioning:
from .queue_engine import AdvancedQueueEngine
engine = AdvancedQueueEngine(instance.queue)
engine.reposition_queue_entries()
except QueueConfiguration.DoesNotExist:
pass
@receiver(post_save, sender=QueueConfiguration)
def handle_configuration_change(sender, instance, created, **kwargs):
"""
Handle queue configuration changes.
- Reposition all patients if weights changed
- Update auto-reposition schedule
"""
if not created:
# Check if positioning weights changed
try:
old_config = QueueConfiguration.objects.get(pk=instance.pk)
weights_changed = (
old_config.priority_weight != instance.priority_weight or
old_config.wait_time_weight != instance.wait_time_weight or
old_config.appointment_time_weight != instance.appointment_time_weight
)
if weights_changed and instance.use_dynamic_positioning:
from .queue_engine import AdvancedQueueEngine
engine = AdvancedQueueEngine(instance.queue)
engine.reposition_queue_entries()
except QueueConfiguration.DoesNotExist:
pass
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
def update_queue_size(queue):
"""
Update the current queue size.
"""
active_count = QueueEntry.objects.filter(
queue=queue,
status__in=['WAITING', 'CALLED']
).count()
# Update queue model if it has a size field
# This would require adding a current_size field to WaitingQueue model
# For now, we'll just return the count
return active_count
def update_hourly_metrics(queue):
"""
Update hourly metrics for the queue.
"""
now = timezone.now()
current_date = now.date()
current_hour = now.hour
# Get or create metrics for current hour
metrics, created = QueueMetrics.objects.get_or_create(
queue=queue,
date=current_date,
hour=current_hour,
defaults={
'total_entries': 0,
'completed_entries': 0,
'no_shows': 0,
'left_queue': 0,
'average_wait_time_minutes': 0,
'max_wait_time_minutes': 0,
'min_wait_time_minutes': 0
}
)
# Calculate metrics for this hour
hour_start = timezone.make_aware(
timezone.datetime.combine(current_date, timezone.datetime.min.time())
) + timedelta(hours=current_hour)
hour_end = hour_start + timedelta(hours=1)
entries = QueueEntry.objects.filter(
queue=queue,
joined_at__gte=hour_start,
joined_at__lt=hour_end
)
# Update counts
metrics.total_entries = entries.count()
metrics.completed_entries = entries.filter(status='SERVED').count()
metrics.no_shows = entries.filter(status='NO_SHOW').count()
metrics.left_queue = entries.filter(status__in=['CANCELLED', 'LEFT']).count()
# Calculate wait times
completed = entries.filter(status='SERVED', served_at__isnull=False)
if completed.exists():
wait_times = []
for entry in completed:
wait_time = (entry.served_at - entry.joined_at).total_seconds() / 60
wait_times.append(wait_time)
metrics.average_wait_time_minutes = sum(wait_times) / len(wait_times)
metrics.max_wait_time_minutes = max(wait_times)
# Calculate service times
served = entries.filter(status='SERVED', called_at__isnull=False, served_at__isnull=False)
if served.exists():
service_times = []
for entry in served:
service_time = (entry.served_at - entry.called_at).total_seconds() / 60
service_times.append(service_time)
metrics.average_service_time_minutes = sum(service_times) / len(service_times)
# Calculate queue size metrics
# This would require tracking queue size over time
# For now, use current size as peak
current_size = update_queue_size(queue)
metrics.peak_queue_size = max(metrics.peak_queue_size, current_size)
metrics.average_queue_size = current_size # Simplified
# Calculate rates
if metrics.total_entries > 0:
metrics.utilization_rate = (metrics.completed_entries / metrics.total_entries) * 100
metrics.no_show_rate = (metrics.no_shows / metrics.total_entries) * 100
metrics.save()
def auto_reposition_queues():
"""
Automatically reposition all queues that have auto-repositioning enabled.
This should be called periodically (e.g., via Celery task).
"""
from .queue_engine import AdvancedQueueEngine
configs = QueueConfiguration.objects.filter(
auto_reposition_enabled=True,
queue__is_active=True
)
repositioned_count = 0
for config in configs:
try:
engine = AdvancedQueueEngine(config.queue)
engine.reposition_queue_entries()
repositioned_count += 1
except Exception as e:
print(f"Error repositioning queue {config.queue.name}: {e}")
return repositioned_count
def cleanup_old_metrics(days=90):
"""
Clean up old queue metrics.
This should be called periodically to prevent database bloat.
"""
cutoff_date = timezone.now().date() - timedelta(days=days)
deleted_count, _ = QueueMetrics.objects.filter(date__lt=cutoff_date).delete()
return deleted_count
def send_position_change_notification(entry, old_position, new_position):
"""
Send notification to patient about position change.
"""
try:
config = QueueConfiguration.objects.get(queue=entry.queue)
if not config.notify_on_position_change:
return
# Check if change is significant enough
position_change = abs(new_position - old_position)
if position_change < config.position_change_threshold:
return
# TODO: Implement actual notification sending
# This could use SMS, email, or push notifications
# For now, just log it
print(f"Position change notification: {entry.patient} moved from {old_position} to {new_position}")
except QueueConfiguration.DoesNotExist:
pass