"use client"; /** * Таблица сотрудников для team-дашборда менеджера (#2556, эпик #2549). * * Блокировка/разблокировка — прямое действие в строке (один PATCH-запрос * с `is_active`), с `window.confirm` перед деструктивным изменением (спек * DoD: «действия деструктивные — с подтверждением»). Квота + сброс пароля * вынесены в отдельный модал (`QuotaEditor`) — они бьют в тот же PATCH * эндпоинт одним запросом, см. `app/api/v1/team.py::update_employee`. */ import { Fragment, useState } from "react"; import type { Employee } from "@/lib/team-api"; import { teamErrorMessage, useUpdateEmployee } from "@/lib/team-api"; function formatDate(iso: string): string { try { return new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", }); } catch { return iso; } } function QuotaCell({ quota }: { quota: Employee["quota"] }) { if (quota.unlimited) { return без лимита; } const low = quota.remaining <= 0; return ( {quota.used}/{quota.limit} ); } interface EmployeeTableProps { employees: Employee[]; onEditQuota: (employee: Employee) => void; onViewHistory: (employee: Employee) => void; limit: number; offset: number; onOffsetChange: (offset: number) => void; hasNextPage: boolean; } export function EmployeeTable({ employees, onEditQuota, onViewHistory, limit, offset, onOffsetChange, hasNextPage, }: EmployeeTableProps) { const updateMutation = useUpdateEmployee(); const [pendingId, setPendingId] = useState(null); const [rowError, setRowError] = useState<{ id: number; message: string } | null>(null); function handleToggleActive(employee: Employee) { const nextActive = !employee.is_active; const confirmed = window.confirm( nextActive ? `Разблокировать сотрудника «${employee.username}»?` : `Заблокировать сотрудника «${employee.username}»? Все его текущие сессии будут завершены.`, ); if (!confirmed) return; setRowError(null); setPendingId(employee.id); updateMutation.mutate( { employeeId: employee.id, patch: { is_active: nextActive } }, { onSettled: () => setPendingId(null), onError: (error) => setRowError({ id: employee.id, message: teamErrorMessage(error) }), }, ); } const isEmpty = employees.length === 0; // Пустая ПЕРВАЯ страница = сотрудников вообще нет (предлагаем создать). // Пустая страница при offset>0 = перешли за границу списка (напр. ровно // 50/100/150 сотрудников и клик «Далее» после последней полной страницы) // — это НЕ «сотрудников нет», нужен путь назад, а не тупик без пейджера. const isEmptyPastEnd = isEmpty && offset > 0; return ( <> {isEmpty ? (

{isEmptyPastEnd ? "На этой странице пусто — вы перешли за конец списка." : "Сотрудников пока нет. Добавьте первого через «+ Добавить сотрудника»."}

) : ( {employees.map((employee) => ( {rowError?.id === employee.id ? ( ) : null} ))}
Логин Имя Статус Квота Создан Действия
{employee.username} {employee.display_name ?? "—"} {employee.org_name ? ` · ${employee.org_name}` : ""} {employee.is_active ? "Активен" : "Заблокирован"} {formatDate(employee.created_at)}

{rowError.message}

)}
{!isEmpty ? ( {offset + 1}–{offset + employees.length} ) : null}
); }