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

486 lines
18 KiB
Python

"""
Queue Management API Views for advanced queue operations.
Provides RESTful API endpoints for:
- Queue management
- Patient queue operations
- Queue analytics
- Real-time status
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.utils import timezone
from django.shortcuts import get_object_or_404
from datetime import timedelta
from appointments.models import WaitingQueue, QueueEntry, QueueConfiguration, QueueMetrics
from appointments.queue import AdvancedQueueEngine
class QueueManagementViewSet(viewsets.ViewSet):
"""
API endpoints for queue management operations.
Provides comprehensive queue management including:
- Adding patients to queue
- Getting queue status
- Calling next patient
- Queue repositioning
- Analytics and metrics
"""
permission_classes = [IsAuthenticated]
@action(detail=True, methods=['post'])
def add_patient(self, request, pk=None):
"""
Add a patient to the queue.
POST /api/appointments/queues/{id}/add_patient/
{
"patient_id": 123,
"appointment_id": 456, // optional
"priority_score": 5.0, // optional, default 1.0
"notes": "Urgent case" // optional
}
"""
queue = get_object_or_404(WaitingQueue, pk=pk, tenant=request.user.tenant)
# Validate input
patient_id = request.data.get('patient_id')
if not patient_id:
return Response(
{'error': 'patient_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
# Get patient
from patients.models import PatientProfile
try:
patient = PatientProfile.objects.get(id=patient_id, tenant=request.user.tenant)
except PatientProfile.DoesNotExist:
return Response(
{'error': 'Patient not found'},
status=status.HTTP_404_NOT_FOUND
)
# Get appointment if provided
appointment = None
appointment_id = request.data.get('appointment_id')
if appointment_id:
from appointments.models import AppointmentRequest
try:
appointment = AppointmentRequest.objects.get(
id=appointment_id,
tenant=request.user.tenant
)
except AppointmentRequest.DoesNotExist:
return Response(
{'error': 'Appointment not found'},
status=status.HTTP_404_NOT_FOUND
)
# Get priority score and notes
priority_score = float(request.data.get('priority_score', 1.0))
notes = request.data.get('notes', '')
# Add to queue using engine
engine = AdvancedQueueEngine(queue)
try:
entry = engine.add_to_queue(
patient=patient,
appointment=appointment,
priority_score=priority_score,
notes=notes
)
return Response({
'success': True,
'entry': {
'id': entry.id,
'patient_id': entry.patient.id,
'patient_name': entry.patient.get_full_name(),
'queue_position': entry.queue_position,
'priority_score': entry.priority_score,
'estimated_service_time': entry.estimated_service_time.isoformat() if entry.estimated_service_time else None,
'status': entry.status,
'joined_at': entry.joined_at.isoformat()
}
}, status=status.HTTP_201_CREATED)
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@action(detail=True, methods=['get'])
def status(self, request, pk=None):
"""
Get current queue status.
GET /api/appointments/queues/{id}/status/
"""
queue = get_object_or_404(WaitingQueue, pk=pk, tenant=request.user.tenant)
engine = AdvancedQueueEngine(queue)
return Response(engine.get_queue_status())
@action(detail=True, methods=['post'])
def call_next(self, request, pk=None):
"""
Call the next patient in queue.
POST /api/appointments/queues/{id}/call_next/
"""
queue = get_object_or_404(WaitingQueue, pk=pk, tenant=request.user.tenant)
engine = AdvancedQueueEngine(queue)
next_patient = engine.get_next_patient()
if next_patient:
return Response({
'success': True,
'patient': {
'id': next_patient.id,
'patient_id': next_patient.patient.id,
'patient_name': next_patient.patient.get_full_name(),
'queue_position': next_patient.queue_position,
'priority_score': next_patient.priority_score,
'wait_time_minutes': next_patient.wait_time_minutes,
'status': next_patient.status,
'called_at': next_patient.called_at.isoformat() if next_patient.called_at else None
}
})
else:
return Response({
'success': False,
'message': 'Queue is empty'
}, status=status.HTTP_404_NOT_FOUND)
@action(detail=True, methods=['post'])
def reposition(self, request, pk=None):
"""
Trigger manual queue repositioning.
POST /api/appointments/queues/{id}/reposition/
"""
queue = get_object_or_404(WaitingQueue, pk=pk, tenant=request.user.tenant)
engine = AdvancedQueueEngine(queue)
try:
engine.reposition_queue_entries()
return Response({
'success': True,
'message': 'Queue repositioned successfully',
'status': engine.get_queue_status()
})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@action(detail=True, methods=['get'])
def analytics(self, request, pk=None):
"""
Get queue analytics.
GET /api/appointments/queues/{id}/analytics/?days=7
"""
queue = get_object_or_404(WaitingQueue, pk=pk, tenant=request.user.tenant)
engine = AdvancedQueueEngine(queue)
days = int(request.query_params.get('days', 7))
analytics = engine.get_analytics_summary(days=days)
return Response(analytics)
@action(detail=True, methods=['get'])
def metrics(self, request, pk=None):
"""
Get detailed queue metrics.
GET /api/appointments/queues/{id}/metrics/?date=2025-01-10&hour=14
"""
queue = get_object_or_404(WaitingQueue, pk=pk, tenant=request.user.tenant)
# Get date and hour from query params
date_str = request.query_params.get('date')
hour_str = request.query_params.get('hour')
if date_str and hour_str:
# Get specific hour metrics
from datetime import datetime
date = datetime.strptime(date_str, '%Y-%m-%d').date()
hour = int(hour_str)
try:
metrics = QueueMetrics.objects.get(
queue=queue,
date=date,
hour=hour
)
return Response({
'date': metrics.date.isoformat(),
'hour': metrics.hour,
'total_entries': metrics.total_entries,
'completed_entries': metrics.completed_entries,
'no_shows': metrics.no_shows,
'left_queue': metrics.left_queue,
'average_wait_time_minutes': float(metrics.average_wait_time_minutes),
'max_wait_time_minutes': metrics.max_wait_time_minutes,
'average_service_time_minutes': float(metrics.average_service_time_minutes),
'peak_queue_size': metrics.peak_queue_size,
'average_queue_size': float(metrics.average_queue_size),
'throughput': metrics.throughput,
'utilization_rate': float(metrics.utilization_rate),
'no_show_rate': float(metrics.no_show_rate),
'abandonment_rate': float(metrics.abandonment_rate),
'repositioning_events': metrics.repositioning_events,
'average_load_factor': float(metrics.average_load_factor),
'peak_load_factor': float(metrics.peak_load_factor)
})
except QueueMetrics.DoesNotExist:
return Response(
{'error': 'Metrics not found for specified date and hour'},
status=status.HTTP_404_NOT_FOUND
)
else:
# Get recent metrics
recent_metrics = QueueMetrics.objects.filter(
queue=queue
).order_by('-date', '-hour')[:24] # Last 24 hours
return Response({
'metrics': [
{
'date': m.date.isoformat(),
'hour': m.hour,
'total_entries': m.total_entries,
'completed_entries': m.completed_entries,
'peak_queue_size': m.peak_queue_size,
'average_wait_time_minutes': float(m.average_wait_time_minutes),
'utilization_rate': float(m.utilization_rate),
'no_show_rate': float(m.no_show_rate)
}
for m in recent_metrics
]
})
@action(detail=True, methods=['get', 'put'])
def configuration(self, request, pk=None):
"""
Get or update queue configuration.
GET /api/appointments/queues/{id}/configuration/
PUT /api/appointments/queues/{id}/configuration/
{
"use_dynamic_positioning": true,
"priority_weight": 0.50,
"wait_time_weight": 0.30,
"appointment_time_weight": 0.20,
...
}
"""
queue = get_object_or_404(WaitingQueue, pk=pk, tenant=request.user.tenant)
# Get or create configuration
config, created = QueueConfiguration.objects.get_or_create(queue=queue)
if request.method == 'GET':
return Response({
'use_dynamic_positioning': config.use_dynamic_positioning,
'priority_weight': float(config.priority_weight),
'wait_time_weight': float(config.wait_time_weight),
'appointment_time_weight': float(config.appointment_time_weight),
'enable_overflow_queue': config.enable_overflow_queue,
'overflow_threshold': config.overflow_threshold,
'use_historical_data': config.use_historical_data,
'default_service_time_minutes': config.default_service_time_minutes,
'historical_data_days': config.historical_data_days,
'enable_websocket_updates': config.enable_websocket_updates,
'update_interval_seconds': config.update_interval_seconds,
'load_factor_normal_threshold': float(config.load_factor_normal_threshold),
'load_factor_moderate_threshold': float(config.load_factor_moderate_threshold),
'load_factor_high_threshold': float(config.load_factor_high_threshold),
'auto_reposition_enabled': config.auto_reposition_enabled,
'reposition_interval_minutes': config.reposition_interval_minutes,
'notify_on_position_change': config.notify_on_position_change,
'position_change_threshold': config.position_change_threshold
})
elif request.method == 'PUT':
# Update configuration
for field in [
'use_dynamic_positioning', 'priority_weight', 'wait_time_weight',
'appointment_time_weight', 'enable_overflow_queue', 'overflow_threshold',
'use_historical_data', 'default_service_time_minutes', 'historical_data_days',
'enable_websocket_updates', 'update_interval_seconds',
'load_factor_normal_threshold', 'load_factor_moderate_threshold',
'load_factor_high_threshold', 'auto_reposition_enabled',
'reposition_interval_minutes', 'notify_on_position_change',
'position_change_threshold'
]:
if field in request.data:
setattr(config, field, request.data[field])
try:
config.save()
return Response({
'success': True,
'message': 'Configuration updated successfully'
})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
class QueueEntryViewSet(viewsets.ViewSet):
"""
API endpoints for individual queue entry operations.
"""
permission_classes = [IsAuthenticated]
@action(detail=True, methods=['post'])
def mark_called(self, request, pk=None):
"""
Mark queue entry as called.
POST /api/appointments/queue-entries/{id}/mark_called/
"""
entry = get_object_or_404(
QueueEntry,
pk=pk,
queue__tenant=request.user.tenant
)
if entry.mark_as_called():
return Response({
'success': True,
'entry': {
'id': entry.id,
'status': entry.status,
'called_at': entry.called_at.isoformat() if entry.called_at else None
}
})
else:
return Response(
{'error': 'Cannot mark entry as called'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=True, methods=['post'])
def mark_in_progress(self, request, pk=None):
"""
Mark queue entry as in progress.
POST /api/appointments/queue-entries/{id}/mark_in_progress/
"""
entry = get_object_or_404(
QueueEntry,
pk=pk,
queue__tenant=request.user.tenant
)
if entry.mark_as_in_progress():
return Response({
'success': True,
'entry': {
'id': entry.id,
'status': entry.status,
'served_at': entry.served_at.isoformat() if entry.served_at else None
}
})
else:
return Response(
{'error': 'Cannot mark entry as in progress'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=True, methods=['post'])
def mark_completed(self, request, pk=None):
"""
Mark queue entry as completed.
POST /api/appointments/queue-entries/{id}/mark_completed/
"""
entry = get_object_or_404(
QueueEntry,
pk=pk,
queue__tenant=request.user.tenant
)
if entry.mark_as_completed():
return Response({
'success': True,
'entry': {
'id': entry.id,
'status': entry.status,
'wait_time_minutes': entry.wait_time_minutes
}
})
else:
return Response(
{'error': 'Cannot mark entry as completed'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=True, methods=['post'])
def mark_no_show(self, request, pk=None):
"""
Mark queue entry as no show.
POST /api/appointments/queue-entries/{id}/mark_no_show/
"""
entry = get_object_or_404(
QueueEntry,
pk=pk,
queue__tenant=request.user.tenant
)
if entry.mark_as_no_show():
return Response({
'success': True,
'entry': {
'id': entry.id,
'status': entry.status
}
})
else:
return Response(
{'error': 'Cannot mark entry as no show'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=True, methods=['delete'])
def remove(self, request, pk=None):
"""
Remove entry from queue.
DELETE /api/appointments/queue-entries/{id}/remove/
"""
entry = get_object_or_404(
QueueEntry,
pk=pk,
queue__tenant=request.user.tenant
)
if entry.mark_as_removed():
return Response({
'success': True,
'message': 'Entry removed from queue'
})
else:
return Response(
{'error': 'Cannot remove entry from queue'},
status=status.HTTP_400_BAD_REQUEST
)