Fix employee sorting and department counters
This commit is contained in:
parent
9ec4c2349f
commit
1296ec9021
@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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"))
|
||||
),
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 (
|
||||
<Link key={department.id} href={`/departments/${department.id}`} className="app-surface-muted block rounded-xl p-4 transition hover:bg-[var(--surface-elevated)]">
|
||||
@ -87,9 +89,14 @@ export default function DepartmentsPage() {
|
||||
<p className="app-text-muted mt-1 text-xs">Руководитель: {headName}</p>
|
||||
<p className="app-text-muted mt-2 text-sm">{department.description || "Описание не заполнено"}</p>
|
||||
</div>
|
||||
<div className="app-selected app-accent-text inline-flex shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-xs">
|
||||
<div
|
||||
className="app-selected app-accent-text inline-flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-1 text-xs"
|
||||
title="Участники отдела • Только роли без членства"
|
||||
>
|
||||
<Users size={12} />
|
||||
{department.employees_count ?? 0}
|
||||
<span>{directMembersCount}</span>
|
||||
<span className="opacity-60">•</span>
|
||||
<span>{roleOnlyCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@ -143,16 +143,10 @@ export default function EmployeesPage() {
|
||||
}, [hasMore, loading, loadingMore, page]);
|
||||
|
||||
const filteredEmployees = useMemo(() => {
|
||||
const sorted = [...employees].sort((left, right) => {
|
||||
const leftName = `${left.last_name || ""} ${left.first_name || ""}`.trim();
|
||||
const rightName = `${right.last_name || ""} ${right.first_name || ""}`.trim();
|
||||
return leftName.localeCompare(rightName, "ru");
|
||||
});
|
||||
|
||||
const query = search.trim().toLowerCase();
|
||||
if (!query) return sorted;
|
||||
if (!query) return employees;
|
||||
|
||||
return sorted.filter((employee) => {
|
||||
return employees.filter((employee) => {
|
||||
const fullName =
|
||||
`${employee.last_name || ""} ${employee.first_name || ""} ${employee.patronymic || ""}`.trim().toLowerCase();
|
||||
const position = (employee.position?.name || "").toLowerCase();
|
||||
|
||||
@ -182,6 +182,15 @@ export class ApiClientBase {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async updateCurrentUserProfile(data: Record<string, any>): Promise<any> {
|
||||
const hasFile = Object.values(data).some((value) => value instanceof File);
|
||||
|
||||
if (!hasFile) {
|
||||
return this.request('/api/v1/employees/me/', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (Array.isArray(data[key])) {
|
||||
|
||||
@ -13,13 +13,14 @@ export function createEmployeesApi(request: RequestFn) {
|
||||
request('/api/v1/directory/me/login/'),
|
||||
refreshDirectoryLogin: () =>
|
||||
request('/api/v1/directory/me/login/refresh/', { method: 'POST' }),
|
||||
getEmployees: (params?: { search?: string; department?: string; page?: number; limit?: number; is_active?: boolean }) => {
|
||||
getEmployees: (params?: { search?: string; department?: string; page?: number; limit?: number; is_active?: boolean; ordering?: string }) => {
|
||||
const qp = new URLSearchParams();
|
||||
if (params?.search) qp.append('search', params.search);
|
||||
if (params?.department) qp.append('department', params.department);
|
||||
if (params?.page) qp.append('page', params.page.toString());
|
||||
if (params?.limit) qp.append('limit', params.limit.toString());
|
||||
if (params?.is_active !== undefined) qp.append('active', params.is_active.toString());
|
||||
if (params?.ordering) qp.append('ordering', params.ordering);
|
||||
const qs = qp.toString();
|
||||
return request(`/api/v1/employees/${qs ? '?' + qs : ''}`);
|
||||
},
|
||||
|
||||
@ -121,6 +121,7 @@ export interface Department {
|
||||
parent?: number;
|
||||
head?: User;
|
||||
employees_count?: number;
|
||||
role_only_count?: number;
|
||||
}
|
||||
|
||||
export interface DepartmentMemberRole {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user