feat(admin-users): enhance admin user management with permissions and roles

- Add permissions and roles multi-select to admin user form
- Update admin user models to include permissions and roles arrays
- Make permissions list page read-only by removing CRUD actions
- Integrate MultiSelectAutocomplete for better UX
This commit is contained in:
hossein taromi 2025-07-27 14:45:01 +03:30
parent 2e9fa5460e
commit dce0f918ef
4 changed files with 60 additions and 104 deletions

View File

@ -5,9 +5,12 @@ import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup'; import * as yup from 'yup';
import { useAdminUser, useCreateAdminUser, useUpdateAdminUser } from '../core/_hooks'; import { useAdminUser, useCreateAdminUser, useUpdateAdminUser } from '../core/_hooks';
import { AdminUserFormData } from '../core/_models'; import { AdminUserFormData } from '../core/_models';
import { usePermissions } from '../../permissions/core/_hooks';
import { useRoles } from '../../roles/core/_hooks';
import { Button } from "@/components/ui/Button"; import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input"; import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAutocomplete";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
const adminUserSchema = yup.object({ const adminUserSchema = yup.object({
@ -22,6 +25,8 @@ const adminUserSchema = yup.object({
}) })
}), }),
status: yup.string().required('وضعیت الزامی است').oneOf(['active', 'deactive'], 'وضعیت نامعتبر است'), status: yup.string().required('وضعیت الزامی است').oneOf(['active', 'deactive'], 'وضعیت نامعتبر است'),
permissions: yup.array().of(yup.number()).default([]),
roles: yup.array().of(yup.number()).default([]),
isEdit: yup.boolean().default(false) isEdit: yup.boolean().default(false)
}); });
@ -34,6 +39,9 @@ const AdminUserFormPage = () => {
const { mutate: createUser, isPending: isCreating } = useCreateAdminUser(); const { mutate: createUser, isPending: isCreating } = useCreateAdminUser();
const { mutate: updateUser, isPending: isUpdating } = useUpdateAdminUser(); const { mutate: updateUser, isPending: isUpdating } = useUpdateAdminUser();
const { data: permissions, isLoading: isLoadingPermissions } = usePermissions();
const { data: roles, isLoading: isLoadingRoles } = useRoles();
const isLoading = isCreating || isUpdating; const isLoading = isCreating || isUpdating;
const { const {
@ -51,6 +59,8 @@ const AdminUserFormPage = () => {
username: '', username: '',
password: '', password: '',
status: 'active' as 'active' | 'deactive', status: 'active' as 'active' | 'deactive',
permissions: [],
roles: [],
isEdit: isEdit isEdit: isEdit
} }
}); });
@ -69,6 +79,8 @@ const AdminUserFormPage = () => {
setValue('last_name', user.last_name, { shouldValidate: true }); setValue('last_name', user.last_name, { shouldValidate: true });
setValue('username', user.username, { shouldValidate: true }); setValue('username', user.username, { shouldValidate: true });
setValue('status', user.status, { shouldValidate: true }); setValue('status', user.status, { shouldValidate: true });
setValue('permissions', user.permissions?.map(p => p.id) || [], { shouldValidate: true });
setValue('roles', user.roles?.map(r => r.id) || [], { shouldValidate: true });
setValue('isEdit', true, { shouldValidate: true }); setValue('isEdit', true, { shouldValidate: true });
} }
}, [isEdit, user, setValue]); }, [isEdit, user, setValue]);
@ -83,7 +95,9 @@ const AdminUserFormPage = () => {
last_name: data.last_name, last_name: data.last_name,
username: data.username, username: data.username,
password: data.password && data.password.trim() ? data.password : undefined, password: data.password && data.password.trim() ? data.password : undefined,
status: data.status status: data.status,
permissions: data.permissions,
roles: data.roles
} }
}, { }, {
onSuccess: () => { onSuccess: () => {
@ -97,7 +111,9 @@ const AdminUserFormPage = () => {
last_name: data.last_name, last_name: data.last_name,
username: data.username, username: data.username,
password: data.password || '', password: data.password || '',
status: data.status status: data.status,
permissions: data.permissions,
roles: data.roles
}, { }, {
onSuccess: (result) => { onSuccess: (result) => {
console.log('✅ Admin user created successfully:', result); console.log('✅ Admin user created successfully:', result);
@ -178,6 +194,36 @@ const AdminUserFormPage = () => {
placeholder={isEdit ? "رمز عبور جدید (در صورت تمایل به تغییر)" : "رمز عبور"} placeholder={isEdit ? "رمز عبور جدید (در صورت تمایل به تغییر)" : "رمز عبور"}
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<MultiSelectAutocomplete
label="دسترسی‌ها"
options={(permissions || []).map((permission): Option => ({
id: permission.id,
title: permission.title,
description: permission.description
}))}
selectedValues={watch('permissions') || []}
onChange={(values) => setValue('permissions', values, { shouldValidate: true })}
placeholder="انتخاب دسترسی‌ها..."
isLoading={isLoadingPermissions}
error={errors.permissions?.message}
/>
<MultiSelectAutocomplete
label="نقش‌ها"
options={(roles || []).map((role): Option => ({
id: role.id,
title: role.title,
description: role.description
}))}
selectedValues={watch('roles') || []}
onChange={(values) => setValue('roles', values, { shouldValidate: true })}
placeholder="انتخاب نقش‌ها..."
isLoading={isLoadingRoles}
error={errors.roles?.message}
/>
</div>
<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">
وضعیت وضعیت

View File

@ -10,6 +10,8 @@ export interface AdminUserFormData {
username: string; username: string;
password?: string; password?: string;
status: "active" | "deactive"; status: "active" | "deactive";
permissions: number[];
roles: number[];
isEdit: boolean; isEdit: boolean;
} }

View File

@ -1,11 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { usePermissions } from '../core/_hooks';
import { usePermissions, useDeletePermission } from '../core/_hooks';
import { Permission } from '../core/_models'; import { Permission } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner"; import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { Trash2, Edit3, Plus, Shield, Eye } from "lucide-react"; import { Shield } from "lucide-react";
import { Modal } from "@/components/ui/Modal";
// Skeleton Loading Component // Skeleton Loading Component
const PermissionsTableSkeleton = () => ( const PermissionsTableSkeleton = () => (
@ -25,9 +22,6 @@ const PermissionsTableSkeleton = () => (
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
تاریخ ایجاد تاریخ ایجاد
</th> </th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
عملیات
</th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> <tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@ -75,36 +69,11 @@ const PermissionsTableSkeleton = () => (
); );
const PermissionsListPage = () => { const PermissionsListPage = () => {
const navigate = useNavigate();
const [deletePermissionId, setDeletePermissionId] = useState<string | null>(null);
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
search: '' search: ''
}); });
const { data: permissions, isLoading, error } = usePermissions(filters); const { data: permissions, isLoading, error } = usePermissions(filters);
const { mutate: deletePermission, isPending: isDeleting } = useDeletePermission();
const handleCreate = () => {
navigate('/permissions/create');
};
const handleView = (permissionId: number) => {
navigate(`/permissions/${permissionId}`);
};
const handleEdit = (permissionId: number) => {
navigate(`/permissions/${permissionId}/edit`);
};
const handleDeleteConfirm = () => {
if (deletePermissionId) {
deletePermission(deletePermissionId, {
onSuccess: () => {
setDeletePermissionId(null);
}
});
}
};
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilters(prev => ({ ...prev, search: e.target.value })); setFilters(prev => ({ ...prev, search: e.target.value }));
@ -127,16 +96,12 @@ const PermissionsListPage = () => {
<div> <div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2"> <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
<Shield className="h-6 w-6" /> <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 onClick={handleCreate} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
دسترسی جدید
</Button>
</div> </div>
{/* Filters */} {/* Filters */}
@ -213,24 +178,6 @@ const PermissionsListPage = () => {
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{new Date(permission.created_at).toLocaleDateString('fa-IR')} {new Date(permission.created_at).toLocaleDateString('fa-IR')}
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm 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> </tr>
))} ))}
</tbody> </tbody>
@ -252,59 +199,16 @@ const PermissionsListPage = () => {
</p> </p>
</div> </div>
</div> </div>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-3"> <div className="text-xs text-gray-500 dark:text-gray-400">
تاریخ ایجاد: {new Date(permission.created_at).toLocaleDateString('fa-IR')} تاریخ ایجاد: {new Date(permission.created_at).toLocaleDateString('fa-IR')}
</div> </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> </div>
</div> </div>
)} )}
{/* Delete Confirmation Modal */}
<Modal
isOpen={!!deletePermissionId}
onClose={() => setDeletePermissionId(null)}
title="حذف دسترسی"
>
<div className="space-y-4">
<p className="text-gray-600 dark:text-gray-400">
آیا از حذف این دسترسی اطمینان دارید؟ این عمل قابل بازگشت نیست و ممکن است بر نقشهایی که از این دسترسی استفاده میکنند تأثیر بگذارد.
</p>
<div className="flex justify-end space-x-2 space-x-reverse">
<Button
variant="secondary"
onClick={() => setDeletePermissionId(null)}
disabled={isDeleting}
>
انصراف
</Button>
<Button
variant="danger"
onClick={handleDeleteConfirm}
loading={isDeleting}
>
حذف
</Button>
</div>
</div>
</Modal>
</div> </div>
); );
}; };

View File

@ -45,6 +45,8 @@ export interface CreateAdminUserRequest {
username: string; username: string;
password: string; password: string;
status: string; status: string;
permissions?: number[];
roles?: number[];
} }
export interface UpdateAdminUserRequest { export interface UpdateAdminUserRequest {
@ -54,6 +56,8 @@ export interface UpdateAdminUserRequest {
username: string; username: string;
password?: string; password?: string;
status: string; status: string;
permissions?: number[];
roles?: number[];
} }
export interface AdminUsersListResponse { export interface AdminUsersListResponse {