From 5466c4d4f22266446ac752aecf69a9871e00fabb Mon Sep 17 00:00:00 2001 From: hltav Date: Mon, 13 Jul 2026 10:25:34 -0300 Subject: [PATCH] =?UTF-8?q?fix(admin):=20listar=20todos=20os=20usu=C3=A1ri?= =?UTF-8?q?os=20no=20gerenciamento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carrega usuários administrativos em lotes paginados até completar o total retornado pela API. Isso permite gerenciar todos os usuários do banco, em vez de apenas os 50 primeiros. Também adiciona suporte a parâmetros de listagem no client do admin e cobre o fluxo com testes. --- front_admin/src/lib/api/users.api.ts | 25 +++++++++++- front_admin/src/modules/users/UsersPage.tsx | 28 +++++++++++-- front_admin/tests/lib/api/resources.test.ts | 7 +++- .../tests/modules/users/UsersPage.test.tsx | 39 +++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/front_admin/src/lib/api/users.api.ts b/front_admin/src/lib/api/users.api.ts index 1c4c08f..51a1931 100644 --- a/front_admin/src/lib/api/users.api.ts +++ b/front_admin/src/lib/api/users.api.ts @@ -5,8 +5,31 @@ type AdminUserResponse = { user: AdminUser; }; +type AdminUsersListParams = { + limit?: number; + offset?: number; + search?: string; + role?: AdminUser["role"]; + isBlocked?: boolean; +}; + +function buildUsersQuery(params?: AdminUsersListParams) { + if (!params) return ""; + + const query = new URLSearchParams(); + + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === "") return; + query.set(key, String(value)); + }); + + const queryString = query.toString(); + return queryString ? `?${queryString}` : ""; +} + export const adminUsersApi = { - list: () => api.get("/admin/users"), + list: (params?: AdminUsersListParams) => + api.get(`/admin/users${buildUsersQuery(params)}`), block: (id: string) => api.patch(`/admin/users/${id}/block`), unblock: (id: string) => api.patch(`/admin/users/${id}/unblock`), diff --git a/front_admin/src/modules/users/UsersPage.tsx b/front_admin/src/modules/users/UsersPage.tsx index a853d82..0227bff 100644 --- a/front_admin/src/modules/users/UsersPage.tsx +++ b/front_admin/src/modules/users/UsersPage.tsx @@ -23,6 +23,7 @@ type RoleFilter = "all" | BackendUserRole; type StatusFilter = "all" | "active" | "blocked"; const USERS_PER_PAGE = 10; +const USERS_FETCH_LIMIT = 100; const ROLE_VALUES: BackendUserRole[] = ["super_admin", "admin", "support", "user"]; const ROLE_SUMMARY: Record< @@ -66,6 +67,28 @@ function mapUser(user: BackendAdminUser): AdminUser { }; } +async function fetchAllUsers() { + const firstPage = await adminUsersApi.list({ + limit: USERS_FETCH_LIMIT, + offset: 0, + }); + const allUsers = [...firstPage.data]; + + for ( + let offset = firstPage.offset + firstPage.limit; + offset < firstPage.total; + offset += firstPage.limit + ) { + const page = await adminUsersApi.list({ + limit: USERS_FETCH_LIMIT, + offset, + }); + allUsers.push(...page.data); + } + + return allUsers; +} + function StatCard({ label, value, @@ -123,11 +146,10 @@ export function UsersPage() { useEffect(() => { let active = true; - adminUsersApi - .list() + fetchAllUsers() .then((result) => { if (!active) return; - setUsers(result.data.map(mapUser)); + setUsers(result.map(mapUser)); setError(null); }) .catch(() => { diff --git a/front_admin/tests/lib/api/resources.test.ts b/front_admin/tests/lib/api/resources.test.ts index c1a514a..4670502 100644 --- a/front_admin/tests/lib/api/resources.test.ts +++ b/front_admin/tests/lib/api/resources.test.ts @@ -98,12 +98,17 @@ describe("api resources", () => { it("calls admin user mutation endpoints", () => { adminUsersApi.list(); + adminUsersApi.list({ limit: 100, offset: 200 }); adminUsersApi.block("u1"); adminUsersApi.unblock("u1"); adminUsersApi.changeRole("u1", "support"); adminUsersApi.delete("u1"); - expect(mockedApi.get).toHaveBeenCalledWith("/admin/users"); + expect(mockedApi.get).toHaveBeenNthCalledWith(1, "/admin/users"); + expect(mockedApi.get).toHaveBeenNthCalledWith( + 2, + "/admin/users?limit=100&offset=200", + ); expect(mockedApi.patch).toHaveBeenNthCalledWith( 1, "/admin/users/u1/block", diff --git a/front_admin/tests/modules/users/UsersPage.test.tsx b/front_admin/tests/modules/users/UsersPage.test.tsx index 02a804d..5863211 100644 --- a/front_admin/tests/modules/users/UsersPage.test.tsx +++ b/front_admin/tests/modules/users/UsersPage.test.tsx @@ -55,6 +55,16 @@ const manyUsers = Array.from({ length: 12 }, (_, index) => ({ isBlocked: index % 3 === 0, })); +const paginatedUsers = Array.from({ length: 105 }, (_, index) => ({ + ...backendUsers[index % 2], + id: `00000000-0000-4000-8000-${String(index + 100).padStart(12, "0")}`, + displayName: `Paged User ${index + 1}`, + username: `paged${index + 1}`, + email: `paged${index + 1}@example.com`, + role: index % 2 === 0 ? ("admin" as const) : ("user" as const), + isBlocked: false, +})); + describe("UsersPage", () => { beforeEach(() => { vi.clearAllMocks(); @@ -164,6 +174,35 @@ describe("UsersPage", () => { expect(screen.getByText(/Exibindo 1-/)).toBeInTheDocument(); }); + it("loads every backend page before paginating locally", async () => { + vi.mocked(adminUsersApi.list).mockImplementation(({ offset = 0 } = {}) => + Promise.resolve({ + data: paginatedUsers.slice(offset, offset + 100), + total: paginatedUsers.length, + limit: 100, + offset, + }), + ); + + renderWithProviders(); + + await screen.findByText("Paged User 1"); + + await waitFor(() => + expect(adminUsersApi.list).toHaveBeenCalledWith({ + limit: 100, + offset: 100, + }), + ); + + for (let page = 1; page < 11; page += 1) { + fireEvent.click(screen.getByRole("button", { name: "Próxima página" })); + } + + expect(screen.getByText("Paged User 101")).toBeInTheDocument(); + expect(screen.getByText("Exibindo 101-105 de 105 usuários")).toBeInTheDocument(); + }); + it("unblocks a selected blocked user without changing role", async () => { renderWithProviders();