diff --git a/backend/employees/utils.py b/backend/employees/utils.py index 67bb89cf..4ebb640c 100644 --- a/backend/employees/utils.py +++ b/backend/employees/utils.py @@ -9,6 +9,7 @@ from employees.models import ( DepartmentPermission, DeptPerm, EmployeeDepartment, + RoleAssignment, ) from phonenumbers import PhoneNumberFormat @@ -183,6 +184,7 @@ def _build_links_for_dept(dept: Department, serializer) -> list[dict]: "is_active": bool}, ...] """ links: list[dict] = [] + seen_employee_ids: set[int] = set() qs = ( EmployeeDepartment.objects.filter(department_id=dept.id) .select_related("employee", "role") @@ -195,6 +197,7 @@ def _build_links_for_dept(dept: Department, serializer) -> list[dict]: ) for link in qs: emp_data = serializer(link.employee).data # содержит display_name + seen_employee_ids.add(link.employee_id) role = ( {"id": link.role_id, "name": link.role.name} if link.role_id @@ -205,6 +208,65 @@ def _build_links_for_dept(dept: Department, serializer) -> list[dict]: "employee": emp_data, "role": role, "is_active": bool(link.is_active), + "via_assignment": False, + } + ) + + assignment_qs = ( + RoleAssignment.objects.filter( + role__department_id=dept.id, + is_active=True, + ) + .select_related("employee", "role") + .order_by( + "employee__last_name", + "employee__first_name", + "employee__patronymic", + "employee_id", + "role__name", + ) + ) + + grouped_assignments: dict[int, dict[str, object]] = {} + for assignment in assignment_qs: + if assignment.employee_id in seen_employee_ids: + continue + + bucket = grouped_assignments.get(assignment.employee_id) + if bucket is None: + bucket = { + "employee": serializer(assignment.employee).data, + "role_ids": [], + "role_names": [], + } + grouped_assignments[assignment.employee_id] = bucket + + role_ids = bucket["role_ids"] + role_names = bucket["role_names"] + assert isinstance(role_ids, list) + assert isinstance(role_names, list) + role_ids.append(assignment.role_id) + role_names.append(assignment.role.name) + + for employee_id, bucket in grouped_assignments.items(): + seen_employee_ids.add(employee_id) + role_ids = bucket["role_ids"] + role_names = bucket["role_names"] + assert isinstance(role_ids, list) + assert isinstance(role_names, list) + links.append( + { + "employee": bucket["employee"], + "role": ( + { + "id": role_ids[0] if role_ids else None, + "name": ", ".join(role_names) if role_names else "", + } + if role_names + else None + ), + "is_active": True, + "via_assignment": True, } ) @@ -214,7 +276,13 @@ def _build_links_for_dept(dept: Department, serializer) -> list[dict]: ): head_data = serializer(dept.head).data links.insert( - 0, {"employee": head_data, "role": None, "is_active": True} + 0, + { + "employee": head_data, + "role": None, + "is_active": True, + "via_assignment": False, + }, ) return links diff --git a/backend/tests/api/v1/employees/test_role_assignment.py b/backend/tests/api/v1/employees/test_role_assignment.py index 96fa9487..9334d182 100644 --- a/backend/tests/api/v1/employees/test_role_assignment.py +++ b/backend/tests/api/v1/employees/test_role_assignment.py @@ -168,6 +168,32 @@ class TestRoleAssignmentEndpoints: assert resp.status_code == status.HTTP_201_CREATED + def test_department_members_include_role_only_employees(self, api_client: APIClient): + """members endpoint показывает сотрудников с ролью в отделе даже без membership.""" + admin = make_user("admin-members@test.com", is_staff=True) + dept = make_dept("IT") + role = make_role(dept, "Developer", []) + role_only_user = make_user("role-only@test.com") + + RoleAssignment.objects.create( + employee=role_only_user, + role=role, + is_active=True, + ) + + api_client.force_authenticate(admin) + resp = api_client.get(f"/api/v1/departments/{dept.id}/members/") + + assert resp.status_code == status.HTTP_200_OK + role_only_link = next( + item + for item in resp.data["results"] + if item["employee"]["id"] == role_only_user.id + ) + assert role_only_link["via_assignment"] is True + assert role_only_link["is_active"] is True + assert role_only_link["role"]["name"] == "Developer" + @pytest.mark.django_db class TestRoleAssignmentPermissions: """Тесты для проверки прав через RoleAssignment.""" diff --git a/frontend/src/app/departments/[id]/page.tsx b/frontend/src/app/departments/[id]/page.tsx index c391a413..47511674 100644 --- a/frontend/src/app/departments/[id]/page.tsx +++ b/frontend/src/app/departments/[id]/page.tsx @@ -103,6 +103,7 @@ function DepartmentMemberRow({ const managementMode = canAssignRoles || canChangeHead || canManage; const roleLabel = member.role?.name || "Без роли"; const isRoleMenuOpen = roleMenuOpenForId === member.employee.id; + const isRoleOnly = member.via_assignment === true; const personName = displayUserName(member.employee); const profileLink = userProfileLink(member.employee, currentUserId); const fallback = ( @@ -139,6 +140,7 @@ function DepartmentMemberRow({ ) : null} + {isRoleOnly ? Роль вне состава : null} ); @@ -282,6 +284,8 @@ function DepartmentMemberRow({ ) : null} + {isRoleOnly ? Роль вне состава : null} + {canChangeHead && !isHead ? (