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 { 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 navigate = useNavigate();
|
||||
const [deleteUserId, setDeleteUserId] = useState<string | null>(null);
|
||||
|
|
@ -48,55 +123,47 @@ const AdminUsersListPage = () => {
|
|||
setFilters(prev => ({ ...prev, status: e.target.value }));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 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>
|
||||
<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>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
مدیریت کاربران ادمین سیستم
|
||||
مدیریت کاربران دسترسی به پنل ادمین
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Button onClick={handleCreate} className="flex items-center gap-2">
|
||||
<UserPlus className="h-4 w-4" />
|
||||
کاربر جدید
|
||||
کاربر ادمین جدید
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<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}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -118,28 +185,37 @@ const AdminUsersListPage = () => {
|
|||
</div>
|
||||
|
||||
{/* Users Table */}
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
{(users || []).length === 0 ? (
|
||||
{isLoading ? (
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
شما هنوز هیچ کاربر ادمین ایجاد نکردهاید
|
||||
{filters.search || filters.status
|
||||
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||
: "شما هنوز هیچ کاربر ادمین ایجاد نکردهاید"
|
||||
}
|
||||
</p>
|
||||
<Button onClick={handleCreate}>
|
||||
<UserPlus 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="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">
|
||||
<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">
|
||||
نام کاربری
|
||||
|
|
@ -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>
|
||||
<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>
|
||||
|
|
@ -161,19 +234,8 @@ const AdminUsersListPage = () => {
|
|||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{(users || []).map((user: AdminUserInfo) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<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">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{user.first_name} {user.last_name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{user.username}
|
||||
|
|
@ -186,51 +248,93 @@ const AdminUsersListPage = () => {
|
|||
{user.status === 'active' ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<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">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{new Date(user.created_at).toLocaleDateString('fa-IR')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2 space-x-reverse">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
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" />
|
||||
مشاهده
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
ویرایش
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
حذف
|
||||
</Button>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</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 */}
|
||||
<Modal
|
||||
isOpen={!!deleteUserId}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,76 @@ import { usePermissions, useDeletePermission } from '../core/_hooks';
|
|||
import { Permission } from '../core/_models';
|
||||
import { Button } from "@/components/ui/Button";
|
||||
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";
|
||||
|
||||
// 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 navigate = useNavigate();
|
||||
const [deletePermissionId, setDeletePermissionId] = useState<string | null>(null);
|
||||
|
|
@ -43,55 +110,47 @@ const PermissionsListPage = () => {
|
|||
setFilters(prev => ({ ...prev, search: e.target.value }));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 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>
|
||||
<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>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
مدیریت دسترسیهای سیستم
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<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 p-4 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<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}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -99,24 +158,33 @@ const PermissionsListPage = () => {
|
|||
</div>
|
||||
|
||||
{/* Permissions Table */}
|
||||
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
{(permissions || []).length === 0 ? (
|
||||
{isLoading ? (
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
شما هنوز هیچ دسترسی ایجاد نکردهاید
|
||||
{filters.search
|
||||
? "نتیجهای برای جستجوی شما یافت نشد"
|
||||
: "شما هنوز هیچ دسترسی ایجاد نکردهاید"
|
||||
}
|
||||
</p>
|
||||
<Button onClick={handleCreate}>
|
||||
<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="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">
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
|
|
@ -139,69 +204,79 @@ const PermissionsListPage = () => {
|
|||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{(permissions || []).map((permission: Permission) => (
|
||||
<tr key={permission.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<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">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{permission.title}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
ID: {permission.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 max-w-xs truncate">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{permission.description}
|
||||
</div>
|
||||
</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')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{new Date(permission.updated_at).toLocaleDateString('fa-IR')}
|
||||
</td>
|
||||
<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"
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
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" />
|
||||
ویرایش
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
حذف
|
||||
</Button>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</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 */}
|
||||
<Modal
|
||||
isOpen={!!deletePermissionId}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,17 @@
|
|||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useRoles, useDeleteRole } from "../core/_hooks";
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRoles, useDeleteRole } from '../core/_hooks';
|
||||
import { Role } from '@/types/auth';
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||
import { Role } from "@/types/auth";
|
||||
import { Trash2, Edit, Plus, Eye, Users, Edit3, Shield } from "lucide-react";
|
||||
import { Trash2, Edit3, Plus, UserCog, Shield, Eye, Settings } from "lucide-react";
|
||||
import { Modal } from "@/components/ui/Modal";
|
||||
|
||||
const RolesListPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const [deleteRoleId, setDeleteRoleId] = useState<string | null>(null);
|
||||
|
||||
const { data: roles, isLoading, error } = useRoles();
|
||||
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">
|
||||
// Skeleton Loading Component
|
||||
const RolesTableSkeleton = () => (
|
||||
<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">
|
||||
|
|
@ -77,7 +23,189 @@ const RolesListPage = () => {
|
|||
توضیحات
|
||||
</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 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 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">
|
||||
{(roles || []).map((role: Role) => (
|
||||
<tr key={role.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{role.title}
|
||||
</div>
|
||||
<div className="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">
|
||||
{role.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{role.description}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<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">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{new Date(role.created_at).toLocaleDateString('fa-IR')}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2 space-x-reverse">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
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" />
|
||||
مشاهده
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
ویرایش
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
<Edit3 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
دسترسیها
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
<Settings className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
حذف
|
||||
</Button>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{roles?.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 dark:text-gray-400">هیچ نقشی یافت نشد</p>
|
||||
{/* Mobile Cards */}
|
||||
<div className="md:hidden p-4 space-y-4">
|
||||
{(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>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal
|
||||
|
|
@ -174,7 +336,7 @@ const RolesListPage = () => {
|
|||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={confirmDelete}
|
||||
onClick={handleDeleteConfirm}
|
||||
loading={isDeleting}
|
||||
>
|
||||
حذف
|
||||
|
|
|
|||
Loading…
Reference in New Issue