""" HR app admin configuration. """ from django.contrib import admin from django.utils.html import format_html from django.urls import reverse from django.utils.safestring import mark_safe from django.db.models import Count, Avg, Sum from decimal import Decimal from datetime import date, timedelta from .models import * # ============================================================================ # INLINE ADMIN CLASSES # ============================================================================ class ScheduleInline(admin.TabularInline): """ Inline admin for employee schedules. """ model = Schedule extra = 0 fields = [ 'name', 'schedule_type', 'effective_date', 'end_date', 'is_active' ] readonly_fields = ['schedule_id'] class TimeEntryInline(admin.TabularInline): """ Inline admin for employee time entries. """ model = TimeEntry extra = 0 fields = [ 'work_date', 'clock_in_time', 'clock_out_time', 'total_hours', 'entry_type', 'status' ] readonly_fields = ['entry_id', 'total_hours'] class PerformanceReviewInline(admin.TabularInline): """ Inline admin for employee performance reviews. """ model = PerformanceReview extra = 0 fields = [ 'review_date', 'review_type', 'overall_rating', 'status' ] readonly_fields = ['review_id'] class TrainingRecordInline(admin.TabularInline): """ Inline admin for employee training records. """ model = TrainingRecord extra = 0 fields = [ 'program', 'session', 'completion_date', 'status', 'passed' ] readonly_fields = ['record_id'] class ProgramModuleInline(admin.TabularInline): """ Inline admin for training program modules. """ model = ProgramModule extra = 0 fields = ['order', 'title', 'hours'] ordering = ['order'] class ProgramPrerequisiteInline(admin.TabularInline): """ Inline admin for training program prerequisites. """ model = ProgramPrerequisite extra = 0 fk_name = 'program' fields = ['required_program'] class TrainingSessionInline(admin.TabularInline): """ Inline admin for training sessions. """ model = TrainingSession extra = 0 fields = [ 'title', 'start_at', 'end_at', 'instructor', 'delivery_method', 'capacity' ] readonly_fields = ['session_id'] class TrainingAttendanceInline(admin.TabularInline): """ Inline admin for training attendance. """ model = TrainingAttendance extra = 0 fields = [ 'checked_in_at', 'checked_out_at', 'status', 'notes' ] class TrainingAssessmentInline(admin.TabularInline): """ Inline admin for training assessments. """ model = TrainingAssessment extra = 0 fields = [ 'name', 'max_score', 'score', 'passed', 'taken_at' ] @admin.register(Employee) class EmployeeAdmin(admin.ModelAdmin): """ Admin interface for employees. """ list_display = [ 'employee_id', 'get_full_name', 'job_title', 'department', 'employment_type', 'employment_status', 'hire_date', 'years_of_service_display', 'license_status_display' ] list_filter = [ 'tenant', 'department', 'employment_type', 'employment_status', 'gender', 'hire_date' ] search_fields = [ 'employee_id', 'first_name', 'last_name', 'email', 'job_title' ] readonly_fields = [ 'age', 'years_of_service', 'is_license_expired', 'created_at', 'updated_at' ] fieldsets = [ ('Employee Information', { 'fields': [ 'tenant', 'user' ] }), ('Personal Information', { 'fields': [ 'first_name', 'father_name', 'grandfather_name', 'last_name', ] }), ('Contact Information', { 'fields': [ 'email', 'phone', 'mobile_phone', 'profile_picture' ] }), ('Address Information', { 'fields': [ 'address_line_1', 'address_line_2', 'city', 'postal_code', 'country' ], 'classes': ['collapse'] }), ('Personal Details', { 'fields': [ 'date_of_birth', 'gender', 'marital_status' ] }), ('Employment Information', { 'fields': [ 'department', 'job_title', 'employment_type', 'employment_status' ] }), ('Employment Dates', { 'fields': [ 'hire_date', 'termination_date' ] }), ('Supervisor Information', { 'fields': [ 'supervisor' ] }), ('Work Schedule Information', { 'fields': [ 'standard_hours_per_week', 'fte_percentage' ] }), ('Compensation Information', { 'fields': [ 'hourly_rate', 'annual_salary' ] }), ('Professional Information', { 'fields': [ 'license_number', 'license_expiry_date', ] }), ('Emergency Contact', { 'fields': [ 'emergency_contact_name', 'emergency_contact_relationship', 'emergency_contact_phone' ], 'classes': ['collapse'] }), ('Calculated Fields', { 'fields': [ 'age', 'years_of_service', 'is_license_expired', ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] inlines = [ScheduleInline, TimeEntryInline, PerformanceReviewInline, TrainingRecordInline] date_hierarchy = 'hire_date' def years_of_service_display(self, obj): """Display years of service.""" years = obj.years_of_service if years: return f"{years:.1f} years" return "0 years" years_of_service_display.short_description = 'Years of Service' def license_status_display(self, obj): """Display license status with color coding.""" if not obj.license_number: return format_html('No License') if obj.is_license_expired: return format_html('⚠️ Expired') if obj.license_expiry_date: days_to_expiry = (obj.license_expiry_date - date.today()).days if days_to_expiry <= 30: return format_html('⚠️ Expires Soon') return format_html('✓ Valid') license_status_display.short_description = 'License Status' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(tenant=request.user.tenant) return qs.select_related('department', 'supervisor') @admin.register(Department) class DepartmentAdmin(admin.ModelAdmin): """ Admin interface for departments. """ list_display = [ 'code', 'name', 'department_type', 'department_head', 'employee_count_display', 'total_fte_display', 'is_active' ] list_filter = [ 'tenant', 'department_type', 'is_active' ] search_fields = [ 'code', 'name', 'description' ] readonly_fields = [ 'department_id', 'employee_count', 'total_fte', 'created_at', 'updated_at' ] fieldsets = [ ('Department Information', { 'fields': [ 'department_id', 'tenant', 'code', 'name', 'description' ] }), ('Department Type', { 'fields': [ 'department_type' ] }), ('Hierarchy', { 'fields': [ 'parent_department' ] }), ('Management', { 'fields': [ 'department_head' ] }), ('Budget Information', { 'fields': [ 'annual_budget', 'cost_center' ] }), ('Location Information', { 'fields': [ 'location' ] }), ('Department Status', { 'fields': [ 'is_active' ] }), ('Summary Information', { 'fields': [ 'employee_count', 'total_fte' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] def employee_count_display(self, obj): """Display employee count.""" return obj.employee_count employee_count_display.short_description = 'Employees' def total_fte_display(self, obj): """Display total FTE.""" return f"{obj.total_fte:.1f}" total_fte_display.short_description = 'Total FTE' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(tenant=request.user.tenant) return qs.select_related('department_head') class ScheduleAssignmentInline(admin.TabularInline): """ Inline admin for schedule assignments. """ model = ScheduleAssignment extra = 0 fields = [ 'assignment_date', 'start_time', 'end_time', 'shift_type', 'status' ] readonly_fields = ['assignment_id', 'total_hours'] @admin.register(Schedule) class ScheduleAdmin(admin.ModelAdmin): """ Admin interface for schedules. """ list_display = [ 'employee_name', 'name', 'schedule_type', 'effective_date', 'end_date', 'is_current_display', 'is_active' ] list_filter = [ 'schedule_type', 'effective_date', 'is_active' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id', 'name' ] readonly_fields = [ 'schedule_id', 'tenant', 'is_current', 'created_at', 'updated_at' ] fieldsets = [ ('Schedule Information', { 'fields': [ 'schedule_id', 'employee', 'name', 'description' ] }), ('Schedule Type', { 'fields': [ 'schedule_type' ] }), ('Schedule Dates', { 'fields': [ 'effective_date', 'end_date' ] }), ('Schedule Pattern', { 'fields': [ 'schedule_pattern' ] }), ('Schedule Status', { 'fields': [ 'is_active' ] }), ('Approval Information', { 'fields': [ 'approved_by', 'approval_date' ] }), ('Status Information', { 'fields': [ 'is_current' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] inlines = [ScheduleAssignmentInline] date_hierarchy = 'effective_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def is_current_display(self, obj): """Display current status with icon.""" if obj.is_current: return format_html('✓ Current') return format_html('Not Current') is_current_display.short_description = 'Current' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'approved_by') @admin.register(ScheduleAssignment) class ScheduleAssignmentAdmin(admin.ModelAdmin): """ Admin interface for schedule assignments. """ list_display = [ 'employee_name', 'assignment_date', 'start_time', 'end_time', 'shift_type', 'total_hours_display', 'status' ] list_filter = [ 'shift_type', 'status', 'assignment_date' ] search_fields = [ 'schedule__employee__first_name', 'schedule__employee__last_name', 'schedule__employee__employee_id' ] readonly_fields = [ 'assignment_id', 'tenant', 'employee', 'total_hours', 'created_at', 'updated_at' ] fieldsets = [ ('Assignment Information', { 'fields': [ 'assignment_id', 'schedule' ] }), ('Date and Time', { 'fields': [ 'assignment_date', 'start_time', 'end_time' ] }), ('Shift Information', { 'fields': [ 'shift_type' ] }), ('Location Information', { 'fields': [ 'department', 'location' ] }), ('Assignment Status', { 'fields': [ 'status' ] }), ('Break Information', { 'fields': [ 'break_minutes', 'lunch_minutes' ] }), ('Calculated Information', { 'fields': [ 'total_hours' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant', 'employee' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at' ], 'classes': ['collapse'] }) ] date_hierarchy = 'assignment_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def total_hours_display(self, obj): """Display total hours.""" return f"{obj.total_hours:.2f}" total_hours_display.short_description = 'Hours' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(schedule__employee__tenant=request.user.tenant) return qs.select_related('schedule__employee', 'department') @admin.register(TimeEntry) class TimeEntryAdmin(admin.ModelAdmin): """ Admin interface for time entries. """ list_display = [ 'employee_name', 'work_date', 'clock_in_time', 'clock_out_time', 'total_hours', 'entry_type', 'status', 'approval_status_display' ] list_filter = [ 'entry_type', 'status', 'work_date' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id' ] readonly_fields = [ 'entry_id', 'tenant', 'regular_hours', 'overtime_hours', 'total_hours', 'is_approved', 'created_at', 'updated_at' ] fieldsets = [ ('Time Entry Information', { 'fields': [ 'entry_id', 'employee' ] }), ('Date and Time', { 'fields': [ 'work_date', 'clock_in_time', 'clock_out_time' ] }), ('Break Times', { 'fields': [ 'break_start_time', 'break_end_time', 'lunch_start_time', 'lunch_end_time' ] }), ('Hours Information', { 'fields': [ 'regular_hours', 'overtime_hours', 'total_hours' ] }), ('Entry Type', { 'fields': [ 'entry_type' ] }), ('Department and Location', { 'fields': [ 'department', 'location' ] }), ('Approval Information', { 'fields': [ 'approved_by', 'approval_date' ] }), ('Entry Status', { 'fields': [ 'status' ] }), ('Status Information', { 'fields': [ 'is_approved' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at' ], 'classes': ['collapse'] }) ] date_hierarchy = 'work_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def approval_status_display(self, obj): """Display approval status with icon.""" if obj.is_approved: return format_html('✓ Approved') elif obj.status == 'REJECTED': return format_html('✗ Rejected') elif obj.status == 'SUBMITTED': return format_html('⏳ Pending') return format_html('Draft') approval_status_display.short_description = 'Approval' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'department', 'approved_by') @admin.register(PerformanceReview) class PerformanceReviewAdmin(admin.ModelAdmin): """ Admin interface for performance reviews. """ list_display = [ 'employee_name', 'review_date', 'review_type', 'overall_rating', 'status', 'is_overdue_display' ] list_filter = [ 'review_type', 'status', 'overall_rating', 'review_date' ] search_fields = [ 'employee__first_name', 'employee__last_name', ] readonly_fields = [ 'review_id', 'is_overdue', 'created_at', 'updated_at' ] fieldsets = [ ('Review Information', { 'fields': [ 'review_id', 'employee' ] }), ('Review Period', { 'fields': [ 'review_period_start', 'review_period_end', 'review_date' ] }), ('Review Type', { 'fields': [ 'review_type' ] }), ('Reviewer Information', { 'fields': [ 'reviewer' ] }), ('Overall Rating', { 'fields': [ 'overall_rating' ] }), ('Competency Ratings', { 'fields': [ 'competency_ratings' ] }), ('Goals and Objectives', { 'fields': [ 'goals_achieved', 'goals_not_achieved', 'future_goals' ] }), ('Strengths and Areas for Improvement', { 'fields': [ 'strengths', 'areas_for_improvement' ] }), ('Development Plan', { 'fields': [ 'development_plan', 'training_recommendations' ] }), ('Employee Comments', { 'fields': [ 'employee_comments', 'employee_signature_date' ] }), ('Review Status', { 'fields': [ 'status' ] }), ('Status Information', { 'fields': [ 'is_overdue' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at' ], 'classes': ['collapse'] }) ] date_hierarchy = 'review_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def is_overdue_display(self, obj): """Display overdue status with icon.""" if obj.is_overdue: return format_html('⚠️ Overdue') return format_html('✓ On Time') is_overdue_display.short_description = 'Status' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'reviewer') # ============================================================================ # TRAINING ADMIN CLASSES # ============================================================================ @admin.register(TrainingPrograms) class TrainingProgramsAdmin(admin.ModelAdmin): """ Admin interface for training programs. """ list_display = [ 'name', 'program_type', 'program_provider', 'instructor', 'duration_hours', 'cost', 'is_certified', 'validity_display' ] list_filter = [ 'tenant', 'program_type', 'is_certified', 'program_provider' ] search_fields = [ 'name', 'description', 'program_provider' ] readonly_fields = [ 'program_id', 'created_at', 'updated_at' ] fieldsets = [ ('Program Information', { 'fields': [ 'program_id', 'tenant', 'name', 'description' ] }), ('Program Details', { 'fields': [ 'program_type', 'program_provider', 'instructor' ] }), ('Schedule Information', { 'fields': [ 'start_date', 'end_date', 'duration_hours' ] }), ('Cost Information', { 'fields': [ 'cost' ] }), ('Certification Information', { 'fields': [ 'is_certified', 'validity_days', 'notify_before_days' ] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] inlines = [ProgramModuleInline, ProgramPrerequisiteInline, TrainingSessionInline] date_hierarchy = 'start_date' def validity_display(self, obj): """Display validity information.""" if obj.is_certified and obj.validity_days: return f"{obj.validity_days} days" return "No expiry" validity_display.short_description = 'Validity' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(tenant=request.user.tenant) return qs.select_related('instructor', 'created_by') @admin.register(ProgramModule) class ProgramModuleAdmin(admin.ModelAdmin): """ Admin interface for program modules. """ list_display = [ 'program', 'order', 'title', 'hours' ] list_filter = [ 'program__tenant', 'program' ] search_fields = [ 'title', 'program__name' ] ordering = ['program', 'order'] def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(program__tenant=request.user.tenant) return qs.select_related('program') @admin.register(ProgramPrerequisite) class ProgramPrerequisiteAdmin(admin.ModelAdmin): """ Admin interface for program prerequisites. """ list_display = [ 'program', 'required_program' ] list_filter = [ 'program__tenant', 'program' ] search_fields = [ 'program__name', 'required_program__name' ] def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(program__tenant=request.user.tenant) return qs.select_related('program', 'required_program') @admin.register(TrainingSession) class TrainingSessionAdmin(admin.ModelAdmin): """ Admin interface for training sessions. """ list_display = [ 'program', 'title_display', 'instructor', 'start_at', 'end_at', 'delivery_method', 'capacity', 'enrollment_count' ] list_filter = [ 'program__tenant', 'delivery_method', 'start_at' ] search_fields = [ 'title', 'program__name', 'instructor__first_name', 'instructor__last_name' ] readonly_fields = [ 'session_id', 'enrollment_count', 'created_at' ] fieldsets = [ ('Session Information', { 'fields': [ 'session_id', 'program', 'title' ] }), ('Instructor Information', { 'fields': [ 'instructor' ] }), ('Schedule Information', { 'fields': [ 'start_at', 'end_at' ] }), ('Delivery Information', { 'fields': [ 'delivery_method', 'location' ] }), ('Capacity Information', { 'fields': [ 'capacity', 'enrollment_count' ] }), ('Cost Override', { 'fields': [ 'cost_override', 'hours_override' ] }), ('Metadata', { 'fields': [ 'created_at', 'created_by' ], 'classes': ['collapse'] }) ] date_hierarchy = 'start_at' def title_display(self, obj): """Display session title or program name.""" return obj.title or obj.program.name title_display.short_description = 'Title' def enrollment_count(self, obj): """Display enrollment count.""" return obj.enrollments.count() enrollment_count.short_description = 'Enrollments' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(program__tenant=request.user.tenant) return qs.select_related('program', 'instructor', 'created_by') @admin.register(TrainingRecord) class TrainingRecordAdmin(admin.ModelAdmin): """ Admin interface for training records (enrollments). """ list_display = [ 'employee_name', 'program', 'session', 'enrolled_at', 'completion_date', 'status', 'passed', 'expiry_status_display' ] list_filter = [ 'employee__tenant', 'status', 'passed', 'program__program_type', 'enrolled_at', 'completion_date' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id', 'program__name' ] readonly_fields = [ 'record_id', 'enrolled_at', 'hours', 'effective_cost', 'eligible_for_certificate', 'completion_percentage', 'created_at', 'updated_at' ] fieldsets = [ ('Enrollment Information', { 'fields': [ 'record_id', 'employee', 'program', 'session' ] }), ('Enrollment Dates', { 'fields': [ 'enrolled_at', 'started_at', 'completion_date', 'expiry_date' ] }), ('Status Information', { 'fields': [ 'status', 'passed' ] }), ('Performance Information', { 'fields': [ 'credits_earned', 'score' ] }), ('Cost Information', { 'fields': [ 'cost_paid', 'effective_cost' ] }), ('Calculated Information', { 'fields': [ 'hours', 'eligible_for_certificate', 'completion_percentage' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] inlines = [TrainingAttendanceInline, TrainingAssessmentInline] date_hierarchy = 'enrolled_at' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def expiry_status_display(self, obj): """Display expiry status with color coding.""" if not obj.expiry_date: return format_html('No Expiry') days_to_expiry = (obj.expiry_date - date.today()).days if days_to_expiry < 0: return format_html('⚠️ Expired') elif days_to_expiry <= 30: return format_html('⚠️ Expires Soon') else: return format_html('✓ Valid') expiry_status_display.short_description = 'Expiry Status' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'program', 'session', 'created_by') @admin.register(TrainingAttendance) class TrainingAttendanceAdmin(admin.ModelAdmin): """ Admin interface for training attendance. """ list_display = [ 'employee_name', 'program_name', 'checked_in_at', 'checked_out_at', 'status', 'duration_display' ] list_filter = [ 'enrollment__employee__tenant', 'status', 'checked_in_at' ] search_fields = [ 'enrollment__employee__first_name', 'enrollment__employee__last_name', 'enrollment__program__name' ] readonly_fields = [ 'duration_display' ] fieldsets = [ ('Attendance Information', { 'fields': [ 'enrollment' ] }), ('Check-in/out Times', { 'fields': [ 'checked_in_at', 'checked_out_at' ] }), ('Status Information', { 'fields': [ 'status' ] }), ('Duration Information', { 'fields': [ 'duration_display' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }) ] date_hierarchy = 'checked_in_at' def employee_name(self, obj): """Display employee name.""" return obj.enrollment.employee.get_full_name() employee_name.short_description = 'Employee' def program_name(self, obj): """Display program name.""" return obj.enrollment.program.name program_name.short_description = 'Program' def duration_display(self, obj): """Display attendance duration.""" if obj.checked_in_at and obj.checked_out_at: duration = obj.checked_out_at - obj.checked_in_at hours = duration.total_seconds() / 3600 return f"{hours:.2f} hours" return "Incomplete" duration_display.short_description = 'Duration' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(enrollment__employee__tenant=request.user.tenant) return qs.select_related('enrollment__employee', 'enrollment__program') @admin.register(TrainingAssessment) class TrainingAssessmentAdmin(admin.ModelAdmin): """ Admin interface for training assessments. """ list_display = [ 'employee_name', 'program_name', 'name', 'score', 'max_score', 'percentage_display', 'passed', 'taken_at' ] list_filter = [ 'enrollment__employee__tenant', 'passed', 'taken_at' ] search_fields = [ 'enrollment__employee__first_name', 'enrollment__employee__last_name', 'enrollment__program__name', 'name' ] readonly_fields = [ 'percentage_display' ] fieldsets = [ ('Assessment Information', { 'fields': [ 'enrollment', 'name' ] }), ('Score Information', { 'fields': [ 'max_score', 'score', 'percentage_display', 'passed' ] }), ('Date Information', { 'fields': [ 'taken_at' ] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }) ] date_hierarchy = 'taken_at' def employee_name(self, obj): """Display employee name.""" return obj.enrollment.employee.get_full_name() employee_name.short_description = 'Employee' def program_name(self, obj): """Display program name.""" return obj.enrollment.program.name program_name.short_description = 'Program' def percentage_display(self, obj): """Display score percentage.""" if obj.score is not None and obj.max_score > 0: percentage = (obj.score / obj.max_score) * 100 color = 'green' if percentage >= 70 else 'orange' if percentage >= 50 else 'red' return format_html( '{:.1f}%', color, percentage ) return "N/A" percentage_display.short_description = 'Percentage' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(enrollment__employee__tenant=request.user.tenant) return qs.select_related('enrollment__employee', 'enrollment__program') @admin.register(TrainingCertificates) class TrainingCertificatesAdmin(admin.ModelAdmin): """ Admin interface for training certificates. """ list_display = [ 'employee_name', 'certificate_name', 'program', 'issued_date', 'expiry_date', 'expiry_status_display', 'certificate_number' ] list_filter = [ 'employee__tenant', 'program', 'issued_date', 'expiry_date' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'certificate_name', 'certificate_number', 'program__name' ] readonly_fields = [ 'certificate_id', 'is_expired', 'days_to_expiry', 'created_at', 'updated_at' ] fieldsets = [ ('Certificate Information', { 'fields': [ 'certificate_id', 'employee', 'program', 'enrollment' ] }), ('Certificate Details', { 'fields': [ 'certificate_name', 'certificate_number', 'certification_body' ] }), ('Date Information', { 'fields': [ 'issued_date', 'expiry_date' ] }), ('File Information', { 'fields': [ 'file' ] }), ('Signature Information', { 'fields': [ 'created_by', 'signed_by' ] }), ('Status Information', { 'fields': [ 'is_expired', 'days_to_expiry' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at' ], 'classes': ['collapse'] }) ] date_hierarchy = 'issued_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def expiry_status_display(self, obj): """Display expiry status with color coding.""" if not obj.expiry_date: return format_html('No Expiry') if obj.is_expired: return format_html('⚠️ Expired') if obj.days_to_expiry is not None and obj.days_to_expiry <= 30: return format_html('⚠️ Expires Soon') return format_html('✓ Valid') expiry_status_display.short_description = 'Expiry Status' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'program', 'enrollment', 'created_by', 'signed_by') # ============================================================================ # LEAVE MANAGEMENT ADMIN CLASSES # ============================================================================ @admin.register(LeaveType) class LeaveTypeAdmin(admin.ModelAdmin): """ Admin interface for leave types. """ list_display = [ 'code', 'name', 'is_paid', 'requires_approval', 'annual_entitlement', 'accrual_method', 'is_active' ] list_filter = [ 'tenant', 'is_paid', 'requires_approval', 'accrual_method', 'is_active', 'gender_specific' ] search_fields = [ 'code', 'name', 'description' ] readonly_fields = [ 'leave_type_id', 'created_at', 'updated_at' ] fieldsets = [ ('Leave Type Information', { 'fields': [ 'leave_type_id', 'tenant', 'name', 'code', 'description' ] }), ('Leave Configuration', { 'fields': [ 'is_paid', 'requires_approval', 'requires_documentation' ] }), ('Accrual Settings', { 'fields': [ 'accrual_method', 'annual_entitlement', 'max_carry_over', 'max_consecutive_days', 'min_notice_days' ] }), ('Availability', { 'fields': [ 'is_active', 'available_for_all', 'gender_specific' ] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(tenant=request.user.tenant) return qs @admin.register(LeaveBalance) class LeaveBalanceAdmin(admin.ModelAdmin): """ Admin interface for leave balances. """ list_display = [ 'employee_name', 'leave_type', 'year', 'total_entitled_display', 'used', 'pending', 'available_display' ] list_filter = [ 'employee__tenant', 'leave_type', 'year' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id', 'leave_type__name' ] readonly_fields = [ 'balance_id', 'available', 'total_entitled', 'created_at', 'updated_at' ] fieldsets = [ ('Balance Information', { 'fields': [ 'balance_id', 'employee', 'leave_type', 'year' ] }), ('Balance Tracking', { 'fields': [ 'opening_balance', 'accrued', 'used', 'pending', 'adjusted' ] }), ('Calculated Fields', { 'fields': [ 'available', 'total_entitled' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'last_accrual_date', 'created_at', 'updated_at' ], 'classes': ['collapse'] }) ] def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def total_entitled_display(self, obj): """Display total entitled days.""" return f"{obj.total_entitled:.2f}" total_entitled_display.short_description = 'Entitled' def available_display(self, obj): """Display available balance with color coding.""" available = obj.available color = 'green' if available > 5 else 'orange' if available > 0 else 'red' return format_html( '{:.2f}', color, available ) available_display.short_description = 'Available' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'leave_type') class LeaveApprovalInline(admin.TabularInline): """ Inline admin for leave approvals. """ model = LeaveApproval extra = 0 fields = [ 'level', 'approver', 'action', 'comments', 'action_date' ] readonly_fields = ['approval_id'] @admin.register(LeaveRequest) class LeaveRequestAdmin(admin.ModelAdmin): """ Admin interface for leave requests. """ list_display = [ 'employee_name', 'leave_type', 'start_date', 'end_date', 'total_days', 'status', 'current_approver', 'submitted_at' ] list_filter = [ 'employee__tenant', 'leave_type', 'status', 'start_date' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id', 'reason' ] readonly_fields = [ 'request_id', 'tenant', 'total_days', 'is_pending', 'is_approved', 'can_cancel', 'can_edit', 'created_at', 'updated_at' ] fieldsets = [ ('Request Information', { 'fields': [ 'request_id', 'employee', 'leave_type' ] }), ('Leave Details', { 'fields': [ 'start_date', 'end_date', 'start_day_type', 'end_day_type', 'total_days' ] }), ('Request Information', { 'fields': [ 'reason', 'contact_number', 'emergency_contact' ] }), ('Supporting Documents', { 'fields': [ 'attachment' ] }), ('Status and Workflow', { 'fields': [ 'status', 'submitted_at' ] }), ('Approval Information', { 'fields': [ 'current_approver', 'final_approver', 'approved_at', 'rejected_at', 'rejection_reason' ] }), ('Cancellation', { 'fields': [ 'cancelled_at', 'cancelled_by', 'cancellation_reason' ] }), ('Status Checks', { 'fields': [ 'is_pending', 'is_approved', 'can_cancel', 'can_edit' ], 'classes': ['collapse'] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] inlines = [LeaveApprovalInline] date_hierarchy = 'start_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related( 'employee', 'leave_type', 'current_approver', 'final_approver', 'cancelled_by', 'created_by' ) @admin.register(LeaveApproval) class LeaveApprovalAdmin(admin.ModelAdmin): """ Admin interface for leave approvals. """ list_display = [ 'leave_request_display', 'level', 'approver', 'action', 'action_date', 'is_delegated_display' ] list_filter = [ 'action', 'level', 'action_date' ] search_fields = [ 'leave_request__employee__first_name', 'leave_request__employee__last_name', 'approver__first_name', 'approver__last_name' ] readonly_fields = [ 'approval_id', 'is_pending', 'is_delegated_approval', 'created_at', 'updated_at' ] fieldsets = [ ('Approval Information', { 'fields': [ 'approval_id', 'leave_request', 'level', 'approver' ] }), ('Delegation', { 'fields': [ 'delegated_by' ] }), ('Approval Details', { 'fields': [ 'action', 'comments', 'action_date' ] }), ('Status Information', { 'fields': [ 'is_pending', 'is_delegated_approval' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at' ], 'classes': ['collapse'] }) ] def leave_request_display(self, obj): """Display leave request summary.""" return f"{obj.leave_request.employee.get_full_name()} - {obj.leave_request.leave_type.name}" leave_request_display.short_description = 'Leave Request' def is_delegated_display(self, obj): """Display delegation status.""" if obj.is_delegated_approval: return format_html( '✓ Delegated from {}', obj.delegated_by.get_full_name() ) return format_html('Direct') is_delegated_display.short_description = 'Delegation' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(leave_request__employee__tenant=request.user.tenant) return qs.select_related( 'leave_request__employee', 'leave_request__leave_type', 'approver', 'delegated_by' ) @admin.register(LeaveDelegate) class LeaveDelegateAdmin(admin.ModelAdmin): """ Admin interface for leave delegations. """ list_display = [ 'delegator', 'delegate', 'start_date', 'end_date', 'is_current_display', 'is_active' ] list_filter = [ 'delegator__tenant', 'is_active', 'start_date', 'end_date' ] search_fields = [ 'delegator__first_name', 'delegator__last_name', 'delegate__first_name', 'delegate__last_name' ] readonly_fields = [ 'delegation_id', 'tenant', 'is_current', 'created_at', 'updated_at' ] fieldsets = [ ('Delegation Information', { 'fields': [ 'delegation_id', 'delegator', 'delegate' ] }), ('Delegation Period', { 'fields': [ 'start_date', 'end_date' ] }), ('Delegation Scope', { 'fields': [ 'reason', 'is_active' ] }), ('Status Information', { 'fields': [ 'is_current' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] date_hierarchy = 'start_date' def is_current_display(self, obj): """Display current status.""" if obj.is_current: return format_html('✓ Active') return format_html('Inactive') is_current_display.short_description = 'Current' def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(delegator__tenant=request.user.tenant) return qs.select_related('delegator', 'delegate', 'created_by') # ============================================================================ # CUSTOM ADMIN ACTIONS # ============================================================================ def mark_training_completed(modeladmin, request, queryset): """Mark selected training records as completed.""" updated = queryset.update(status='COMPLETED', completion_date=date.today()) modeladmin.message_user(request, f'{updated} training records marked as completed.') mark_training_completed.short_description = "Mark selected training as completed" def mark_training_passed(modeladmin, request, queryset): """Mark selected training records as passed.""" updated = queryset.update(passed=True) modeladmin.message_user(request, f'{updated} training records marked as passed.') mark_training_passed.short_description = "Mark selected training as passed" def generate_certificates(modeladmin, request, queryset): """Generate certificates for completed training.""" count = 0 for record in queryset.filter(status='COMPLETED', passed=True): if record.program.is_certified and not hasattr(record, 'certificate'): TrainingCertificates.objects.create( program=record.program, employee=record.employee, enrollment=record, certificate_name=f"{record.program.name} Certificate", expiry_date=TrainingCertificates.compute_expiry(record.program, date.today()), created_by=request.user ) count += 1 modeladmin.message_user(request, f'{count} certificates generated.') generate_certificates.short_description = "Generate certificates for completed training" # Add actions to TrainingRecord admin TrainingRecordAdmin.actions = [mark_training_completed, mark_training_passed, generate_certificates] # ============================================================================ # ADMIN SITE CUSTOMIZATION # ============================================================================ # Customize admin site admin.site.site_header = "Hospital Management System - HR" admin.site.site_title = "HR Admin" admin.site.index_title = "Human Resources Administration" # ============================================================================ # SALARY & COMPENSATION ADMIN CLASSES # ============================================================================ @admin.register(SalaryInformation) class SalaryInformationAdmin(admin.ModelAdmin): """ Admin interface for salary information. """ list_display = [ 'employee_name', 'effective_date', 'total_salary_display', 'currency', 'payment_frequency', 'is_active', 'created_at' ] list_filter = [ 'employee__tenant', 'is_active', 'currency', 'payment_frequency', 'effective_date' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id' ] readonly_fields = [ 'salary_id', 'total_salary', 'tenant', 'created_at', 'updated_at' ] fieldsets = [ ('Employee Information', { 'fields': [ 'salary_id', 'employee', 'effective_date', 'end_date', 'is_active' ] }), ('Salary Components', { 'fields': [ 'basic_salary', 'housing_allowance', 'transportation_allowance', 'food_allowance', 'other_allowances', 'total_salary' ] }), ('Payment Details', { 'fields': [ 'currency', 'payment_frequency', 'bank_name', 'account_number', 'iban', 'swift_code' ] }), ('Additional Information', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] date_hierarchy = 'effective_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def total_salary_display(self, obj): """Display total salary with formatting.""" return format_html( '{:,.2f} {}', obj.total_salary, obj.currency ) total_salary_display.short_description = 'Total Salary' def save_model(self, request, obj, form, change): """Set created_by on creation.""" if not change: obj.created_by = request.user super().save_model(request, obj, form, change) def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'created_by') @admin.register(SalaryAdjustment) class SalaryAdjustmentAdmin(admin.ModelAdmin): """ Admin interface for salary adjustments. """ list_display = [ 'employee_name', 'adjustment_type', 'effective_date', 'adjustment_amount_display', 'adjustment_percentage_display', 'approved_by', 'approval_date' ] list_filter = [ 'employee__tenant', 'adjustment_type', 'effective_date', 'approved_by' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id', 'adjustment_reason' ] readonly_fields = [ 'adjustment_id', 'tenant', 'adjustment_amount', 'adjustment_percentage', 'created_at', 'updated_at' ] fieldsets = [ ('Adjustment Information', { 'fields': [ 'adjustment_id', 'employee' ] }), ('Salary References', { 'fields': [ 'previous_salary', 'new_salary' ] }), ('Adjustment Details', { 'fields': [ 'adjustment_type', 'adjustment_reason', 'adjustment_amount', 'adjustment_percentage' ] }), ('Effective Date', { 'fields': [ 'effective_date' ] }), ('Approval', { 'fields': [ 'approved_by', 'approval_date' ] }), ('Notes', { 'fields': [ 'notes' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] date_hierarchy = 'effective_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def adjustment_amount_display(self, obj): """Display adjustment amount with color coding.""" color = 'green' if obj.adjustment_amount >= 0 else 'red' return format_html( '{:+,.2f}', color, obj.adjustment_amount ) adjustment_amount_display.short_description = 'Amount Change' def adjustment_percentage_display(self, obj): """Display adjustment percentage.""" if obj.adjustment_percentage: color = 'green' if obj.adjustment_percentage >= 0 else 'red' return format_html( '{:+.2f}%', color, obj.adjustment_percentage ) return 'N/A' adjustment_percentage_display.short_description = 'Percentage' def save_model(self, request, obj, form, change): """Set created_by on creation.""" if not change: obj.created_by = request.user super().save_model(request, obj, form, change) def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related( 'employee', 'previous_salary', 'new_salary', 'approved_by', 'created_by' ) # ============================================================================ # DOCUMENT REQUEST ADMIN CLASSES # ============================================================================ @admin.register(DocumentRequest) class DocumentRequestAdmin(admin.ModelAdmin): """ Admin interface for document requests. """ list_display = [ 'employee_name', 'document_type', 'status', 'requested_date', 'required_by_date', 'urgency_indicator', 'processed_by' ] list_filter = [ 'employee__tenant', 'status', 'document_type', 'language', 'delivery_method', 'requested_date', 'required_by_date' ] search_fields = [ 'employee__first_name', 'employee__last_name', 'employee__employee_id', 'purpose', 'document_number' ] readonly_fields = [ 'request_id', 'tenant', 'requested_date', 'document_number', 'is_urgent', 'is_overdue', 'can_cancel', 'created_at', 'updated_at' ] list_editable = ['status'] actions = ['mark_as_ready', 'mark_as_delivered', 'mark_as_rejected'] fieldsets = [ ('Request Information', { 'fields': [ 'request_id', 'employee', 'document_type', 'custom_document_name' ] }), ('Request Details', { 'fields': [ 'purpose', 'addressee', 'include_salary' ] }), ('Language and Delivery', { 'fields': [ 'language', 'delivery_method', 'delivery_address', 'delivery_email' ] }), ('Dates', { 'fields': [ 'requested_date', 'required_by_date' ] }), ('Status', { 'fields': [ 'status' ] }), ('Processing', { 'fields': [ 'processed_by', 'processed_date' ] }), ('Generated Document', { 'fields': [ 'generated_document', 'document_number' ] }), ('Rejection', { 'fields': [ 'rejection_reason' ] }), ('Additional Information', { 'fields': [ 'additional_notes' ], 'classes': ['collapse'] }), ('Status Checks', { 'fields': [ 'is_urgent', 'is_overdue', 'can_cancel' ], 'classes': ['collapse'] }), ('Related Information', { 'fields': [ 'tenant' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] date_hierarchy = 'requested_date' def employee_name(self, obj): """Display employee name.""" return obj.employee.get_full_name() employee_name.short_description = 'Employee' def urgency_indicator(self, obj): """Display urgency indicator.""" if obj.is_urgent: return format_html('🔴 Urgent') elif obj.is_overdue: return format_html('⚠️ Overdue') return '✓' urgency_indicator.short_description = 'Urgency' def mark_as_ready(self, request, queryset): """Mark selected requests as ready.""" updated = queryset.update(status='READY') self.message_user(request, f'{updated} requests marked as ready.') mark_as_ready.short_description = 'Mark selected as Ready' def mark_as_delivered(self, request, queryset): """Mark selected requests as delivered.""" updated = queryset.update(status='DELIVERED') self.message_user(request, f'{updated} requests marked as delivered.') mark_as_delivered.short_description = 'Mark selected as Delivered' def mark_as_rejected(self, request, queryset): """Mark selected requests as rejected.""" updated = queryset.update(status='REJECTED') self.message_user(request, f'{updated} requests marked as rejected.') mark_as_rejected.short_description = 'Mark selected as Rejected' def save_model(self, request, obj, form, change): """Set created_by on creation.""" if not change: obj.created_by = request.user super().save_model(request, obj, form, change) def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(employee__tenant=request.user.tenant) return qs.select_related('employee', 'processed_by', 'created_by') @admin.register(DocumentTemplate) class DocumentTemplateAdmin(admin.ModelAdmin): """ Admin interface for document templates. """ list_display = [ 'name', 'document_type', 'language', 'is_active', 'is_default', 'requires_approval', 'created_at' ] list_filter = [ 'tenant', 'document_type', 'language', 'is_active', 'is_default' ] search_fields = [ 'name', 'description', 'template_content' ] readonly_fields = [ 'template_id', 'created_at', 'updated_at' ] fieldsets = [ ('Template Information', { 'fields': [ 'template_id', 'tenant', 'name', 'description' ] }), ('Document Type', { 'fields': [ 'document_type', 'language' ] }), ('Template Content', { 'fields': [ 'template_content', 'header_content', 'footer_content' ] }), ('Placeholders', { 'fields': [ 'available_placeholders' ] }), ('Settings', { 'fields': [ 'is_active', 'is_default', 'requires_approval' ] }), ('Styling', { 'fields': [ 'css_styles' ], 'classes': ['collapse'] }), ('Metadata', { 'fields': [ 'created_at', 'updated_at', 'created_by' ], 'classes': ['collapse'] }) ] def save_model(self, request, obj, form, change): """Set created_by on creation.""" if not change: obj.created_by = request.user super().save_model(request, obj, form, change) def get_queryset(self, request): """Filter by user's tenant.""" qs = super().get_queryset(request) if hasattr(request.user, 'tenant'): qs = qs.filter(tenant=request.user.tenant) return qs.select_related('created_by')