Quran_Tech_Server / F_Pro /src /components /admin /UserProfileView.tsx
aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
26.3 kB
// src/components/admin/UserProfileView.tsx
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useLanguage } from '../../lib/languageContext';
import { translations } from '../../lib/translations';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '../ui/card';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '../ui/alert-dialog';
import { Progress } from '../ui/progress';
import { Separator } from '../ui/separator';
import { toast } from 'sonner';
import {
ArrowLeft,
Mail,
Calendar,
MapPin,
Award,
Clock,
DollarSign,
Star,
Users,
Ban,
Trash2,
CheckCircle,
AlertTriangle,
Video,
BookOpen,
Target,
Loader2,
} from 'lucide-react';
// Import API
import adminUserManagementApi, {
type StudentDtoAdmin,
type SheikhDtoAdmin,
type UserManagementApiError,
type StudentProfileResponse,
type SheikhProfileResponse,
type SessionHistoryDto,
} from '../../lib/api/adminUserManagementApi';
// ─── Types & Interfaces ──────────────────────────────────────────────────────
type UserType = 'student' | 'sheikh' | 'admin';
interface DisplayUser {
id: number;
name: string;
email: string;
registrationDate: string;
type: UserType;
status: string;
// Student fields
streak?: number | null;
totalSessions?: number | null;
// Sheikh fields
numberOfSessions?: number | null;
totalRevenue?: number | null;
averageRating?: number | null;
}
interface StudentProfileData {
profile: StudentProfileResponse['studentProfile'];
sessions: SessionHistoryDto[];
}
interface SheikhProfileData {
profile: SheikhProfileResponse['sheikhProfile'];
sessions: SessionHistoryDto[];
}
// ─── 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: 'long',
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);
}
function formatDuration(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours === 0) return `${mins} ${mins === 1 ? 'min' : 'mins'}`;
if (mins === 0) return `${hours} ${hours === 1 ? 'hr' : 'hrs'}`;
return `${hours}h ${mins}m`;
}
// ─── Main Component ───────────────────────────────────────────────────────────
export function UserProfileView() {
const { userId } = useParams<{ userId: string }>();
const navigate = useNavigate();
const { language } = useLanguage();
const t = translations[language];
const isArabic = language === 'ar';
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [userType, setUserType] = useState<UserType | null>(null);
const [studentData, setStudentData] = useState<StudentProfileData | null>(null);
const [sheikhData, setSheikhData] = useState<SheikhProfileData | null>(null);
const [actionLoading, setActionLoading] = useState(false);
// ── Load user profile ─────────────────────────────────────────────────────
useEffect(() => {
loadUserProfile();
}, [userId]);
const loadUserProfile = async () => {
if (!userId) {
setError('User ID not provided');
setLoading(false);
return;
}
setLoading(true);
setError(null);
try {
// Try to load as student first
try {
const studentProfile = await adminUserManagementApi.fetchStudentProfile(parseInt(userId));
setUserType('student');
setStudentData({
profile: studentProfile.studentProfile,
sessions: studentProfile.sessionHistories,
});
setLoading(false);
return;
} catch (err) {
// Not a student, continue to try sheikh
}
// Try to load as sheikh
try {
const sheikhProfile = await adminUserManagementApi.fetchSheikhProfile(parseInt(userId));
setUserType('sheikh');
setSheikhData({
profile: sheikhProfile.sheikhProfile,
sessions: sheikhProfile.sessionHistories,
});
setLoading(false);
return;
} catch (err) {
// Not a sheikh either
if (err instanceof UserManagementApiError) {
if (err.status === 404) {
setError(isArabic ? 'المستخدم غير موجود' : 'User not found');
} else if (err.status === 403) {
setError(isArabic ? 'صلاحية المسؤول مطلوبة' : 'Admin access required');
} else {
setError(err.message);
}
} else {
setError(isArabic ? 'فشل تحميل بيانات المستخدم' : 'Failed to load user data');
}
}
} finally {
setLoading(false);
}
};
// ── Actions ───────────────────────────────────────────────────────────────
const handleBlock = async () => {
if (!userId) return;
setActionLoading(true);
try {
await adminUserManagementApi.blockUser(parseInt(userId));
toast.success(
isArabic
? 'تم حظر المستخدم بنجاح'
: 'User has been blocked'
);
// Refresh data
await loadUserProfile();
} catch (err) {
console.error('Block failed:', err);
toast.error(
isArabic
? 'فشل حظر المستخدم'
: 'Failed to block user'
);
} finally {
setActionLoading(false);
}
};
const handleUnblock = async () => {
if (!userId) return;
setActionLoading(true);
try {
await adminUserManagementApi.unblockUser(parseInt(userId));
toast.success(
isArabic
? 'تم إلغاء حظر المستخدم بنجاح'
: 'User has been unblocked'
);
// Refresh data
await loadUserProfile();
} catch (err) {
console.error('Unblock failed:', err);
toast.error(
isArabic
? 'فشل إلغاء حظر المستخدم'
: 'Failed to unblock user'
);
} finally {
setActionLoading(false);
}
};
const handleDelete = async () => {
if (!userId) return;
setActionLoading(true);
try {
// Note: Delete API not available yet
toast.error(
isArabic
? 'حذف المستخدمين غير متاح حالياً'
: 'Delete user not available yet'
);
} catch (err) {
console.error('Delete failed:', err);
} finally {
setActionLoading(false);
}
};
const handleBack = () => {
navigate('/admin/users');
};
// ── Loading State ─────────────────────────────────────────────────────────
if (loading) {
return (
<div className="container mx-auto py-16 px-4 flex justify-center items-center">
<div className="text-center space-y-4">
<Loader2 className="h-12 w-12 animate-spin text-emerald-600 mx-auto" />
<p className="text-muted-foreground">
{isArabic ? 'جاري تحميل الملف الشخصي...' : 'Loading profile...'}
</p>
</div>
</div>
);
}
// ── Error State ───────────────────────────────────────────────────────────
if (error || !userType) {
return (
<div className="container mx-auto py-16 px-4">
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle className="text-center text-destructive">
<AlertTriangle className="h-12 w-12 mx-auto mb-4 text-destructive" />
{isArabic ? 'خطأ' : 'Error'}
</CardTitle>
<CardDescription className="text-center">
{error || (isArabic ? 'المستخدم غير موجود' : 'User not found')}
</CardDescription>
</CardHeader>
<CardContent className="text-center">
<Button onClick={handleBack}>
<ArrowLeft className={`h-4 w-4 ${isArabic ? 'ml-2' : 'mr-2'}`} />
{isArabic ? 'العودة' : 'Go Back'}
</Button>
</CardContent>
</Card>
</div>
);
}
const isStudent = userType === 'student';
const isSheikh = userType === 'sheikh';
const currentStatus = isStudent ? studentData?.profile.status : sheikhData?.profile.status;
const isBlocked = currentStatus === 'BLOCKED';
const student = studentData?.profile;
const sheikh = sheikhData?.profile;
const sessions = studentData?.sessions || sheikhData?.sessions || [];
return (
<div dir={isArabic ? 'rtl' : 'ltr'} className="container mx-auto py-8 px-4 max-w-7xl space-y-8">
{/* Header + Actions */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<div className="flex items-center gap-4">
<Button variant="outline" size="sm" onClick={handleBack}>
<ArrowLeft className={`h-4 w-4 ${isArabic ? 'ml-2' : 'mr-2'}`} />
{isArabic ? 'العودة' : 'Back'}
</Button>
<div>
<h1 className="text-2xl font-bold tracking-tight">
{isArabic ? 'الملف الشخصي' : 'User Profile'}
</h1>
<p className="text-muted-foreground mt-1">
{isStudent ? student?.name : sheikh?.name}
</p>
</div>
</div>
<div className="flex flex-wrap gap-3">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="sm" disabled={actionLoading}>
<Ban className={`h-4 w-4 ${isArabic ? 'ml-2' : 'mr-2'}`} />
{isBlocked
? (isArabic ? 'إلغاء الحظر' : 'Unblock')
: (isArabic ? 'حظر' : 'Block')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{isBlocked
? (isArabic ? 'إلغاء حظر المستخدم؟' : 'Unblock User?')
: (isArabic ? 'حظر المستخدم؟' : 'Block User?')}
</AlertDialogTitle>
<AlertDialogDescription>
{isBlocked
? (isArabic
? 'سيُعاد للمستخدم الوصول إلى المنصة.'
: "This will restore the user's access to the platform.")
: (isArabic
? 'سيمنع هذا الإجراء المستخدم من الوصول إلى المنصة. يمكن إلغاء الحظر لاحقًا.'
: 'This will prevent the user from accessing the platform. They can be unblocked later.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{isArabic ? 'إلغاء' : 'Cancel'}</AlertDialogCancel>
<AlertDialogAction onClick={isBlocked ? handleUnblock : handleBlock}>
{isBlocked
? (isArabic ? 'إلغاء الحظر' : 'Unblock')
: (isArabic ? 'حظر' : 'Block')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" disabled={actionLoading}>
<Trash2 className={`h-4 w-4 ${isArabic ? 'ml-2' : 'mr-2'}`} />
{isArabic ? 'حذف' : 'Delete'}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{isArabic ? 'حذف المستخدم نهائيًا؟' : 'Delete User Permanently?'}
</AlertDialogTitle>
<AlertDialogDescription>
{isArabic
? 'هذا الإجراء لا يمكن التراجع عنه. سيتم حذف الحساب وجميع البيانات المرتبطة به نهائيًا.'
: 'This action cannot be undone. This will permanently delete the user account and all associated data.'}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{isArabic ? 'إلغاء' : 'Cancel'}</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isArabic ? 'حذف نهائي' : 'Delete Permanently'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
{/* Main User Card */}
<Card className="border shadow-sm">
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row gap-6">
{/* Avatar */}
<div className="shrink-0">
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white text-4xl font-bold shadow-md">
{(isStudent ? student?.name : sheikh?.name)?.charAt(0).toUpperCase() || '?'}
</div>
</div>
{/* Info */}
<div className="flex-1 space-y-4">
<div className="flex flex-wrap items-center gap-3">
<h2 className="text-2xl font-bold">{isStudent ? student?.name : sheikh?.name}</h2>
<Badge variant={isStudent ? 'default' : isSheikh ? 'secondary' : 'destructive'} className="px-3 py-1">
{userType.toUpperCase()}
</Badge>
<Badge
variant={!isBlocked ? 'outline' : 'destructive'}
className={!isBlocked ? 'text-emerald-600 border-emerald-600' : ''}
>
{!isBlocked ? (
<>
<CheckCircle className="h-3 w-3 mr-1" />
{isArabic ? 'نشط' : 'Active'}
</>
) : (
<>
<Ban className="h-3 w-3 mr-1" />
{isArabic ? 'محظور' : 'Blocked'}
</>
)}
</Badge>
</div>
<div className="grid gap-2 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4" />
{isStudent ? student?.email : sheikh?.email}
</div>
{isSheikh && sheikh?.country && (
<div className="flex items-center gap-2">
<MapPin className="h-4 w-4" />
{sheikh.country}
</div>
)}
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
{isArabic ? 'انضم في ' : 'Joined '}
{formatDate(
isStudent ? student?.registrationDate || '' : sheikh?.registrationDate || '',
language
)}
</div>
{isSheikh && sheikh?.bio && (
<div className="mt-2 text-sm border-t pt-2">
<p className="text-muted-foreground">{sheikh.bio}</p>
</div>
)}
</div>
</div>
{/* Right side stats */}
{isSheikh && sheikh && (
<div className="text-right space-y-2 min-w-[180px]">
<div className="text-3xl font-bold text-emerald-600">
{formatCurrency(sheikh.totalEarnings, language)}
</div>
<div className="text-sm text-muted-foreground">{isArabic ? 'الأرباح' : 'Earnings'}</div>
<div className="flex items-center justify-end gap-1 text-yellow-600">
<Star className="h-5 w-5 fill-yellow-500 text-yellow-500" />
<span className="font-bold text-xl">{formatRating(sheikh.averageRating)}</span>
</div>
</div>
)}
{isStudent && student && (
<div className="text-right space-y-2 min-w-[180px]">
<div className="text-3xl font-bold">{formatNumber(student.sessionCount, language)}</div>
<div className="text-sm text-muted-foreground">{isArabic ? 'الجلسات' : 'Sessions'}</div>
<div className="text-2xl font-bold text-orange-600">
{student.streak ?? 0} <span className="text-xl">🔥</span>
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Statistics Cards Grid */}
{isStudent && student && (
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'إجمالي الجلسات' : 'Total Sessions'}</CardTitle>
<BookOpen className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(student.sessionCount, language)}</div>
</CardContent>
</Card>
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'الجلسات المكتملة' : 'Completed Sessions'}</CardTitle>
<CheckCircle className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(student.numberOfCompletedSessions, language)}</div>
</CardContent>
</Card>
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'وقت التعلم' : 'Learning Time'}</CardTitle>
<Clock className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatDuration(student.totalSpentTime)}</div>
</CardContent>
</Card>
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'السلسلة' : 'Streak'}</CardTitle>
<Target className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">
{student.streak ?? 0} <span className="text-xl">🔥</span>
</div>
</CardContent>
</Card>
</div>
)}
{isSheikh && sheikh && (
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'إجمالي الجلسات' : 'Total Sessions'}</CardTitle>
<Video className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(sheikh.numberOfCompletedSessions, language)}</div>
</CardContent>
</Card>
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'عدد الطلاب' : 'Students'}</CardTitle>
<Users className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatNumber(sheikh.numberOfStudents, language)}</div>
</CardContent>
</Card>
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'التقييم' : 'Rating'}</CardTitle>
<Star className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-600">{formatRating(sheikh.averageRating)}</div>
</CardContent>
</Card>
<Card className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2 space-y-0">
<CardTitle className="text-sm font-medium">{isArabic ? 'سعر الساعة' : 'Hourly Rate'}</CardTitle>
<DollarSign className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatCurrency(sheikh.hourlyRate, language)}</div>
</CardContent>
</Card>
</div>
)}
{/* Session History */}
<Card>
<CardHeader>
<CardTitle>{isArabic ? 'سجل الجلسات' : 'Session History'}</CardTitle>
<CardDescription>
{isArabic ? 'آخر الجلسات والأنشطة' : 'Recent sessions and activities'}
</CardDescription>
</CardHeader>
<CardContent>
{sessions.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{isArabic ? 'لا توجد جلسات سابقة' : 'No session history'}
</div>
) : (
<div className="space-y-4">
{sessions.map((session) => (
<div
key={session.sessionId}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-4">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${
session.status === 'COMPLETED' ? 'bg-emerald-100' : 'bg-red-100'
}`}
>
<Video
className={`h-5 w-5 ${
session.status === 'COMPLETED' ? 'text-emerald-600' : 'text-red-600'
}`}
/>
</div>
<div>
<p className="font-medium">
{isStudent ? session.sheikhName : session.studentName}
</p>
<p className="text-sm text-muted-foreground mt-0.5">
{formatDate(session.date, language)}
{session.durationInMinutes && (
<span className="ml-2">· {formatDuration(session.durationInMinutes)}</span>
)}
</p>
</div>
</div>
<div className="text-right">
<p className="font-semibold">{formatCurrency(session.price, language)}</p>
<Badge
variant={session.status === 'COMPLETED' ? 'default' : 'destructive'}
className="mt-1"
>
{session.status === 'COMPLETED'
? (isArabic ? 'مكتملة' : 'Completed')
: session.status === 'CANCELLED'
? (isArabic ? 'ملغية' : 'Cancelled')
: (isArabic ? 'معلقة' : 'Pending')}
</Badge>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}