486 lines
18 KiB
Python
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
|
|
)
|