diff --git a/frontend/app/hooks/useUsers.ts b/frontend/app/hooks/useUsers.ts new file mode 100644 index 00000000..aadc2430 --- /dev/null +++ b/frontend/app/hooks/useUsers.ts @@ -0,0 +1,43 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getUsers, updateUserRole, updateProfile } from '../lib/api/usersApi'; +import { getReportsSummary } from '../lib/api/reportsApi'; +import { User, ReportSummary } from '../lib/types/users'; +import { useAuthStore } from '../store/authStore'; + +export function useUsersList() { + 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'] }); + }, + }); +} + +export function useUpdateProfile() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, payload }: { id: string; payload: Partial }) => + updateProfile(id, payload), + onSuccess: (updatedUser) => { + // Update Zustand store to keep sidebar in sync + useAuthStore.getState().setUser(updatedUser); + queryClient.invalidateQueries({ queryKey: ['users'] }); + }, + }); +} + +export function useReportsSummary() { + return useQuery({ + queryKey: ['reportsSummary'], + queryFn: getReportsSummary, + }); +} 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/reportsApi.ts b/frontend/lib/api/reportsApi.ts new file mode 100644 index 00000000..c8564927 --- /dev/null +++ b/frontend/lib/api/reportsApi.ts @@ -0,0 +1,7 @@ +import api from './client'; +import { ReportSummary } from '../users'; + +export async function getReportsSummary(): Promise { + const res = await api.get('/api/reports/summary'); + return res.data; +} diff --git a/frontend/lib/api/usersApi.ts b/frontend/lib/api/usersApi.ts new file mode 100644 index 00000000..8737ae3b --- /dev/null +++ b/frontend/lib/api/usersApi.ts @@ -0,0 +1,17 @@ +import api from './client'; +import { User } from '../users'; + +export async function getUsers(): Promise { + const res = await api.get('/api/users'); + return res.data; +} + +export async function updateUserRole(id: string, role: string): Promise { + const res = await api.patch(`/api/users/${id}/role`, { role }); + return res.data; +} + +export async function updateProfile(id: string, payload: Partial): Promise { + const res = await api.patch(`/api/users/${id}`, payload); + return res.data; +} diff --git a/frontend/lib/users.ts b/frontend/lib/users.ts new file mode 100644 index 00000000..1a0e89e7 --- /dev/null +++ b/frontend/lib/users.ts @@ -0,0 +1,13 @@ +export interface User { + id: string; + name: string; + email: string; + role: 'admin' | 'manager' | 'staff'; + joinedAt: string; +} + +export interface ReportSummary { + totalUsers: number; + activeUsers: number; + inactiveUsers: number; +}