fix(roles): handle errors properly in edit role API
This commit is contained in:
@@ -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
34
src/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
7
src/pages/Users.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function Users() {
|
||||
return (
|
||||
<div>Users</div>
|
||||
)
|
||||
}
|
||||
@@ -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' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user