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 { 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}

View File

@ -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}

View File

@ -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}
>
حذف