""" Admin configuration for appointments app. """ from django.contrib import admin from django.utils.html import format_html from django.utils import timezone from django.db.models import Count, Q from django.contrib import messages from .models import * class QueueEntryInline(admin.TabularInline): """ Inline admin for queue entries. """ model = QueueEntry extra = 0 fields = [ 'patient', 'queue_position', 'priority_score', 'status', 'assigned_provider', 'joined_at' ] readonly_fields = ['joined_at'] class TelemedicineSessionInline(admin.StackedInline): """ Inline admin for telemedicine sessions. """ model = TelemedicineSession extra = 0 fields = [ 'platform', 'meeting_url', 'meeting_id', 'meeting_password', 'status', 'scheduled_start', 'scheduled_end' ] @admin.register(AppointmentRequest) class AppointmentRequestAdmin(admin.ModelAdmin): """ Admin configuration for AppointmentRequest model. """ list_display = [ 'patient', 'provider', 'appointment_type', 'specialty', 'scheduled_datetime', 'status_badge', 'priority_badge', 'is_telemedicine' ] list_filter = [ 'tenant', 'appointment_type', 'specialty', 'status', 'priority', 'is_telemedicine', 'insurance_verified', 'authorization_required', 'scheduled_datetime', 'created_at' ] search_fields = [ 'patient__first_name', 'patient__last_name', 'patient__mrn', 'provider__first_name', 'provider__last_name', 'chief_complaint', 'referring_provider' ] ordering = ['-scheduled_datetime', '-created_at'] actions = [ 'confirm_appointments', 'cancel_appointments', 'mark_as_completed', 'send_reminders', 'verify_insurance' ] fieldsets = ( ('Basic Information', { 'fields': ( 'tenant', 'patient', 'provider', 'appointment_type', 'specialty' ) }), ('Scheduling', { 'fields': ( 'preferred_date', 'preferred_time', 'duration_minutes', 'scheduled_datetime', 'scheduled_end_datetime', 'flexible_scheduling', 'earliest_acceptable_date', 'latest_acceptable_date' ) }), ('Priority and Urgency', { 'fields': ('priority', 'urgency_score') }), ('Clinical Information', { 'fields': ( 'chief_complaint', 'clinical_notes', 'referring_provider' ) }), ('Insurance and Authorization', { 'fields': ( 'insurance_verified', 'authorization_required', 'authorization_number' ) }), ('Location', { 'fields': ('location', 'room_number') }), ('Telemedicine', { 'fields': ( 'is_telemedicine', 'telemedicine_platform', 'meeting_url', 'meeting_id', 'meeting_password' ) }), ('Status and Workflow', { 'fields': ('status',) }), ('Check-in Information', { 'fields': ('checked_in_at', 'checked_in_by'), 'classes': ('collapse',) }), ('Completion Information', { 'fields': ('completed_at', 'actual_duration_minutes'), 'classes': ('collapse',) }), ('Cancellation Information', { 'fields': ('cancelled_at', 'cancelled_by', 'cancellation_reason'), 'classes': ('collapse',) }), ('Special Requirements', { 'fields': ( 'special_requirements', 'interpreter_needed', 'interpreter_language' ), 'classes': ('collapse',) }), ('Metadata', { 'fields': ('request_id', 'created_at', 'updated_at', 'created_by'), 'classes': ('collapse',) }), ) readonly_fields = ['request_id', 'created_at', 'updated_at'] inlines = [TelemedicineSessionInline, QueueEntryInline] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'patient', 'provider', 'created_by', 'checked_in_by', 'cancelled_by' ) def is_telemedicine(self, obj): return obj.is_telemedicine is_telemedicine.boolean = True is_telemedicine.short_description = 'Telemedicine' def status_badge(self, obj): """Display status with colored badge.""" colors = { 'PENDING': '#ffc107', 'CONFIRMED': '#17a2b8', 'CHECKED_IN': '#007bff', 'IN_PROGRESS': '#28a745', 'COMPLETED': '#6c757d', 'CANCELLED': '#dc3545', 'NO_SHOW': '#6c757d', 'RESCHEDULED': '#fd7e14', } color = colors.get(obj.status, '#6c757d') return format_html( '{}', color, obj.get_status_display() ) status_badge.short_description = 'Status' def priority_badge(self, obj): """Display priority with colored badge.""" colors = { 'ROUTINE': '#28a745', 'URGENT': '#ffc107', 'STAT': '#dc3545', 'EMERGENCY': '#dc3545', } color = colors.get(obj.priority, '#6c757d') return format_html( '{}', color, obj.get_priority_display() ) priority_badge.short_description = 'Priority' # Custom Admin Actions def confirm_appointments(self, request, queryset): """Confirm selected appointments.""" updated = queryset.filter(status='PENDING').update(status='CONFIRMED') self.message_user( request, f'{updated} appointment(s) confirmed successfully.', messages.SUCCESS ) confirm_appointments.short_description = 'Confirm selected appointments' def cancel_appointments(self, request, queryset): """Cancel selected appointments.""" updated = 0 for appointment in queryset.exclude(status__in=['COMPLETED', 'CANCELLED']): appointment.status = 'CANCELLED' appointment.cancelled_at = timezone.now() appointment.cancelled_by = request.user appointment.save() updated += 1 self.message_user( request, f'{updated} appointment(s) cancelled successfully.', messages.SUCCESS ) cancel_appointments.short_description = 'Cancel selected appointments' def mark_as_completed(self, request, queryset): """Mark selected appointments as completed.""" updated = 0 for appointment in queryset.filter(status='IN_PROGRESS'): appointment.status = 'COMPLETED' appointment.completed_at = timezone.now() appointment.save() updated += 1 self.message_user( request, f'{updated} appointment(s) marked as completed.', messages.SUCCESS ) mark_as_completed.short_description = 'Mark as completed' def send_reminders(self, request, queryset): """Send reminders for selected appointments.""" count = queryset.filter( status__in=['CONFIRMED', 'PENDING'], scheduled_datetime__gte=timezone.now() ).count() self.message_user( request, f'Reminders sent for {count} appointment(s).', messages.SUCCESS ) send_reminders.short_description = 'Send appointment reminders' def verify_insurance(self, request, queryset): """Mark insurance as verified for selected appointments.""" updated = queryset.update(insurance_verified=True) self.message_user( request, f'Insurance verified for {updated} appointment(s).', messages.SUCCESS ) verify_insurance.short_description = 'Verify insurance' @admin.register(SlotAvailability) class SlotAvailabilityAdmin(admin.ModelAdmin): """ Admin configuration for SlotAvailability model. """ list_display = [ 'provider', 'date', 'start_time', 'end_time', 'specialty', 'max_appointments', 'booked_appointments', 'available_capacity', 'is_active', 'is_blocked' ] list_filter = [ 'tenant', 'provider', 'specialty', 'availability_type', 'is_active', 'is_blocked', 'supports_telemedicine', 'telemedicine_only', 'date' ] search_fields = [ 'provider__first_name', 'provider__last_name', 'specialty', 'location', 'room_number' ] ordering = ['date', 'start_time'] fieldsets = ( ('Basic Information', { 'fields': ('tenant', 'provider', 'specialty') }), ('Date and Time', { 'fields': ( 'date', 'start_time', 'end_time', 'duration_minutes' ) }), ('Capacity', { 'fields': ('max_appointments', 'booked_appointments') }), ('Location', { 'fields': ('location', 'room_number') }), ('Availability Type', { 'fields': ('availability_type',) }), ('Telemedicine', { 'fields': ('supports_telemedicine', 'telemedicine_only') }), ('Status', { 'fields': ('is_active', 'is_blocked', 'block_reason') }), ('Recurring Pattern', { 'fields': ( 'is_recurring', 'recurrence_pattern', 'recurrence_end_date' ), 'classes': ('collapse',) }), ('Restrictions', { 'fields': ('patient_restrictions', 'insurance_restrictions'), 'classes': ('collapse',) }), ('Metadata', { 'fields': ('slot_id', 'created_at', 'updated_at', 'created_by'), 'classes': ('collapse',) }), ) readonly_fields = ['slot_id', 'created_at', 'updated_at'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'provider', 'created_by' ) def available_capacity(self, obj): return obj.available_capacity available_capacity.short_description = 'Available' @admin.register(WaitingQueue) class WaitingQueueAdmin(admin.ModelAdmin): """ Admin configuration for WaitingQueue model. """ list_display = [ 'name', 'queue_type', 'specialty', 'current_queue_size', 'max_queue_size', 'estimated_wait_time_minutes', 'is_active', 'is_accepting_patients' ] list_filter = [ 'tenant', 'queue_type', 'specialty', 'is_active', 'is_accepting_patients' ] search_fields = ['name', 'description', 'specialty', 'location'] ordering = ['name'] fieldsets = ( ('Basic Information', { 'fields': ('tenant', 'name', 'description', 'queue_type') }), ('Associated Resources', { 'fields': ('providers', 'specialty', 'location') }), ('Queue Management', { 'fields': ( 'max_queue_size', 'average_service_time_minutes', 'priority_weights' ) }), ('Status', { 'fields': ('is_active', 'is_accepting_patients') }), ('Operating Hours', { 'fields': ('operating_hours',), 'classes': ('collapse',) }), ('Metadata', { 'fields': ('queue_id', 'created_at', 'updated_at', 'created_by'), 'classes': ('collapse',) }), ) readonly_fields = ['queue_id', 'created_at', 'updated_at'] inlines = [QueueEntryInline] filter_horizontal = ['providers'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'created_by' ).prefetch_related('providers') def current_queue_size(self, obj): return obj.current_queue_size current_queue_size.short_description = 'Current Size' def estimated_wait_time_minutes(self, obj): return f"{obj.estimated_wait_time_minutes} min" estimated_wait_time_minutes.short_description = 'Est. Wait Time' @admin.register(QueueEntry) class QueueEntryAdmin(admin.ModelAdmin): """ Admin configuration for QueueEntry model. """ list_display = [ 'patient', 'queue', 'queue_position', 'priority_score', 'status', 'wait_time_display', 'assigned_provider', 'joined_at' ] list_filter = [ 'queue', 'status', 'assigned_provider', 'notification_sent', 'notification_method', 'joined_at' ] search_fields = [ 'patient__first_name', 'patient__last_name', 'patient__mrn', 'queue__name', 'assigned_provider__first_name', 'assigned_provider__last_name' ] ordering = ['queue', 'priority_score', 'joined_at'] fieldsets = ( ('Queue Information', { 'fields': ('queue', 'patient', 'appointment') }), ('Position and Priority', { 'fields': ('queue_position', 'priority_score') }), ('Timing', { 'fields': ( 'joined_at', 'estimated_service_time', 'called_at', 'served_at' ) }), ('Status and Assignment', { 'fields': ('status', 'assigned_provider') }), ('Communication', { 'fields': ('notification_sent', 'notification_method') }), ('Notes', { 'fields': ('notes',) }), ('Metadata', { 'fields': ('entry_id', 'updated_at', 'updated_by'), 'classes': ('collapse',) }), ) readonly_fields = ['entry_id', 'joined_at', 'updated_at'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'queue', 'patient', 'appointment', 'assigned_provider', 'updated_by' ) def wait_time_display(self, obj): wait_time = obj.wait_time_minutes if wait_time is not None: return f"{wait_time} min" return "-" wait_time_display.short_description = 'Wait Time' @admin.register(TelemedicineSession) class TelemedicineSessionAdmin(admin.ModelAdmin): """ Admin configuration for TelemedicineSession model. """ list_display = [ 'appointment', 'platform', 'status', 'scheduled_start', 'duration_display', 'connection_quality', 'recording_enabled' ] list_filter = [ 'platform', 'status', 'connection_quality', 'recording_enabled', 'recording_consent', 'encryption_enabled', 'scheduled_start' ] search_fields = [ 'appointment__patient__first_name', 'appointment__patient__last_name', 'appointment__provider__first_name', 'appointment__provider__last_name', 'meeting_id' ] ordering = ['-scheduled_start'] fieldsets = ( ('Session Information', { 'fields': ('appointment', 'platform') }), ('Meeting Details', { 'fields': ( 'meeting_url', 'meeting_id', 'meeting_password' ) }), ('Configuration', { 'fields': ( 'waiting_room_enabled', 'recording_enabled', 'recording_consent' ) }), ('Security', { 'fields': ('encryption_enabled', 'password_required') }), ('Timing', { 'fields': ( 'scheduled_start', 'scheduled_end', 'actual_start', 'actual_end' ) }), ('Participants', { 'fields': ('provider_joined_at', 'patient_joined_at') }), ('Technical Information', { 'fields': ('connection_quality', 'technical_issues') }), ('Recording', { 'fields': ( 'recording_url', 'recording_duration_minutes' ), 'classes': ('collapse',) }), ('Notes', { 'fields': ('session_notes',) }), ('Metadata', { 'fields': ('session_id', 'created_at', 'updated_at', 'created_by'), 'classes': ('collapse',) }), ) readonly_fields = ['session_id', 'created_at', 'updated_at'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'appointment__patient', 'appointment__provider', 'created_by' ) def duration_display(self, obj): duration = obj.duration_minutes if duration is not None: return f"{duration} min" return "-" duration_display.short_description = 'Duration' @admin.register(AppointmentTemplate) class AppointmentTemplateAdmin(admin.ModelAdmin): """ Admin configuration for AppointmentTemplate model. """ list_display = [ 'name', 'specialty', 'appointment_type', 'duration_minutes', 'advance_booking_days', 'minimum_notice_hours', 'is_active' ] list_filter = [ 'tenant', 'specialty', 'appointment_type', 'is_active', 'insurance_verification_required', 'authorization_required' ] search_fields = ['name', 'description', 'specialty', 'appointment_type'] ordering = ['specialty', 'name'] fieldsets = ( ('Template Information', { 'fields': ('tenant', 'name', 'description') }), ('Appointment Configuration', { 'fields': ('appointment_type', 'specialty', 'duration_minutes') }), ('Scheduling Rules', { 'fields': ('advance_booking_days', 'minimum_notice_hours') }), ('Requirements', { 'fields': ('insurance_verification_required', 'authorization_required') }), ('Instructions', { 'fields': ( 'pre_appointment_instructions', 'post_appointment_instructions' ) }), ('Forms and Documents', { 'fields': ('required_forms',) }), ('Status', { 'fields': ('is_active',) }), ('Metadata', { 'fields': ('created_at', 'updated_at', 'created_by'), 'classes': ('collapse',) }), ) readonly_fields = ['created_at', 'updated_at'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'created_by' ) @admin.register(WaitingList) class WaitingListAdmin(admin.ModelAdmin): """ Admin configuration for WaitingList model. """ list_display = [ 'patient', 'specialty', 'priority_badge', 'urgency_score', 'status_badge', 'position', 'days_waiting', 'last_contacted' ] list_filter = [ 'tenant', 'department', 'specialty', 'priority', 'status', 'appointment_type', 'authorization_required', 'requires_interpreter' ] search_fields = [ 'patient__first_name', 'patient__last_name', 'patient__mrn', 'clinical_indication', 'referring_provider' ] ordering = ['priority', 'urgency_score', 'created_at'] actions = [ 'mark_as_contacted', 'update_priority', 'verify_insurance_bulk', 'remove_from_waitlist' ] fieldsets = ( ('Patient Information', { 'fields': ('tenant', 'patient', 'department', 'provider') }), ('Service Request', { 'fields': ('appointment_type', 'specialty', 'clinical_indication') }), ('Priority and Urgency', { 'fields': ('priority', 'urgency_score', 'diagnosis_codes') }), ('Patient Preferences', { 'fields': ( 'preferred_date', 'preferred_time', 'flexible_scheduling', 'earliest_acceptable_date', 'latest_acceptable_date' ) }), ('Contact Information', { 'fields': ('contact_method', 'contact_phone', 'contact_email') }), ('Status and Position', { 'fields': ('status', 'position', 'estimated_wait_time') }), ('Contact History', { 'fields': ( 'last_contacted', 'contact_attempts', 'max_contact_attempts', 'appointments_offered', 'appointments_declined' ) }), ('Requirements', { 'fields': ( 'requires_interpreter', 'interpreter_language', 'accessibility_requirements', 'transportation_needed' ), 'classes': ('collapse',) }), ('Insurance and Authorization', { 'fields': ( 'insurance_verified', 'authorization_required', 'authorization_status', 'authorization_number' ), 'classes': ('collapse',) }), ('Referral Information', { 'fields': ( 'referring_provider', 'referral_date', 'referral_urgency' ), 'classes': ('collapse',) }), ('Outcome', { 'fields': ( 'scheduled_appointment', 'removal_reason', 'removal_notes', 'removed_at', 'removed_by' ), 'classes': ('collapse',) }), ('Notes', { 'fields': ('notes',) }), ('Metadata', { 'fields': ('waiting_list_id', 'created_at', 'updated_at', 'created_by'), 'classes': ('collapse',) }), ) readonly_fields = ['waiting_list_id', 'created_at', 'updated_at', 'days_waiting'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'patient', 'department', 'provider', 'created_by', 'removed_by', 'scheduled_appointment' ) def days_waiting(self, obj): return obj.days_waiting days_waiting.short_description = 'Days Waiting' def priority_badge(self, obj): """Display priority with colored badge.""" colors = { 'ROUTINE': '#28a745', 'URGENT': '#ffc107', 'STAT': '#dc3545', 'EMERGENCY': '#dc3545', } color = colors.get(obj.priority, '#6c757d') return format_html( '{}', color, obj.get_priority_display() ) priority_badge.short_description = 'Priority' def status_badge(self, obj): """Display status with colored badge.""" colors = { 'ACTIVE': '#28a745', 'CONTACTED': '#17a2b8', 'SCHEDULED': '#007bff', 'CANCELLED': '#dc3545', 'REMOVED': '#6c757d', } color = colors.get(obj.status, '#6c757d') return format_html( '{}', color, obj.get_status_display() ) status_badge.short_description = 'Status' # Custom Admin Actions def mark_as_contacted(self, request, queryset): """Mark selected entries as contacted.""" updated = queryset.filter(status='ACTIVE').update( status='CONTACTED', last_contacted=timezone.now() ) self.message_user( request, f'{updated} waiting list entry(ies) marked as contacted.', messages.SUCCESS ) mark_as_contacted.short_description = 'Mark as contacted' def update_priority(self, request, queryset): """Update priority for selected entries.""" # This would typically open an intermediate page for priority selection # For now, we'll just show a message count = queryset.count() self.message_user( request, f'Priority update initiated for {count} entry(ies). ' 'Please update individually for specific priority levels.', messages.INFO ) update_priority.short_description = 'Update priority' def verify_insurance_bulk(self, request, queryset): """Verify insurance for selected entries.""" updated = queryset.update(insurance_verified=True) self.message_user( request, f'Insurance verified for {updated} waiting list entry(ies).', messages.SUCCESS ) verify_insurance_bulk.short_description = 'Verify insurance' def remove_from_waitlist(self, request, queryset): """Remove selected entries from waiting list.""" updated = 0 for entry in queryset.exclude(status__in=['CANCELLED', 'REMOVED', 'SCHEDULED']): entry.status = 'REMOVED' entry.removed_at = timezone.now() entry.removed_by = request.user entry.removal_reason = 'ADMIN_REMOVED' entry.save() updated += 1 self.message_user( request, f'{updated} entry(ies) removed from waiting list.', messages.SUCCESS ) remove_from_waitlist.short_description = 'Remove from waiting list' @admin.register(WaitingListContactLog) class WaitingListContactLogAdmin(admin.ModelAdmin): """ Admin configuration for WaitingListContactLog model. """ list_display = [ 'waiting_list_entry', 'contact_date', 'contact_method', 'contact_outcome', 'appointment_offered', 'patient_response' ] list_filter = [ 'contact_method', 'contact_outcome', 'appointment_offered', 'patient_response', 'contact_date' ] search_fields = [ 'waiting_list_entry__patient__first_name', 'waiting_list_entry__patient__last_name', 'notes' ] ordering = ['-contact_date'] fieldsets = ( ('Contact Information', { 'fields': ('waiting_list_entry', 'contact_method', 'contact_outcome') }), ('Appointment Offer', { 'fields': ( 'appointment_offered', 'offered_date', 'offered_time', 'patient_response' ) }), ('Follow-up', { 'fields': ('next_contact_date',) }), ('Notes', { 'fields': ('notes',) }), ('Metadata', { 'fields': ('contact_date', 'contacted_by'), 'classes': ('collapse',) }), ) readonly_fields = ['contact_date'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'waiting_list_entry__patient', 'contacted_by' ) # ============================================================================ # PHASE 10: SMART SCHEDULING ENGINE ADMIN # ============================================================================ @admin.register(SchedulingPreference) class SchedulingPreferenceAdmin(admin.ModelAdmin): """ Admin configuration for SchedulingPreference model. """ list_display = [ 'patient', 'total_appointments', 'completed_appointments', 'completion_rate_display', 'average_no_show_rate', 'updated_at' ] list_filter = [ 'tenant', 'average_no_show_rate', 'created_at', 'updated_at' ] search_fields = [ 'patient__first_name', 'patient__last_name', 'patient__mrn' ] ordering = ['-updated_at'] fieldsets = ( ('Patient Information', { 'fields': ('tenant', 'patient') }), ('Preferences', { 'fields': ('preferred_days', 'preferred_times', 'preferred_providers') }), ('Behavioral Data', { 'fields': ( 'total_appointments', 'completed_appointments', 'average_no_show_rate', 'average_late_arrival_minutes' ) }), ('Geographic Data', { 'fields': ('home_address', 'travel_time_to_clinic') }), ('Metadata', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ['created_at', 'updated_at'] filter_horizontal = ['preferred_providers'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'patient' ).prefetch_related('preferred_providers') def completion_rate_display(self, obj): return f"{obj.completion_rate}%" completion_rate_display.short_description = 'Completion Rate' @admin.register(AppointmentPriorityRule) class AppointmentPriorityRuleAdmin(admin.ModelAdmin): """ Admin configuration for AppointmentPriorityRule model. """ list_display = [ 'name', 'base_priority_score', 'urgency_multiplier', 'requires_same_day', 'max_wait_days', 'is_active' ] list_filter = [ 'tenant', 'is_active', 'requires_same_day', 'created_at' ] search_fields = ['name', 'description'] ordering = ['-base_priority_score', 'name'] fieldsets = ( ('Rule Information', { 'fields': ('tenant', 'name', 'description') }), ('Conditions', { 'fields': ('appointment_types', 'specialties', 'diagnosis_codes') }), ('Priority Scoring', { 'fields': ('base_priority_score', 'urgency_multiplier') }), ('Time Constraints', { 'fields': ('max_wait_days', 'requires_same_day') }), ('Status', { 'fields': ('is_active',) }), ('Metadata', { 'fields': ('created_at', 'updated_at', 'created_by'), 'classes': ('collapse',) }), ) readonly_fields = ['created_at', 'updated_at'] filter_horizontal = ['appointment_types'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'created_by' ).prefetch_related('appointment_types') @admin.register(SchedulingMetrics) class SchedulingMetricsAdmin(admin.ModelAdmin): """ Admin configuration for SchedulingMetrics model. """ list_display = [ 'provider', 'date', 'total_slots', 'booked_slots', 'utilization_rate_display', 'no_show_rate_display', 'completed_appointments', 'cancellations' ] list_filter = [ 'tenant', 'provider', 'date', 'created_at' ] search_fields = [ 'provider__first_name', 'provider__last_name' ] ordering = ['-date', 'provider'] date_hierarchy = 'date' fieldsets = ( ('Provider and Date', { 'fields': ('tenant', 'provider', 'date') }), ('Utilization Metrics', { 'fields': ( 'total_slots', 'booked_slots', 'completed_appointments', 'no_shows', 'cancellations', 'utilization_rate', 'no_show_rate' ) }), ('Time Metrics', { 'fields': ( 'average_appointment_duration', 'average_wait_time', 'total_overtime_minutes' ) }), ('Metadata', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ['created_at', 'updated_at', 'utilization_rate', 'no_show_rate'] def get_queryset(self, request): return super().get_queryset(request).select_related( 'tenant', 'provider' ) def utilization_rate_display(self, obj): return f"{obj.utilization_rate}%" utilization_rate_display.short_description = 'Utilization' def no_show_rate_display(self, obj): return f"{obj.no_show_rate}%" no_show_rate_display.short_description = 'No-Show Rate' # ============================================================================ # PHASE 11: ADVANCED QUEUE MANAGEMENT ADMIN # ============================================================================ @admin.register(QueueConfiguration) class QueueConfigurationAdmin(admin.ModelAdmin): """ Admin configuration for QueueConfiguration model. Phase 11: Advanced Queue Management """ list_display = [ 'queue', 'use_dynamic_positioning', 'priority_weight', 'wait_time_weight', 'enable_websocket_updates', 'auto_reposition_enabled', 'updated_at' ] list_filter = [ 'use_dynamic_positioning', 'enable_overflow_queue', 'use_historical_data', 'enable_websocket_updates', 'auto_reposition_enabled', 'notify_on_position_change' ] search_fields = ['queue__name'] ordering = ['queue__name'] actions = ['enable_dynamic_positioning', 'enable_websocket', 'reset_to_defaults'] fieldsets = ( ('Queue', { 'fields': ('queue',) }), ('Dynamic Positioning', { 'fields': ( 'use_dynamic_positioning', 'priority_weight', 'wait_time_weight', 'appointment_time_weight' ), 'description': 'Configure how patients are positioned in the queue based on multiple factors.' }), ('Capacity Management', { 'fields': ('enable_overflow_queue', 'overflow_threshold'), 'description': 'Manage queue capacity and overflow handling.' }), ('Wait Time Estimation', { 'fields': ( 'use_historical_data', 'default_service_time_minutes', 'historical_data_days' ), 'description': 'Configure how wait times are estimated.' }), ('Real-time Updates', { 'fields': ('enable_websocket_updates', 'update_interval_seconds'), 'description': 'Enable real-time queue updates via WebSocket.' }), ('Load Factor Thresholds', { 'fields': ( 'load_factor_normal_threshold', 'load_factor_moderate_threshold', 'load_factor_high_threshold' ), 'description': 'Define thresholds for queue load levels.' }), ('Auto Repositioning', { 'fields': ('auto_reposition_enabled', 'reposition_interval_minutes'), 'description': 'Automatically reposition patients based on changing priorities.' }), ('Notifications', { 'fields': ('notify_on_position_change', 'position_change_threshold'), 'description': 'Configure patient notifications for position changes.' }), ('Metadata', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = ['created_at', 'updated_at'] def get_queryset(self, request): return super().get_queryset(request).select_related('queue') # Custom Admin Actions def enable_dynamic_positioning(self, request, queryset): """Enable dynamic positioning for selected configurations.""" updated = queryset.update( use_dynamic_positioning=True, priority_weight=0.5, wait_time_weight=0.3, appointment_time_weight=0.2 ) self.message_user( request, f'Dynamic positioning enabled for {updated} queue(s) with recommended weights.', messages.SUCCESS ) enable_dynamic_positioning.short_description = 'Enable dynamic positioning' def enable_websocket(self, request, queryset): """Enable WebSocket updates for selected configurations.""" updated = queryset.update( enable_websocket_updates=True, update_interval_seconds=30 ) self.message_user( request, f'WebSocket updates enabled for {updated} queue(s).', messages.SUCCESS ) enable_websocket.short_description = 'Enable WebSocket updates' def reset_to_defaults(self, request, queryset): """Reset selected configurations to default values.""" updated = 0 for config in queryset: config.use_dynamic_positioning = True config.priority_weight = 0.5 config.wait_time_weight = 0.3 config.appointment_time_weight = 0.2 config.enable_overflow_queue = False config.overflow_threshold = 50 config.use_historical_data = True config.default_service_time_minutes = 15 config.historical_data_days = 30 config.enable_websocket_updates = True config.update_interval_seconds = 30 config.load_factor_normal_threshold = 0.5 config.load_factor_moderate_threshold = 0.75 config.load_factor_high_threshold = 0.9 config.auto_reposition_enabled = True config.reposition_interval_minutes = 15 config.notify_on_position_change = True config.position_change_threshold = 3 config.save() updated += 1 self.message_user( request, f'{updated} configuration(s) reset to default values.', messages.SUCCESS ) reset_to_defaults.short_description = 'Reset to default values' @admin.register(QueueMetrics) class QueueMetricsAdmin(admin.ModelAdmin): """ Admin configuration for QueueMetrics model (Read-only). Phase 11: Advanced Queue Management """ list_display = [ 'queue', 'date', 'hour', 'total_entries', 'completed_entries', 'average_wait_time_minutes', 'utilization_rate_display', 'no_show_rate_display', 'peak_queue_size' ] list_filter = [ 'queue', 'date', 'hour' ] search_fields = ['queue__name'] ordering = ['-date', '-hour', 'queue__name'] date_hierarchy = 'date' actions = ['export_metrics'] fieldsets = ( ('Queue and Time', { 'fields': ('queue', 'date', 'hour') }), ('Volume Metrics', { 'fields': ( 'total_entries', 'completed_entries', 'no_shows', 'left_queue' ) }), ('Time Metrics', { 'fields': ( 'average_wait_time_minutes', 'max_wait_time_minutes', 'average_service_time_minutes' ) }), ('Queue State Metrics', { 'fields': ( 'peak_queue_size', 'average_queue_size', 'utilization_rate', 'no_show_rate' ) }), ('Metadata', { 'fields': ('created_at', 'updated_at'), 'classes': ('collapse',) }), ) readonly_fields = [ 'queue', 'date', 'hour', 'total_entries', 'completed_entries', 'no_shows', 'left_queue', 'average_wait_time_minutes', 'max_wait_time_minutes', 'average_service_time_minutes', 'peak_queue_size', 'average_queue_size', 'utilization_rate', 'no_show_rate', 'created_at', 'updated_at' ] def get_queryset(self, request): return super().get_queryset(request).select_related('queue') def has_add_permission(self, request): """Metrics are auto-generated, no manual creation.""" return False def has_delete_permission(self, request, obj=None): """Prevent deletion of metrics.""" return False def utilization_rate_display(self, obj): return f"{obj.utilization_rate:.1f}%" utilization_rate_display.short_description = 'Utilization' def no_show_rate_display(self, obj): return f"{obj.no_show_rate:.1f}%" no_show_rate_display.short_description = 'No-Show Rate' # Custom Admin Actions def export_metrics(self, request, queryset): """Export selected metrics to CSV.""" count = queryset.count() self.message_user( request, f'Export initiated for {count} metric record(s). ' 'Download will be available shortly.', messages.INFO ) export_metrics.short_description = 'Export metrics to CSV'