1187 lines
40 KiB
Python
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'
|