fix(tradein/ui): пагинация на границе страницы + запрос списка только для admin/manager (#2556)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m30s

EmployeeTable: убран ранний return при пустом списке — на offset>0 (ровно
50/100/150 сотрудников) пейджер и кнопка «Назад» теперь остаются доступны;
текст различает «сотрудников вообще нет» (offset=0) и «страница за концом
списка» (offset>0).

team/page.tsx: useEmployees получает enabled=isAllowedRole, вычисленный ДО
вызова хука — прямой заход employee/analyst/pilot на /team больше не шлёт
обречённый GET до отрисовки role-gate.
This commit is contained in:
bot-backend 2026-07-30 22:12:18 +03:00
parent f112a4affb
commit 1ed0338b95
3 changed files with 112 additions and 85 deletions

View file

@ -29,8 +29,15 @@ const PAGE_LIMIT = 50;
export default function TeamPage() { export default function TeamPage() {
const meQ = useMe(); const meQ = useMe();
// Вычисляем ДО useEmployees (не после) — иначе для employee/analyst/pilot
// при прямом заходе на /team улетает обречённый GET (401/403) ещё до того,
// как ниже отрисуется role-gate. Порядок хуков не меняется — это просто
// производное значение, не условный вызов хука.
const role = meQ.data?.role;
const isAllowedRole = role === "admin" || role === "manager";
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const employeesQ = useEmployees(PAGE_LIMIT, offset); const employeesQ = useEmployees(PAGE_LIMIT, offset, isAllowedRole);
const [showCreateForm, setShowCreateForm] = useState(false); const [showCreateForm, setShowCreateForm] = useState(false);
const [quotaEmployee, setQuotaEmployee] = useState<Employee | null>(null); const [quotaEmployee, setQuotaEmployee] = useState<Employee | null>(null);
@ -49,9 +56,6 @@ export default function TeamPage() {
); );
} }
const role = meQ.data?.role;
const isAllowedRole = role === "admin" || role === "manager";
if (!isAllowedRole) { if (!isAllowedRole) {
return <NoAccessScreen variant="user" />; return <NoAccessScreen variant="user" />;
} }

View file

@ -82,12 +82,22 @@ export function EmployeeTable({
); );
} }
if (employees.length === 0) { const isEmpty = employees.length === 0;
return <p className="scraper-hint">Сотрудников пока нет.</p>; // Пустая ПЕРВАЯ страница = сотрудников вообще нет (предлагаем создать).
} // Пустая страница при offset>0 = перешли за границу списка (напр. ровно
// 50/100/150 сотрудников и клик «Далее» после последней полной страницы)
// — это НЕ «сотрудников нет», нужен путь назад, а не тупик без пейджера.
const isEmptyPastEnd = isEmpty && offset > 0;
return ( return (
<> <>
{isEmpty ? (
<p className="scraper-hint">
{isEmptyPastEnd
? "На этой странице пусто — вы перешли за конец списка."
: "Сотрудников пока нет. Добавьте первого через «+ Добавить сотрудника»."}
</p>
) : (
<table className="runs-table team-table"> <table className="runs-table team-table">
<thead> <thead>
<tr> <tr>
@ -167,6 +177,7 @@ export function EmployeeTable({
))} ))}
</tbody> </tbody>
</table> </table>
)}
<div className="team-pager"> <div className="team-pager">
<button <button
@ -176,10 +187,16 @@ export function EmployeeTable({
> >
Назад Назад
</button> </button>
{!isEmpty ? (
<span> <span>
{offset + 1}{offset + employees.length} {offset + 1}{offset + employees.length}
</span> </span>
<button type="button" disabled={!hasNextPage} onClick={() => onOffsetChange(offset + limit)}> ) : null}
<button
type="button"
disabled={isEmpty || !hasNextPage}
onClick={() => onOffsetChange(offset + limit)}
>
Далее Далее
</button> </button>
</div> </div>

View file

@ -81,12 +81,18 @@ const EMPLOYEES_LIST_KEY = ["team", "employees"] as const;
/** /**
* GET /api/v1/team/employees?limit=&offset= * GET /api/v1/team/employees?limit=&offset=
* Backend сам скоупит по роли (manager только свои, admin все). * Backend сам скоупит по роли (manager только свои, admin все).
*
* `enabled` (default true) вызывающая страница обязана передать `false`
* для ролей, которым эндпоинт всё равно ответит 401/403 (employee/analyst/
* pilot и т.п.): без этого прямой заход на /team шлёт обречённый round-trip
* ДО отрисовки role-gate, который лишний и палит наличие эндпоинта.
*/ */
export function useEmployees(limit: number, offset: number) { export function useEmployees(limit: number, offset: number, enabled = true) {
return useQuery<Employee[]>({ return useQuery<Employee[]>({
queryKey: [...EMPLOYEES_LIST_KEY, limit, offset], queryKey: [...EMPLOYEES_LIST_KEY, limit, offset],
queryFn: () => queryFn: () =>
apiFetch<Employee[]>(`${BASE}/employees?limit=${limit}&offset=${offset}`), apiFetch<Employee[]>(`${BASE}/employees?limit=${limit}&offset=${offset}`),
enabled,
staleTime: 15_000, staleTime: 15_000,
retry: false, retry: false,
}); });