refactor(departments): label role-only members

This commit is contained in:
lizerk 2026-04-14 00:35:51 +03:00
parent a4910421c3
commit 274e5767c9
4 changed files with 101 additions and 2 deletions

View File

@ -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

View File

@ -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."""

View File

@ -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({
<Crown size={12} />
</MetaChip>
) : null}
{isRoleOnly ? <MetaChip>Роль вне состава</MetaChip> : null}
</span>
);
@ -282,6 +284,8 @@ function DepartmentMemberRow({
</MetaChip>
) : null}
{isRoleOnly ? <MetaChip>Роль вне состава</MetaChip> : null}
{canChangeHead && !isHead ? (
<button
type="button"
@ -294,7 +298,7 @@ function DepartmentMemberRow({
</button>
) : null}
{canManage && !isHead ? (
{canManage && !isHead && !isRoleOnly ? (
<button
type="button"
onClick={() => void onRemoveMember(member.employee.id)}

View File

@ -131,6 +131,7 @@ export interface DepartmentMemberLink {
employee: User;
role?: DepartmentMemberRole | null;
is_active: boolean;
via_assignment?: boolean;
}
export interface DepartmentUserPermissions {