feat(roles): add roles list page
- Complete table view with role information - CRUD operations (view, edit, delete, permissions) - Delete confirmation modal - Responsive design with proper Persian RTL - Loading states and error handling
This commit is contained in:
parent
efb92ac136
commit
0ee448d9be
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useRoles, useDeleteRole } from "../core/_hooks";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||||
|
import { Role } from "@/types/auth";
|
||||||
|
import { Trash2, Edit, Plus, Eye, Users } 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">
|
||||||
|
<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">
|
||||||
|
{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>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-300 max-w-xs truncate">
|
||||||
|
{role.description}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
|
||||||
|
{role.permissions?.length || 0} دسترسی
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{new Date(role.created_at).toLocaleDateString('fa-IR')}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleView(role.id)}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Eye className="h-3 w-3" />
|
||||||
|
مشاهده
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => handlePermissions(role.id)}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Users className="h-3 w-3" />
|
||||||
|
دسترسیها
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleEdit(role.id)}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
ویرایش
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => handleDelete(role.id)}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
حذف
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={!!deleteRoleId}
|
||||||
|
onClose={cancelDelete}
|
||||||
|
title="تأیید حذف"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
آیا از حذف این نقش اطمینان دارید؟ این عمل قابل بازگشت نیست.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button variant="secondary" onClick={cancelDelete}>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={confirmDelete}
|
||||||
|
loading={isDeleting}
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RolesListPage;
|
||||||
Loading…
Reference in New Issue