admin-users: add skeleton loading component and improve error handling

This commit is contained in:
hosseintaromi 2025-07-22 09:02:00 +03:30
parent 5862bd97a1
commit 2e9fa5460e
3 changed files with 687 additions and 346 deletions

View File

@ -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">
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری کاربران ادمین</p> <div className="text-center py-12">
<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,118 +185,155 @@ 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="overflow-x-auto"> ) : (
<table className="w-full"> <div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<thead className="bg-gray-50 dark:bg-gray-700"> {/* Desktop Table */}
<tr> <div className="hidden md:block">
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> <div className="overflow-x-auto">
کاربر <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
</th> <thead className="bg-gray-50 dark:bg-gray-700">
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> <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> وضعیت
<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> </th>
</thead>
<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">
{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}
</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 ${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>
</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">
{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"
onClick={() => handleView(user.id)}
className="ml-2"
>
<Eye className="h-4 w-4 ml-1" />
مشاهده
</Button>
<Button
variant="primary"
size="sm"
onClick={() => handleEdit(user.id)}
className="ml-2"
>
<Edit3 className="h-4 w-4 ml-1" />
ویرایش
</Button>
<Button
variant="danger"
size="sm"
onClick={() => setDeleteUserId(user.id.toString())}
className="ml-2"
>
<Trash2 className="h-4 w-4 ml-1" />
حذف
</Button>
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
</table> {(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 text-sm text-gray-900 dark:text-gray-100">
{user.first_name} {user.last_name}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{user.username}
</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 ${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>
</td>
<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">
<div className="flex items-center gap-2">
<button
onClick={() => handleView(user.id)}
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
title="مشاهده"
>
<Eye className="h-4 w-4" />
</button>
<button
onClick={() => handleEdit(user.id)}
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
title="ویرایش"
>
<Edit3 className="h-4 w-4" />
</button>
<button
onClick={() => setDeleteUserId(user.id.toString())}
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
title="حذف"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</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

View File

@ -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">
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری دسترسیها</p> <div className="text-center py-12">
<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,108 +158,124 @@ 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="overflow-x-auto"> ) : (
<table className="w-full"> <div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<thead className="bg-gray-50 dark:bg-gray-700"> {/* Desktop Table */}
<tr> <div className="hidden md:block">
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> <div className="overflow-x-auto">
عنوان <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
</th> <thead className="bg-gray-50 dark:bg-gray-700">
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> <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> تاریخ ایجاد
<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> </th>
</thead>
<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">
{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">
{permission.description}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{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"
onClick={() => handleEdit(permission.id)}
className="ml-2"
>
<Edit3 className="h-4 w-4 ml-1" />
ویرایش
</Button>
<Button
variant="danger"
size="sm"
onClick={() => setDeletePermissionId(permission.id.toString())}
className="ml-2"
>
<Trash2 className="h-4 w-4 ml-1" />
حذف
</Button>
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
</table> {(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 text-sm text-gray-900 dark:text-gray-100">
{permission.title}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{permission.description}
</td>
<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 font-medium">
<div className="flex items-center gap-2">
<button
onClick={() => handleEdit(permission.id)}
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
title="ویرایش"
>
<Edit3 className="h-4 w-4" />
</button>
<button
onClick={() => setDeletePermissionId(permission.id.toString())}
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
title="حذف"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</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

View File

@ -1,36 +1,108 @@
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";
// 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">
<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 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 RolesListPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [deleteRoleId, setDeleteRoleId] = useState<string | null>(null); const [deleteRoleId, setDeleteRoleId] = useState<string | null>(null);
const [filters, setFilters] = useState({
search: ''
});
const { data: roles, isLoading, error } = useRoles(); const { data: roles, isLoading, error } = useRoles(filters);
const { mutate: deleteRole, isPending: isDeleting } = useDeleteRole(); const { mutate: deleteRole, isPending: isDeleting } = useDeleteRole();
const handleEdit = (roleId: number) => { const handleCreate = () => {
navigate(`/roles/${roleId}/edit`); navigate('/roles/create');
}; };
const handleView = (roleId: number) => { const handleView = (roleId: number) => {
navigate(`/roles/${roleId}`); navigate(`/roles/${roleId}`);
}; };
const handleEdit = (roleId: number) => {
navigate(`/roles/${roleId}/edit`);
};
const handlePermissions = (roleId: number) => { const handlePermissions = (roleId: number) => {
navigate(`/roles/${roleId}/permissions`); navigate(`/roles/${roleId}/permissions`);
}; };
const handleDelete = (roleId: number) => { const handleDeleteConfirm = () => {
setDeleteRoleId(roleId.toString());
};
const confirmDelete = () => {
if (deleteRoleId) { if (deleteRoleId) {
deleteRole(deleteRoleId, { deleteRole(deleteRoleId, {
onSuccess: () => { onSuccess: () => {
@ -40,123 +112,213 @@ const RolesListPage = () => {
} }
}; };
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilters(prev => ({ ...prev, search: e.target.value }));
};
const handleDelete = (roleId: number) => {
setDeleteRoleId(roleId.toString());
};
const cancelDelete = () => { const cancelDelete = () => {
setDeleteRoleId(null); setDeleteRoleId(null);
}; };
if (isLoading) return <LoadingSpinner />; if (error) {
if (error) return <div className="text-red-600">خطا در بارگذاری نقشها</div>; return (
<div className="p-6">
<div className="text-center py-12">
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری نقشها</p>
</div>
</div>
);
}
return ( return (
<div className="p-6"> <div className="p-6 space-y-6">
<div className="mb-6"> {/* Header */}
<div className="flex justify-between items-center mb-4"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100"> <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> </h1>
<Button <p className="text-gray-600 dark:text-gray-400 mt-1">
variant="primary" مدیریت نقشها و دسترسیهای سیستم
onClick={() => navigate('/roles/create')} </p>
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
افزودن نقش جدید
</Button>
</div> </div>
<Button onClick={handleCreate} 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"> {/* Filters */}
<div className="overflow-x-auto"> <div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<thead className="bg-gray-50 dark:bg-gray-700"> <div>
<tr> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> جستجو
نام نقش </label>
</th> <input
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> type="text"
توضیحات placeholder="جستجو در نام یا توضیحات نقش..."
</th> value={filters.search}
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> 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"
</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">
{(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">
{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">
{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"
onClick={() => handleView(role.id)}
className="ml-2"
>
<Eye className="h-4 w-4 ml-1" />
مشاهده
</Button>
<Button
variant="primary"
size="sm"
onClick={() => handleEdit(role.id)}
className="ml-2"
>
<Edit3 className="h-4 w-4 ml-1" />
ویرایش
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => handlePermissions(role.id)}
className="ml-2"
>
<Users className="h-4 w-4 ml-1" />
دسترسیها
</Button>
<Button
variant="danger"
size="sm"
onClick={() => setDeleteRoleId(role.id.toString())}
className="ml-2"
>
<Trash2 className="h-4 w-4 ml-1" />
حذف
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{roles?.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400">هیچ نقشی یافت نشد</p>
</div> </div>
)} </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">
تاریخ ایجاد
</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">
{(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 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}
</td>
<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">
<div className="flex items-center gap-2">
<button
onClick={() => handleView(role.id)}
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
title="مشاهده"
>
<Eye className="h-4 w-4" />
</button>
<button
onClick={() => handleEdit(role.id)}
className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
title="ویرایش"
>
<Edit3 className="h-4 w-4" />
</button>
<button
onClick={() => handlePermissions(role.id)}
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
title="مدیریت دسترسی‌ها"
>
<Settings className="h-4 w-4" />
</button>
<button
onClick={() => setDeleteRoleId(role.id.toString())}
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
title="حذف"
>
<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">
{(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>
)}
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
<Modal <Modal
isOpen={!!deleteRoleId} isOpen={!!deleteRoleId}
@ -174,7 +336,7 @@ const RolesListPage = () => {
</Button> </Button>
<Button <Button
variant="danger" variant="danger"
onClick={confirmDelete} onClick={handleDeleteConfirm}
loading={isDeleting} loading={isDeleting}
> >
حذف حذف