From db444a0767b47d2a4af4ad555f7d79ee018649f9 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Mon, 23 Feb 2026 18:21:54 +0100 Subject: [PATCH] feat(users): build users management page with filtering and role management (#460) --- frontend/app/hooks/useUsers.ts | 17 +++++ frontend/app/users/page.tsx | 108 ++++++++++++++++++++++++++++++ frontend/components/ui/Avatar.tsx | 22 ++++++ frontend/lib/api/usersApi.ts | 11 +++ 4 files changed, 158 insertions(+) create mode 100644 frontend/app/hooks/useUsers.ts create mode 100644 frontend/app/users/page.tsx create mode 100644 frontend/components/ui/Avatar.tsx create mode 100644 frontend/lib/api/usersApi.ts diff --git a/frontend/app/hooks/useUsers.ts b/frontend/app/hooks/useUsers.ts new file mode 100644 index 00000000..d7133fc5 --- /dev/null +++ b/frontend/app/hooks/useUsers.ts @@ -0,0 +1,17 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getUsers, updateUserRole } from '../lib/api/usersApi'; + +export function useUsers() { + return useQuery({ queryKey: ['users'], queryFn: getUsers }); +} + +export function useUpdateUserRole() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, role }: { id: string; role: string }) => + updateUserRole(id, role), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }); + }, + }); +} diff --git a/frontend/app/users/page.tsx b/frontend/app/users/page.tsx new file mode 100644 index 00000000..287b41f0 --- /dev/null +++ b/frontend/app/users/page.tsx @@ -0,0 +1,108 @@ +'use client'; + +import React, { useState } from 'react'; +import { useUsers, useUpdateUserRole } from '../hooks/useUsers'; +import { Avatar } from '../../components/ui/Avatar'; + +export default function UsersPage() { + const { data: users = [], isLoading } = useUsers(); + const updateRole = useUpdateUserRole(); + + const [search, setSearch] = useState(''); + const [roleFilter, setRoleFilter] = useState(''); + + if (isLoading) return
Loading...
; + + const filteredUsers = users.filter((u: any) => + (u.name.toLowerCase().includes(search.toLowerCase()) || + u.email.toLowerCase().includes(search.toLowerCase())) && + (roleFilter ? u.role === roleFilter : true) + ); + + return ( +
+

Users Management

+ +
+ setSearch(e.target.value)} + className="border p-2" + /> + +
+ + + + + + + + + + + + {filteredUsers.map((user: any) => ( + + + + + + + ))} + +
Avatar + NameEmailRoleJoined
+ + {user.name}{' '} + {user.isCurrentUser && ( + + You + + )} + {user.email} + + {new Date(user.joinedAt).toLocaleDateString()}
+ +
+

Role Legend

+
    +
  • + Admin — full access +
  • +
  • + Manager — manage teams +
  • +
  • + Staff — standard user +
  • +
+
+
+ ); +} diff --git a/frontend/components/ui/Avatar.tsx b/frontend/components/ui/Avatar.tsx new file mode 100644 index 00000000..e03dc214 --- /dev/null +++ b/frontend/components/ui/Avatar.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +export function Avatar({ + firstName, + lastName, + isCurrentUser, +}: { + firstName: string; + lastName: string; + isCurrentUser?: boolean; +}) { + const initials = `${firstName[0]}${lastName[0]}`.toUpperCase(); + const bgColor = isCurrentUser ? 'bg-gray-800' : 'bg-gray-400'; + + return ( +
+ {initials} +
+ ); +} diff --git a/frontend/lib/api/usersApi.ts b/frontend/lib/api/usersApi.ts new file mode 100644 index 00000000..71b9bfe8 --- /dev/null +++ b/frontend/lib/api/usersApi.ts @@ -0,0 +1,11 @@ +import api from './client'; + +export async function getUsers() { + const res = await api.get('/api/users'); + return res.data; +} + +export async function updateUserRole(id: string, role: string) { + const res = await api.patch(`/api/users/${id}/role`, { role }); + return res.data; +}