From 453cc81c70a9c25564742cb847c5d55590b456cd Mon Sep 17 00:00:00 2001 From: ghazall-ag Date: Fri, 31 Oct 2025 21:23:36 +0330 Subject: [PATCH] feat(roles): add roles management page with add, edit and delete functionality --- src/App.jsx | 11 ++ src/components/Sidebar.jsx | 4 +- src/pages/Roles.jsx | 358 +++++++++++++++++++++++++++++++++++++ src/services/api.js | 138 +++++++++++++- 4 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 src/pages/Roles.jsx diff --git a/src/App.jsx b/src/App.jsx index 5ac7495..a12d4d3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ import Dashboard from './pages/Dashboard'; import Transactions from './pages/Transactions'; import Settings from './pages/Settings'; import ForgotPassword from './pages/ForgotPassword'; +import Roles from './pages/Roles'; // Protected Route Component const ProtectedRoute = ({ children }) => { @@ -91,6 +92,16 @@ const AppRoutes = () => { } /> + + + + + + } + /> } /> ); diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 7d72fb9..05e76e2 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -5,13 +5,15 @@ import { CreditCard, Settings, Menu, - X + X, + Shield } from 'lucide-react'; const Sidebar = ({ isOpen, onToggle }) => { const navigation = [ { name: 'Dashboard', href: '/', icon: LayoutDashboard }, { name: 'Transactions', href: '/transactions', icon: CreditCard }, + { name: 'Roles', href: '/roles', icon: Shield }, { name: 'Settings', href: '/settings', icon: Settings }, ]; diff --git a/src/pages/Roles.jsx b/src/pages/Roles.jsx new file mode 100644 index 0000000..e973b36 --- /dev/null +++ b/src/pages/Roles.jsx @@ -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) => ( +
+ + +
+ ) }, + ], []); + + return ( +
+
+
+

Roles

+

Manage roles: add, filter by name, and delete

+
+ +
+ + {/* Add Role Modal */} + {isModalOpen && ( + <> +
setIsModalOpen(false)} + /> +
+
+
+

Add Role

+

Create a new role with permissions and user type

+
+
+
+ + 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" + /> +
+
+ +
+ {permissionOptions.map((perm) => { + const checked = selectedPermissions.includes(perm); + return ( + + ); + })} +
+
+ +
+
+ + 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" + /> +
+
+
+ + 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" + /> +
+
+ + +
+
+
+
+ + )} + + {/* Edit Role Modal */} + {isEditModalOpen && ( + <> +
setIsEditModalOpen(false)} + /> +
+
+
+

Edit Role

+

Update role name, permissions and user type

+
+
+
+ + 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" + /> +
+
+ +
+ {permissionOptions.map((perm) => { + const checked = selectedPermissions.includes(perm); + return ( + + ); + })} +
+
+ +
+
+ + 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" + /> +
+
+
+ + 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" + /> +
+
+ + +
+
+
+
+ + )} + + {/* Filter by name */} +
+
+ + { + 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" + /> +
+
+ + + + {error && ( +
{error}
+ )} +
+ ); +}; + +export default Roles; + + diff --git a/src/services/api.js b/src/services/api.js index a06fe15..ed822a8 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -19,7 +19,8 @@ const api = axios.create({ api.interceptors.response.use( (response) => response, (error) => { - if (error.response?.status === 401) { + const skipRedirect = error?.config?.skipAuthRedirect === true; + if (error.response?.status === 401 && !skipRedirect) { // session منقضی شده → هدایت به login const setLoggedIn = useAuthStore.getState().setLoggedIn; setLoggedIn(false); @@ -160,3 +161,138 @@ export const paymentsAPI = { 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; + }, +};