from django import forms from django.contrib import admin from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from unfold.admin import ModelAdmin, TabularInline from core.models import Directory from .models import Employee, PermissionGroup from apps.visit_manager.models import WorkSchedule, AttendanceLog class HierarchicalDepartmentChoiceField(forms.ModelChoiceField): """Поле выбора отдела с отступами по иерархии в выпадающем списке.""" def __init__(self, *args, **kwargs): queryset = kwargs.get('queryset') or Directory.objects.filter( directory_type__code='department', is_active=True, ).select_related('directory_type').order_by('_path') kwargs['queryset'] = queryset super().__init__(*args, **kwargs) def label_from_instance(self, obj): depth = (getattr(obj, '_path', '') or '').count('.') indent = '---' * depth + ('--- ' if depth else '') return f'{indent}{obj.name}' class OfficeWarehouseChoiceField(forms.ModelChoiceField): def label_from_instance(self, obj): type_label = obj.get_location_type_display() addr = obj.get_full_address() or obj.address or '' return f"{type_label} — {addr}" if addr else type_label class EmployeeAdminForm(forms.ModelForm): department = HierarchicalDepartmentChoiceField( label=_('Отдел'), required=False, empty_label=_('— Выберите отдел —'), widget=forms.Select(attrs={ 'class': 'hierarchy-department-select', 'data-placeholder': 'Департамент → Отдел', }), help_text=_('Выберите из списка. Уровни вложенности отображаются отступами.'), ) user_location = OfficeWarehouseChoiceField( queryset=None, required=False, empty_label=_('— Выберите офис или склад —'), label=_('Локация (офис/склад)'), help_text=_('Заполняется для location_type=office или warehouse'), ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from apps.locations.models import Location self.fields['user_location'].queryset = ( Location.objects .filter(location_type__in=['office', 'warehouse'], is_active=True) .select_related('location_region') .order_by('location_type', 'address') ) class Meta: model = Employee fields = '__all__' class WorkScheduleInline(TabularInline): model = WorkSchedule fk_name = 'employee' extra = 0 fields = ('schedule_template', 'location', 'start_date', 'end_date', 'is_primary', 'is_active') readonly_fields = ('is_active',) ordering = ('-start_date',) show_change_link = True def has_add_permission(self, request, obj=None): return False class AttendanceLogInline(TabularInline): model = AttendanceLog fk_name = 'employee' extra = 0 max_num = 0 fields = ('timestamp', 'direction', 'location', 'device_id', 'synced') readonly_fields = ('timestamp', 'direction', 'location', 'device_id', 'synced') ordering = ('-timestamp',) show_change_link = True def has_add_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): return False @admin.register(Employee) class EmployeeAdmin(ModelAdmin): form = EmployeeAdminForm inlines = [WorkScheduleInline, AttendanceLogInline] class Media: css = {'all': ('users/css/employee_admin.css',)} js = ('assets/js/admin_phone_format.js',) list_display = [ 'photo_thumbnail', 'username', 'last_name', 'first_name', 'groups_display', 'user_location_type', 'user_pharmacy', 'department_display', 'post', 'is_active', 'twofa_status', 'hire_date' ] list_filter = ['groups', 'is_active', 'is_staff', 'user_location_type', 'department', 'totp_enabled'] search_fields = ['username', 'email', 'last_name', 'first_name', 'middle_name'] list_select_related = ['user_pharmacy', 'department', 'post'] readonly_fields = ('photo_preview',) actions = ['reset_2fa'] @admin.display(description='2FA', boolean=True) def twofa_status(self, obj): return obj.totp_enabled @admin.action(description='Сбросить 2FA (отключить у выбранных)') def reset_2fa(self, request, queryset): from apps.employees.services import twofa n = 0 for emp in queryset: if emp.totp_enabled or emp.totp_secret: twofa.disable(emp) n += 1 self.message_user(request, f'2FA сброшена у {n} сотрудник(ов).') fieldsets = ( (_('Аутентификация'), { 'fields': ('username', 'email', 'groups', 'is_active'), 'classes': ('tab',), }), (_('ФИО'), { 'fields': ('last_name', 'first_name', 'middle_name'), 'classes': ('tab',), }), (_('Рабочая информация'), { 'fields': ( 'user_location_type', 'user_organization', 'user_pharmacy', 'user_location', 'department', 'post', 'manager', ), 'classes': ('tab',), }), (_('Даты'), { 'fields': ('hire_date', 'termination_date', 'probation_end_date', 'date_joined'), 'classes': ('tab',), }), (_('Личные данные'), { 'fields': ('birth_date', 'inn_code', 'photo', 'photo_preview'), 'classes': ('tab',), }), (_('Контакты'), { 'fields': ( 'phone', 'internal_number', 'personal_phone', 'personal_email', 'emergency_contact_name', 'emergency_contact_phone', 'emergency_contact_relation', ), 'classes': ('tab',), }), ('Mattermost', { 'fields': ('mattermost_user_id', 'mattermost_username', 'mattermost_sync_status', 'mattermost_last_sync', 'mattermost_sync_enabled'), 'classes': ('tab', 'collapse'), }), ('Active Directory', { 'fields': ('ad_user_dn', 'ad_sync_status', 'ad_sync_error', 'ad_last_sync', 'ad_sync_enabled'), 'classes': ('tab', 'collapse'), }), (_('Графік та відвідуваність'), { 'fields': (), 'classes': ('tab',), 'description': _('Графіки роботи та записи відвідуваності відображаються нижче.'), }), ) def get_fieldsets(self, request, obj=None): fieldsets = list(super().get_fieldsets(request, obj)) if request.user.is_superuser: fieldsets.append(( _('Системный доступ'), { 'fields': ('is_staff', 'user_permissions'), 'classes': ('tab', 'collapse'), 'description': _('Только для IT-администраторов. is_staff — вход в Django admin.'), }, )) return fieldsets # def permissions_summary(self, obj): # perms = obj.effective_permissions() # group = obj.permission_group.name if obj.permission_group_id else None # if not perms and not group: # return mark_safe('—') # parts = [] # if group: # parts.append(format_html( # '📁 {}', # group, # )) # for p in perms: # parts.append(format_html( # '{}', # p, # )) # return mark_safe(' '.join(parts)) # permissions_summary.short_description = _('Группа / Разрешения') def photo_thumbnail(self, obj): if obj.photo: return format_html( '', obj.photo.url ) return mark_safe('—') photo_thumbnail.short_description = '' def photo_preview(self, obj): if obj.photo: return format_html( '', obj.photo.url ) return mark_safe('Фото не загружено') photo_preview.short_description = _('Предпросмотр') def groups_display(self, obj): names = list(obj.groups.values_list('name', flat=True)) if not names: return mark_safe('—') return ', '.join(names) groups_display.short_description = _('Группы') def department_display(self, obj): # noqa: E303 """Отдел с полным путём иерархии без дубликатов (Офис → HR → Кадры).""" if not obj.department: return mark_safe('—') try: parts = [n.name for n in obj.department.get_ancestors()] + [obj.department.name] # Убираем подряд идущие дубликаты (родитель и ребёнок с одинаковым именем) unique = [] for p in parts: if not unique or unique[-1] != p: unique.append(p) path = ' → '.join(unique) return format_html( '{}', path, path ) except Exception: return obj.department.name or '—' department_display.short_description = _('Отдел')