"""
Complaints admin
"""
from django.contrib import admin
from django.utils.html import format_html, mark_safe
from .models import (
Complaint,
ComplaintAttachment,
ComplaintCategory,
ComplaintMeeting,
ComplaintPRInteraction,
ComplaintSLAConfig,
ComplaintThreshold,
ComplaintUpdate,
EscalationRule,
Inquiry,
ExplanationSLAConfig,
ComplaintInvolvedDepartment,
ComplaintInvolvedStaff,
OnCallAdminSchedule,
OnCallAdmin,
ComplaintAdverseAction,
ComplaintAdverseActionAttachment,
)
class ExplanationSLAConfigAdmin(admin.ModelAdmin):
list_display = (
"hospital",
"response_hours",
"reminder_hours_before",
"second_reminder_enabled",
"second_reminder_hours_before",
"auto_escalate_enabled",
"is_active",
)
list_filter = ("is_active", "second_reminder_enabled", "auto_escalate_enabled")
fieldsets = (
(
None,
{
"fields": (
"hospital",
"response_hours",
"reminder_hours_before",
"second_reminder_enabled",
"second_reminder_hours_before",
"is_active",
)
},
),
("Escalation", {"fields": ("auto_escalate_enabled", "escalation_hours_overdue", "max_escalation_levels")}),
)
admin.site.register(ExplanationSLAConfig, ExplanationSLAConfigAdmin)
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"]
class ComplaintInvolvedDepartmentInline(admin.TabularInline):
"""Inline admin for involved departments"""
model = ComplaintInvolvedDepartment
extra = 0
fields = [
"department",
"role",
"is_primary",
"assigned_to",
"forwarded_at",
"first_reminder_sent_at",
"second_reminder_sent_at",
"response_submitted",
"response_submitted_at",
"delay_reason",
"delayed_person",
]
autocomplete_fields = ["department", "assigned_to"]
readonly_fields = ["response_submitted_at"]
class ComplaintInvolvedStaffInline(admin.TabularInline):
"""Inline admin for involved staff"""
model = ComplaintInvolvedStaff
extra = 0
fields = ["staff", "role", "explanation_requested", "explanation_received"]
autocomplete_fields = ["staff"]
@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",
"satisfaction_badge",
"created_by",
"assigned_to",
"created_at",
]
list_filter = [
"status",
"severity",
"priority",
"category",
"source",
"location",
"main_section",
"subsection",
"is_overdue",
"hospital",
"created_by",
"created_at",
"satisfaction",
]
search_fields = [
"title",
"description",
"patient__mrn",
"patient__first_name",
"patient__last_name",
"encounter_id",
"file_number",
"moh_reference",
"chi_reference",
"complaint_subject",
]
ordering = ["-created_at"]
date_hierarchy = "created_at"
inlines = [
ComplaintUpdateInline,
ComplaintAttachmentInline,
ComplaintInvolvedDepartmentInline,
ComplaintInvolvedStaffInline,
]
fieldsets = (
(
"Patient & Encounter",
{"fields": ("patient", "patient_name", "file_number", "encounter_id", "incident_date", "contact_phone")},
),
("Organization", {"fields": ("hospital", "department", "staff")}),
("Location Hierarchy", {"fields": ("location", "main_section", "subsection")}),
(
"Complaint Details",
{
"fields": (
"complaint_type",
"title",
"ai_brief_en",
"ai_brief_ar",
"description",
"complaint_subject",
"category",
"subcategory",
)
},
),
("Classification", {"fields": ("priority", "severity", "source")}),
(
"External References",
{"fields": ("moh_reference", "moh_reference_date", "chi_reference", "chi_reference_date")},
),
("Creator Tracking", {"fields": ("created_by",)}),
("Status & Assignment", {"fields": ("status", "assigned_to", "assigned_at", "activated_at")}),
("Workflow Timeline", {"fields": ("form_sent_at", "forwarded_to_dept_at", "response_date")}),
(
"SLA Tracking",
{
"fields": (
"due_at",
"is_overdue",
"reminder_sent_at",
"second_reminder_sent_at",
"breached_at",
"escalated_at",
)
},
),
(
"Resolution",
{
"fields": (
"resolution",
"resolution_category",
"resolution_outcome",
"resolution_outcome_other",
"resolved_at",
"resolved_by",
)
},
),
(
"Satisfaction",
{
"fields": (
"satisfaction",
"action_taken_by_dept",
"action_result",
"recommendation_action_plan",
"delay_reason_closure",
"explanation_delay_reason",
)
},
),
("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",
"activated_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_en or obj.location.name_ar or str(obj.location))
if obj.main_section:
parts.append(obj.main_section.name_en or obj.main_section.name_ar or str(obj.main_section))
if obj.subsection:
parts.append(obj.subsection.name_en or obj.subsection.name_ar or str(obj.subsection))
if not parts:
return "—"
hierarchy = " → ".join(parts)
return format_html('{0}', 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('{1}', color, obj.get_severity_display())
severity_badge.short_description = "Severity"
def status_badge(self, obj):
"""Display status with color badge"""
colors = {
"open": "warning",
"in_progress": "info",
"partially_resolved": "info",
"resolved": "success",
"closed": "secondary",
"cancelled": "secondary",
"contacted": "info",
"contacted_no_response": "danger",
}
color = colors.get(obj.status, "secondary")
return format_html('{1}', color, obj.get_status_display())
status_badge.short_description = "Status"
def satisfaction_badge(self, obj):
"""Display satisfaction with color badge"""
if not obj.satisfaction:
return "—"
colors = {
"satisfied": "success",
"neutral": "warning",
"dissatisfied": "danger",
"no_response": "secondary",
"escalated": "danger",
}
color = colors.get(obj.satisfaction, "secondary")
return format_html('{1}', color, obj.get_satisfaction_display())
satisfaction_badge.short_description = "Satisfaction"
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('{1}', 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 mark_safe('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 mark_safe('DUE SOON')
else:
return mark_safe('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: {0}h / {1}h',
obj.first_reminder_hours_after,
obj.second_reminder_hours_after or "N/A",
)
elif obj.reminder_hours_before:
return format_html(
'Deadline-based: {0}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"
@admin.register(ComplaintInvolvedDepartment)
class ComplaintInvolvedDepartmentAdmin(admin.ModelAdmin):
"""Complaint Involved Department admin"""
list_display = ["complaint", "department", "role", "is_primary", "assigned_to", "response_submitted", "created_at"]
list_filter = ["role", "is_primary", "response_submitted", "created_at"]
search_fields = ["complaint__title", "complaint__reference_number", "department__name", "notes"]
ordering = ["-is_primary", "-created_at"]
autocomplete_fields = ["complaint", "department", "assigned_to", "added_by"]
fieldsets = (
("Complaint", {"fields": ("complaint",)}),
("Department & Role", {"fields": ("department", "role", "is_primary")}),
("Assignment", {"fields": ("assigned_to", "assigned_at")}),
("Response", {"fields": ("response_submitted", "response_submitted_at", "response_notes")}),
("Notes", {"fields": ("notes",)}),
("Metadata", {"fields": ("added_by", "created_at", "updated_at"), "classes": ("collapse",)}),
)
readonly_fields = ["assigned_at", "response_submitted_at", "created_at", "updated_at"]
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("complaint", "department", "assigned_to", "added_by")
@admin.register(ComplaintInvolvedStaff)
class ComplaintInvolvedStaffAdmin(admin.ModelAdmin):
"""Complaint Involved Staff admin"""
list_display = ["complaint", "staff", "role", "explanation_requested", "explanation_received", "created_at"]
list_filter = ["role", "explanation_requested", "explanation_received", "created_at"]
search_fields = [
"complaint__title",
"complaint__reference_number",
"staff__first_name",
"staff__last_name",
"notes",
]
ordering = ["role", "-created_at"]
autocomplete_fields = ["complaint", "staff", "added_by"]
fieldsets = (
("Complaint", {"fields": ("complaint",)}),
("Staff & Role", {"fields": ("staff", "role")}),
(
"Explanation",
{
"fields": (
"explanation_requested",
"explanation_requested_at",
"explanation_received",
"explanation_received_at",
"explanation",
)
},
),
("Notes", {"fields": ("notes",)}),
("Metadata", {"fields": ("added_by", "created_at", "updated_at"), "classes": ("collapse",)}),
)
readonly_fields = ["explanation_requested_at", "explanation_received_at", "created_at", "updated_at"]
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("complaint", "staff", "added_by")
class OnCallAdminInline(admin.TabularInline):
"""Inline admin for on-call admins"""
model = OnCallAdmin
extra = 1
fields = [
"admin_user",
"start_date",
"end_date",
"notification_priority",
"is_active",
"notify_email",
"notify_sms",
"sms_phone",
]
autocomplete_fields = ["admin_user"]
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("admin_user")
@admin.register(OnCallAdminSchedule)
class OnCallAdminScheduleAdmin(admin.ModelAdmin):
"""On-Call Admin Schedule admin"""
list_display = [
"hospital_or_system",
"working_hours_display",
"working_days_display",
"timezone",
"is_active",
"created_at",
]
list_filter = ["is_active", "timezone", "created_at"]
search_fields = ["hospital__name"]
inlines = [OnCallAdminInline]
fieldsets = (
("Scope", {"fields": ("hospital", "is_active")}),
(
"Working Hours Configuration",
{
"fields": ("work_start_time", "work_end_time", "timezone", "working_days"),
"description": "Configure working hours. Outside these hours, only on-call admins will be notified.",
},
),
)
readonly_fields = ["created_at", "updated_at"]
def hospital_or_system(self, obj):
"""Display hospital name or 'System-wide'"""
if obj.hospital:
return obj.hospital.name
return mark_safe('System-wide')
hospital_or_system.short_description = "Scope"
hospital_or_system.admin_order_field = "hospital__name"
def working_hours_display(self, obj):
"""Display working hours"""
return f"{obj.work_start_time.strftime('%H:%M')} - {obj.work_end_time.strftime('%H:%M')}"
working_hours_display.short_description = "Working Hours"
def working_days_display(self, obj):
"""Display working days as abbreviated day names"""
days = obj.get_working_days_list()
day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
selected_days = [day_names[d] for d in days if 0 <= d <= 6]
return ", ".join(selected_days) if selected_days else "None"
working_days_display.short_description = "Working Days"
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("hospital")
@admin.register(OnCallAdmin)
class OnCallAdminAdmin(admin.ModelAdmin):
"""On-Call Admin admin"""
list_display = ["admin_user", "schedule", "notification_priority", "date_range", "contact_preferences", "is_active"]
list_filter = ["is_active", "notify_email", "notify_sms", "schedule__hospital", "created_at"]
search_fields = ["admin_user__email", "admin_user__first_name", "admin_user__last_name", "sms_phone"]
autocomplete_fields = ["admin_user", "schedule"]
fieldsets = (
("Assignment", {"fields": ("schedule", "admin_user", "is_active")}),
(
"Active Period (Optional)",
{"fields": ("start_date", "end_date"), "description": "Leave empty for permanent assignment"},
),
(
"Notification Settings",
{
"fields": ("notification_priority", "notify_email", "notify_sms", "sms_phone"),
"description": "Configure how this admin should be notified for after-hours complaints",
},
),
)
readonly_fields = ["created_at", "updated_at"]
def date_range(self, obj):
"""Display date range"""
if obj.start_date and obj.end_date:
return f"{obj.start_date} to {obj.end_date}"
elif obj.start_date:
return f"From {obj.start_date}"
elif obj.end_date:
return f"Until {obj.end_date}"
return mark_safe('Permanent')
date_range.short_description = "Active Period"
def contact_preferences(self, obj):
"""Display contact preferences"""
prefs = []
if obj.notify_email:
prefs.append("📧 Email")
if obj.notify_sms:
prefs.append(f"📱 SMS ({obj.sms_phone or 'user phone'})")
return ", ".join(prefs) if prefs else "None"
contact_preferences.short_description = "Contact"
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("admin_user", "schedule", "schedule__hospital")
class ComplaintAdverseActionAttachmentInline(admin.TabularInline):
"""Inline admin for adverse action attachments"""
model = ComplaintAdverseActionAttachment
extra = 0
fields = ["file", "filename", "description", "uploaded_by"]
readonly_fields = ["filename", "file_size"]
@admin.register(ComplaintAdverseAction)
class ComplaintAdverseActionAdmin(admin.ModelAdmin):
"""Admin for complaint adverse actions"""
list_display = [
"complaint_reference",
"action_type_display",
"severity_badge",
"incident_date",
"status_badge",
"is_escalated",
"created_at",
]
list_filter = ["action_type", "severity", "status", "is_escalated", "incident_date", "created_at"]
search_fields = ["complaint__reference_number", "complaint__title", "description", "patient_impact"]
date_hierarchy = "incident_date"
inlines = [ComplaintAdverseActionAttachmentInline]
fieldsets = (
("Complaint Information", {"fields": ("complaint",)}),
("Adverse Action Details", {"fields": ("action_type", "severity", "description", "incident_date", "location")}),
("Impact & Staff", {"fields": ("patient_impact", "involved_staff")}),
(
"Verification & Investigation",
{
"fields": ("status", "reported_by", "investigation_notes", "investigated_by", "investigated_at"),
"classes": ("collapse",),
},
),
("Resolution", {"fields": ("resolution", "resolved_by", "resolved_at"), "classes": ("collapse",)}),
("Escalation", {"fields": ("is_escalated", "escalated_at")}),
)
readonly_fields = ["created_at", "updated_at"]
def complaint_reference(self, obj):
"""Display complaint reference"""
return format_html(
'{}', obj.complaint.id, obj.complaint.reference_number
)
complaint_reference.short_description = "Complaint"
def action_type_display(self, obj):
"""Display action type with formatting"""
return obj.get_action_type_display()
action_type_display.short_description = "Action Type"
def severity_badge(self, obj):
"""Display severity as colored badge"""
colors = {
"low": "#22c55e", # green
"medium": "#f59e0b", # amber
"high": "#ef4444", # red
"critical": "#7f1d1d", # dark red
}
color = colors.get(obj.severity, "#64748b")
return format_html(
'{}',
color,
obj.get_severity_display(),
)
severity_badge.short_description = "Severity"
def status_badge(self, obj):
"""Display status as colored badge"""
colors = {
"reported": "#f59e0b",
"under_investigation": "#3b82f6",
"verified": "#22c55e",
"unfounded": "#64748b",
"resolved": "#10b981",
}
color = colors.get(obj.status, "#64748b")
return format_html(
'{}',
color,
obj.get_status_display(),
)
status_badge.short_description = "Status"
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("complaint", "reported_by", "investigated_by", "resolved_by")
@admin.register(ComplaintAdverseActionAttachment)
class ComplaintAdverseActionAttachmentAdmin(admin.ModelAdmin):
"""Admin for adverse action attachments"""
list_display = ["adverse_action", "filename", "file_type", "uploaded_by", "created_at"]
list_filter = ["file_type", "created_at"]
search_fields = ["filename", "description", "adverse_action__complaint__reference_number"]
ordering = ["-created_at"]