feat(roles): add roles management page with add, edit and delete functionality

This commit is contained in:
ghazall-ag
2025-10-31 21:23:36 +03:30
parent 5079a5bc56
commit 453cc81c70
4 changed files with 509 additions and 2 deletions

358
src/pages/Roles.jsx Normal file
View File

@@ -0,0 +1,358 @@
import React, { useEffect, useMemo, useState } from 'react';
import DataTable from '../components/DataTable';
import { rolesAPI } from '../services/api';
import { Plus, Trash2, Search, Pencil } from 'lucide-react';
const Roles = () => {
const [roles, setRoles] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [name, setName] = useState('');
const [permissionsInput, setPermissionsInput] = useState('');
const [userType, setUserType] = useState('');
const [nameFilter, setNameFilter] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [editingRole, setEditingRole] = useState(null);
const [selectedPermissions, setSelectedPermissions] = useState([]);
const permissionOptions = [
'Administrator',
'UserManagement',
'AddUser',
'EditUser',
'UserPasswordManagement',
'UserRoleManagement',
'RoleManagement',
'AddRole',
'EditRole',
'DeleteRole',
];
const fetchRoles = async (q = '') => {
try {
setLoading(true);
const list = await rolesAPI.list(q);
setRoles(list);
} catch (e) {
setError('Failed to load roles');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchRoles();
}, []);
const onAddRole = async (e) => {
e.preventDefault();
if (!name.trim()) return;
const perms = selectedPermissions.length > 0
? selectedPermissions
: permissionsInput.split(',').map(p => p.trim()).filter(Boolean);
try {
await rolesAPI.create({ name: name.trim(), permissions: perms, userType: userType.trim() });
setName('');
setPermissionsInput('');
setUserType('');
setSelectedPermissions([]);
await fetchRoles(nameFilter);
setIsModalOpen(false);
} catch (_) {
setError('Failed to create role');
}
};
const onDelete = async (id) => {
await rolesAPI.remove(id);
await fetchRoles(nameFilter);
};
const openEdit = (role) => {
setEditingRole(role);
setName(role.name || '');
setUserType(role.userType || '');
setSelectedPermissions(Array.isArray(role.permissions) ? role.permissions : []);
setPermissionsInput('');
setIsEditModalOpen(true);
};
const onUpdateRole = async (e) => {
e.preventDefault();
if (!editingRole) return;
const perms = selectedPermissions.length > 0
? selectedPermissions
: permissionsInput.split(',').map(p => p.trim()).filter(Boolean);
try {
await rolesAPI.update(editingRole.id, { name: name.trim(), permissions: perms, userType: userType.trim() });
await fetchRoles(nameFilter);
setIsEditModalOpen(false);
setEditingRole(null);
setName('');
setPermissionsInput('');
setUserType('');
setSelectedPermissions([]);
} catch (_) {
setError('Failed to update role');
}
};
const columns = useMemo(() => [
{ key: 'name', header: 'Name' },
{ key: 'permissions', header: 'Permissions', render: (val) => Array.isArray(val) ? val.join(', ') : '' },
{ key: 'userType', header: 'User Type' },
{ key: 'actions', header: 'Actions', render: (_val, row) => (
<div className="flex items-center space-x-2">
<button
onClick={() => openEdit(row)}
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 hover:opacity-90"
>
<Pencil className="h-4 w-4 mr-1" /> Edit
</button>
<button
onClick={() => onDelete(row.id)}
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300 hover:opacity-90"
>
<Trash2 className="h-4 w-4 mr-1" /> Delete
</button>
</div>
) },
], []);
return (
<div className="p-6">
<div className="mb-6 flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Roles</h1>
<p className="text-gray-600 dark:text-gray-400">Manage roles: add, filter by name, and delete</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="btn-primary inline-flex items-center"
>
<Plus className="h-4 w-4 mr-2" /> Add Role
</button>
</div>
{/* Add Role Modal */}
{isModalOpen && (
<>
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={() => setIsModalOpen(false)}
/>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="w-full max-w-lg card p-6 relative">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Add Role</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">Create a new role with permissions and user type</p>
</div>
<form className="space-y-4" onSubmit={onAddRole}>
<div>
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Role Name</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="e.g., Admin"
/>
</div>
<div>
<label className="block text-sm mb-2 text-gray-700 dark:text-gray-300">Permissions</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{permissionOptions.map((perm) => {
const checked = selectedPermissions.includes(perm);
return (
<label key={perm} className="inline-flex items-center space-x-2 space-x-reverse justify-start">
<input
type="checkbox"
className="h-4 w-4 text-primary-600 border-gray-300 rounded"
checked={checked}
onChange={(e) => {
if (e.target.checked) {
setSelectedPermissions((prev) => [...prev, perm]);
} else {
setSelectedPermissions((prev) => prev.filter(p => p !== perm));
}
}}
/>
<span className="text-sm text-gray-700 dark:text-gray-300">{perm}</span>
</label>
);
})}
</div>
<div className="mt-2">
<button
type="button"
onClick={() => setSelectedPermissions([])}
className="text-xs text-gray-600 dark:text-gray-300 hover:underline"
>
Clear selection
</button>
</div>
<div className="mt-3">
<label className="block text-xs mb-1 text-gray-500 dark:text-gray-400">Or enter custom permissions (comma-separated)</label>
<input
value={permissionsInput}
onChange={(e) => setPermissionsInput(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="Administrator, RoleManagement"
/>
</div>
</div>
<div>
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">User Type</label>
<input
value={userType}
onChange={(e) => setUserType(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="e.g., internal or external"
/>
</div>
<div className="flex items-center justify-end space-x-2">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="inline-flex items-center px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"
>
Cancel
</button>
<button type="submit" className="btn-primary inline-flex items-center">
<Plus className="h-4 w-4 mr-2" /> Create Role
</button>
</div>
</form>
</div>
</div>
</>
)}
{/* Edit Role Modal */}
{isEditModalOpen && (
<>
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={() => setIsEditModalOpen(false)}
/>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="w-full max-w-lg card p-6 relative">
<div className="mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Edit Role</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">Update role name, permissions and user type</p>
</div>
<form className="space-y-4" onSubmit={onUpdateRole}>
<div>
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Role Name</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="e.g., Admin"
/>
</div>
<div>
<label className="block text-sm mb-2 text-gray-700 dark:text-gray-300">Permissions</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{permissionOptions.map((perm) => {
const checked = selectedPermissions.includes(perm);
return (
<label key={perm} className="inline-flex items-center space-x-2 space-x-reverse justify-start">
<input
type="checkbox"
className="h-4 w-4 text-primary-600 border-gray-300 rounded"
checked={checked}
onChange={(e) => {
if (e.target.checked) {
setSelectedPermissions((prev) => [...prev, perm]);
} else {
setSelectedPermissions((prev) => prev.filter(p => p !== perm));
}
}}
/>
<span className="text-sm text-gray-700 dark:text-gray-300">{perm}</span>
</label>
);
})}
</div>
<div className="mt-2">
<button
type="button"
onClick={() => setSelectedPermissions([])}
className="text-xs text-gray-600 dark:text-gray-300 hover:underline"
>
Clear selection
</button>
</div>
<div className="mt-3">
<label className="block text-xs mb-1 text-gray-500 dark:text-gray-400">Or enter custom permissions (comma-separated)</label>
<input
value={permissionsInput}
onChange={(e) => setPermissionsInput(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="Administrator, RoleManagement"
/>
</div>
</div>
<div>
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">User Type</label>
<input
value={userType}
onChange={(e) => setUserType(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="e.g., internal or external"
/>
</div>
<div className="flex items-center justify-end space-x-2">
<button
type="button"
onClick={() => setIsEditModalOpen(false)}
className="inline-flex items-center px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"
>
Cancel
</button>
<button type="submit" className="btn-primary inline-flex items-center">
<Pencil className="h-4 w-4 mr-2" /> Update Role
</button>
</div>
</form>
</div>
</div>
</>
)}
{/* Filter by name */}
<div className="card p-4 mb-4">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
value={nameFilter}
onChange={async (e) => {
const v = e.target.value;
setNameFilter(v);
await fetchRoles(v);
}}
placeholder="Filter by role name"
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
</div>
<DataTable
data={roles}
columns={columns}
loading={loading}
searchable={false}
className=""
/>
{error && (
<div className="mt-4 text-sm text-red-600 dark:text-red-400">{error}</div>
)}
</div>
);
};
export default Roles;