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:
parent
2e9fa5460e
commit
dce0f918ef
|
|
@ -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)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -33,6 +38,9 @@ const AdminUserFormPage = () => {
|
||||||
const { data: user, isLoading: isLoadingUser } = useAdminUser(id || '', isEdit);
|
const { data: user, isLoading: isLoadingUser } = useAdminUser(id || '', isEdit);
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -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">
|
||||||
وضعیت
|
وضعیت
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue