"""
Complaints admin
"""
from django.contrib import admin
from django.utils.html import format_html
from .models import (
Complaint,
ComplaintAttachment,
ComplaintCategory,
ComplaintMeeting,
ComplaintPRInteraction,
ComplaintSLAConfig,
ComplaintThreshold,
ComplaintUpdate,
EscalationRule,
Inquiry,
ExplanationSLAConfig
)
admin.site.register(ExplanationSLAConfig)
class ComplaintAttachmentInline(admin.TabularInline):
"""Inline admin for complaint attachments"""
model = ComplaintAttachment
extra = 0
fields = ['file', 'filename', 'file_size', 'uploaded_by', 'description']
readonly_fields = ['file_size']
class ComplaintUpdateInline(admin.TabularInline):
"""Inline admin for complaint updates"""
model = ComplaintUpdate
extra = 1
fields = ['update_type', 'message', 'created_by', 'created_at']
readonly_fields = ['created_at']
ordering = ['-created_at']
@admin.register(Complaint)
class ComplaintAdmin(admin.ModelAdmin):
"""Complaint admin"""
list_display = [
'title_preview', 'complaint_type_badge', 'patient', 'hospital',
'location_hierarchy', 'category',
'severity_badge', 'status_badge', 'sla_indicator',
'created_by', 'assigned_to', 'created_at'
]
list_filter = [
'status', 'severity', 'priority', 'category', 'source',
'location', 'main_section', 'subsection',
'is_overdue', 'hospital', 'created_by', 'created_at'
]
search_fields = [
'title', 'description', 'patient__mrn',
'patient__first_name', 'patient__last_name', 'encounter_id'
]
ordering = ['-created_at']
date_hierarchy = 'created_at'
inlines = [ComplaintUpdateInline, ComplaintAttachmentInline]
fieldsets = (
('Patient & Encounter', {
'fields': ('patient', 'encounter_id')
}),
('Organization', {
'fields': ('hospital', 'department', 'staff')
}),
('Location Hierarchy', {
'fields': ('location', 'main_section', 'subsection')
}),
('Complaint Details', {
'fields': ('complaint_type', 'title', 'description', 'category', 'subcategory')
}),
('Classification', {
'fields': ('priority', 'severity', 'source')
}),
('Creator Tracking', {
'fields': ('created_by',)
}),
('Status & Assignment', {
'fields': ('status', 'assigned_to', 'assigned_at')
}),
('SLA Tracking', {
'fields': ('due_at', 'is_overdue', 'reminder_sent_at', 'escalated_at')
}),
('Resolution', {
'fields': ('resolution', 'resolved_at', 'resolved_by')
}),
('Closure', {
'fields': ('closed_at', 'closed_by', 'resolution_survey', 'resolution_survey_sent_at')
}),
('Metadata', {
'fields': ('metadata', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = [
'assigned_at', 'reminder_sent_at', 'escalated_at',
'resolved_at', 'closed_at', 'resolution_survey_sent_at',
'created_at', 'updated_at'
]
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related(
'patient', 'hospital', 'department', 'staff',
'location', 'main_section', 'subsection',
'assigned_to', 'resolved_by', 'closed_by', 'resolution_survey',
'created_by'
)
def title_preview(self, obj):
"""Show preview of title"""
return obj.title[:60] + '...' if len(obj.title) > 60 else obj.title
title_preview.short_description = 'Title'
def location_hierarchy(self, obj):
"""Display location hierarchy in admin"""
parts = []
if obj.location:
parts.append(obj.location.name)
if obj.main_section:
parts.append(obj.main_section.name)
if obj.subsection:
parts.append(obj.subsection.name)
if not parts:
return '—'
hierarchy = ' → '.join(parts)
return format_html('{}', hierarchy)
location_hierarchy.short_description = 'Location'
def severity_badge(self, obj):
"""Display severity with color badge"""
colors = {
'low': 'info',
'medium': 'warning',
'high': 'danger',
'critical': 'danger',
}
color = colors.get(obj.severity, 'secondary')
return format_html(
'{}',
color,
obj.get_severity_display()
)
severity_badge.short_description = 'Severity'
def status_badge(self, obj):
"""Display status with color badge"""
colors = {
'open': 'danger',
'in_progress': 'warning',
'resolved': 'info',
'closed': 'success',
'cancelled': 'secondary',
}
color = colors.get(obj.status, 'secondary')
return format_html(
'{}',
color,
obj.get_status_display()
)
status_badge.short_description = 'Status'
def complaint_type_badge(self, obj):
"""Display complaint type with color badge"""
colors = {
'complaint': 'danger',
'appreciation': 'success',
}
color = colors.get(obj.complaint_type, 'secondary')
return format_html(
'{}',
color,
obj.get_complaint_type_display()
)
complaint_type_badge.short_description = 'Type'
def sla_indicator(self, obj):
"""Display SLA status"""
if obj.is_overdue:
return format_html('OVERDUE')
from django.utils import timezone
time_remaining = obj.due_at - timezone.now()
hours_remaining = time_remaining.total_seconds() / 3600
if hours_remaining < 4:
return format_html('DUE SOON')
else:
return format_html('ON TIME')
sla_indicator.short_description = 'SLA'
@admin.register(ComplaintAttachment)
class ComplaintAttachmentAdmin(admin.ModelAdmin):
"""Complaint attachment admin"""
list_display = ['complaint', 'filename', 'file_type', 'file_size', 'uploaded_by', 'created_at']
list_filter = ['file_type', 'created_at']
search_fields = ['filename', 'description', 'complaint__title']
ordering = ['-created_at']
fieldsets = (
(None, {
'fields': ('complaint', 'file', 'filename', 'file_type', 'file_size')
}),
('Details', {
'fields': ('uploaded_by', 'description')
}),
('Metadata', {
'fields': ('created_at', 'updated_at')
}),
)
readonly_fields = ['file_size', 'created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('complaint', 'uploaded_by')
@admin.register(ComplaintUpdate)
class ComplaintUpdateAdmin(admin.ModelAdmin):
"""Complaint update admin"""
list_display = ['complaint', 'update_type', 'message_preview', 'created_by', 'created_at']
list_filter = ['update_type', 'created_at']
search_fields = ['message', 'complaint__title']
ordering = ['-created_at']
fieldsets = (
(None, {
'fields': ('complaint', 'update_type', 'message')
}),
('Status Change', {
'fields': ('old_status', 'new_status'),
'classes': ('collapse',)
}),
('Details', {
'fields': ('created_by', 'metadata')
}),
('Metadata', {
'fields': ('created_at', 'updated_at')
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('complaint', 'created_by')
def message_preview(self, obj):
"""Show preview of message"""
return obj.message[:100] + '...' if len(obj.message) > 100 else obj.message
message_preview.short_description = 'Message'
@admin.register(Inquiry)
class InquiryAdmin(admin.ModelAdmin):
"""Inquiry admin"""
list_display = [
'subject_preview', 'patient', 'contact_name',
'hospital', 'category', 'status', 'created_by', 'assigned_to', 'created_at'
]
list_filter = ['status', 'category', 'source', 'hospital', 'created_by', 'created_at']
search_fields = [
'subject', 'message', 'contact_name', 'contact_phone',
'patient__mrn', 'patient__first_name', 'patient__last_name'
]
ordering = ['-created_at']
fieldsets = (
('Patient Information', {
'fields': ('patient',)
}),
('Contact Information (if no patient)', {
'fields': ('contact_name', 'contact_phone', 'contact_email'),
'classes': ('collapse',)
}),
('Organization', {
'fields': ('hospital', 'department')
}),
('Inquiry Details', {
'fields': ('subject', 'message', 'category', 'source')
}),
('Creator Tracking', {
'fields': ('created_by',)
}),
('Status & Assignment', {
'fields': ('status', 'assigned_to')
}),
('Response', {
'fields': ('response', 'responded_at', 'responded_by')
}),
('Metadata', {
'fields': ('created_at', 'updated_at')
}),
)
readonly_fields = ['responded_at', 'created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related(
'patient', 'hospital', 'department',
'assigned_to', 'responded_by', 'created_by'
)
def subject_preview(self, obj):
"""Show preview of subject"""
return obj.subject[:60] + '...' if len(obj.subject) > 60 else obj.subject
subject_preview.short_description = 'Subject'
@admin.register(ComplaintSLAConfig)
class ComplaintSLAConfigAdmin(admin.ModelAdmin):
"""Complaint SLA Configuration admin"""
list_display = [
'hospital', 'source', 'severity', 'priority', 'sla_hours',
'reminder_timing_display', 'is_active'
]
list_filter = ['hospital', 'source', 'severity', 'priority', 'is_active']
search_fields = ['hospital__name_en', 'hospital__name_ar', 'source__name_en']
ordering = ['hospital', 'source', 'severity', 'priority']
fieldsets = (
('Hospital', {
'fields': ('hospital',)
}),
('Source & Classification', {
'fields': ('source', 'severity', 'priority')
}),
('SLA Configuration', {
'fields': ('sla_hours', 'reminder_hours_before')
}),
('Source-Based Timing (Hours After Creation)', {
'fields': (
'first_reminder_hours_after',
'second_reminder_hours_after',
'escalation_hours_after'
),
'description': 'When set, these override the "Hours Before Deadline" timing. Used for source-based SLAs (e.g., MOH, CCHI).'
}),
('Status', {
'fields': ('is_active',)
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('hospital', 'source')
def reminder_timing_display(self, obj):
"""Display reminder timing method"""
if obj.source and obj.first_reminder_hours_after:
return format_html(
'Source-based: {}h / {}h',
obj.first_reminder_hours_after,
obj.second_reminder_hours_after or 'N/A'
)
elif obj.reminder_hours_before:
return format_html(
'Deadline-based: {}h before',
obj.reminder_hours_before
)
else:
return '—'
reminder_timing_display.short_description = 'Reminder Timing'
@admin.register(ComplaintCategory)
class ComplaintCategoryAdmin(admin.ModelAdmin):
"""Complaint Category admin"""
list_display = [
'name_en', 'code', 'hospitals_display', 'parent',
'order', 'is_active'
]
list_filter = ['is_active', 'parent']
search_fields = ['name_en', 'name_ar', 'code', 'description_en']
ordering = ['order', 'name_en']
fieldsets = (
('Hospitals', {
'fields': ('hospitals',)
}),
('Category Details', {
'fields': ('code', 'name_en', 'name_ar')
}),
('Description', {
'fields': ('description_en', 'description_ar'),
'classes': ('collapse',)
}),
('Hierarchy', {
'fields': ('parent', 'order')
}),
('Status', {
'fields': ('is_active',)
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
filter_horizontal = ['hospitals']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('parent').prefetch_related('hospitals')
def hospitals_display(self, obj):
"""Display hospitals for category"""
hospital_count = obj.hospitals.count()
if hospital_count == 0:
return 'System-wide'
elif hospital_count == 1:
return obj.hospitals.first().name
else:
return f'{hospital_count} hospitals'
hospitals_display.short_description = 'Hospitals'
@admin.register(EscalationRule)
class EscalationRuleAdmin(admin.ModelAdmin):
"""Escalation Rule admin"""
list_display = [
'name', 'hospital', 'escalate_to_role',
'trigger_on_overdue', 'order', 'is_active'
]
list_filter = [
'hospital', 'escalate_to_role', 'trigger_on_overdue',
'severity_filter', 'priority_filter', 'is_active'
]
search_fields = ['name', 'description', 'hospital__name_en']
ordering = ['hospital', 'order']
fieldsets = (
('Hospital', {
'fields': ('hospital',)
}),
('Rule Details', {
'fields': ('name', 'description')
}),
('Trigger Conditions', {
'fields': ('trigger_on_overdue', 'trigger_hours_overdue')
}),
('Escalation Target', {
'fields': ('escalate_to_role', 'escalate_to_user')
}),
('Filters', {
'fields': ('severity_filter', 'priority_filter'),
'classes': ('collapse',)
}),
('Order & Status', {
'fields': ('order', 'is_active')
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('hospital', 'escalate_to_user')
@admin.register(ComplaintThreshold)
class ComplaintThresholdAdmin(admin.ModelAdmin):
"""Complaint Threshold admin"""
list_display = [
'hospital', 'threshold_type', 'comparison_display',
'threshold_value', 'action_type', 'is_active'
]
list_filter = [
'hospital', 'threshold_type', 'comparison_operator',
'action_type', 'is_active'
]
search_fields = ['hospital__name_en', 'hospital__name_ar']
ordering = ['hospital', 'threshold_type']
fieldsets = (
('Hospital', {
'fields': ('hospital',)
}),
('Threshold Configuration', {
'fields': ('threshold_type', 'threshold_value', 'comparison_operator')
}),
('Action', {
'fields': ('action_type',)
}),
('Status', {
'fields': ('is_active',)
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('hospital')
def comparison_display(self, obj):
"""Display comparison operator"""
return f"{obj.get_comparison_operator_display()}"
comparison_display.short_description = 'Comparison'
@admin.register(ComplaintPRInteraction)
class ComplaintPRInteractionAdmin(admin.ModelAdmin):
"""PR Interaction admin"""
list_display = [
'complaint', 'contact_date', 'contact_method_display',
'pr_staff', 'procedure_explained', 'created_at'
]
list_filter = [
'contact_method', 'procedure_explained', 'created_at'
]
search_fields = [
'complaint__title', 'statement_text', 'notes',
'pr_staff__first_name', 'pr_staff__last_name'
]
ordering = ['-contact_date']
fieldsets = (
('Complaint', {
'fields': ('complaint',)
}),
('Contact Details', {
'fields': ('contact_date', 'contact_method', 'pr_staff')
}),
('Interaction Details', {
'fields': ('statement_text', 'procedure_explained', 'notes')
}),
('Metadata', {
'fields': ('created_by', 'created_at', 'updated_at')
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('complaint', 'pr_staff', 'created_by')
def contact_method_display(self, obj):
"""Display contact method"""
return obj.get_contact_method_display()
contact_method_display.short_description = 'Method'
@admin.register(ComplaintMeeting)
class ComplaintMeetingAdmin(admin.ModelAdmin):
"""Complaint Meeting admin"""
list_display = [
'complaint', 'meeting_date', 'meeting_type_display',
'outcome_preview', 'created_by', 'created_at'
]
list_filter = [
'meeting_type', 'created_at'
]
search_fields = [
'complaint__title', 'outcome', 'notes'
]
ordering = ['-meeting_date']
fieldsets = (
('Complaint', {
'fields': ('complaint',)
}),
('Meeting Details', {
'fields': ('meeting_date', 'meeting_type')
}),
('Meeting Outcome', {
'fields': ('outcome', 'notes')
}),
('Metadata', {
'fields': ('created_by', 'created_at', 'updated_at')
}),
)
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('complaint', 'created_by')
def meeting_type_display(self, obj):
"""Display meeting type"""
return obj.get_meeting_type_display()
meeting_type_display.short_description = 'Type'
def outcome_preview(self, obj):
"""Show preview of outcome"""
return obj.outcome[:100] + '...' if len(obj.outcome) > 100 else obj.outcome
outcome_preview.short_description = 'Outcome'