feat(roles): add roles management page with add, edit and delete functionality
This commit is contained in:
11
src/App.jsx
11
src/App.jsx
@@ -8,6 +8,7 @@ import Dashboard from './pages/Dashboard';
|
|||||||
import Transactions from './pages/Transactions';
|
import Transactions from './pages/Transactions';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
import ForgotPassword from './pages/ForgotPassword';
|
import ForgotPassword from './pages/ForgotPassword';
|
||||||
|
import Roles from './pages/Roles';
|
||||||
|
|
||||||
// Protected Route Component
|
// Protected Route Component
|
||||||
const ProtectedRoute = ({ children }) => {
|
const ProtectedRoute = ({ children }) => {
|
||||||
@@ -91,6 +92,16 @@ const AppRoutes = () => {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/roles"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Layout>
|
||||||
|
<Roles />
|
||||||
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/" />} />
|
<Route path="*" element={<Navigate to="/" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import {
|
|||||||
CreditCard,
|
CreditCard,
|
||||||
Settings,
|
Settings,
|
||||||
Menu,
|
Menu,
|
||||||
X
|
X,
|
||||||
|
Shield
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const Sidebar = ({ isOpen, onToggle }) => {
|
const Sidebar = ({ isOpen, onToggle }) => {
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||||
{ name: 'Transactions', href: '/transactions', icon: CreditCard },
|
{ name: 'Transactions', href: '/transactions', icon: CreditCard },
|
||||||
|
{ name: 'Roles', href: '/roles', icon: Shield },
|
||||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
358
src/pages/Roles.jsx
Normal file
358
src/pages/Roles.jsx
Normal 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;
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@ const api = axios.create({
|
|||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response?.status === 401) {
|
const skipRedirect = error?.config?.skipAuthRedirect === true;
|
||||||
|
if (error.response?.status === 401 && !skipRedirect) {
|
||||||
// session منقضی شده → هدایت به login
|
// session منقضی شده → هدایت به login
|
||||||
const setLoggedIn = useAuthStore.getState().setLoggedIn;
|
const setLoggedIn = useAuthStore.getState().setLoggedIn;
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
@@ -160,3 +161,138 @@ export const paymentsAPI = {
|
|||||||
return mockData.chartData;
|
return mockData.chartData;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Roles API (localStorage mock)
|
||||||
|
// -----------------------------
|
||||||
|
const ROLES_STORAGE_KEY = 'app_roles_v1';
|
||||||
|
|
||||||
|
function readRolesFromStorage() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(ROLES_STORAGE_KEY);
|
||||||
|
if (!raw) return [];
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!Array.isArray(parsed)) return [];
|
||||||
|
return parsed;
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRolesToStorage(roles) {
|
||||||
|
localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSeedRoles() {
|
||||||
|
const existing = readRolesFromStorage();
|
||||||
|
if (existing.length === 0) {
|
||||||
|
const seed = [
|
||||||
|
{ id: crypto.randomUUID(), name: 'Admin', permissions: ['read', 'write', 'delete'], userType: 'internal' },
|
||||||
|
{ id: crypto.randomUUID(), name: 'Editor', permissions: ['read', 'write'], userType: 'internal' },
|
||||||
|
{ id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' },
|
||||||
|
];
|
||||||
|
writeRolesToStorage(seed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rolesAPI = {
|
||||||
|
async list(queryOrOptions = '') {
|
||||||
|
// Support both: list('admin') and list({ nameQuery, currentPage, pageSize })
|
||||||
|
const opts = typeof queryOrOptions === 'string'
|
||||||
|
? { nameQuery: queryOrOptions, currentPage: 1, pageSize: 100 }
|
||||||
|
: {
|
||||||
|
nameQuery: queryOrOptions?.nameQuery || '',
|
||||||
|
currentPage: queryOrOptions?.currentPage ?? 1,
|
||||||
|
pageSize: queryOrOptions?.pageSize ?? 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/v1/Role', {
|
||||||
|
params: {
|
||||||
|
nameQuery: opts.nameQuery,
|
||||||
|
currentPage: opts.currentPage,
|
||||||
|
pageSize: opts.pageSize,
|
||||||
|
},
|
||||||
|
// prevent global 401 redirect for optional fetch
|
||||||
|
skipAuthRedirect: true,
|
||||||
|
});
|
||||||
|
const data = res?.data;
|
||||||
|
// Try common shapes: { items: Role[], total: number } or Role[]
|
||||||
|
const items = Array.isArray(data) ? data : (Array.isArray(data?.items) ? data.items : []);
|
||||||
|
// Sync a snapshot to local storage for offline UX
|
||||||
|
writeRolesToStorage(items.map(r => ({ id: r.id || crypto.randomUUID(), ...r })));
|
||||||
|
return items;
|
||||||
|
} catch (_) {
|
||||||
|
// Fallback to local filtering
|
||||||
|
ensureSeedRoles();
|
||||||
|
const roles = readRolesFromStorage();
|
||||||
|
const trimmed = String(opts.nameQuery || '').toLowerCase();
|
||||||
|
if (!trimmed) return roles;
|
||||||
|
return roles.filter(r => r.name?.toLowerCase().includes(trimmed));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(role) {
|
||||||
|
const payload = {
|
||||||
|
name: String(role?.name || '').trim(),
|
||||||
|
permissions: Array.isArray(role?.permissions) ? role.permissions : [],
|
||||||
|
userType: String(role?.userType || '').trim(),
|
||||||
|
};
|
||||||
|
let created = null;
|
||||||
|
try {
|
||||||
|
// Try real backend
|
||||||
|
const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect: true });
|
||||||
|
created = res?.data || payload;
|
||||||
|
} catch (_) {
|
||||||
|
// Fallback to local-only if backend unavailable
|
||||||
|
created = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync into local storage list so UI updates immediately
|
||||||
|
const roles = readRolesFromStorage();
|
||||||
|
const newRole = { id: crypto.randomUUID(), ...created };
|
||||||
|
roles.push(newRole);
|
||||||
|
writeRolesToStorage(roles);
|
||||||
|
return newRole;
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove(id) {
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect: true });
|
||||||
|
} catch (_) {
|
||||||
|
// ignore backend failure, proceed with local removal for UX
|
||||||
|
}
|
||||||
|
const roles = readRolesFromStorage();
|
||||||
|
const next = roles.filter(r => r.id !== id);
|
||||||
|
writeRolesToStorage(next);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(id, role) {
|
||||||
|
const payload = {
|
||||||
|
name: String(role?.name || '').trim(),
|
||||||
|
permissions: Array.isArray(role?.permissions) ? role.permissions : [],
|
||||||
|
userType: String(role?.userType || '').trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect: true });
|
||||||
|
} catch (_) {
|
||||||
|
// ignore; apply optimistic local update
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = readRolesFromStorage();
|
||||||
|
const idx = roles.findIndex(r => r.id === id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
roles[idx] = { ...roles[idx], ...payload };
|
||||||
|
writeRolesToStorage(roles);
|
||||||
|
return roles[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found locally, append
|
||||||
|
const updated = { id, ...payload };
|
||||||
|
roles.push(updated);
|
||||||
|
writeRolesToStorage(roles);
|
||||||
|
return updated;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user