diff --git a/backend/api/v1/employees/serializers/department.py b/backend/api/v1/employees/serializers/department.py index 0667c78b..f7063faa 100644 --- a/backend/api/v1/employees/serializers/department.py +++ b/backend/api/v1/employees/serializers/department.py @@ -19,6 +19,7 @@ class DepartmentSerializer(serializers.ModelSerializer): allow_null=True, ) employees_count = serializers.IntegerField(read_only=True) + role_only_count = serializers.IntegerField(read_only=True) class Meta: model = Department @@ -29,6 +30,7 @@ class DepartmentSerializer(serializers.ModelSerializer): "head", "head_id", "employees_count", + "role_only_count", ) diff --git a/backend/api/v1/employees/views/departments.py b/backend/api/v1/employees/views/departments.py index 0bf96626..fc0ed460 100644 --- a/backend/api/v1/employees/views/departments.py +++ b/backend/api/v1/employees/views/departments.py @@ -130,18 +130,34 @@ class DepartmentViewSet(viewsets.ModelViewSet): active_links = EmployeeDepartment.objects.filter( department_id=OuterRef("pk"), is_active=True ) + active_role_assignments = RoleAssignment.objects.filter( + role__department_id=OuterRef("pk"), + is_active=True, + ).exclude( + employee__departments_links__department_id=OuterRef("pk"), + employee__departments_links__is_active=True, + ) active_count_subq = ( active_links.values("department_id") .annotate(c=Count("employee_id", distinct=True)) .values("c")[:1] ) + role_only_count_subq = ( + active_role_assignments.values("role__department_id") + .annotate(c=Count("employee_id", distinct=True)) + .values("c")[:1] + ) qs = qs.annotate( active_count=Coalesce( Subquery(active_count_subq, output_field=IntegerField()), Value(0), ), + role_only_count=Coalesce( + Subquery(role_only_count_subq, output_field=IntegerField()), + Value(0), + ), head_in_active=Exists( active_links.filter(employee_id=OuterRef("head_id")) ), diff --git a/backend/tests/api/v1/employees/test_departments.py b/backend/tests/api/v1/employees/test_departments.py index ed4c05d8..787fb5d0 100644 --- a/backend/tests/api/v1/employees/test_departments.py +++ b/backend/tests/api/v1/employees/test_departments.py @@ -57,6 +57,7 @@ from employees.models import ( DepartmentPermission, DepartmentRole, EmployeeDepartment, + RoleAssignment, ) from rest_framework import status from rest_framework.test import APIClient @@ -201,6 +202,57 @@ def test_employees_count_not_double_count_head_if_in_links(api_client: APIClient data = resp.json() assert data["employees_count"] == 3 + +def test_role_only_count_counts_only_active_role_assignees_without_membership( + api_client: APIClient, +): + user = make_user("u-role-only@example.com") + api_client.force_authenticate(user=user) + + dept = Department.objects.create(name="Dept") + role = make_role(dept, "Auditor") + + role_only_1 = make_user("role-only-1@example.com") + role_only_2 = make_user("role-only-2@example.com") + member_with_role = make_user("member-with-role@example.com") + inactive_member_with_role = make_user("inactive-member-with-role@example.com") + + EmployeeDepartment.objects.create( + employee=member_with_role, + department=dept, + is_active=True, + ) + EmployeeDepartment.objects.create( + employee=inactive_member_with_role, + department=dept, + is_active=False, + ) + + RoleAssignment.objects.create(employee=role_only_1, role=role, is_active=True) + RoleAssignment.objects.create(employee=role_only_2, role=role, is_active=True) + RoleAssignment.objects.create( + employee=member_with_role, + role=role, + is_active=True, + ) + RoleAssignment.objects.create( + employee=inactive_member_with_role, + role=role, + is_active=True, + ) + RoleAssignment.objects.create( + employee=make_user("inactive-role-only@example.com"), + role=role, + is_active=False, + ) + + url = reverse("api:v1:departments-detail", args=[dept.pk]) + resp = api_client.get(url) + + assert resp.status_code == status.HTTP_200_OK + data = resp.json() + assert data["role_only_count"] == 3 + # ---------- tests: create/destroy ---------- def test_create_requires_staff(api_client: APIClient): diff --git a/frontend/src/app/departments/page.tsx b/frontend/src/app/departments/page.tsx index 3b53d1fe..af940b53 100644 --- a/frontend/src/app/departments/page.tsx +++ b/frontend/src/app/departments/page.tsx @@ -78,6 +78,8 @@ export default function DepartmentsPage() { const headName = department.head ? `${department.head.last_name} ${department.head.first_name}`.trim() : "Не назначен"; + const directMembersCount = department.employees_count ?? 0; + const roleOnlyCount = department.role_only_count ?? 0; return ( @@ -87,9 +89,14 @@ export default function DepartmentsPage() {
Руководитель: {headName}
{department.description || "Описание не заполнено"}
-