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 ? (