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