admin-users: add skeleton loading component and improve error handling
This commit is contained in:
parent
5862bd97a1
commit
2e9fa5460e
|
|
@ -7,6 +7,81 @@ import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||||
import { Trash2, Edit3, Plus, Eye, Users, UserPlus } from "lucide-react";
|
import { Trash2, Edit3, Plus, Eye, Users, UserPlus } from "lucide-react";
|
||||||
import { Modal } from "@/components/ui/Modal";
|
import { Modal } from "@/components/ui/Modal";
|
||||||
|
|
||||||
|
// Skeleton Loading Component
|
||||||
|
const AdminUserTableSkeleton = () => (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
{/* Desktop Table Skeleton */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
نام و نام خانوادگی
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
نام کاربری
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
وضعیت
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
تاریخ ایجاد
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
عملیات
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{[...Array(5)].map((_, index) => (
|
||||||
|
<tr key={index} className="animate-pulse">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-32"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-24"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded-full w-16"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-20"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Cards Skeleton */}
|
||||||
|
<div className="md:hidden p-4 space-y-4">
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<div key={index} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 animate-pulse">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-5 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-1/2"></div>
|
||||||
|
<div className="h-6 bg-gray-300 dark:bg-gray-600 rounded-full w-16"></div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const AdminUsersListPage = () => {
|
const AdminUsersListPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [deleteUserId, setDeleteUserId] = useState<string | null>(null);
|
const [deleteUserId, setDeleteUserId] = useState<string | null>(null);
|
||||||
|
|
@ -48,55 +123,47 @@ const AdminUsersListPage = () => {
|
||||||
setFilters(prev => ({ ...prev, status: e.target.value }));
|
setFilters(prev => ({ ...prev, status: e.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center h-64">
|
|
||||||
<LoadingSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="p-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری کاربران ادمین</p>
|
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری کاربران ادمین</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||||
|
<Users className="h-6 w-6" />
|
||||||
مدیریت کاربران ادمین
|
مدیریت کاربران ادمین
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
مدیریت کاربران ادمین سیستم
|
مدیریت کاربران دسترسی به پنل ادمین
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={handleCreate} className="flex items-center gap-2">
|
||||||
onClick={handleCreate}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<UserPlus className="h-4 w-4" />
|
<UserPlus className="h-4 w-4" />
|
||||||
کاربر جدید
|
کاربر ادمین جدید
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
جستجو
|
جستجو
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="جستجو در نام، نام خانوادگی یا نام کاربری..."
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
placeholder="نام، نام کاربری یا نام خانوادگی..."
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -118,28 +185,37 @@ const AdminUsersListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Users Table */}
|
{/* Users Table */}
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
{isLoading ? (
|
||||||
{(users || []).length === 0 ? (
|
<AdminUserTableSkeleton />
|
||||||
|
) : (users || []).length === 0 ? (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<Users className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
<Users className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||||
هیچ کاربر ادمین یافت نشد
|
هیچ کاربر ادمین یافت نشد
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
شما هنوز هیچ کاربر ادمین ایجاد نکردهاید
|
{filters.search || filters.status
|
||||||
|
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||||
|
: "شما هنوز هیچ کاربر ادمین ایجاد نکردهاید"
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={handleCreate}>
|
<Button onClick={handleCreate}>
|
||||||
<UserPlus className="h-4 w-4 ml-2" />
|
<UserPlus className="h-4 w-4 ml-2" />
|
||||||
اولین کاربر ادمین را ایجاد کنید
|
اولین کاربر ادمین را ایجاد کنید
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<div className="hidden md:block">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
کاربر
|
نام و نام خانوادگی
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
نام کاربری
|
نام کاربری
|
||||||
|
|
@ -147,9 +223,6 @@ const AdminUsersListPage = () => {
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
وضعیت
|
وضعیت
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
نقشها
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
تاریخ ایجاد
|
تاریخ ایجاد
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -161,19 +234,8 @@ const AdminUsersListPage = () => {
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{(users || []).map((user: AdminUserInfo) => (
|
{(users || []).map((user: AdminUserInfo) => (
|
||||||
<tr key={user.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
<tr key={user.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="h-10 w-10 rounded-full bg-primary-100 dark:bg-primary-800 flex items-center justify-center">
|
|
||||||
<span className="text-sm font-medium text-primary-600 dark:text-primary-300">
|
|
||||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mr-4">
|
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{user.first_name} {user.last_name}
|
{user.first_name} {user.last_name}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
{user.username}
|
{user.username}
|
||||||
|
|
@ -186,51 +248,93 @@ const AdminUsersListPage = () => {
|
||||||
{user.status === 'active' ? 'فعال' : 'غیرفعال'}
|
{user.status === 'active' ? 'فعال' : 'غیرفعال'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100">
|
|
||||||
{user.roles?.length || 0} نقش
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{new Date(user.created_at).toLocaleDateString('fa-IR')}
|
{new Date(user.created_at).toLocaleDateString('fa-IR')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2 space-x-reverse">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="secondary"
|
<button
|
||||||
size="sm"
|
|
||||||
onClick={() => handleView(user.id)}
|
onClick={() => handleView(user.id)}
|
||||||
className="ml-2"
|
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
title="مشاهده"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4 ml-1" />
|
<Eye className="h-4 w-4" />
|
||||||
مشاهده
|
</button>
|
||||||
</Button>
|
<button
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleEdit(user.id)}
|
onClick={() => handleEdit(user.id)}
|
||||||
className="ml-2"
|
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
|
title="ویرایش"
|
||||||
>
|
>
|
||||||
<Edit3 className="h-4 w-4 ml-1" />
|
<Edit3 className="h-4 w-4" />
|
||||||
ویرایش
|
</button>
|
||||||
</Button>
|
<button
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setDeleteUserId(user.id.toString())}
|
onClick={() => setDeleteUserId(user.id.toString())}
|
||||||
className="ml-2"
|
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
title="حذف"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 ml-1" />
|
<Trash2 className="h-4 w-4" />
|
||||||
حذف
|
</button>
|
||||||
</Button>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<div className="md:hidden p-4 space-y-4">
|
||||||
|
{(users || []).map((user: AdminUserInfo) => (
|
||||||
|
<div key={user.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{user.username}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${user.status === 'active'
|
||||||
|
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||||
|
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'
|
||||||
|
}`}>
|
||||||
|
{user.status === 'active' ? 'فعال' : 'غیرفعال'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
|
تاریخ ایجاد: {new Date(user.created_at).toLocaleDateString('fa-IR')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleView(user.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" />
|
||||||
|
مشاهده
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(user.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteUserId(user.id.toString())}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!!deleteUserId}
|
isOpen={!!deleteUserId}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,76 @@ import { usePermissions, useDeletePermission } from '../core/_hooks';
|
||||||
import { Permission } from '../core/_models';
|
import { Permission } from '../core/_models';
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||||
import { Trash2, Edit3, Plus, Eye, Shield } from "lucide-react";
|
import { Trash2, Edit3, Plus, Shield, Eye } from "lucide-react";
|
||||||
import { Modal } from "@/components/ui/Modal";
|
import { Modal } from "@/components/ui/Modal";
|
||||||
|
|
||||||
|
// Skeleton Loading Component
|
||||||
|
const PermissionsTableSkeleton = () => (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
{/* Desktop Table Skeleton */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
عنوان
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
توضیحات
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
تاریخ ایجاد
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
عملیات
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{[...Array(5)].map((_, index) => (
|
||||||
|
<tr key={index} className="animate-pulse">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-32"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-48"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-20"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Cards Skeleton */}
|
||||||
|
<div className="md:hidden p-4 space-y-4">
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<div key={index} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 animate-pulse">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-5 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-full"></div>
|
||||||
|
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-1/3"></div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const PermissionsListPage = () => {
|
const PermissionsListPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [deletePermissionId, setDeletePermissionId] = useState<string | null>(null);
|
const [deletePermissionId, setDeletePermissionId] = useState<string | null>(null);
|
||||||
|
|
@ -43,55 +110,47 @@ const PermissionsListPage = () => {
|
||||||
setFilters(prev => ({ ...prev, search: e.target.value }));
|
setFilters(prev => ({ ...prev, search: e.target.value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center items-center h-64">
|
|
||||||
<LoadingSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-8">
|
<div className="p-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری دسترسیها</p>
|
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری دسترسیها</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||||
|
<Shield className="h-6 w-6" />
|
||||||
مدیریت دسترسیها
|
مدیریت دسترسیها
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
مدیریت دسترسیهای سیستم
|
مدیریت دسترسیهای سیستم
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button onClick={handleCreate} className="flex items-center gap-2">
|
||||||
onClick={handleCreate}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
دسترسی جدید
|
دسترسی جدید
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
جستجو
|
جستجو
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="جستجو در عنوان یا توضیحات..."
|
||||||
value={filters.search}
|
value={filters.search}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
placeholder="عنوان یا توضیحات دسترسی..."
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -99,24 +158,33 @@ const PermissionsListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Permissions Table */}
|
{/* Permissions Table */}
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
{isLoading ? (
|
||||||
{(permissions || []).length === 0 ? (
|
<PermissionsTableSkeleton />
|
||||||
|
) : (permissions || []).length === 0 ? (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<Shield className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
<Shield className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||||
هیچ دسترسی یافت نشد
|
هیچ دسترسی یافت نشد
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
شما هنوز هیچ دسترسی ایجاد نکردهاید
|
{filters.search
|
||||||
|
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||||
|
: "شما هنوز هیچ دسترسی ایجاد نکردهاید"
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={handleCreate}>
|
<Button onClick={handleCreate}>
|
||||||
<Plus className="h-4 w-4 ml-2" />
|
<Plus className="h-4 w-4 ml-2" />
|
||||||
اولین دسترسی را ایجاد کنید
|
اولین دسترسی را ایجاد کنید
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<div className="hidden md:block">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
|
@ -128,9 +196,6 @@ const PermissionsListPage = () => {
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
تاریخ ایجاد
|
تاریخ ایجاد
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
|
||||||
تاریخ بهروزرسانی
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
عملیات
|
عملیات
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -139,69 +204,79 @@ const PermissionsListPage = () => {
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{(permissions || []).map((permission: Permission) => (
|
{(permissions || []).map((permission: Permission) => (
|
||||||
<tr key={permission.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
<tr key={permission.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div className="flex items-center">
|
|
||||||
<div className="h-10 w-10 rounded-full bg-primary-100 dark:bg-primary-800 flex items-center justify-center">
|
|
||||||
<Shield className="h-5 w-5 text-primary-600 dark:text-primary-300" />
|
|
||||||
</div>
|
|
||||||
<div className="mr-4">
|
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{permission.title}
|
{permission.title}
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
ID: {permission.id}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-300 max-w-xs truncate">
|
|
||||||
{permission.description}
|
{permission.description}
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
{new Date(permission.created_at).toLocaleDateString('fa-IR')}
|
{new Date(permission.created_at).toLocaleDateString('fa-IR')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
{new Date(permission.updated_at).toLocaleDateString('fa-IR')}
|
<div className="flex items-center gap-2">
|
||||||
</td>
|
<button
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2 space-x-reverse">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleView(permission.id)}
|
|
||||||
className="ml-2"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4 ml-1" />
|
|
||||||
مشاهده
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleEdit(permission.id)}
|
onClick={() => handleEdit(permission.id)}
|
||||||
className="ml-2"
|
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
|
title="ویرایش"
|
||||||
>
|
>
|
||||||
<Edit3 className="h-4 w-4 ml-1" />
|
<Edit3 className="h-4 w-4" />
|
||||||
ویرایش
|
</button>
|
||||||
</Button>
|
<button
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setDeletePermissionId(permission.id.toString())}
|
onClick={() => setDeletePermissionId(permission.id.toString())}
|
||||||
className="ml-2"
|
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
title="حذف"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 ml-1" />
|
<Trash2 className="h-4 w-4" />
|
||||||
حذف
|
</button>
|
||||||
</Button>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Cards */}
|
||||||
|
<div className="md:hidden p-4 space-y-4">
|
||||||
|
{(permissions || []).map((permission: Permission) => (
|
||||||
|
<div key={permission.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{permission.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{permission.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
|
تاریخ ایجاد: {new Date(permission.created_at).toLocaleDateString('fa-IR')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(permission.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeletePermissionId(permission.id.toString())}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!!deletePermissionId}
|
isOpen={!!deletePermissionId}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,17 @@
|
||||||
import { useState } from "react";
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useRoles, useDeleteRole } from "../core/_hooks";
|
import { useRoles, useDeleteRole } from '../core/_hooks';
|
||||||
|
import { Role } from '@/types/auth';
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||||
import { Role } from "@/types/auth";
|
import { Trash2, Edit3, Plus, UserCog, Shield, Eye, Settings } from "lucide-react";
|
||||||
import { Trash2, Edit, Plus, Eye, Users, Edit3, Shield } from "lucide-react";
|
|
||||||
import { Modal } from "@/components/ui/Modal";
|
import { Modal } from "@/components/ui/Modal";
|
||||||
|
|
||||||
const RolesListPage = () => {
|
// Skeleton Loading Component
|
||||||
const navigate = useNavigate();
|
const RolesTableSkeleton = () => (
|
||||||
const [deleteRoleId, setDeleteRoleId] = useState<string | null>(null);
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
{/* Desktop Table Skeleton */}
|
||||||
const { data: roles, isLoading, error } = useRoles();
|
<div className="hidden md:block">
|
||||||
const { mutate: deleteRole, isPending: isDeleting } = useDeleteRole();
|
|
||||||
|
|
||||||
const handleEdit = (roleId: number) => {
|
|
||||||
navigate(`/roles/${roleId}/edit`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleView = (roleId: number) => {
|
|
||||||
navigate(`/roles/${roleId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePermissions = (roleId: number) => {
|
|
||||||
navigate(`/roles/${roleId}/permissions`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (roleId: number) => {
|
|
||||||
setDeleteRoleId(roleId.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (deleteRoleId) {
|
|
||||||
deleteRole(deleteRoleId, {
|
|
||||||
onSuccess: () => {
|
|
||||||
setDeleteRoleId(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelDelete = () => {
|
|
||||||
setDeleteRoleId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) return <LoadingSpinner />;
|
|
||||||
if (error) return <div className="text-red-600">خطا در بارگذاری نقشها</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
مدیریت نقشها
|
|
||||||
</h1>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => navigate('/roles/create')}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
افزودن نقش جدید
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden">
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
|
|
@ -77,7 +23,189 @@ const RolesListPage = () => {
|
||||||
توضیحات
|
توضیحات
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
تعداد دسترسیها
|
تاریخ ایجاد
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
عملیات
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{[...Array(5)].map((_, index) => (
|
||||||
|
<tr key={index} className="animate-pulse">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-32"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-48"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-20"></div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Cards Skeleton */}
|
||||||
|
<div className="md:hidden p-4 space-y-4">
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<div key={index} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 animate-pulse">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-5 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-full"></div>
|
||||||
|
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-1/3"></div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
<div className="h-8 w-8 bg-gray-300 dark:bg-gray-600 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RolesListPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [deleteRoleId, setDeleteRoleId] = useState<string | null>(null);
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
search: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: roles, isLoading, error } = useRoles(filters);
|
||||||
|
const { mutate: deleteRole, isPending: isDeleting } = useDeleteRole();
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
navigate('/roles/create');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (roleId: number) => {
|
||||||
|
navigate(`/roles/${roleId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (roleId: number) => {
|
||||||
|
navigate(`/roles/${roleId}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePermissions = (roleId: number) => {
|
||||||
|
navigate(`/roles/${roleId}/permissions`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = () => {
|
||||||
|
if (deleteRoleId) {
|
||||||
|
deleteRole(deleteRoleId, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setDeleteRoleId(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilters(prev => ({ ...prev, search: e.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (roleId: number) => {
|
||||||
|
setDeleteRoleId(roleId.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelDelete = () => {
|
||||||
|
setDeleteRoleId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری نقشها</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||||
|
<UserCog className="h-6 w-6" />
|
||||||
|
مدیریت نقشها
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
مدیریت نقشها و دسترسیهای سیستم
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleCreate} className="flex items-center gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
نقش جدید
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
جستجو
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="جستجو در نام یا توضیحات نقش..."
|
||||||
|
value={filters.search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Roles Table */}
|
||||||
|
{isLoading ? (
|
||||||
|
<RolesTableSkeleton />
|
||||||
|
) : (roles || []).length === 0 ? (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<UserCog className="h-12 w-12 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
هیچ نقش یافت نشد
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{filters.search
|
||||||
|
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||||
|
: "شما هنوز هیچ نقش ایجاد نکردهاید"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<Button onClick={handleCreate} className="flex items-center gap-2">
|
||||||
|
<Plus className="h-4 w-4 ml-2" />
|
||||||
|
اولین نقش را ایجاد کنید
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||||
|
{/* Desktop Table */}
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
نام نقش
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
|
توضیحات
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
||||||
تاریخ ایجاد
|
تاریخ ایجاد
|
||||||
|
|
@ -90,72 +218,106 @@ const RolesListPage = () => {
|
||||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{(roles || []).map((role: Role) => (
|
{(roles || []).map((role: Role) => (
|
||||||
<tr key={role.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
<tr key={role.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
{role.name}
|
||||||
{role.title}
|
</td>
|
||||||
</div>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{role.description}
|
{role.description}
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">
|
|
||||||
{role.permissions?.length || 0} دسترسی
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{new Date(role.created_at).toLocaleDateString('fa-IR')}
|
{new Date(role.created_at).toLocaleDateString('fa-IR')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2 space-x-reverse">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="secondary"
|
<button
|
||||||
size="sm"
|
|
||||||
onClick={() => handleView(role.id)}
|
onClick={() => handleView(role.id)}
|
||||||
className="ml-2"
|
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
title="مشاهده"
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4 ml-1" />
|
<Eye className="h-4 w-4" />
|
||||||
مشاهده
|
</button>
|
||||||
</Button>
|
<button
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleEdit(role.id)}
|
onClick={() => handleEdit(role.id)}
|
||||||
className="ml-2"
|
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
|
title="ویرایش"
|
||||||
>
|
>
|
||||||
<Edit3 className="h-4 w-4 ml-1" />
|
<Edit3 className="h-4 w-4" />
|
||||||
ویرایش
|
</button>
|
||||||
</Button>
|
<button
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handlePermissions(role.id)}
|
onClick={() => handlePermissions(role.id)}
|
||||||
className="ml-2"
|
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
title="مدیریت دسترسیها"
|
||||||
>
|
>
|
||||||
<Users className="h-4 w-4 ml-1" />
|
<Settings className="h-4 w-4" />
|
||||||
دسترسیها
|
</button>
|
||||||
</Button>
|
<button
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setDeleteRoleId(role.id.toString())}
|
onClick={() => setDeleteRoleId(role.id.toString())}
|
||||||
className="ml-2"
|
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
title="حذف"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 ml-1" />
|
<Trash2 className="h-4 w-4" />
|
||||||
حذف
|
</button>
|
||||||
</Button>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{roles?.length === 0 && (
|
{/* Mobile Cards */}
|
||||||
<div className="text-center py-12">
|
<div className="md:hidden p-4 space-y-4">
|
||||||
<p className="text-gray-500 dark:text-gray-400">هیچ نقشی یافت نشد</p>
|
{(roles || []).map((role: Role) => (
|
||||||
|
<div key={role.id} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{role.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{role.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
|
تاریخ ایجاد: {new Date(role.created_at).toLocaleDateString('fa-IR')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleView(role.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" />
|
||||||
|
مشاهده
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleEdit(role.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
ویرایش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePermissions(role.id)}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
>
|
||||||
|
<Settings className="h-3 w-3" />
|
||||||
|
دسترسیها
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteRoleId(role.id.toString())}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 text-xs text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -174,7 +336,7 @@ const RolesListPage = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={confirmDelete}
|
onClick={handleDeleteConfirm}
|
||||||
loading={isDeleting}
|
loading={isDeleting}
|
||||||
>
|
>
|
||||||
حذف
|
حذف
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue