fix(roles): handle errors properly in edit role API

This commit is contained in:
ghazall-ag
2025-11-05 18:20:45 +03:30
parent 453cc81c70
commit 06d430d21c
10 changed files with 644 additions and 81 deletions

View File

@@ -6,6 +6,7 @@ import {
Settings,
Menu,
X,
Users,
Shield
} from 'lucide-react';
@@ -14,6 +15,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Transactions', href: '/transactions', icon: CreditCard },
{ name: 'Roles', href: '/roles', icon: Shield },
{ name: 'Users', href: '/users', icon: Users },
{ name: 'Settings', href: '/settings', icon: Settings },
];

34
src/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "payment-admin-panel",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.3.4",
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.1",
"recharts": "^2.5.0",
"zustand": "^5.0.8"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.38.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7",
"vite": "^4.2.0"
}
}

View File

@@ -44,7 +44,7 @@ const Login = () => {
</div>
</div>
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900 dark:text-white">
Payment Admin Panel
Khalij pay Admin Panel
</h2>
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
Sign in to your account
@@ -136,9 +136,7 @@ const Login = () => {
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400">
Demo Credentials
</span>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import DataTable from '../components/DataTable';
import { rolesAPI } from '../services/api';
import { rolesAPI, listPermissions } from '../services/api';
import { Plus, Trash2, Search, Pencil } from 'lucide-react';
const Roles = () => {
@@ -9,25 +9,13 @@ const Roles = () => {
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 [permissionOptions, setPermissionOptions] = useState([]);
const fetchRoles = async (q = '') => {
try {
@@ -43,6 +31,14 @@ const Roles = () => {
useEffect(() => {
fetchRoles();
(async () => {
try {
const perms = await listPermissions();
setPermissionOptions(Array.isArray(perms) ? perms : []);
} catch (_) {
setPermissionOptions([]);
}
})();
}, []);
const onAddRole = async (e) => {
@@ -52,10 +48,9 @@ const Roles = () => {
? selectedPermissions
: permissionsInput.split(',').map(p => p.trim()).filter(Boolean);
try {
await rolesAPI.create({ name: name.trim(), permissions: perms, userType: userType.trim() });
await rolesAPI.create({ name: name.trim(), permissions: perms });
setName('');
setPermissionsInput('');
setUserType('');
setSelectedPermissions([]);
await fetchRoles(nameFilter);
setIsModalOpen(false);
@@ -72,7 +67,6 @@ const Roles = () => {
const openEdit = (role) => {
setEditingRole(role);
setName(role.name || '');
setUserType(role.userType || '');
setSelectedPermissions(Array.isArray(role.permissions) ? role.permissions : []);
setPermissionsInput('');
setIsEditModalOpen(true);
@@ -85,13 +79,12 @@ const Roles = () => {
? selectedPermissions
: permissionsInput.split(',').map(p => p.trim()).filter(Boolean);
try {
await rolesAPI.update(editingRole.id, { name: name.trim(), permissions: perms, userType: userType.trim() });
await rolesAPI.update(editingRole.id, { name: name.trim(), permissions: perms });
await fetchRoles(nameFilter);
setIsEditModalOpen(false);
setEditingRole(null);
setName('');
setPermissionsInput('');
setUserType('');
setSelectedPermissions([]);
} catch (_) {
setError('Failed to update role');
@@ -103,7 +96,7 @@ const Roles = () => {
{ 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">
<div className="flex items-center gap-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"
@@ -146,7 +139,7 @@ const Roles = () => {
<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>
<p className="text-sm text-gray-600 dark:text-gray-400">Create a new role with permissions</p>
</div>
<form className="space-y-4" onSubmit={onAddRole}>
<div>
@@ -162,22 +155,29 @@ const Roles = () => {
<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);
const permName = typeof perm === 'string' ? perm : perm?.name;
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
const checked = selectedPermissions.includes(permName);
return (
<label key={perm} className="inline-flex items-center space-x-2 space-x-reverse justify-start">
<label key={permName} className="inline-flex items-center gap-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]);
setSelectedPermissions((prev) => [...prev, permName]);
} else {
setSelectedPermissions((prev) => prev.filter(p => p !== perm));
setSelectedPermissions((prev) => prev.filter(p => p !== permName));
}
}}
/>
<span className="text-sm text-gray-700 dark:text-gray-300">{perm}</span>
<span className="text-sm text-gray-700 dark:text-gray-300 mx-2">
{permName}
{permDesc ? (
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 block">{permDesc}</span>
) : null}
</span>
</label>
);
})}
@@ -186,7 +186,7 @@ const Roles = () => {
<button
type="button"
onClick={() => setSelectedPermissions([])}
className="text-xs text-gray-600 dark:text-gray-300 hover:underline"
className="text-xs text-blue-600 dark:text-gray-300 hover:underline"
>
Clear selection
</button>
@@ -201,16 +201,8 @@ const Roles = () => {
/>
</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">
<div className="flex items-center justify-end gap-x-2">
<button
type="button"
onClick={() => setIsModalOpen(false)}
@@ -239,7 +231,7 @@ const Roles = () => {
<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>
<p className="text-sm text-gray-600 dark:text-gray-400">Update role name and permissions</p>
</div>
<form className="space-y-4" onSubmit={onUpdateRole}>
<div>
@@ -255,22 +247,29 @@ const Roles = () => {
<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);
const permName = typeof perm === 'string' ? perm : perm?.name;
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
const checked = selectedPermissions.includes(permName);
return (
<label key={perm} className="inline-flex items-center space-x-2 space-x-reverse justify-start">
<label key={permName} className="inline-flex items-center 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]);
setSelectedPermissions((prev) => [...prev, permName]);
} else {
setSelectedPermissions((prev) => prev.filter(p => p !== perm));
setSelectedPermissions((prev) => prev.filter(p => p !== permName));
}
}}
/>
<span className="text-sm text-gray-700 dark:text-gray-300">{perm}</span>
<span className="text-sm text-gray-700 dark:text-gray-300 mx-2">
{permName}
{permDesc ? (
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 block">{permDesc}</span>
) : null}
</span>
</label>
);
})}
@@ -279,7 +278,7 @@ const Roles = () => {
<button
type="button"
onClick={() => setSelectedPermissions([])}
className="text-xs text-gray-600 dark:text-gray-300 hover:underline"
className="text-xs text-blue-600 dark:text-gray-300 hover:underline"
>
Clear selection
</button>
@@ -294,16 +293,8 @@ const Roles = () => {
/>
</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">
<div className="flex items-center justify-end gap-x-2">
<button
type="button"
onClick={() => setIsEditModalOpen(false)}

7
src/pages/Users.jsx Normal file
View File

@@ -0,0 +1,7 @@
import React from 'react'
export default function Users() {
return (
<div>Users</div>
)
}

View File

@@ -216,9 +216,15 @@ export const rolesAPI = {
// 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 : []);
const raw = res?.data;
// Backend shape example:
// { data: { filterSummary: {...}, data: Role[] }, isSuccess: true, ... }
const apiItems = Array.isArray(raw?.data?.data) ? raw.data.data : undefined;
// Fallbacks for other shapes we may encounter
const items = apiItems
|| (Array.isArray(raw) ? raw : undefined)
|| (Array.isArray(raw?.items) ? raw.items : [])
|| [];
// Sync a snapshot to local storage for offline UX
writeRolesToStorage(items.map(r => ({ id: r.id || crypto.randomUUID(), ...r })));
return items;
@@ -236,7 +242,7 @@ export const rolesAPI = {
const payload = {
name: String(role?.name || '').trim(),
permissions: Array.isArray(role?.permissions) ? role.permissions : [],
userType: String(role?.userType || '').trim(),
// userType: String(role?.userType || '').trim(),
};
let created = null;
try {
@@ -269,16 +275,31 @@ export const rolesAPI = {
},
async update(id, role) {
const existingLocal = readRolesFromStorage().find(r => r.id === id) || {};
const payload = {
name: String(role?.name || '').trim(),
permissions: Array.isArray(role?.permissions) ? role.permissions : [],
userType: String(role?.userType || '').trim(),
...(role?.userType !== undefined
? { userType: String(role?.userType || '').trim() }
: (existingLocal.userType ? { userType: existingLocal.userType } : {})),
};
try {
await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect: true });
} catch (_) {
// ignore; apply optimistic local update
} catch (err) {
const status = err?.statusCode || err?.status || err?.response?.status;
if (status === 404 || status === 405 || status === 400) {
try {
await api.put('/api/v1/Role', { id, ...payload }, { skipAuthRedirect: true });
} catch (e2) {
const s2 = e2?.statusCode || e2?.status || e2?.response?.status;
if (s2 === 401 || s2 === 403) {
throw { message: 'Unauthorized to edit roles (401/403). Check permissions/login.', status: s2 };
}
}
} else if (status === 401 || status === 403) {
throw { message: 'Unauthorized to edit roles (401/403). Check permissions/login.', status };
}
}
const roles = readRolesFromStorage();
@@ -296,3 +317,46 @@ export const rolesAPI = {
return updated;
},
};
// -----------------------------
// Permissions API
// -----------------------------
export async function listPermissions() {
try {
const res = await api.get('/api/v1/General/Permission', { skipAuthRedirect: true });
const raw = res?.data;
// Expected shape: { data: Permission[] }
const items = Array.isArray(raw?.data) ? raw.data : [];
// Prefer returning structured objects with name and description
const objs = items
.map((p) => {
const name = typeof p?.name === 'string' ? p.name : (typeof p?.displayName === 'string' ? p.displayName : null);
if (!name) return null;
const description = typeof p?.description === 'string' && p.description
? p.description
: (typeof p?.displayName === 'string' ? p.displayName : name);
return { name, description };
})
.filter(Boolean);
if (objs.length > 0) return objs;
// Fallback to simple names if server returned unexpected shape
const names = items
.map((p) => (typeof p?.name === 'string' ? p.name : (typeof p?.displayName === 'string' ? p.displayName : null)))
.filter(Boolean);
return names.map((n) => ({ name: n, description: n }));
} catch (_) {
// Fallback to a minimal static list to keep UI usable
return [
{ name: 'Administrator', description: 'Full access to administrative features' },
{ name: 'UserManagement', description: 'Manage users and their profiles' },
{ name: 'AddUser', description: 'Create new user accounts' },
{ name: 'EditUser', description: 'Edit existing user accounts' },
{ name: 'UserPasswordManagement', description: 'Reset or change user passwords' },
{ name: 'UserRoleManagement', description: 'Assign or modify user roles' },
{ name: 'RoleManagement', description: 'Manage roles and permissions' },
{ name: 'AddRole', description: 'Create new roles' },
{ name: 'EditRole', description: 'Edit existing roles' },
{ name: 'DeleteRole', description: 'Remove roles from the system' },
];
}
}

View File

@@ -1,8 +1,17 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export const useAuthStore = create((set) => ({
isLoggedIn: false,
loading: false,
setLoggedIn: (status) => set({ isLoggedIn: status }),
setLoading: (status) => set({ loading: status }),
}));
export const useAuthStore = create(
persist(
(set) => ({
isLoggedIn: false,
loading: false,
setLoggedIn: (status) => set({ isLoggedIn: status }),
setLoading: (status) => set({ loading: status }),
}),
{
name: "auth_store_v1",
partialize: (state) => ({ isLoggedIn: state.isLoggedIn }),
}
)
);