""" Django admin configuration for core app. """ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.utils.translation import gettext_lazy as _ from django.utils.html import format_html from django.utils import timezone from simple_history.admin import SimpleHistoryAdmin from .models import ( Tenant, User, Patient, Clinic, File, SubFile, Consent, ConsentTemplate, ConsentToken, Attachment, AuditLog, SettingTemplate, TenantSetting, ContactMessage, ) from .settings_service import get_tenant_settings_service class TenantSettingInline(admin.TabularInline): """Inline admin for tenant settings.""" model = TenantSetting extra = 0 fields = ['template', 'get_setting_info', 'value', 'file_value', 'updated_by', 'updated_at'] readonly_fields = ['get_setting_info', 'updated_by', 'updated_at'] def get_setting_info(self, obj): """Display setting information.""" if obj.template: required_badge = '' if obj.template.is_required: has_value = bool(obj.value or obj.file_value or obj.encrypted_value) if has_value: required_badge = format_html( '✓ Required' ) else: required_badge = format_html( '✗ Required (Missing)' ) help_text = obj.template.help_text_en if obj.template.help_text_en else '' return format_html( '{}
' '{}
' '{}
' '{}', obj.template.label_en, obj.template.get_category_display(), help_text, required_badge ) return '-' get_setting_info.short_description = _('Setting Information') def formfield_for_foreignkey(self, db_field, request, **kwargs): """Filter templates by category for better organization.""" if db_field.name == "template": kwargs["queryset"] = SettingTemplate.objects.filter(is_active=True).order_by('category', 'order') return super().formfield_for_foreignkey(db_field, request, **kwargs) @admin.register(Tenant) class TenantAdmin(admin.ModelAdmin): """Admin interface for Tenant model with settings management.""" list_display = ['name', 'code', 'is_active', 'settings_status', 'created_at'] list_filter = ['is_active', 'created_at'] search_fields = ['name', 'code', 'name_ar'] readonly_fields = ['id', 'created_at', 'updated_at', 'settings_status_detail'] inlines = [TenantSettingInline] fieldsets = ( (None, { 'fields': ('name', 'name_ar', 'code', 'is_active') }), (_('ZATCA Information'), { 'fields': ('vat_number', 'address', 'city', 'postal_code', 'country_code'), }), (_('Settings Status'), { 'fields': ('settings_status_detail',), }), (_('Legacy Settings (Deprecated)'), { 'fields': ('settings',), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) def settings_status(self, obj): """Display settings completion status.""" service = get_tenant_settings_service(obj) missing = service.get_missing_required_settings() if not missing: return format_html( '✓ Complete' ) else: return format_html( '✗ {} Missing', len(missing) ) settings_status.short_description = _('Settings Status') def settings_status_detail(self, obj): """Display detailed settings status.""" service = get_tenant_settings_service(obj) missing = service.get_missing_required_settings() if not missing: return format_html( '
' '✓ All required settings are configured' '
' ) else: missing_list = '' return format_html( '
' '✗ Missing Required Settings:' '{}' '
', missing_list ) settings_status_detail.short_description = _('Settings Status') @admin.register(User) class UserAdmin(SimpleHistoryAdmin, BaseUserAdmin): """Admin interface for User model.""" list_display = ['username', 'email', 'first_name', 'last_name', 'role', 'tenant', 'is_active'] list_filter = ['role', 'is_active', 'is_staff', 'tenant', 'date_joined'] search_fields = ['username', 'email', 'first_name', 'last_name', 'employee_id'] readonly_fields = ['id', 'date_joined', 'last_login'] fieldsets = ( (None, { 'fields': ('username', 'password') }), (_('Personal Info'), { 'fields': ('first_name', 'last_name', 'email', 'phone_number', 'employee_id') }), (_('Organization'), { 'fields': ('tenant', 'role') }), (_('Permissions'), { 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), 'classes': ('collapse',) }), (_('Important Dates'), { 'fields': ('last_login', 'date_joined'), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id',), 'classes': ('collapse',) }), ) add_fieldsets = ( (None, { 'classes': ('wide',), 'fields': ('username', 'password1', 'password2', 'tenant', 'role'), }), ) @admin.register(Patient) class PatientAdmin(SimpleHistoryAdmin): """Admin interface for Patient model.""" list_display = ['mrn', 'full_name_en', 'date_of_birth', 'sex', 'phone', 'tenant', 'created_at'] list_filter = ['sex', 'tenant', 'created_at'] search_fields = ['mrn', 'national_id', 'first_name_en', 'last_name_en', 'first_name_ar', 'last_name_ar', 'phone', 'email'] readonly_fields = ['id', 'mrn', 'age', 'created_at', 'updated_at'] fieldsets = ( (_('Identification'), { 'fields': ('mrn', 'national_id', 'tenant') }), (_('Personal Information (English)'), { 'fields': ('first_name_en','father_name_en','grandfather_name_en', 'last_name_en') }), (_('Personal Information (Arabic)'), { 'fields': ('first_name_ar','father_name_ar','grandfather_name_ar', 'last_name_ar'), 'classes': ('collapse',) }), (_('Demographics'), { 'fields': ('date_of_birth', 'age', 'sex') }), (_('Contact Information'), { 'fields': ('phone', 'email', 'address', 'city', 'postal_code') }), (_('Caregiver Information'), { 'fields': ('caregiver_name', 'caregiver_phone', 'caregiver_relationship'), 'classes': ('collapse',) }), (_('Emergency Contact'), { 'fields': ('emergency_contact',), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) @admin.register(Clinic) class ClinicAdmin(admin.ModelAdmin): """Admin interface for Clinic model.""" list_display = ['name_en', 'specialty', 'code', 'tenant', 'is_active'] list_filter = ['specialty', 'is_active', 'tenant'] search_fields = ['name_en', 'name_ar', 'code'] readonly_fields = ['id', 'created_at', 'updated_at'] fieldsets = ( (None, { 'fields': ('name_en', 'name_ar', 'specialty', 'code', 'tenant', 'is_active') }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) @admin.register(File) class FileAdmin(admin.ModelAdmin): """Admin interface for File model.""" list_display = ['file_number', 'patient', 'status', 'opened_date', 'tenant'] list_filter = ['status', 'tenant', 'opened_date'] search_fields = ['file_number', 'patient__mrn', 'patient__first_name_en', 'patient__last_name_en'] readonly_fields = ['id', 'file_number', 'opened_date'] fieldsets = ( (None, { 'fields': ('file_number', 'patient', 'tenant', 'status') }), (_('Dates'), { 'fields': ('opened_date',) }), (_('Metadata'), { 'fields': ('id',), 'classes': ('collapse',) }), ) @admin.register(SubFile) class SubFileAdmin(admin.ModelAdmin): """Admin interface for SubFile model.""" list_display = ['sub_file_number', 'file', 'clinic', 'status', 'assigned_provider', 'opened_date'] list_filter = ['status', 'clinic', 'opened_date'] search_fields = ['sub_file_number', 'file__file_number', 'file__patient__mrn'] readonly_fields = ['id', 'opened_date', 'created_at', 'updated_at'] fieldsets = ( (None, { 'fields': ('sub_file_number', 'file', 'clinic', 'status', 'assigned_provider') }), (_('Dates'), { 'fields': ('opened_date', 'created_at', 'updated_at'), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id',), 'classes': ('collapse',) }), ) @admin.register(ConsentTemplate) class ConsentTemplateAdmin(admin.ModelAdmin): """Admin interface for Consent Template model.""" list_display = ['consent_type', 'title_en', 'version', 'is_active', 'tenant', 'created_at'] list_filter = ['consent_type', 'is_active', 'tenant', 'version'] search_fields = ['title_en', 'title_ar', 'content_en', 'content_ar'] readonly_fields = ['id', 'created_at', 'updated_at'] fieldsets = ( (None, { 'fields': ('tenant', 'consent_type', 'version', 'is_active') }), (_('Titles'), { 'fields': ('title_en', 'title_ar') }), (_('Content (English)'), { 'fields': ('content_en',), 'description': 'Use placeholders: {patient_name}, {patient_mrn}, {date}, {patient_dob}, {patient_age}' }), (_('Content (Arabic)'), { 'fields': ('content_ar',), 'classes': ('collapse',), 'description': 'Use placeholders: {patient_name}, {patient_mrn}, {date}, {patient_dob}, {patient_age}' }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) @admin.register(Consent) class ConsentAdmin(SimpleHistoryAdmin): """Admin interface for Consent model.""" list_display = ['patient', 'consent_type', 'signed_by_name', 'signed_at', 'is_active', 'tenant'] list_filter = ['consent_type', 'is_active', 'tenant', 'signed_at', 'created_at'] search_fields = ['patient__mrn', 'patient__first_name_en', 'patient__last_name_en', 'signed_by_name'] readonly_fields = ['id', 'signature_hash', 'created_at', 'updated_at'] fieldsets = ( (None, { 'fields': ('patient', 'tenant', 'consent_type', 'is_active', 'version') }), (_('Content'), { 'fields': ('content_text',) }), (_('Signature Information'), { 'fields': ('signed_by_name', 'signed_by_relationship', 'signed_at', 'signature_method', 'signature_image', 'signature_hash') }), (_('Technical Details'), { 'fields': ('signed_ip', 'signed_user_agent'), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) @admin.register(ConsentToken) class ConsentTokenAdmin(admin.ModelAdmin): """Admin interface for Consent Token model.""" list_display = ['consent', 'email', 'is_valid_status', 'sent_by', 'created_at', 'expires_at', 'used_at'] list_filter = ['is_active', 'created_at', 'expires_at', 'used_at'] search_fields = ['email', 'consent__patient__first_name_en', 'consent__patient__last_name_en', 'consent__patient__mrn'] readonly_fields = ['id', 'token', 'created_at', 'updated_at', 'is_valid_status'] date_hierarchy = 'created_at' fieldsets = ( (None, { 'fields': ('consent', 'email', 'token', 'is_active') }), (_('Timing'), { 'fields': ('created_at', 'expires_at', 'used_at') }), (_('Audit'), { 'fields': ('sent_by', 'is_valid_status'), }), (_('Metadata'), { 'fields': ('id', 'updated_at'), 'classes': ('collapse',) }), ) def is_valid_status(self, obj): """Display if token is currently valid.""" if obj.is_valid(): return format_html( '✓ Valid' ) elif obj.used_at: return format_html( 'Used on {}', obj.used_at.strftime('%Y-%m-%d %H:%M') ) elif obj.expires_at < timezone.now(): return format_html( 'Expired' ) else: return format_html( 'Inactive' ) is_valid_status.short_description = _('Status') def has_add_permission(self, request): """Tokens should be created via the system, not manually.""" return False @admin.register(Attachment) class AttachmentAdmin(admin.ModelAdmin): """Admin interface for Attachment model.""" list_display = ['file', 'file_type', 'content_type', 'uploaded_by', 'tenant', 'created_at'] list_filter = ['file_type', 'content_type', 'tenant', 'created_at'] search_fields = ['description', 'file'] readonly_fields = ['id', 'created_at', 'updated_at'] fieldsets = ( (None, { 'fields': ('content_type', 'object_id', 'tenant') }), (_('File Information'), { 'fields': ('file', 'file_type', 'description', 'uploaded_by') }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) @admin.register(AuditLog) class AuditLogAdmin(admin.ModelAdmin): """Admin interface for AuditLog model.""" list_display = ['user', 'action', 'content_type', 'timestamp', 'tenant'] list_filter = ['action', 'content_type', 'tenant', 'timestamp'] search_fields = ['user__username', 'user__email'] readonly_fields = ['id', 'timestamp', 'changes', 'ip_address'] fieldsets = ( (None, { 'fields': ('user', 'action', 'content_type', 'object_id', 'tenant') }), (_('Details'), { 'fields': ('timestamp', 'changes', 'ip_address') }), (_('Metadata'), { 'fields': ('id',), 'classes': ('collapse',) }), ) def has_add_permission(self, request): """Audit logs should not be manually created.""" return False def has_change_permission(self, request, obj=None): """Audit logs should not be modified.""" return False def has_delete_permission(self, request, obj=None): """Audit logs should not be deleted.""" return False @admin.register(SettingTemplate) class SettingTemplateAdmin(admin.ModelAdmin): """Admin interface for Setting Template model.""" list_display = ['key', 'label_en', 'category', 'data_type', 'is_required', 'is_active', 'order'] list_filter = ['category', 'data_type', 'is_required', 'is_active'] search_fields = ['key', 'label_en', 'label_ar', 'help_text_en'] readonly_fields = ['id', 'created_at', 'updated_at'] list_editable = ['order', 'is_active'] fieldsets = ( (None, { 'fields': ('key', 'category', 'data_type', 'is_required', 'is_active', 'order') }), (_('Labels'), { 'fields': ('label_en', 'label_ar') }), (_('Help Text'), { 'fields': ('help_text_en', 'help_text_ar') }), (_('Validation'), { 'fields': ('default_value', 'validation_regex', 'choices'), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) def get_queryset(self, request): """Order by category and order by default.""" qs = super().get_queryset(request) return qs.order_by('category', 'order', 'label_en') @admin.register(TenantSetting) class TenantSettingAdmin(SimpleHistoryAdmin): """Admin interface for Tenant Setting model.""" list_display = ['tenant', 'get_template_label', 'get_category', 'get_value_preview', 'updated_by', 'updated_at'] list_filter = ['template__category', 'tenant', 'updated_at'] search_fields = ['tenant__name', 'template__label_en', 'template__key', 'value'] readonly_fields = ['id', 'created_at', 'updated_at', 'get_value_preview'] fieldsets = ( (None, { 'fields': ('tenant', 'template') }), (_('Value'), { 'fields': ('value', 'file_value', 'get_value_preview') }), (_('Audit'), { 'fields': ('updated_by', 'created_at', 'updated_at'), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id',), 'classes': ('collapse',) }), ) def get_template_label(self, obj): """Display template label.""" return obj.template.label_en get_template_label.short_description = _('Setting') get_template_label.admin_order_field = 'template__label_en' def get_category(self, obj): """Display category.""" return obj.template.get_category_display() get_category.short_description = _('Category') get_category.admin_order_field = 'template__category' def get_value_preview(self, obj): """Display value preview (masked for encrypted).""" if obj.template.data_type == SettingTemplate.DataType.ENCRYPTED: if obj.encrypted_value: return format_html('***ENCRYPTED***') return '-' elif obj.template.data_type == SettingTemplate.DataType.FILE: if obj.file_value: return format_html('View File', obj.file_value.url) return '-' else: value = obj.value[:100] if obj.value else '-' return value get_value_preview.short_description = _('Value Preview') @admin.register(ContactMessage) class ContactMessageAdmin(admin.ModelAdmin): """Admin interface for Contact Message model.""" list_display = ['name', 'email', 'phone', 'get_message_preview', 'submitted_at', 'status_badge', 'responded_by'] list_filter = ['is_read', 'submitted_at', 'responded_at'] search_fields = ['name', 'email', 'phone', 'message', 'ip_address'] readonly_fields = ['id', 'submitted_at', 'ip_address', 'user_agent', 'created_at', 'updated_at'] date_hierarchy = 'submitted_at' actions = ['mark_as_read', 'mark_as_unread'] fieldsets = ( (_('Contact Information'), { 'fields': ('name', 'email', 'phone') }), (_('Message'), { 'fields': ('message',) }), (_('Status'), { 'fields': ('is_read', 'responded_at', 'responded_by', 'notes') }), (_('Technical Details'), { 'fields': ('submitted_at', 'ip_address', 'user_agent'), 'classes': ('collapse',) }), (_('Metadata'), { 'fields': ('id', 'created_at', 'updated_at'), 'classes': ('collapse',) }), ) def get_message_preview(self, obj): """Display message preview (first 100 characters).""" if len(obj.message) > 100: return f"{obj.message[:100]}..." return obj.message get_message_preview.short_description = _('Message Preview') def status_badge(self, obj): """Display read/unread status with badge.""" if obj.responded_at: return format_html( '✓ Responded' ) elif obj.is_read: return format_html( '👁 Read' ) else: return format_html( '● New' ) status_badge.short_description = _('Status') def mark_as_read(self, request, queryset): """Mark selected messages as read.""" updated = queryset.update(is_read=True) self.message_user( request, f'{updated} message(s) marked as read.' ) mark_as_read.short_description = _('Mark selected as read') def mark_as_unread(self, request, queryset): """Mark selected messages as unread.""" updated = queryset.update(is_read=False) self.message_user( request, f'{updated} message(s) marked as unread.' ) mark_as_unread.short_description = _('Mark selected as unread') def save_model(self, request, obj, form, change): """Auto-mark as read when admin views/edits.""" if change and not obj.is_read: obj.mark_as_read() super().save_model(request, obj, form, change) def has_add_permission(self, request): """Contact messages should only be created via the form.""" return False def get_queryset(self, request): """Order by unread first, then by date.""" qs = super().get_queryset(request) return qs.order_by('is_read', '-submitted_at')