Spaces:
Sleeping
Sleeping
| import { useState } from "react"; | |
| import { | |
| useListUsers, | |
| useUpdateUser, | |
| useGetDashboardAnalytics, | |
| useListTaskTypes, | |
| useToggleTaskType, | |
| getListTaskTypesQueryKey, | |
| getListUsersQueryKey, | |
| } from "@workspace/api-client-react"; | |
| import { useQueryClient } from "@tanstack/react-query"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Avatar, AvatarFallback } from "@/components/ui/avatar"; | |
| import { Skeleton } from "@/components/ui/skeleton"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogHeader, | |
| DialogTitle, | |
| } from "@/components/ui/dialog"; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from "@/components/ui/select"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Input } from "@/components/ui/input"; | |
| import { TEAMS, getTeamColor } from "../lib/constants"; | |
| import { Users, BarChart3, Settings, ShieldCheck, ShieldAlert, Eye, EyeOff } from "lucide-react"; | |
| import { useToast } from "@/hooks/use-toast"; | |
| import { useAuth } from "../context/auth-context"; | |
| type TabId = "users" | "analytics" | "taskTypes"; | |
| const TABS: { id: TabId; label: string; icon: typeof Users }[] = [ | |
| { id: "users", label: "المستخدمون", icon: Users }, | |
| { id: "analytics", label: "التحليلات", icon: BarChart3 }, | |
| { id: "taskTypes", label: "أنواع المهام", icon: Settings }, | |
| ]; | |
| // Updated roles with new structure | |
| const CANONICAL_ROLES = [ | |
| { value: "admin", label: "مدير النظام", cls: "bg-red-100 text-red-700 border-red-200" }, | |
| { value: "head_of_team", label: "رئيس الفرق", cls: "bg-orange-100 text-orange-700 border-orange-200" }, | |
| { value: "team_lead", label: "قائد فريق", cls: "bg-blue-100 text-blue-700 border-blue-200" }, | |
| { value: "member", label: "عضو", cls: "bg-slate-100 text-slate-600 border-slate-200" }, | |
| ]; | |
| const ROLE_MAP: Record<string, { label: string; cls: string }> = { | |
| admin: { label: "مدير النظام", cls: "bg-red-100 text-red-700 border-red-200" }, | |
| head_of_team: { label: "رئيس الفرق", cls: "bg-orange-100 text-orange-700 border-orange-200" }, | |
| team_lead: { label: "قائد فريق", cls: "bg-blue-100 text-blue-700 border-blue-200" }, | |
| member: { label: "عضو", cls: "bg-slate-100 text-slate-600 border-slate-200" }, | |
| // Legacy support for display | |
| dept_manager: { label: "قائد فريق", cls: "bg-blue-100 text-blue-700 border-blue-200" }, | |
| team_leader: { label: "قائد فريق", cls: "bg-blue-100 text-blue-700 border-blue-200" }, | |
| }; | |
| function RoleBadge({ role }: { role: string }) { | |
| const r = ROLE_MAP[role] ?? { label: role, cls: "bg-slate-100 text-slate-500 border-slate-200" }; | |
| return ( | |
| <span className={`px-2 py-0.5 rounded-full text-[10px] font-bold border ${r.cls}`}> | |
| {r.label} | |
| </span> | |
| ); | |
| } | |
| export default function Admin() { | |
| const [tab, setTab] = useState<TabId>("users"); | |
| const queryClient = useQueryClient(); | |
| const { toast } = useToast(); | |
| const { activeUser, token } = useAuth(); | |
| const { data: users, isLoading: usersLoading } = useListUsers(); | |
| const { data: analytics, isLoading: analyticsLoading } = useGetDashboardAnalytics(); | |
| const { data: taskTypes, isLoading: taskTypesLoading } = useListTaskTypes({}); | |
| const toggleTaskType = useToggleTaskType(); | |
| const updateUser = useUpdateUser(); | |
| const handleToggleTaskType = (id: number) => { | |
| toggleTaskType.mutate({ id }, { | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: getListTaskTypesQueryKey() }); | |
| toast({ title: "تم تحديث حالة نوع المهمة" }); | |
| }, | |
| onError: () => toast({ title: "فشل التحديث", variant: "destructive" }), | |
| }); | |
| }; | |
| const [editUser, setEditUser] = useState<{ id: number; name: string; team: string; role: string } | null>(null); | |
| const [editTeam, setEditTeam] = useState(""); | |
| const [editRole, setEditRole] = useState(""); | |
| // Add User Form States | |
| const [isAddUserOpen, setIsAddUserOpen] = useState(false); | |
| const [newUserName, setNewUserName] = useState(""); | |
| const [newUserEmail, setNewUserEmail] = useState(""); | |
| const [newUserPassword, setNewUserPassword] = useState(""); | |
| const [newUserTeam, setNewUserTeam] = useState(""); | |
| const [newUserRole, setNewUserRole] = useState("member"); | |
| const [showPassword, setShowPassword] = useState(false); | |
| const [isSubmittingUser, setIsSubmittingUser] = useState(false); | |
| const openEditUser = (u: { id: number; name: string; team: string; role: string }) => { | |
| setEditUser(u); | |
| setEditTeam(u.team); | |
| setEditRole(u.role); | |
| }; | |
| const handleSaveUser = () => { | |
| if (!editUser) return; | |
| updateUser.mutate( | |
| { id: editUser.id, data: { team: editTeam, role: editRole } }, | |
| { | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: getListUsersQueryKey() }); | |
| toast({ title: "تم تحديث بيانات المستخدم" }); | |
| setEditUser(null); | |
| }, | |
| onError: () => toast({ title: "فشل التحديث", variant: "destructive" }), | |
| } | |
| ); | |
| }; | |
| const handleRoleChange = (userId: number, newRole: string) => { | |
| updateUser.mutate( | |
| { id: userId, data: { role: newRole } }, | |
| { | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: getListUsersQueryKey() }); | |
| toast({ title: "تم تحديث الدور" }); | |
| }, | |
| onError: () => toast({ title: "فشل التحديث", variant: "destructive" }), | |
| } | |
| ); | |
| }; | |
| // Grant/revoke admin — only admin can do this | |
| const handleGrantAdmin = (userId: number, currentRole: string) => { | |
| const newRole = currentRole === "admin" ? "team_lead" : "admin"; | |
| const confirmMsg = currentRole === "admin" | |
| ? "سيتم إلغاء صلاحيات الإدارة من هذا المستخدم. هل أنت متأكد؟" | |
| : "سيتم منح صلاحيات الإدارة الكاملة لهذا المستخدم. هل أنت متأكد؟"; | |
| if (!window.confirm(confirmMsg)) return; | |
| updateUser.mutate( | |
| { id: userId, data: { role: newRole } }, | |
| { | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ queryKey: getListUsersQueryKey() }); | |
| toast({ title: currentRole === "admin" ? "تم إلغاء صلاحيات الإدارة" : "تم منح صلاحيات الإدارة" }); | |
| }, | |
| onError: () => toast({ title: "فشل التحديث", variant: "destructive" }), | |
| } | |
| ); | |
| }; | |
| const handleCreateUser = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!newUserName.trim() || !newUserEmail.trim() || newUserPassword.length < 8 || !newUserTeam) { | |
| return; | |
| } | |
| setIsSubmittingUser(true); | |
| try { | |
| const res = await fetch("/api/auth/register", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| Authorization: `Bearer ${token}`, | |
| }, | |
| body: JSON.stringify({ | |
| name: newUserName, | |
| email: newUserEmail, | |
| password: newUserPassword, | |
| team: newUserTeam, | |
| role: newUserRole, | |
| }), | |
| }); | |
| const data = await res.json(); | |
| if (!res.ok) { | |
| throw new Error(data.error || "حدث خطأ أثناء إضافة الموظف"); | |
| } | |
| queryClient.invalidateQueries({ queryKey: getListUsersQueryKey() }); | |
| toast({ title: "تم إضافة الموظف بنجاح" }); | |
| setIsAddUserOpen(false); | |
| // Reset form | |
| setNewUserName(""); | |
| setNewUserEmail(""); | |
| setNewUserPassword(""); | |
| setNewUserTeam(""); | |
| setNewUserRole("member"); | |
| setShowPassword(false); | |
| } catch (err: any) { | |
| toast({ | |
| title: err.message || "فشل إضافة الموظف", | |
| variant: "destructive", | |
| }); | |
| } finally { | |
| setIsSubmittingUser(false); | |
| } | |
| }; | |
| const isFormInvalid = !newUserName.trim() || !newUserEmail.trim() || newUserPassword.length < 8 || !newUserTeam; | |
| return ( | |
| <div className="p-8 max-w-6xl mx-auto space-y-6"> | |
| <div> | |
| <h1 className="text-3xl font-bold tracking-tight">لوحة الإدارة</h1> | |
| <p className="text-slate-500 mt-1">إدارة المستخدمين والأدوار والتحليلات</p> | |
| </div> | |
| <div className="flex gap-1 bg-slate-100 rounded-lg p-1 w-fit"> | |
| {TABS.map(t => { | |
| const Icon = t.icon; | |
| return ( | |
| <button | |
| key={t.id} | |
| onClick={() => setTab(t.id)} | |
| className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${ | |
| tab === t.id ? "bg-white text-slate-900 shadow-sm" : "text-slate-600 hover:text-slate-900" | |
| }`} | |
| > | |
| <Icon className="h-4 w-4" /> | |
| {t.label} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| {tab === "users" && ( | |
| <Card> | |
| <CardHeader> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-4"> | |
| <CardTitle>جميع المستخدمين ({users?.length ?? 0})</CardTitle> | |
| <Button size="sm" variant="default" onClick={() => setIsAddUserOpen(true)}>+ إضافة موظف</Button> | |
| </div> | |
| <div className="flex items-center gap-1.5 text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-md px-2.5 py-1.5"> | |
| <ShieldCheck className="h-3.5 w-3.5" /> | |
| صلاحية الإدارة — فقط لك كمدير النظام | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| {usersLoading ? ( | |
| <div className="space-y-3"> | |
| {[1,2,3].map(i => <Skeleton key={i} className="h-16 w-full" />)} | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {users?.map(user => ( | |
| <div | |
| key={user.id} | |
| className="flex items-center gap-4 p-3 rounded-lg border hover:bg-slate-50 transition-colors" | |
| > | |
| <Avatar className="h-10 w-10"> | |
| <AvatarFallback style={{ backgroundColor: user.avatarColor, color: "#fff", fontSize: "12px" }}> | |
| {user.name.substring(0, 2)} | |
| </AvatarFallback> | |
| </Avatar> | |
| <div className="flex-1 min-w-0"> | |
| <p className="font-medium text-sm">{user.name}</p> | |
| <p className="text-xs text-slate-400">{user.email}</p> | |
| </div> | |
| <div | |
| className="px-2.5 py-1 rounded-full text-xs font-medium text-white flex-shrink-0" | |
| style={{ backgroundColor: getTeamColor(user.team) }} | |
| > | |
| {user.team} | |
| </div> | |
| <RoleBadge role={user.role} /> | |
| <div className="flex items-center gap-2 flex-shrink-0"> | |
| {/* Grant/revoke admin — only available to the current admin, and not on themselves */} | |
| {activeUser?.role === "admin" && user.id !== activeUser?.id && ( | |
| <Button | |
| variant={user.role === "admin" ? "destructive" : "outline"} | |
| size="sm" | |
| className="text-xs gap-1.5" | |
| onClick={() => handleGrantAdmin(user.id, user.role)} | |
| title={user.role === "admin" ? "إلغاء صلاحيات الإدارة" : "منح صلاحيات الإدارة"} | |
| > | |
| {user.role === "admin" | |
| ? <><ShieldAlert className="h-3.5 w-3.5" /> إلغاء الإدارة</> | |
| : <><ShieldCheck className="h-3.5 w-3.5" /> منح إدارة</> | |
| } | |
| </Button> | |
| )} | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="text-xs flex-shrink-0" | |
| onClick={() => openEditUser(user)} | |
| > | |
| تعديل | |
| </Button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| )} | |
| {tab === "analytics" && ( | |
| <div className="space-y-6"> | |
| {analyticsLoading ? ( | |
| <div className="space-y-4"> | |
| <Skeleton className="h-48 w-full" /> | |
| <Skeleton className="h-64 w-full" /> | |
| </div> | |
| ) : analytics ? ( | |
| <> | |
| <Card> | |
| <CardHeader><CardTitle>المهام هذا الأسبوع مقارنة بالأسبوع الماضي</CardTitle></CardHeader> | |
| <CardContent> | |
| <div className="flex items-end gap-2 h-40 mt-2"> | |
| {analytics.weekly.map(d => { | |
| const maxVal = Math.max(...analytics.weekly.flatMap(w => [w.thisWeek, w.lastWeek]), 1); | |
| const thisH = Math.round((d.thisWeek / maxVal) * 100); | |
| const lastH = Math.round((d.lastWeek / maxVal) * 100); | |
| return ( | |
| <div key={d.day} className="flex-1 flex flex-col items-center gap-1"> | |
| <div className="flex items-end gap-0.5 w-full justify-center h-32"> | |
| <div className="w-4 rounded-t bg-primary/30 transition-all" style={{ height: `${lastH}%` }} title={`الأسبوع الماضي: ${d.lastWeek}`} /> | |
| <div className="w-4 rounded-t bg-primary transition-all" style={{ height: `${thisH}%` }} title={`هذا الأسبوع: ${d.thisWeek}`} /> | |
| </div> | |
| <span className="text-xs text-slate-400">{d.day}</span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| <div className="flex items-center gap-4 mt-3 text-xs text-slate-500 justify-center"> | |
| <div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded bg-primary/30" /> الأسبوع الماضي</div> | |
| <div className="flex items-center gap-1.5"><div className="w-3 h-3 rounded bg-primary" /> هذا الأسبوع</div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <div className="grid grid-cols-2 gap-6"> | |
| <Card> | |
| <CardHeader><CardTitle>المهام حسب الفريق</CardTitle></CardHeader> | |
| <CardContent> | |
| <div className="space-y-3"> | |
| {analytics.byTeam.filter(t => t.totalTasks > 0).map(t => { | |
| const max = Math.max(...analytics.byTeam.map(x => x.totalTasks), 1); | |
| const pct = Math.round((t.totalTasks / max) * 100); | |
| return ( | |
| <div key={t.team} className="space-y-1"> | |
| <div className="flex items-center justify-between text-sm"> | |
| <span className="text-slate-600 truncate max-w-[160px]">{t.team}</span> | |
| <span className="font-semibold text-slate-800">{t.totalTasks}</span> | |
| </div> | |
| <div className="w-full bg-slate-100 rounded-full h-1.5"> | |
| <div className="h-1.5 rounded-full transition-all" style={{ width: `${pct}%`, backgroundColor: getTeamColor(t.team) }} /> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| <Card> | |
| <CardHeader><CardTitle>أكثر 5 عملاء نشاطاً</CardTitle></CardHeader> | |
| <CardContent> | |
| {analytics.topClients.length === 0 ? ( | |
| <p className="text-sm text-slate-400 text-center py-8">لا توجد مهام مرتبطة بعملاء بعد</p> | |
| ) : ( | |
| <div className="space-y-3"> | |
| {analytics.topClients.map((c, i) => ( | |
| <div key={c?.id} className="flex items-center gap-3"> | |
| <span className="text-slate-400 text-sm font-medium w-5 text-center">{i + 1}</span> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm font-medium truncate">{c?.name}</p> | |
| <p className="text-xs text-slate-400">{c?.completedTasks} / {c?.totalTasks} مكتملة</p> | |
| </div> | |
| <div className="w-16 bg-slate-100 rounded-full h-1.5"> | |
| <div className="h-1.5 rounded-full bg-primary" style={{ width: `${c?.totalTasks ? Math.round(((c?.completedTasks ?? 0) / c.totalTasks) * 100) : 0}%` }} /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </> | |
| ) : null} | |
| </div> | |
| )} | |
| {tab === "taskTypes" && ( | |
| <Card> | |
| <CardHeader> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle>أنواع المهام ({taskTypes?.length ?? 0})</CardTitle> | |
| <span className="text-xs text-slate-400">يمكن تفعيل/إيقاف كل نوع</span> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| {taskTypesLoading ? ( | |
| <div className="space-y-3"> | |
| {[1,2,3,4].map(i => <Skeleton key={i} className="h-14 w-full" />)} | |
| </div> | |
| ) : taskTypes && taskTypes.length > 0 ? ( | |
| <div className="space-y-2"> | |
| {taskTypes.map(tt => ( | |
| <div key={tt.id} className="flex items-center gap-4 p-3 rounded-lg border hover:bg-slate-50 transition-colors"> | |
| <div className="flex-1 min-w-0"> | |
| <p className="font-medium text-sm">{tt.name}</p> | |
| <div className="flex items-center gap-2 text-xs text-slate-500 mt-0.5"> | |
| <span className="px-2 py-0.5 rounded-full bg-blue-100 text-blue-700 font-medium">{tt.fromDeptSlug}</span> | |
| <span>←</span> | |
| <span className="px-2 py-0.5 rounded-full bg-purple-100 text-purple-700 font-medium">{tt.toDeptSlug}</span> | |
| </div> | |
| </div> | |
| <Badge | |
| variant="outline" | |
| className={`text-xs cursor-pointer select-none ${ | |
| tt.isActive | |
| ? "text-green-600 border-green-200 bg-green-50 hover:bg-green-100" | |
| : "text-slate-400 border-slate-200 bg-slate-50 hover:bg-slate-100" | |
| }`} | |
| onClick={() => handleToggleTaskType(tt.id)} | |
| > | |
| {tt.isActive ? "نشط" : "متوقف"} | |
| </Badge> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="text-center py-10 text-slate-400"> | |
| <Settings className="h-10 w-10 mx-auto mb-3 opacity-40" /> | |
| <p className="text-sm">لا توجد أنواع مهام محددة</p> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| )} | |
| {/* Add new user dialog */} | |
| <Dialog open={isAddUserOpen} onOpenChange={setIsAddUserOpen}> | |
| <DialogContent className="sm:max-w-[425px]"> | |
| <DialogHeader> | |
| <DialogTitle>إضافة موظف جديد</DialogTitle> | |
| </DialogHeader> | |
| <form onSubmit={handleCreateUser} className="space-y-4 mt-2"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="name">الاسم الكامل*</Label> | |
| <Input | |
| id="name" | |
| value={newUserName} | |
| onChange={(e) => setNewUserName(e.target.value)} | |
| placeholder="الاسم الكامل" | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="email">البريد الإلكتروني*</Label> | |
| <Input | |
| id="email" | |
| type="email" | |
| value={newUserEmail} | |
| onChange={(e) => setNewUserEmail(e.target.value)} | |
| placeholder="example@domain.com" | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="password">كلمة المرور*</Label> | |
| <div className="relative"> | |
| <Input | |
| id="password" | |
| type={showPassword ? "text" : "password"} | |
| value={newUserPassword} | |
| onChange={(e) => setNewUserPassword(e.target.value)} | |
| placeholder="كلمة المرور (8 أحرف على الأقل)" | |
| required | |
| /> | |
| <button | |
| type="button" | |
| onClick={() => setShowPassword(!showPassword)} | |
| className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 hover:text-slate-800 focus:outline-none" | |
| > | |
| {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="team">القسم*</Label> | |
| <Select value={newUserTeam} onValueChange={setNewUserTeam}> | |
| <SelectTrigger id="team"> | |
| <SelectValue placeholder="اختر القسم" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {TEAMS.map(t => <SelectItem key={t} value={t}>{t}</SelectItem>)} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="role">الدور</Label> | |
| <Select value={newUserRole} onValueChange={setNewUserRole}> | |
| <SelectTrigger id="role"> | |
| <SelectValue placeholder="اختر الدور" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| {CANONICAL_ROLES.map(r => ( | |
| <SelectItem key={r.value} value={r.value}>{r.label}</SelectItem> | |
| ))} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <Button type="submit" className="w-full mt-4" disabled={isFormInvalid || isSubmittingUser}> | |
| {isSubmittingUser ? "جاري الإضافة..." : "إضافة موظف"} | |
| </Button> | |
| </form> | |
| </DialogContent> | |
| </Dialog> | |
| {/* Edit user dialog */} | |
| <Dialog open={!!editUser} onOpenChange={(o) => { if (!o) setEditUser(null); }}> | |
| <DialogContent> | |
| <DialogHeader> | |
| <DialogTitle>تعديل بيانات: {editUser?.name}</DialogTitle> | |
| </DialogHeader> | |
| <div className="space-y-4 mt-2"> | |
| <div className="space-y-2"> | |
| <Label>الفريق</Label> | |
| <Select value={editTeam} onValueChange={setEditTeam}> | |
| <SelectTrigger><SelectValue /></SelectTrigger> | |
| <SelectContent> | |
| {TEAMS.map(t => <SelectItem key={t} value={t}>{t}</SelectItem>)} | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label>الدور</Label> | |
| <Select value={editRole} onValueChange={setEditRole}> | |
| <SelectTrigger><SelectValue /></SelectTrigger> | |
| <SelectContent> | |
| {CANONICAL_ROLES | |
| // Only admin can assign admin role | |
| .filter(r => activeUser?.role === "admin" || r.value !== "admin") | |
| .map(r => ( | |
| <SelectItem key={r.value} value={r.value}>{r.label}</SelectItem> | |
| )) | |
| } | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| <Button onClick={handleSaveUser} className="w-full" disabled={updateUser.isPending}> | |
| {updateUser.isPending ? "جاري الحفظ..." : "حفظ التعديلات"} | |
| </Button> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| </div> | |
| ); | |
| } | |