Spaces:
Sleeping
Sleeping
| // ───────────────────────────────────────────────────────────────────────────── | |
| // src/components/admin/UserManagement.tsx | |
| // Fixed: Added null checks for all optional fields | |
| // ───────────────────────────────────────────────────────────────────────────── | |
| import { useState, useEffect, useCallback } from 'react'; | |
| import { | |
| Card, CardContent, CardDescription, | |
| CardHeader, CardTitle, | |
| } from '../ui/card'; | |
| import { Input } from '../ui/input'; | |
| import { Button } from '../ui/button'; | |
| import { Badge } from '../ui/badge'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; | |
| import { | |
| Search, Users, GraduationCap, Shield, | |
| MoreVertical, Eye, Ban, Trash2, | |
| Mail, Calendar, TrendingUp, Loader2, RefreshCw, | |
| } from 'lucide-react'; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuSeparator, | |
| DropdownMenuTrigger, | |
| } from '../ui/dropdown-menu'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { useLanguage } from '../../lib/languageContext'; | |
| import { translations } from '../../lib/translations'; | |
| // Import from the User Management API | |
| import adminUserManagementApi, { | |
| type StudentDtoAdmin, | |
| type SheikhDtoAdmin, | |
| type UserManagementStatsDto, | |
| type UserManagementApiError, | |
| } from '../../lib/api/adminUserManagementApi'; | |
| import { toast } from 'sonner'; | |
| // ─── Types ─────────────────────────────────────────────────────────────────── | |
| // Combined user type for display | |
| interface DisplayUser { | |
| id: number; | |
| name: string; | |
| email: string; | |
| registrationDate: string; | |
| type: 'student' | 'sheikh' | 'admin'; | |
| status: string; | |
| // Student fields | |
| streak?: number | null; | |
| totalSessions?: number | null; | |
| // Sheikh fields | |
| numberOfSessions?: number | null; | |
| totalRevenue?: number | null; | |
| averageRating?: number | null; | |
| } | |
| // ─── Helpers ────────────────────────────────────────────────────────────────── | |
| function formatDate(dateString: string, locale: string): string { | |
| const d = new Date(dateString); | |
| return d.toLocaleDateString(locale === 'ar' ? 'ar-EG' : 'en-US', { | |
| year: 'numeric', month: 'short', day: 'numeric', | |
| }); | |
| } | |
| function formatNumber(value: number | null | undefined, locale: string): string { | |
| if (value === null || value === undefined) return '—'; | |
| return value.toLocaleString(locale === 'ar' ? 'ar-EG' : 'en-US'); | |
| } | |
| function formatCurrency(value: number | null | undefined, locale: string): string { | |
| if (value === null || value === undefined) return '—'; | |
| return `${value.toLocaleString(locale === 'ar' ? 'ar-EG' : 'en-US')} ${locale === 'ar' ? 'ج.م' : 'EGP'}`; | |
| } | |
| function formatRating(value: number | null | undefined): string { | |
| if (value === null || value === undefined) return '—'; | |
| return value.toFixed(1); | |
| } | |
| // ─── Main Component ─────────────────────────────────────────────────────────── | |
| export function UserManagement() { | |
| const { language } = useLanguage(); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const t = (translations[language] as any).userManagement ?? {}; | |
| const isAr = language === 'ar'; | |
| const navigate = useNavigate(); | |
| // ── State ───────────────────────────────────────────────────────────────── | |
| const [students, setStudents] = useState<StudentDtoAdmin[]>([]); | |
| const [sheikhs, setSheikhs] = useState<SheikhDtoAdmin[]>([]); | |
| const [stats, setStats] = useState<UserManagementStatsDto | null>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [actionLoading, setActionLoading] = useState<number | null>(null); | |
| const [error, setError] = useState<string | null>(null); | |
| const [searchQuery, setSearchQuery] = useState(''); | |
| const [activeTab, setActiveTab] = useState('all'); | |
| // ── Fetch all data ──────────────────────────────────────────────────────── | |
| const loadData = useCallback(async () => { | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| // Fetch students, sheikhs, and stats in parallel | |
| const [studentsData, sheikhsData, statsData] = await Promise.all([ | |
| adminUserManagementApi.fetchAllStudents(), | |
| adminUserManagementApi.fetchAllSheikhs(), | |
| adminUserManagementApi.fetchUserManagementStats(), | |
| ]); | |
| setStudents(studentsData); | |
| setSheikhs(sheikhsData); | |
| setStats(statsData); | |
| } catch (err) { | |
| console.error('Failed to load data:', err); | |
| // Handle different error types | |
| if (err instanceof UserManagementApiError) { | |
| if (err.status === 403) { | |
| setError(isAr ? 'صلاحية المسؤول مطلوبة' : 'Admin access required'); | |
| } else if (err.status === 401) { | |
| setError(isAr ? 'انتهت الجلسة، يرجى تسجيل الدخول مرة أخرى' : 'Session expired'); | |
| } else { | |
| setError(isAr ? 'فشل تحميل البيانات' : 'Failed to load data'); | |
| } | |
| } else { | |
| setError(isAr ? 'خطأ في الشبكة' : 'Network error'); | |
| } | |
| setStudents([]); | |
| setSheikhs([]); | |
| setStats(null); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, [isAr]); | |
| useEffect(() => { loadData(); }, [loadData]); | |
| // ── Combine users for display ───────────────────────────────────────────── | |
| const getAllUsers = useCallback((): DisplayUser[] => { | |
| const allUsers: DisplayUser[] = [ | |
| ...students.map(s => ({ | |
| id: s.id, | |
| name: s.name, | |
| email: s.email, | |
| registrationDate: s.registrationDate, | |
| type: 'student' as const, | |
| status: s.status, | |
| streak: s.streak, | |
| totalSessions: s.totalSessions, | |
| })), | |
| ...sheikhs.map(sh => ({ | |
| id: sh.id, | |
| name: sh.name, | |
| email: sh.email, | |
| registrationDate: sh.registrationDate, | |
| type: 'sheikh' as const, | |
| status: sh.status, | |
| numberOfSessions: sh.numberOfSessions, | |
| totalRevenue: sh.totalRevenue, | |
| averageRating: sh.averageRating, | |
| })), | |
| ]; | |
| // Add mock admins (since admin list not available from API) | |
| allUsers.push({ | |
| id: 999, | |
| name: isAr ? 'مدير النظام' : 'System Admin', | |
| email: 'admin@example.com', | |
| registrationDate: new Date().toISOString(), | |
| type: 'admin', | |
| status: 'ACTIVE', | |
| }); | |
| return allUsers; | |
| }, [students, sheikhs, isAr]); | |
| // ── Filter ──────────────────────────────────────────────────────────────── | |
| const filterUsers = (type?: 'student' | 'sheikh' | 'admin') => { | |
| let list = getAllUsers(); | |
| if (type) { | |
| list = list.filter(u => u.type === type); | |
| } | |
| if (searchQuery.trim()) { | |
| const q = searchQuery.toLowerCase(); | |
| list = list.filter( | |
| u => | |
| (u.name ?? '').toLowerCase().includes(q) || | |
| (u.email ?? '').toLowerCase().includes(q), | |
| ); | |
| } | |
| return list; | |
| }; | |
| const allUsers = filterUsers(); | |
| const studentList = filterUsers('student'); | |
| const sheikhList = filterUsers('sheikh'); | |
| const adminList = filterUsers('admin'); | |
| // ── Actions ─────────────────────────────────────────────────────────────── | |
| const handleBlockUser = async (user: DisplayUser) => { | |
| setActionLoading(user.id); | |
| try { | |
| await adminUserManagementApi.blockUser(user.id); | |
| // Refresh data after blocking | |
| await loadData(); | |
| } catch (err) { | |
| console.error('Block failed:', err); | |
| toast.error(isAr ? 'فشلت العملية، حاول مرة أخرى' : 'Operation failed, please try again'); | |
| } finally { | |
| setActionLoading(null); | |
| } | |
| }; | |
| const handleUnblockUser = async (user: DisplayUser) => { | |
| setActionLoading(user.id); | |
| try { | |
| await adminUserManagementApi.unblockUser(user.id); | |
| // Refresh data after unblocking | |
| await loadData(); | |
| } catch (err) { | |
| console.error('Unblock failed:', err); | |
| toast.error(isAr ? 'فشلت العملية، حاول مرة أخرى' : 'Operation failed, please try again'); | |
| } finally { | |
| setActionLoading(null); | |
| } | |
| }; | |
| // const handleDeleteUser = async (user: DisplayUser) => { | |
| // const msg = isAr | |
| // ? 'هل أنت متأكد من حذف هذا المستخدم؟ لا يمكن التراجع عن هذا الإجراء.' | |
| // : 'Are you sure you want to delete this user? This action cannot be undone.'; | |
| // if (!confirm(msg)) return; | |
| // setActionLoading(user.id); | |
| // try { | |
| // // Note: Delete user API not in docs | |
| // alert(isAr ? 'حذف المستخدمين غير متاح حالياً' : 'Delete user not available yet'); | |
| // } catch (err) { | |
| // console.error('Delete failed:', err); | |
| // alert(isAr ? 'فشل الحذف، حاول مرة أخرى' : 'Delete failed, please try again'); | |
| // } finally { | |
| // setActionLoading(null); | |
| // } | |
| // }; | |
| const handleViewProfile = (user: DisplayUser) => { | |
| if (user.type === 'student') { | |
| navigate(`/admin/student/${user.id}`); | |
| } else if (user.type === 'sheikh') { | |
| navigate(`/admin/sheikh/${user.id}`); | |
| } else { | |
| navigate(`/admin/user/${user.id}`); | |
| } | |
| }; | |
| // ── Role helpers ────────────────────────────────────────────────────────── | |
| const getRoleIcon = (type: string) => { | |
| if (type === 'student') return <Users className="h-4 w-4" />; | |
| if (type === 'sheikh') return <GraduationCap className="h-4 w-4" />; | |
| return <Shield className="h-4 w-4" />; | |
| }; | |
| const getRoleBadgeVariant = (type: string) => { | |
| if (type === 'student') return 'default' as const; | |
| if (type === 'sheikh') return 'secondary' as const; | |
| return 'destructive' as const; | |
| }; | |
| const getRoleLabel = (type: string) => { | |
| if (!isAr) { | |
| if (type === 'student') return 'Student'; | |
| if (type === 'sheikh') return 'Sheikh'; | |
| return 'Admin'; | |
| } | |
| if (type === 'student') return 'طالب'; | |
| if (type === 'sheikh') return 'شيخ'; | |
| return 'مسؤول'; | |
| }; | |
| const getStatusBadge = (status: string) => { | |
| if (status === 'BLOCKED') { | |
| return ( | |
| <Badge variant="destructive"> | |
| <Ban className="h-3 w-3 mr-1" /> | |
| {isAr ? 'محظور' : 'Blocked'} | |
| </Badge> | |
| ); | |
| } | |
| if (status === 'PENDING' || status === 'INACTIVE') { | |
| return ( | |
| <Badge variant="outline" className="text-yellow-600 border-yellow-400"> | |
| {isAr ? 'قيد المراجعة' : 'Pending'} | |
| </Badge> | |
| ); | |
| } | |
| return null; | |
| }; | |
| // ── Stats ───────────────────────────────────────────────────────────────── | |
| const totalUsers = stats?.totalUsers ?? (students.length + sheikhs.length + 1); // +1 for mock admin | |
| const totalStudents = stats?.totalStudents ?? students.length; | |
| const totalSheikhs = stats?.totalSheikhs ?? sheikhs.length; | |
| // const totalBlocked = stats?.blockedUsers ?? users.filter(u => u.status === 'BLOCKED').length; | |
| const totalBlocked = | |
| stats?.blockedUsers ?? | |
| [...students, ...sheikhs].filter(u => u.status === 'BLOCKED').length; | |
| // ── User Table ──────────────────────────────────────────────────────────── | |
| const UserTable = ({ list }: { list: DisplayUser[] }) => { | |
| if (loading) { | |
| return ( | |
| <div className="flex justify-center items-center py-16 gap-2 text-muted-foreground"> | |
| <Loader2 className="h-5 w-5 animate-spin" /> | |
| {isAr ? 'جاري التحميل…' : 'Loading…'} | |
| </div> | |
| ); | |
| } | |
| if (list.length === 0) { | |
| return ( | |
| <div className="text-center py-12 text-muted-foreground"> | |
| <Users className="h-12 w-12 mx-auto mb-4 opacity-20" /> | |
| <p>{t.noUsersFound ?? (isAr ? 'لا يوجد مستخدمون' : 'No users found')}</p> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-2"> | |
| {list.map((user) => { | |
| const isBlocked = user.status === 'BLOCKED'; | |
| const isActioning = actionLoading === user.id; | |
| return ( | |
| <div | |
| key={`${user.type}-${user.id}`} | |
| className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent transition-colors" | |
| > | |
| <div className="flex items-center gap-3 sm:gap-4 flex-1 min-w-0 w-full sm:w-auto"> | |
| <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-full bg-gradient-to-br from-emerald-400 to-emerald-600 flex items-center justify-center text-white font-semibold shrink-0 text-sm sm:text-base"> | |
| {(user.name ?? '?').charAt(0).toUpperCase()} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-1.5 sm:gap-2 flex-wrap"> | |
| <span className="font-medium text-sm sm:text-base truncate max-w-[120px] sm:max-w-[200px] md:max-w-none"> | |
| {user.name ?? '—'} | |
| </span> | |
| <Badge | |
| variant={getRoleBadgeVariant(user.type)} | |
| className="text-[10px] text-xs shrink-0" | |
| > | |
| {getRoleIcon(user.type)} | |
| <span className="ml-1 ">{getRoleLabel(user.type)}</span> | |
| </Badge> | |
| <span>{getStatusBadge(user.status)}</span> | |
| </div> | |
| <div className="flex flex-wrap items-center gap-1.5 sm:gap-2 text-xs sm:text-sm text-muted-foreground mt-1"> | |
| <span className="flex items-center gap-0.5 sm:gap-1 truncate max-w-[100px] sm:max-w-[150px] md:max-w-none"> | |
| <Mail className="h-3 w-3 mr-1" /> | |
| {user.email} | |
| </span> | |
| <span className="flex items-center gap-0.5 sm:gap-1 shrink-0 my-1"> | |
| <Calendar className="h-3 w-3" /> | |
| {t.joined ?? (isAr ? 'انضم' : 'Joined')}{' '} | |
| {formatDate(user.registrationDate, language)} | |
| </span> | |
| </div> | |
| {/* Mobile stats row */} | |
| <div className="flex items-center gap-3 mt-1.5 sm:hidden text-xs"> | |
| {user.type === 'student' && ( | |
| <> | |
| {user.totalSessions !== null && user.totalSessions !== undefined && ( | |
| <span className="text-muted-foreground"> | |
| <span className="font-semibold">{formatNumber(user.totalSessions, language)}</span> {t.sessions ?? (isAr ? 'ج' : 'Sess')} | |
| </span> | |
| )} | |
| {user.streak !== null && user.streak !== undefined && ( | |
| <span className="text-muted-foreground"> | |
| <span className="font-semibold">{formatNumber(user.streak, language)}</span> 🔥 | |
| </span> | |
| )} | |
| </> | |
| )} | |
| {user.type === 'sheikh' && ( | |
| <> | |
| {user.numberOfSessions !== null && user.numberOfSessions !== undefined && ( | |
| <span className="text-muted-foreground"> | |
| <span className="font-semibold">{formatNumber(user.numberOfSessions, language)}</span> {t.sessions ?? (isAr ? 'ج' : 'Sess')} | |
| </span> | |
| )} | |
| {user.averageRating !== null && user.averageRating !== undefined && ( | |
| <span className="text-muted-foreground"> | |
| <span className="font-semibold">{formatRating(user.averageRating)}</span> ⭐ | |
| </span> | |
| )} | |
| </> | |
| )} | |
| {user.status === 'BLOCKED' && ( | |
| <Badge variant="destructive" className="text-[10px]"> | |
| <Ban className="h-2 w-2 mr-1" /> | |
| {isAr ? 'محظور' : 'Blocked'} | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* ── Left: avatar + info ── */} | |
| {/* ── Right: stats + actions ── */} | |
| <div className="flex items-center gap-4 shrink-0"> | |
| <div className="flex gap-4 text-sm"> | |
| {user.type === 'student' && ( | |
| <> | |
| {user.totalSessions !== null && user.totalSessions !== undefined && ( | |
| <div className="text-center hidden sm:block"> | |
| <div className="font-semibold">{formatNumber(user.totalSessions, language)}</div> | |
| <div className="text-muted-foreground text-xs"> | |
| {t.sessions ?? (isAr ? 'جلسة' : 'Sessions')} | |
| </div> | |
| </div> | |
| )} | |
| {user.streak !== null && user.streak !== undefined && ( | |
| <div className="text-center hidden sm:block"> | |
| <div className="font-semibold">{formatNumber(user.streak, language)} 🔥</div> | |
| <div className="text-muted-foreground text-xs"> | |
| {t.streak ?? (isAr ? 'سلسلة' : 'Streak')} | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| {user.type === 'sheikh' && ( | |
| <> | |
| {user.numberOfSessions !== null && user.numberOfSessions !== undefined && ( | |
| <div className="text-center hidden sm:block"> | |
| <div className="font-semibold">{formatNumber(user.numberOfSessions, language)}</div> | |
| <div className="text-muted-foreground text-xs"> | |
| {t.sessions ?? (isAr ? 'جلسة' : 'Sessions')} | |
| </div> | |
| </div> | |
| )} | |
| {user.averageRating !== null && user.averageRating !== undefined && ( | |
| <div className="text-center hidden sm:block"> | |
| <div className="font-semibold">{formatRating(user.averageRating)} ⭐</div> | |
| <div className="text-muted-foreground text-xs"> | |
| {t.rating ?? (isAr ? 'تقييم' : 'Rating')} | |
| </div> | |
| </div> | |
| )} | |
| {user.totalRevenue !== null && user.totalRevenue !== undefined && ( | |
| <div className="text-center hidden sm:block"> | |
| <div className="font-semibold">{formatCurrency(user.totalRevenue, language)}</div> | |
| <div className="text-muted-foreground text-xs"> | |
| {t.earnings ?? (isAr ? 'أرباح' : 'Earnings')} | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| </div> | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button variant="ghost" size="sm" disabled={isActioning}> | |
| {isActioning | |
| ? <Loader2 className="h-4 w-4 animate-spin" /> | |
| : <MoreVertical className="h-4 w-4" />} | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end"> | |
| <DropdownMenuItem onClick={() => handleViewProfile(user)}> | |
| <Eye className="h-4 w-4 mr-2" /> | |
| {t.viewProfile ?? (isAr ? 'عرض الملف' : 'View Profile')} | |
| </DropdownMenuItem> | |
| <DropdownMenuSeparator /> | |
| {isBlocked ? ( | |
| <DropdownMenuItem onClick={() => handleUnblockUser(user)}> | |
| <Ban className="h-4 w-4 mr-2" /> | |
| {t.unblockUser ?? (isAr ? 'إلغاء الحظر' : 'Unblock User')} | |
| </DropdownMenuItem> | |
| ) : ( | |
| <DropdownMenuItem onClick={() => handleBlockUser(user)}> | |
| <Ban className="h-4 w-4 mr-2" /> | |
| {t.blockUser ?? (isAr ? 'حظر المستخدم' : 'Block User')} | |
| </DropdownMenuItem> | |
| )} | |
| {/* <DropdownMenuItem | |
| onClick={() => handleDeleteUser(user)} | |
| className="text-red-600" | |
| > | |
| <Trash2 className="h-4 w-4 mr-2" /> | |
| {t.deleteUser ?? (isAr ? 'حذف المستخدم' : 'Delete User')} | |
| </DropdownMenuItem> */} | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| }; | |
| // ───────────────────────────────────────────────────────────────────────── | |
| return ( | |
| <div className="container mx-auto p-6 space-y-6"> | |
| {/* ── Page Title ── */} | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h1 className="text-3xl font-bold"> | |
| {t.title ?? (isAr ? 'إدارة المستخدمين' : 'User Management')} | |
| </h1> | |
| <p className="text-muted-foreground"> | |
| {t.description ?? (isAr ? 'بحث وإدارة جميع مستخدمي المنصة' : 'Search and manage all platform users')} | |
| </p> | |
| </div> | |
| {/* <Button variant="outline" onClick={loadData} disabled={loading}> | |
| <RefreshCw className={`h-4 w-4 me-2 ${loading ? 'animate-spin' : ''}`} /> | |
| {isAr ? 'تحديث' : 'Refresh'} | |
| </Button> */} | |
| </div> | |
| {/* ── Error Banner ── */} | |
| {error && ( | |
| <div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg flex items-center justify-between"> | |
| <span>{error}</span> | |
| <Button variant="outline" size="sm" onClick={loadData}> | |
| {isAr ? 'إعادة المحاولة' : 'Retry'} | |
| </Button> | |
| </div> | |
| )} | |
| {/* ── Statistics Cards ── */} | |
| <div className="grid gap-4 md:grid-cols-4"> | |
| <Card> | |
| <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> | |
| <CardTitle className="text-sm font-medium"> | |
| {t.totalUsers ?? (isAr ? 'إجمالي المستخدمين' : 'Total Users')} | |
| </CardTitle> | |
| <Users className="h-4 w-4 text-muted-foreground" /> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold"> | |
| {loading ? <Loader2 className="h-5 w-5 animate-spin" /> : totalUsers} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> | |
| <CardTitle className="text-sm font-medium"> | |
| {t.students ?? (isAr ? 'الطلاب' : 'Students')} | |
| </CardTitle> | |
| <Users className="h-4 w-4 text-blue-500" /> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold"> | |
| {loading ? <Loader2 className="h-5 w-5 animate-spin" /> : totalStudents} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> | |
| <CardTitle className="text-sm font-medium"> | |
| {t.sheikhs ?? (isAr ? 'الشيوخ' : 'Sheikhs')} | |
| </CardTitle> | |
| <GraduationCap className="h-4 w-4 text-purple-500" /> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold"> | |
| {loading ? <Loader2 className="h-5 w-5 animate-spin" /> : totalSheikhs} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> | |
| <CardTitle className="text-sm font-medium"> | |
| {t.blockedUsers ?? (isAr ? 'المحظورون' : 'Blocked Users')} | |
| </CardTitle> | |
| <Ban className="h-4 w-4 text-red-500" /> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-2xl font-bold"> | |
| {loading ? <Loader2 className="h-5 w-5 animate-spin" /> : totalBlocked} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* ── Search ── */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle> | |
| {t.searchUsers ?? (isAr ? 'بحث عن مستخدم' : 'Search Users')} | |
| </CardTitle> | |
| <CardDescription> | |
| {t.searchDescription ?? (isAr ? 'ابحث بالاسم أو البريد الإلكتروني' : 'Find users by name or email address')} | |
| </CardDescription> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="relative"> | |
| <Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" /> | |
| <Input | |
| placeholder={t.searchPlaceholder ?? (isAr ? 'ابحث بالاسم أو البريد…' : 'Search by name or email...')} | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-10" | |
| /> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* ── Users Table with Tabs ── */} | |
| {/* ── Users Table with Tabs ── */} | |
| <Card> | |
| <CardContent className="pt-4 sm:pt-6 px-2 sm:px-4"> | |
| <Tabs value={activeTab} onValueChange={setActiveTab}> | |
| <TabsList className="grid w-full grid-cols-4 h-auto p-0.5 sm:p-1"> | |
| <TabsTrigger | |
| value="all" | |
| className="text-[10px] sm:text-xs md:text-sm py-1.5 sm:py-2 px-1 sm:px-2 data-[state=active]:bg-background" | |
| > | |
| <span className="hidden xs:inline">{t.allUsers ?? (isAr ? 'الكل' : 'All')}</span> | |
| <span className="xs:hidden">{isAr ? 'الكل' : 'All'}</span> | |
| <span className="ml-0.5 sm:ml-1 text-muted-foreground">({allUsers.length})</span> | |
| </TabsTrigger> | |
| <TabsTrigger | |
| value="students" | |
| className="text-[10px] sm:text-xs md:text-sm py-1.5 sm:py-2 px-1 sm:px-2 data-[state=active]:bg-background" | |
| > | |
| <span className="hidden xs:inline">{t.students ?? (isAr ? 'طلاب' : 'Students')}</span> | |
| <span className="xs:hidden">{isAr ? 'طلاب' : 'Stu'}</span> | |
| <span className="ml-0.5 sm:ml-1 text-muted-foreground">({studentList.length})</span> | |
| </TabsTrigger> | |
| <TabsTrigger | |
| value="sheikhs" | |
| className="text-[10px] sm:text-xs md:text-sm py-1.5 sm:py-2 px-1 sm:px-2 data-[state=active]:bg-background" | |
| > | |
| <span className="hidden xs:inline">{t.sheikhs ?? (isAr ? 'شيوخ' : 'Sheikhs')}</span> | |
| <span className="xs:hidden">{isAr ? 'شيوخ' : 'Sh'}</span> | |
| <span className="ml-0.5 sm:ml-1 text-muted-foreground">({sheikhList.length})</span> | |
| </TabsTrigger> | |
| <TabsTrigger | |
| value="admins" | |
| className="text-[10px] sm:text-xs md:text-sm py-1.5 sm:py-2 px-1 sm:px-2 data-[state=active]:bg-background" | |
| > | |
| <span className="hidden xs:inline">{t.admins ?? (isAr ? 'مسؤولون' : 'Admins')}</span> | |
| <span className="xs:hidden">{isAr ? 'مسؤول' : 'Adm'}</span> | |
| <span className="ml-0.5 sm:ml-1 text-muted-foreground">({adminList.length})</span> | |
| </TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="all" className="mt-4 sm:mt-6"><UserTable list={allUsers} /></TabsContent> | |
| <TabsContent value="students" className="mt-4 sm:mt-6"><UserTable list={studentList} /></TabsContent> | |
| <TabsContent value="sheikhs" className="mt-4 sm:mt-6"><UserTable list={sheikhList} /></TabsContent> | |
| <TabsContent value="admins" className="mt-4 sm:mt-6"><UserTable list={adminList} /></TabsContent> | |
| </Tabs> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| } |