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 { useAdminUser, useCreateAdminUser, useUpdateAdminUser } from '../core/_hooks';
import { AdminUserFormData } from '../core/_models';
import { usePermissions } from '../../permissions/core/_hooks';
import { useRoles } from '../../roles/core/_hooks';
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAutocomplete";
import { ArrowRight } from "lucide-react";
const adminUserSchema = yup.object({
@ -22,6 +25,8 @@ const adminUserSchema = yup.object({
})
}),
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)
});
@ -33,6 +38,9 @@ const AdminUserFormPage = () => {
const { data: user, isLoading: isLoadingUser } = useAdminUser(id || '', isEdit);
const { mutate: createUser, isPending: isCreating } = useCreateAdminUser();
const { mutate: updateUser, isPending: isUpdating } = useUpdateAdminUser();
const { data: permissions, isLoading: isLoadingPermissions } = usePermissions();
const { data: roles, isLoading: isLoadingRoles } = useRoles();
const isLoading = isCreating || isUpdating;
@ -51,6 +59,8 @@ const AdminUserFormPage = () => {
username: '',
password: '',
status: 'active' as 'active' | 'deactive',
permissions: [],
roles: [],
isEdit: isEdit
}
});
@ -69,6 +79,8 @@ const AdminUserFormPage = () => {
setValue('last_name', user.last_name, { shouldValidate: true });
setValue('username', user.username, { 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 });
}
}, [isEdit, user, setValue]);
@ -83,7 +95,9 @@ const AdminUserFormPage = () => {
last_name: data.last_name,
username: data.username,
password: data.password && data.password.trim() ? data.password : undefined,
status: data.status
status: data.status,
permissions: data.permissions,
roles: data.roles
}
}, {
onSuccess: () => {
@ -97,7 +111,9 @@ const AdminUserFormPage = () => {
last_name: data.last_name,
username: data.username,
password: data.password || '',
status: data.status
status: data.status,
permissions: data.permissions,
roles: data.roles
}, {
onSuccess: (result) => {
console.log('✅ Admin user created successfully:', result);
@ -178,6 +194,36 @@ const AdminUserFormPage = () => {
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>
<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;
password?: string;
status: "active" | "deactive";
permissions: number[];
roles: number[];
isEdit: boolean;
}

View File

@ -1,11 +1,8 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { usePermissions, useDeletePermission } from '../core/_hooks';
import { usePermissions } from '../core/_hooks';
import { Permission } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { Trash2, Edit3, Plus, Shield, Eye } from "lucide-react";
import { Modal } from "@/components/ui/Modal";
import { Shield } from "lucide-react";
// Skeleton Loading Component
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>
<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">
@ -75,36 +69,11 @@ const PermissionsTableSkeleton = () => (
);
const PermissionsListPage = () => {
const navigate = useNavigate();
const [deletePermissionId, setDeletePermissionId] = useState<string | null>(null);
const [filters, setFilters] = useState({
search: ''
});
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>) => {
setFilters(prev => ({ ...prev, search: e.target.value }));
@ -127,16 +96,12 @@ const PermissionsListPage = () => {
<div>
<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">
<Plus className="h-4 w-4" />
دسترسی جدید
</Button>
</div>
{/* Filters */}
@ -213,24 +178,6 @@ const PermissionsListPage = () => {
<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>
@ -252,59 +199,16 @@ const PermissionsListPage = () => {
</p>
</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')}
</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}
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>
);
};

View File

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