user tab enhanced and user search bug fixed
This commit is contained in:
parent
f022cbf899
commit
891096a184
@ -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 });
|
||||||
|
|
||||||
// Ref for search container (click outside to close)
|
// 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;
|
||||||
|
|
||||||
|
// 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('Users response:', response);
|
||||||
console.log('Management response:', managementResponse);
|
|
||||||
|
|
||||||
// Backend returns { success: true, data: { users: [...], summary: {...} } }
|
// Backend returns { success: true, data: { users: [...], pagination: {...}, summary: {...} } }
|
||||||
const admins = adminResponse.data?.data?.users || [];
|
const usersData = response.data?.data?.users || [];
|
||||||
const managers = managementResponse.data?.data?.users || [];
|
const paginationData = response.data?.data?.pagination;
|
||||||
|
const summaryData = response.data?.data?.summary;
|
||||||
|
|
||||||
console.log('Parsed admins:', admins);
|
console.log('Parsed users:', usersData);
|
||||||
console.log('Parsed managers:', managers);
|
console.log('Pagination:', paginationData);
|
||||||
|
console.log('Summary:', summaryData);
|
||||||
|
|
||||||
setElevatedUsers([...admins, ...managers]);
|
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 */}
|
||||||
|
<div ref={userListRef}>
|
||||||
<Card className="shadow-lg border">
|
<Card className="shadow-lg border">
|
||||||
<CardHeader className="border-b pb-4">
|
<CardHeader className="border-b pb-4">
|
||||||
<div className="flex items-center justify-between">
|
<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,21 +613,27 @@ 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">
|
||||||
|
{users.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={user.userId}
|
key={user.userId}
|
||||||
className="border-2 border-gray-100 hover:border-purple-200 hover:shadow-md transition-all rounded-lg bg-white p-4"
|
className="border-2 border-gray-100 hover:border-purple-200 hover:shadow-md transition-all rounded-lg bg-white p-4"
|
||||||
data-testid={`elevated-user-${user.email}`}
|
data-testid={`user-${user.email}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-4">
|
<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 gap-3 flex-1 min-w-0">
|
||||||
@ -541,10 +657,71 @@ export function UserRoleManager() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user