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

1187 lines
40 KiB
Python

"""
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(
'<span style="background-color: {}; color: white; padding: 3px 10px; '
'border-radius: 3px; font-size: 11px; font-weight: bold;">{}</span>',
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(
'<span style="background-color: {}; color: white; padding: 3px 10px; '
'border-radius: 3px; font-size: 11px; font-weight: bold;">{}</span>',
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(
'<span style="background-color: {}; color: white; padding: 3px 10px; '
'border-radius: 3px; font-size: 11px; font-weight: bold;">{}</span>',
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(
'<span style="background-color: {}; color: white; padding: 3px 10px; '
'border-radius: 3px; font-size: 11px; font-weight: bold;">{}</span>',
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'