693 lines
25 KiB
TypeScript
693 lines
25 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import {
|
|
Plus,
|
|
Search,
|
|
Users,
|
|
Shield,
|
|
Loader2,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Crown,
|
|
User as UserIcon,
|
|
} from 'lucide-react';
|
|
import { userApi } from '@/services/userApi';
|
|
import { toast } from 'sonner';
|
|
import { UserTable } from './UserTable';
|
|
import { UserStatsCards } from './UserStatsCards';
|
|
|
|
// Simple debounce function
|
|
function debounce<T extends (...args: any[]) => any>(
|
|
func: T,
|
|
wait: number
|
|
): (...args: Parameters<T>) => void {
|
|
let timeout: NodeJS.Timeout | null = null;
|
|
return function executedFunction(...args: Parameters<T>) {
|
|
const later = () => {
|
|
timeout = null;
|
|
func(...args);
|
|
};
|
|
if (timeout) clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
interface OktaUser {
|
|
userId: string;
|
|
email: string;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
displayName?: string;
|
|
department?: string;
|
|
designation?: string;
|
|
}
|
|
|
|
export interface User {
|
|
userId: string;
|
|
email: string;
|
|
displayName: string;
|
|
role: 'USER' | 'MANAGEMENT' | 'ADMIN';
|
|
department?: string;
|
|
designation?: string;
|
|
isActive?: boolean;
|
|
}
|
|
|
|
export function UserManagement() {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [searchResults, setSearchResults] = useState<OktaUser[]>([]);
|
|
const [searching, setSearching] = useState(false);
|
|
const [selectedUser, setSelectedUser] = useState<OktaUser | null>(null);
|
|
const [selectedRole, setSelectedRole] = useState<'USER' | 'MANAGEMENT' | 'ADMIN'>('USER');
|
|
const [updating, setUpdating] = useState(false);
|
|
const [fetchingRole, setFetchingRole] = useState(false);
|
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
|
|
// Users list with filtering and pagination
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loadingUsers, setLoadingUsers] = useState(false);
|
|
const [roleStats, setRoleStats] = useState({ admins: 0, management: 0, users: 0, total: 0, active: 0, inactive: 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;
|
|
|
|
// Refs for search container
|
|
const searchContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Search users from Okta
|
|
const searchUsers = useCallback(
|
|
debounce(async (query: string) => {
|
|
// Only trigger search when using @ sign
|
|
if (!query || !query.startsWith('@') || query.length < 2) {
|
|
setSearchResults([]);
|
|
setSearching(false);
|
|
return;
|
|
}
|
|
|
|
setSearching(true);
|
|
try {
|
|
const term = query.slice(1); // Remove @ prefix
|
|
const response = await userApi.searchUsers(term, 20);
|
|
const users = response.data?.data || [];
|
|
setSearchResults(users);
|
|
} catch (error: any) {
|
|
console.error('Search failed:', error);
|
|
setMessage({
|
|
type: 'error',
|
|
text: error.response?.data?.message || 'Failed to search users'
|
|
});
|
|
} finally {
|
|
setSearching(false);
|
|
}
|
|
}, 300),
|
|
[]
|
|
);
|
|
|
|
// Handle search input
|
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const query = e.target.value;
|
|
setSearchQuery(query);
|
|
searchUsers(query);
|
|
};
|
|
|
|
// Fetch user's current role
|
|
const fetchUserRole = async (email: string): Promise<'USER' | 'MANAGEMENT' | 'ADMIN' | null> => {
|
|
try {
|
|
// First check if user exists in current users list
|
|
const existingUser = users.find(u => u.email.toLowerCase() === email.toLowerCase());
|
|
if (existingUser) {
|
|
return existingUser.role;
|
|
}
|
|
|
|
// If not found, try to fetch from backend by checking all users
|
|
// We'll search with a broader filter to find the user
|
|
const response = await userApi.getUsersByRole('ALL', 1, 1000);
|
|
const allUsers = response.data?.data?.users || [];
|
|
const foundUser = allUsers.find((u: any) =>
|
|
u.email?.toLowerCase() === email.toLowerCase()
|
|
);
|
|
|
|
if (foundUser && foundUser.role) {
|
|
return foundUser.role as 'USER' | 'MANAGEMENT' | 'ADMIN';
|
|
}
|
|
|
|
return null; // User not found in system, no role assigned
|
|
} catch (error) {
|
|
console.error('Failed to fetch user role:', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Select user from search results
|
|
const handleSelectUser = async (user: OktaUser) => {
|
|
setSelectedUser(user);
|
|
setSearchQuery(user.email);
|
|
setSearchResults([]);
|
|
setFetchingRole(true);
|
|
|
|
try {
|
|
// Fetch and set the user's current role if they have one
|
|
const currentRole = await fetchUserRole(user.email);
|
|
if (currentRole) {
|
|
setSelectedRole(currentRole);
|
|
} else {
|
|
// Default to USER if no role found
|
|
setSelectedRole('USER');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch user role:', error);
|
|
setSelectedRole('USER'); // Default on error
|
|
} finally {
|
|
setFetchingRole(false);
|
|
}
|
|
};
|
|
|
|
// Assign role to user
|
|
const handleAssignRole = async () => {
|
|
if (!selectedUser || !selectedRole) {
|
|
setMessage({ type: 'error', text: 'Please select a user and role' });
|
|
return;
|
|
}
|
|
|
|
setUpdating(true);
|
|
setMessage(null);
|
|
|
|
try {
|
|
await userApi.assignRole(selectedUser.email, selectedRole);
|
|
|
|
setMessage({
|
|
type: 'success',
|
|
text: `Successfully assigned ${selectedRole} role to ${selectedUser.displayName || selectedUser.email}`
|
|
});
|
|
|
|
// Reset form
|
|
setSelectedUser(null);
|
|
setSearchQuery('');
|
|
setSelectedRole('USER');
|
|
|
|
// Refresh the users list
|
|
await fetchUsers();
|
|
await fetchRoleStatistics();
|
|
|
|
toast.success(`Role assigned successfully`);
|
|
} catch (error: any) {
|
|
console.error('Role assignment failed:', error);
|
|
const errorMsg = error.response?.data?.error || 'Failed to assign role';
|
|
setMessage({
|
|
type: 'error',
|
|
text: errorMsg
|
|
});
|
|
toast.error(errorMsg);
|
|
} finally {
|
|
setUpdating(false);
|
|
}
|
|
};
|
|
|
|
// Fetch users with filtering and pagination
|
|
const fetchUsers = async (page: number = currentPage) => {
|
|
setLoadingUsers(true);
|
|
try {
|
|
const response = await userApi.getUsersByRole(roleFilter, page, limit);
|
|
|
|
const usersData = response.data?.data?.users || [];
|
|
const paginationData = response.data?.data?.pagination;
|
|
const summaryData = response.data?.data?.summary;
|
|
|
|
setUsers(usersData.map((u: any) => ({
|
|
userId: u.userId,
|
|
email: u.email,
|
|
displayName: u.displayName || u.email,
|
|
role: u.role || 'USER',
|
|
department: u.department,
|
|
designation: u.designation,
|
|
isActive: u.isActive !== false // Default to true if not specified
|
|
})));
|
|
|
|
if (paginationData) {
|
|
setCurrentPage(paginationData.currentPage);
|
|
setTotalPages(paginationData.totalPages);
|
|
setTotalUsers(paginationData.totalUsers);
|
|
}
|
|
|
|
// Update summary stats if available
|
|
if (summaryData) {
|
|
setRoleStats(prev => ({
|
|
...prev,
|
|
admins: summaryData.ADMIN || 0,
|
|
management: summaryData.MANAGEMENT || 0,
|
|
users: summaryData.USER || 0,
|
|
total: (summaryData.ADMIN || 0) + (summaryData.MANAGEMENT || 0) + (summaryData.USER || 0)
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch users:', error);
|
|
toast.error('Failed to load users');
|
|
} finally {
|
|
setLoadingUsers(false);
|
|
}
|
|
};
|
|
|
|
// Fetch role statistics
|
|
const fetchRoleStatistics = async () => {
|
|
try {
|
|
const response = await userApi.getRoleStatistics();
|
|
const statsData = response.data?.data?.statistics || response.data?.statistics || [];
|
|
|
|
const stats = {
|
|
admins: parseInt(statsData.find((s: any) => s.role === 'ADMIN')?.count || '0'),
|
|
management: parseInt(statsData.find((s: any) => s.role === 'MANAGEMENT')?.count || '0'),
|
|
users: parseInt(statsData.find((s: any) => s.role === 'USER')?.count || '0')
|
|
};
|
|
|
|
setRoleStats(prev => ({
|
|
...prev,
|
|
...stats,
|
|
total: stats.admins + stats.management + stats.users,
|
|
active: prev.active || stats.admins + stats.management + stats.users,
|
|
inactive: prev.inactive || 0
|
|
}));
|
|
} catch (error) {
|
|
console.error('Failed to fetch statistics:', error);
|
|
}
|
|
};
|
|
|
|
// Load data on mount and when filter changes
|
|
useEffect(() => {
|
|
fetchUsers(1); // Reset to page 1 when filter changes
|
|
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 edit user role
|
|
const handleEditUser = async (userId: string, newRole: 'USER' | 'MANAGEMENT' | 'ADMIN') => {
|
|
try {
|
|
await userApi.updateUserRole(userId, newRole);
|
|
toast.success('User role updated successfully');
|
|
await fetchUsers();
|
|
await fetchRoleStatistics();
|
|
} catch (error: any) {
|
|
console.error('Failed to update user role:', error);
|
|
toast.error(error.response?.data?.error || 'Failed to update user role');
|
|
}
|
|
};
|
|
|
|
// Handle toggle user status (placeholder - needs backend support)
|
|
const handleToggleUserStatus = async (userId: string) => {
|
|
const user = users.find(u => u.userId === userId);
|
|
if (!user) return;
|
|
|
|
|
|
toast.info('User status toggle functionality coming soon');
|
|
};
|
|
|
|
// Handle delete user (placeholder - needs backend support)
|
|
const handleDeleteUser = async (userId: string) => {
|
|
const user = users.find(u => u.userId === userId);
|
|
if (!user) return;
|
|
|
|
if (user.role === 'ADMIN') {
|
|
toast.error('Cannot delete admin user');
|
|
return;
|
|
}
|
|
|
|
toast.info('User deletion functionality coming soon');
|
|
};
|
|
|
|
// Handle click outside to close search results
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) {
|
|
setSearchResults([]);
|
|
}
|
|
};
|
|
|
|
if (searchResults.length > 0) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [searchResults]);
|
|
|
|
|
|
// Calculate stats for UserStatsCards
|
|
const stats = {
|
|
total: roleStats.total,
|
|
active: roleStats.active,
|
|
inactive: roleStats.inactive,
|
|
admins: roleStats.admins
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Statistics Cards */}
|
|
<UserStatsCards stats={stats} />
|
|
|
|
{/* Assign Role Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Assign User Role</CardTitle>
|
|
<CardDescription>
|
|
Search for a user in Okta and assign them a role
|
|
</CardDescription>
|
|
</div>
|
|
<Button onClick={handleAssignRole} disabled={!selectedUser || updating} className="bg-re-green hover:bg-re-green/90">
|
|
{updating ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
Assigning...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Assign Role
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Search Input */}
|
|
<div className="space-y-2" ref={searchContainerRef}>
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<Input
|
|
type="text"
|
|
placeholder="Type @ to search users..."
|
|
value={searchQuery}
|
|
onChange={handleSearchChange}
|
|
className="pl-10 border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20"
|
|
/>
|
|
{searching && (
|
|
<Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-re-green animate-spin" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Start with @ to search users (e.g., @john)</p>
|
|
|
|
{/* Search Results Dropdown */}
|
|
{searchResults.length > 0 && (
|
|
<div className="border rounded-lg shadow-lg bg-white max-h-60 overflow-y-auto">
|
|
<div className="sticky top-0 bg-muted px-4 py-2 border-b">
|
|
<p className="text-xs font-semibold text-muted-foreground">
|
|
{searchResults.length} user{searchResults.length > 1 ? 's' : ''} found
|
|
</p>
|
|
</div>
|
|
<div className="p-3">
|
|
{searchResults.map((user) => (
|
|
<button
|
|
key={user.userId}
|
|
onClick={() => handleSelectUser(user)}
|
|
className="w-full text-left p-3 hover:bg-muted rounded-lg transition-colors mb-1 last:mb-0"
|
|
>
|
|
<p className="font-medium text-gray-900">{user.displayName || user.email}</p>
|
|
<p className="text-sm text-muted-foreground">{user.email}</p>
|
|
{user.department && (
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{user.department}{user.designation ? ` • ${user.designation}` : ''}
|
|
</p>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Selected User */}
|
|
{selectedUser && (
|
|
<div className="border-2 border-re-green/20 bg-re-green/5 rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-re-green flex items-center justify-center text-white font-bold shadow-md">
|
|
{(selectedUser.displayName || selectedUser.email).charAt(0).toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-gray-900">
|
|
{selectedUser.displayName || selectedUser.email}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">{selectedUser.email}</p>
|
|
{selectedUser.department && (
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{selectedUser.department}{selectedUser.designation ? ` • ${selectedUser.designation}` : ''}
|
|
</p>
|
|
)}
|
|
{fetchingRole && (
|
|
<p className="text-xs text-re-green mt-1 flex items-center gap-1">
|
|
<Loader2 className="w-3 h-3 animate-spin" />
|
|
Checking current role...
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setSelectedUser(null);
|
|
setSearchQuery('');
|
|
setSelectedRole('USER');
|
|
setFetchingRole(false);
|
|
}}
|
|
>
|
|
Clear
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Role Selection */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Select Role</label>
|
|
<Select value={selectedRole} onValueChange={(value: any) => setSelectedRole(value)} disabled={fetchingRole}>
|
|
<SelectTrigger className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20">
|
|
<SelectValue placeholder={fetchingRole ? "Loading current role..." : "Select role"} />
|
|
</SelectTrigger>
|
|
<SelectContent className="rounded-lg">
|
|
<SelectItem value="USER" className="p-3 rounded-lg my-1">
|
|
<div className="flex items-center gap-2">
|
|
<UserIcon className="w-4 h-4 text-gray-600" />
|
|
<span>User - Regular access</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="MANAGEMENT" className="p-3 rounded-lg my-1">
|
|
<div className="flex items-center gap-2">
|
|
<Users className="w-4 h-4 text-blue-600" />
|
|
<span>Management - Read all data</span>
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="ADMIN" className="p-3 rounded-lg my-1">
|
|
<div className="flex items-center gap-2">
|
|
<Crown className="w-4 h-4 text-yellow-600" />
|
|
<span>Administrator - Full access</span>
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Message */}
|
|
{message && (
|
|
<div className={`border-2 rounded-lg p-4 ${message.type === 'success'
|
|
? 'border-green-200 bg-green-50'
|
|
: 'border-red-200 bg-red-50'
|
|
}`}>
|
|
<div className="flex items-start gap-3">
|
|
{message.type === 'success' ? (
|
|
<CheckCircle className="w-5 h-5 text-green-600 shrink-0 mt-0.5" />
|
|
) : (
|
|
<AlertCircle className="w-5 h-5 text-red-600 shrink-0 mt-0.5" />
|
|
)}
|
|
<p className={`text-sm ${message.type === 'success' ? 'text-green-800' : 'text-red-800'}`}>
|
|
{message.text}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Users List with Filter and Pagination */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Users className="w-5 h-5 text-re-green" />
|
|
User Management
|
|
</CardTitle>
|
|
<CardDescription>
|
|
View and manage user accounts and roles ({totalUsers} {roleFilter !== 'ALL' && roleFilter !== 'ELEVATED' ? roleFilter.toLowerCase() : ''} users)
|
|
</CardDescription>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Select value={roleFilter} onValueChange={handleFilterChange}>
|
|
<SelectTrigger className="w-[200px] border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20">
|
|
<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>
|
|
</CardHeader>
|
|
<CardContent className="pt-6">
|
|
{loadingUsers ? (
|
|
<div className="flex flex-col items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-re-green mb-2" />
|
|
<p className="text-sm text-muted-foreground">Loading users...</p>
|
|
</div>
|
|
) : users.length === 0 ? (
|
|
<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">
|
|
<Users className="w-6 h-6 text-gray-400" />
|
|
</div>
|
|
<p className="font-medium text-gray-700">No users found</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>
|
|
) : (
|
|
<>
|
|
<UserTable
|
|
users={users.map(u => ({
|
|
id: u.userId,
|
|
name: u.displayName,
|
|
email: u.email,
|
|
role: u.role, // Keep as ADMIN, MANAGEMENT, or USER
|
|
department: u.department || 'N/A',
|
|
status: u.isActive ? 'active' : 'inactive'
|
|
}))}
|
|
onEdit={(userId) => {
|
|
const user = users.find(u => u.userId === userId);
|
|
if (user) {
|
|
// Open role selection dialog
|
|
const newRole = user.role === 'USER' ? 'MANAGEMENT' : user.role === 'MANAGEMENT' ? 'ADMIN' : 'USER';
|
|
handleEditUser(userId, newRole);
|
|
}
|
|
}}
|
|
onToggleStatus={handleToggleUserStatus}
|
|
onDelete={handleDeleteUser}
|
|
/>
|
|
|
|
{/* Pagination Controls */}
|
|
{totalPages > 1 && (
|
|
<div className="flex items-center justify-between pt-4 border-t mt-4">
|
|
<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}
|
|
>
|
|
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-re-green hover:bg-re-green/90'
|
|
: ''
|
|
}`}
|
|
>
|
|
{pageNum}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handlePageChange(currentPage + 1)}
|
|
disabled={currentPage === totalPages}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|