user tab enhanced and user search bug fixed

This commit is contained in:
laxmanhalaki 2025-11-12 18:24:19 +05:30
parent f022cbf899
commit 891096a184
5 changed files with 290 additions and 91 deletions

View File

@ -67,13 +67,21 @@ export function UserRoleManager() {
const [updating, setUpdating] = useState(false); const [updating, setUpdating] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
// Users with elevated roles // Users list with filtering and pagination
const [elevatedUsers, setElevatedUsers] = useState<UserWithRole[]>([]); const [users, setUsers] = useState<UserWithRole[]>([]);
const [loadingUsers, setLoadingUsers] = useState(false); const [loadingUsers, setLoadingUsers] = useState(false);
const [roleStats, setRoleStats] = useState({ admins: 0, management: 0, users: 0 }); const [roleStats, setRoleStats] = useState({ admins: 0, management: 0, users: 0 });
// Pagination and filtering
const [roleFilter, setRoleFilter] = useState<'ELEVATED' | 'ALL' | 'ADMIN' | 'MANAGEMENT' | 'USER'>('ELEVATED');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalUsers, setTotalUsers] = useState(0);
const limit = 10;
// Ref for search container (click outside to close) // Refs for search container and user list
const searchContainerRef = useRef<HTMLDivElement>(null); const searchContainerRef = useRef<HTMLDivElement>(null);
const userListRef = useRef<HTMLDivElement>(null);
// Search users from Okta // Search users from Okta
const searchUsers = useCallback( const searchUsers = useCallback(
@ -134,7 +142,7 @@ export function UserRoleManager() {
try { try {
// Call backend to assign role (will create user if doesn't exist) // Call backend to assign role (will create user if doesn't exist)
const response = await userApi.assignRole(selectedUser.email, selectedRole); await userApi.assignRole(selectedUser.email, selectedRole);
setMessage({ setMessage({
type: 'success', type: 'success',
@ -146,8 +154,8 @@ export function UserRoleManager() {
setSearchQuery(''); setSearchQuery('');
setSelectedRole('USER'); setSelectedRole('USER');
// Refresh the elevated users list // Refresh the users list
await fetchElevatedUsers(); await fetchUsers();
await fetchRoleStatistics(); await fetchRoleStatistics();
} catch (error: any) { } catch (error: any) {
console.error('Role assignment failed:', error); console.error('Role assignment failed:', error);
@ -160,26 +168,39 @@ export function UserRoleManager() {
} }
}; };
// Fetch users with ADMIN and MANAGEMENT roles // Fetch users with filtering and pagination
const fetchElevatedUsers = async () => { const fetchUsers = async (page: number = currentPage) => {
setLoadingUsers(true); setLoadingUsers(true);
try { try {
const [adminResponse, managementResponse] = await Promise.all([ const response = await userApi.getUsersByRole(roleFilter, page, limit);
userApi.getUsersByRole('ADMIN'),
userApi.getUsersByRole('MANAGEMENT')
]);
console.log('Admin response:', adminResponse);
console.log('Management response:', managementResponse);
// Backend returns { success: true, data: { users: [...], summary: {...} } }
const admins = adminResponse.data?.data?.users || [];
const managers = managementResponse.data?.data?.users || [];
console.log('Parsed admins:', admins); console.log('Users response:', response);
console.log('Parsed managers:', managers);
setElevatedUsers([...admins, ...managers]); // Backend returns { success: true, data: { users: [...], pagination: {...}, summary: {...} } }
const usersData = response.data?.data?.users || [];
const paginationData = response.data?.data?.pagination;
const summaryData = response.data?.data?.summary;
console.log('Parsed users:', usersData);
console.log('Pagination:', paginationData);
console.log('Summary:', summaryData);
setUsers(usersData);
if (paginationData) {
setCurrentPage(paginationData.currentPage);
setTotalPages(paginationData.totalPages);
setTotalUsers(paginationData.totalUsers);
}
// Update summary stats if available
if (summaryData) {
setRoleStats({
admins: summaryData.ADMIN || 0,
management: summaryData.MANAGEMENT || 0,
users: summaryData.USER || 0
});
}
} catch (error) { } catch (error) {
console.error('Failed to fetch users:', error); console.error('Failed to fetch users:', error);
} finally { } finally {
@ -207,11 +228,39 @@ export function UserRoleManager() {
} }
}; };
// Load data on mount // Load data on mount and when filter changes
useEffect(() => { useEffect(() => {
fetchElevatedUsers(); fetchUsers(1); // Reset to page 1 when filter changes
fetchRoleStatistics(); fetchRoleStatistics();
}, []); }, [roleFilter]);
// Handle filter change
const handleFilterChange = (value: string) => {
setRoleFilter(value as any);
setCurrentPage(1);
};
// Handle page change
const handlePageChange = (page: number) => {
fetchUsers(page);
};
// Handle statistics card click - filter and scroll to user list
const handleStatCardClick = (filter: 'ADMIN' | 'MANAGEMENT' | 'USER') => {
setRoleFilter(filter);
setCurrentPage(1);
// Immediate scroll without waiting for data load
requestAnimationFrame(() => {
const element = userListRef.current;
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
};
// Handle click outside to close search results // Handle click outside to close search results
useEffect(() => { useEffect(() => {
@ -256,13 +305,21 @@ export function UserRoleManager() {
<div className="space-y-6"> <div className="space-y-6">
{/* Statistics Cards */} {/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 sm:gap-6">
<Card className="shadow-lg border-0 bg-gradient-to-br from-yellow-50 to-yellow-100/50 hover:shadow-xl transition-all rounded-xl" data-testid="admin-count-card"> <Card
className={`border-2 bg-gradient-to-br from-yellow-50 to-yellow-100/50 hover:shadow-lg transition-all rounded-xl cursor-pointer ${
roleFilter === 'ADMIN' ? 'border-yellow-400 shadow-lg' : 'border-transparent shadow-md'
}`}
data-testid="admin-count-card"
onClick={() => handleStatCardClick('ADMIN')}
>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-semibold text-gray-600 uppercase tracking-wide">Administrators</p> <p className="text-xs font-semibold text-gray-600 uppercase tracking-wide">Administrators</p>
<p className="text-3xl font-bold text-gray-900 mt-2" data-testid="admin-count">{roleStats.admins}</p> <p className="text-3xl font-bold text-gray-900 mt-2" data-testid="admin-count">{roleStats.admins}</p>
<p className="text-xs text-gray-500 mt-1">Full system access</p> <p className="text-xs text-yellow-700 mt-1 font-semibold">
{roleFilter === 'ADMIN' ? '✓ Viewing' : 'Click to view'}
</p>
</div> </div>
<div className="p-3 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-xl shadow-md"> <div className="p-3 bg-gradient-to-br from-yellow-400 to-yellow-500 rounded-xl shadow-md">
<Crown className="w-6 h-6 text-slate-900" /> <Crown className="w-6 h-6 text-slate-900" />
@ -271,13 +328,21 @@ export function UserRoleManager() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="shadow-lg border-0 bg-gradient-to-br from-blue-50 to-blue-100/50 hover:shadow-xl transition-all rounded-xl" data-testid="management-count-card"> <Card
className={`border-2 bg-gradient-to-br from-blue-50 to-blue-100/50 hover:shadow-lg transition-all rounded-xl cursor-pointer ${
roleFilter === 'MANAGEMENT' ? 'border-blue-400 shadow-lg' : 'border-transparent shadow-md'
}`}
data-testid="management-count-card"
onClick={() => handleStatCardClick('MANAGEMENT')}
>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-semibold text-gray-600 uppercase tracking-wide">Management</p> <p className="text-xs font-semibold text-gray-600 uppercase tracking-wide">Management</p>
<p className="text-3xl font-bold text-gray-900 mt-2" data-testid="management-count">{roleStats.management}</p> <p className="text-3xl font-bold text-gray-900 mt-2" data-testid="management-count">{roleStats.management}</p>
<p className="text-xs text-gray-500 mt-1">Read all data access</p> <p className="text-xs text-blue-700 mt-1 font-semibold">
{roleFilter === 'MANAGEMENT' ? '✓ Viewing' : 'Click to view'}
</p>
</div> </div>
<div className="p-3 bg-gradient-to-br from-blue-400 to-blue-500 rounded-xl shadow-md"> <div className="p-3 bg-gradient-to-br from-blue-400 to-blue-500 rounded-xl shadow-md">
<Users className="w-6 h-6 text-slate-900" /> <Users className="w-6 h-6 text-slate-900" />
@ -286,13 +351,21 @@ export function UserRoleManager() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="shadow-lg border-0 bg-gradient-to-br from-gray-50 to-gray-100/50 hover:shadow-xl transition-all rounded-xl" data-testid="user-count-card"> <Card
className={`border-2 bg-gradient-to-br from-gray-50 to-gray-100/50 hover:shadow-lg transition-all rounded-xl cursor-pointer ${
roleFilter === 'USER' ? 'border-gray-400 shadow-lg' : 'border-transparent shadow-md'
}`}
data-testid="user-count-card"
onClick={() => handleStatCardClick('USER')}
>
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-semibold text-gray-600 uppercase tracking-wide">Regular Users</p> <p className="text-xs font-semibold text-gray-600 uppercase tracking-wide">Regular Users</p>
<p className="text-3xl font-bold text-gray-900 mt-2" data-testid="user-count">{roleStats.users}</p> <p className="text-3xl font-bold text-gray-900 mt-2" data-testid="user-count">{roleStats.users}</p>
<p className="text-xs text-gray-500 mt-1">Standard access</p> <p className="text-xs text-gray-700 mt-1 font-semibold">
{roleFilter === 'USER' ? '✓ Viewing' : 'Click to view'}
</p>
</div> </div>
<div className="p-3 bg-gradient-to-br from-gray-400 to-gray-500 rounded-xl shadow-md"> <div className="p-3 bg-gradient-to-br from-gray-400 to-gray-500 rounded-xl shadow-md">
<UserIcon className="w-6 h-6 text-white" /> <UserIcon className="w-6 h-6 text-white" />
@ -328,7 +401,7 @@ export function UserRoleManager() {
placeholder="Type name or email address..." placeholder="Type name or email address..."
value={searchQuery} value={searchQuery}
onChange={handleSearchChange} onChange={handleSearchChange}
className="pl-10 pr-10 h-12 border-2 rounded-lg focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all" className="pl-10 pr-10 h-12 border rounded-lg border-gray-300 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all"
data-testid="user-search-input" data-testid="user-search-input"
/> />
{searching && ( {searching && (
@ -339,13 +412,13 @@ export function UserRoleManager() {
{/* Search Results Dropdown */} {/* Search Results Dropdown */}
{searchResults.length > 0 && ( {searchResults.length > 0 && (
<div className="border-2 border-purple-200 rounded-lg shadow-lg bg-white max-h-60 overflow-y-auto"> <div className="border border-purple-200 rounded-lg shadow-lg bg-white max-h-60 overflow-y-auto">
<div className="sticky top-0 bg-purple-50 px-4 py-2 border-b border-purple-100"> <div className="sticky top-0 bg-purple-50 px-4 py-2 border-b border-purple-100">
<p className="text-xs font-semibold text-purple-700"> <p className="text-xs font-semibold text-purple-700">
{searchResults.length} user{searchResults.length > 1 ? 's' : ''} found {searchResults.length} user{searchResults.length > 1 ? 's' : ''} found
</p> </p>
</div> </div>
<div className="p-2"> <div className="p-3">
{searchResults.map((user) => ( {searchResults.map((user) => (
<button <button
key={user.userId} key={user.userId}
@ -407,25 +480,25 @@ export function UserRoleManager() {
<label className="text-sm font-medium text-gray-700">Select Role</label> <label className="text-sm font-medium text-gray-700">Select Role</label>
<Select value={selectedRole} onValueChange={(value: any) => setSelectedRole(value)}> <Select value={selectedRole} onValueChange={(value: any) => setSelectedRole(value)}>
<SelectTrigger <SelectTrigger
className="h-12 border-2 rounded-lg focus:border-purple-500 focus:ring-2 focus:ring-purple-200 transition-all" className="h-12 border border-gray-300 py-2 rounded-lg focus:border-purple-500 focus:ring-1 focus:ring-purple-200 transition-all"
data-testid="role-select" data-testid="role-select"
> >
<SelectValue placeholder="Select role" /> <SelectValue placeholder="Select role" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent className="rounded-lg">
<SelectItem value="USER"> <SelectItem value="USER" className="p-3 rounded-lg my-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<UserIcon className="w-4 h-4 text-gray-600" /> <UserIcon className="w-4 h-4 text-gray-600" />
<span>User - Regular access</span> <span>User - Regular access</span>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value="MANAGEMENT"> <SelectItem value="MANAGEMENT" className="p-3 rounded-lg my-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="w-4 h-4 text-blue-600" /> <Users className="w-4 h-4 text-blue-600" />
<span>Management - Read all data</span> <span>Management - Read all data</span>
</div> </div>
</SelectItem> </SelectItem>
<SelectItem value="ADMIN"> <SelectItem value="ADMIN" className="p-3 rounded-lg my-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Crown className="w-4 h-4 text-yellow-600" /> <Crown className="w-4 h-4 text-yellow-600" />
<span>Administrator - Full access</span> <span>Administrator - Full access</span>
@ -477,24 +550,61 @@ export function UserRoleManager() {
</CardContent> </CardContent>
</Card> </Card>
{/* Elevated Users List */} {/* Users List with Filter and Pagination */}
<Card className="shadow-lg border"> <div ref={userListRef}>
<CardHeader className="border-b pb-4"> <Card className="shadow-lg border">
<div className="flex items-center justify-between"> <CardHeader className="border-b pb-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-3 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-lg shadow-md"> <div className="p-3 bg-gradient-to-br from-indigo-500 to-indigo-600 rounded-lg shadow-md">
<Shield className="w-5 h-5 text-white" /> <Shield className="w-5 h-5 text-white" />
</div> </div>
<div> <div>
<CardTitle className="text-lg font-semibold">Users with Elevated Roles</CardTitle> <CardTitle className="text-lg font-semibold">User Management</CardTitle>
<CardDescription className="text-sm"> <CardDescription className="text-sm">
Administrators and Management team members View and manage user roles ({totalUsers} {roleFilter !== 'ALL' && roleFilter !== 'ELEVATED' ? roleFilter.toLowerCase() : ''} users)
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge variant="outline" className="text-sm"> <div className="flex items-center gap-3">
{elevatedUsers.length} user{elevatedUsers.length !== 1 ? 's' : ''} <Select value={roleFilter} onValueChange={handleFilterChange}>
</Badge> <SelectTrigger className="w-[200px] h-10 border rounded-lg border-gray-300">
<SelectValue placeholder="Filter by role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ELEVATED">
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-purple-600" />
<span>Elevated ({roleStats.admins + roleStats.management})</span>
</div>
</SelectItem>
<SelectItem value="ADMIN">
<div className="flex items-center gap-2">
<Crown className="w-4 h-4 text-yellow-600" />
<span>Admins ({roleStats.admins})</span>
</div>
</SelectItem>
<SelectItem value="MANAGEMENT">
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-blue-600" />
<span>Management ({roleStats.management})</span>
</div>
</SelectItem>
<SelectItem value="USER">
<div className="flex items-center gap-2">
<UserIcon className="w-4 h-4 text-gray-600" />
<span>Users ({roleStats.users})</span>
</div>
</SelectItem>
<SelectItem value="ALL">
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-gray-600" />
<span>All Users ({roleStats.admins + roleStats.management + roleStats.users})</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pt-6"> <CardContent className="pt-6">
@ -503,47 +613,114 @@ export function UserRoleManager() {
<Loader2 className="w-6 h-6 animate-spin text-purple-500 mb-2" /> <Loader2 className="w-6 h-6 animate-spin text-purple-500 mb-2" />
<p className="text-sm text-gray-500">Loading users...</p> <p className="text-sm text-gray-500">Loading users...</p>
</div> </div>
) : elevatedUsers.length === 0 ? ( ) : users.length === 0 ? (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mx-auto mb-3"> <div className="w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mx-auto mb-3">
<Users className="w-6 h-6 text-gray-400" /> <Users className="w-6 h-6 text-gray-400" />
</div> </div>
<p className="font-medium text-gray-700">No elevated users found</p> <p className="font-medium text-gray-700">No users found</p>
<p className="text-sm text-gray-500 mt-1">Assign ADMIN or MANAGEMENT roles to see users here</p> <p className="text-sm text-gray-500 mt-1">
{roleFilter === 'ELEVATED'
? 'Assign ADMIN or MANAGEMENT roles to see users here'
: 'No users match the selected filter'
}
</p>
</div> </div>
) : ( ) : (
<div className="space-y-2 max-h-96 overflow-y-auto pr-2" data-testid="elevated-users-list"> <>
{elevatedUsers.map((user) => ( <div className="space-y-2" data-testid="users-list">
<div {users.map((user) => (
key={user.userId} <div
className="border-2 border-gray-100 hover:border-purple-200 hover:shadow-md transition-all rounded-lg bg-white p-4" key={user.userId}
data-testid={`elevated-user-${user.email}`} className="border-2 border-gray-100 hover:border-purple-200 hover:shadow-md transition-all rounded-lg bg-white p-4"
> data-testid={`user-${user.email}`}
<div className="flex items-center justify-between gap-4"> >
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center justify-between gap-4">
<div className={`w-10 h-10 rounded-lg ${getRoleBadgeColor(user.role)} flex items-center justify-center shadow-sm`}> <div className="flex items-center gap-3 flex-1 min-w-0">
{getRoleIcon(user.role)} <div className={`w-10 h-10 rounded-lg ${getRoleBadgeColor(user.role)} flex items-center justify-center shadow-sm`}>
</div> {getRoleIcon(user.role)}
<div className="flex-1 min-w-0"> </div>
<p className="font-semibold text-gray-900 truncate">{user.displayName}</p> <div className="flex-1 min-w-0">
<p className="text-sm text-gray-600 truncate">{user.email}</p> <p className="font-semibold text-gray-900 truncate">{user.displayName}</p>
{user.department && ( <p className="text-sm text-gray-600 truncate">{user.email}</p>
<p className="text-xs text-gray-500 mt-1 truncate"> {user.department && (
{user.department}{user.designation ? `${user.designation}` : ''} <p className="text-xs text-gray-500 mt-1 truncate">
</p> {user.department}{user.designation ? `${user.designation}` : ''}
)} </p>
)}
</div>
</div> </div>
<Badge className={`${getRoleBadgeColor(user.role)} shrink-0`} data-testid={`role-badge-${user.role}`}>
{user.role}
</Badge>
</div> </div>
<Badge className={`${getRoleBadgeColor(user.role)} shrink-0`} data-testid={`role-badge-${user.role}`}> </div>
{user.role} ))}
</Badge> </div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-gray-600">
Showing {((currentPage - 1) * limit) + 1} to {Math.min(currentPage * limit, totalUsers)} of {totalUsers} users
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
data-testid="prev-page-button"
>
Previous
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (currentPage <= 3) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 2 + i;
}
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "outline"}
size="sm"
onClick={() => handlePageChange(pageNum)}
className={`w-9 h-9 p-0 ${
currentPage === pageNum
? 'bg-purple-500 hover:bg-purple-600'
: ''
}`}
data-testid={`page-${pageNum}-button`}
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
data-testid="next-page-button"
>
Next
</Button>
</div> </div>
</div> </div>
))} )}
</div> </>
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div>
</div> </div>
); );
} }

View File

@ -189,7 +189,9 @@ export function AddApproverModal({
// If user was NOT selected via @ search, validate against Okta // If user was NOT selected via @ search, validate against Okta
if (!selectedUser || selectedUser.email.toLowerCase() !== emailToAdd) { if (!selectedUser || selectedUser.email.toLowerCase() !== emailToAdd) {
try { try {
const searchOktaResults = await searchUsers(emailToAdd, 1); const response = await searchUsers(emailToAdd, 1);
// Backend returns { success: true, data: [...users] }
const searchOktaResults = response.data?.data || [];
if (searchOktaResults.length === 0) { if (searchOktaResults.length === 0) {
// User not found in Okta // User not found in Okta
@ -310,7 +312,9 @@ export function AddApproverModal({
searchTimer.current = setTimeout(async () => { searchTimer.current = setTimeout(async () => {
try { try {
const term = value.slice(1); // Remove @ prefix const term = value.slice(1); // Remove @ prefix
const results = await searchUsers(term, 10); const response = await searchUsers(term, 10);
// Backend returns { success: true, data: [...users] }
const results = response.data?.data || [];
setSearchResults(results); setSearchResults(results);
} catch (error) { } catch (error) {
console.error('Search failed:', error); console.error('Search failed:', error);
@ -352,7 +356,7 @@ export function AddApproverModal({
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md max-h-[90vh] flex flex-col p-0"> <DialogContent className="sm:max-w-md min-h-[60vh] max-h-[90vh] flex flex-col p-0">
<button <button
onClick={handleClose} onClick={handleClose}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none z-50" className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none z-50"

View File

@ -116,7 +116,9 @@ export function AddSpectatorModal({
// If user was NOT selected via @ search, validate against Okta // If user was NOT selected via @ search, validate against Okta
if (!selectedUser || selectedUser.email.toLowerCase() !== emailToAdd) { if (!selectedUser || selectedUser.email.toLowerCase() !== emailToAdd) {
try { try {
const searchOktaResults = await searchUsers(emailToAdd, 1); const response = await searchUsers(emailToAdd, 1);
// Backend returns { success: true, data: [...users] }
const searchOktaResults = response.data?.data || [];
if (searchOktaResults.length === 0) { if (searchOktaResults.length === 0) {
// User not found in Okta // User not found in Okta
@ -223,7 +225,9 @@ export function AddSpectatorModal({
searchTimer.current = setTimeout(async () => { searchTimer.current = setTimeout(async () => {
try { try {
const term = value.slice(1); // Remove @ prefix const term = value.slice(1); // Remove @ prefix
const results = await searchUsers(term, 10); const response = await searchUsers(term, 10);
// Backend returns { success: true, data: [...users] }
const results = response.data?.data || [];
setSearchResults(results); setSearchResults(results);
} catch (error) { } catch (error) {
console.error('Search failed:', error); console.error('Search failed:', error);
@ -265,7 +269,7 @@ export function AddSpectatorModal({
return ( return (
<Dialog open={open} onOpenChange={handleClose}> <Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md max-h-[90vh] flex flex-col p-0"> <DialogContent className="sm:max-w-md min-h-[60vh] max-h-[90vh] flex flex-col p-0">
<button <button
onClick={handleClose} onClick={handleClose}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none z-50" className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none z-50"

View File

@ -475,7 +475,9 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
// Search for the user by email in Okta directory // Search for the user by email in Okta directory
try { try {
const searchResults = await searchUsers(approver.email, 1); const response = await searchUsers(approver.email, 1);
// Backend returns { success: true, data: [...users] }
const searchResults = response.data?.data || [];
if (searchResults.length === 0) { if (searchResults.length === 0) {
// User NOT found in Okta directory // User NOT found in Okta directory
@ -700,7 +702,9 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
// Search for user in Okta directory // Search for user in Okta directory
try { try {
const searchResults = await searchUsers(emailInput, 1); const response = await searchUsers(emailInput, 1);
// Backend returns { success: true, data: [...users] }
const searchResults = response.data?.data || [];
if (searchResults.length === 0) { if (searchResults.length === 0) {
// User NOT found in Okta directory // User NOT found in Okta directory
@ -1699,7 +1703,9 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
searchTimers.current[index] = setTimeout(async () => { searchTimers.current[index] = setTimeout(async () => {
try { try {
const term = value.slice(1); // remove leading '@' const term = value.slice(1); // remove leading '@'
const results = await searchUsers(term, 10); const response = await searchUsers(term, 10);
// Backend returns { success: true, data: [...users] }
const results = response.data?.data || [];
setUserSearchResults(prev => ({ ...prev, [index]: results })); setUserSearchResults(prev => ({ ...prev, [index]: results }));
} catch (err) { } catch (err) {
setUserSearchResults(prev => ({ ...prev, [index]: [] })); setUserSearchResults(prev => ({ ...prev, [index]: [] }));
@ -2098,8 +2104,10 @@ export function CreateRequest({ onBack, onSubmit, requestId: propRequestId, isEd
spectatorTimer.current = setTimeout(async () => { spectatorTimer.current = setTimeout(async () => {
try { try {
const term = value.slice(1); const term = value.slice(1);
const res = await searchUsers(term, 10); const response = await searchUsers(term, 10);
setSpectatorSearchResults(res); // Backend returns { success: true, data: [...users] }
const results = response.data?.data || [];
setSpectatorSearchResults(results);
} catch { } catch {
setSpectatorSearchResults([]); setSpectatorSearchResults([]);
} finally { } finally {

View File

@ -49,10 +49,16 @@ export async function updateUserRole(userId: string, role: 'USER' | 'MANAGEMENT'
} }
/** /**
* Get users by role * Get users by role (with pagination)
*/ */
export async function getUsersByRole(role: 'USER' | 'MANAGEMENT' | 'ADMIN') { export async function getUsersByRole(
return await apiClient.get('/admin/users/by-role', { params: { role } }); role?: 'USER' | 'MANAGEMENT' | 'ADMIN' | 'ALL' | 'ELEVATED',
page: number = 1,
limit: number = 10
) {
return await apiClient.get('/admin/users/by-role', {
params: { role: role || 'ELEVATED', page, limit }
});
} }
/** /**