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

@@ -1,59 +1,65 @@
{
"hash": "ff6fda40",
"browserHash": "b50068b3",
"browserHash": "c03c03c2",
"optimized": {
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "ddd613d5",
"fileHash": "e8b04feb",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "db7123d4",
"fileHash": "778d2d19",
"needsInterop": true
},
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "f0784960",
"fileHash": "9085912a",
"needsInterop": true
},
"axios": {
"src": "../../axios/index.js",
"file": "axios.js",
"fileHash": "b9f81ab8",
"fileHash": "387f77ed",
"needsInterop": false
},
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.mjs",
"file": "lucide-react.js",
"fileHash": "a5656c81",
"fileHash": "63623648",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "ae58904b",
"fileHash": "993544a9",
"needsInterop": true
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.js",
"file": "react-router-dom.js",
"fileHash": "420348ee",
"fileHash": "d3f984dc",
"needsInterop": false
},
"recharts": {
"src": "../../recharts/es6/index.js",
"file": "recharts.js",
"fileHash": "dafddd35",
"fileHash": "e1665e3e",
"needsInterop": false
},
"zustand": {
"src": "../../zustand/esm/index.mjs",
"file": "zustand.js",
"fileHash": "f2ee525b",
"fileHash": "8be28924",
"needsInterop": false
},
"zustand/middleware": {
"src": "../../zustand/esm/middleware.mjs",
"file": "zustand_middleware.js",
"fileHash": "936c3138",
"needsInterop": false
}
},

479
node_modules/.vite/deps/zustand_middleware.js generated vendored Normal file
View File

@@ -0,0 +1,479 @@
import "./chunk-5WWUZCGV.js";
// node_modules/zustand/esm/middleware.mjs
var reduxImpl = (reducer, initial) => (set, _get, api) => {
api.dispatch = (action) => {
set((state) => reducer(state, action), false, action);
return action;
};
api.dispatchFromDevtools = true;
return { dispatch: (...args) => api.dispatch(...args), ...initial };
};
var redux = reduxImpl;
var trackedConnections = /* @__PURE__ */ new Map();
var getTrackedConnectionState = (name) => {
const api = trackedConnections.get(name);
if (!api)
return {};
return Object.fromEntries(
Object.entries(api.stores).map(([key, api2]) => [key, api2.getState()])
);
};
var extractConnectionInformation = (store, extensionConnector, options) => {
if (store === void 0) {
return {
type: "untracked",
connection: extensionConnector.connect(options)
};
}
const existingConnection = trackedConnections.get(options.name);
if (existingConnection) {
return { type: "tracked", store, ...existingConnection };
}
const newConnection = {
connection: extensionConnector.connect(options),
stores: {}
};
trackedConnections.set(options.name, newConnection);
return { type: "tracked", store, ...newConnection };
};
var removeStoreFromTrackedConnections = (name, store) => {
if (store === void 0)
return;
const connectionInfo = trackedConnections.get(name);
if (!connectionInfo)
return;
delete connectionInfo.stores[store];
if (Object.keys(connectionInfo.stores).length === 0) {
trackedConnections.delete(name);
}
};
var findCallerName = (stack) => {
var _a, _b;
if (!stack)
return void 0;
const traceLines = stack.split("\n");
const apiSetStateLineIndex = traceLines.findIndex(
(traceLine) => traceLine.includes("api.setState")
);
if (apiSetStateLineIndex < 0)
return void 0;
const callerLine = ((_a = traceLines[apiSetStateLineIndex + 1]) == null ? void 0 : _a.trim()) || "";
return (_b = /.+ (.+) .+/.exec(callerLine)) == null ? void 0 : _b[1];
};
var devtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => {
const { enabled, anonymousActionType, store, ...options } = devtoolsOptions;
let extensionConnector;
try {
extensionConnector = (enabled != null ? enabled : (import.meta.env ? import.meta.env.MODE : void 0) !== "production") && window.__REDUX_DEVTOOLS_EXTENSION__;
} catch (e) {
}
if (!extensionConnector) {
return fn(set, get, api);
}
const { connection, ...connectionInformation } = extractConnectionInformation(store, extensionConnector, options);
let isRecording = true;
api.setState = (state, replace, nameOrAction) => {
const r = set(state, replace);
if (!isRecording)
return r;
const action = nameOrAction === void 0 ? {
type: anonymousActionType || findCallerName(new Error().stack) || "anonymous"
} : typeof nameOrAction === "string" ? { type: nameOrAction } : nameOrAction;
if (store === void 0) {
connection == null ? void 0 : connection.send(action, get());
return r;
}
connection == null ? void 0 : connection.send(
{
...action,
type: `${store}/${action.type}`
},
{
...getTrackedConnectionState(options.name),
[store]: api.getState()
}
);
return r;
};
api.devtools = {
cleanup: () => {
if (connection && typeof connection.unsubscribe === "function") {
connection.unsubscribe();
}
removeStoreFromTrackedConnections(options.name, store);
}
};
const setStateFromDevtools = (...a) => {
const originalIsRecording = isRecording;
isRecording = false;
set(...a);
isRecording = originalIsRecording;
};
const initialState = fn(api.setState, get, api);
if (connectionInformation.type === "untracked") {
connection == null ? void 0 : connection.init(initialState);
} else {
connectionInformation.stores[connectionInformation.store] = api;
connection == null ? void 0 : connection.init(
Object.fromEntries(
Object.entries(connectionInformation.stores).map(([key, store2]) => [
key,
key === connectionInformation.store ? initialState : store2.getState()
])
)
);
}
if (api.dispatchFromDevtools && typeof api.dispatch === "function") {
let didWarnAboutReservedActionType = false;
const originalDispatch = api.dispatch;
api.dispatch = (...args) => {
if ((import.meta.env ? import.meta.env.MODE : void 0) !== "production" && args[0].type === "__setState" && !didWarnAboutReservedActionType) {
console.warn(
'[zustand devtools middleware] "__setState" action type is reserved to set state from the devtools. Avoid using it.'
);
didWarnAboutReservedActionType = true;
}
originalDispatch(...args);
};
}
connection.subscribe((message) => {
var _a;
switch (message.type) {
case "ACTION":
if (typeof message.payload !== "string") {
console.error(
"[zustand devtools middleware] Unsupported action format"
);
return;
}
return parseJsonThen(
message.payload,
(action) => {
if (action.type === "__setState") {
if (store === void 0) {
setStateFromDevtools(action.state);
return;
}
if (Object.keys(action.state).length !== 1) {
console.error(
`
[zustand devtools middleware] Unsupported __setState action format.
When using 'store' option in devtools(), the 'state' should have only one key, which is a value of 'store' that was passed in devtools(),
and value of this only key should be a state object. Example: { "type": "__setState", "state": { "abc123Store": { "foo": "bar" } } }
`
);
}
const stateFromDevtools = action.state[store];
if (stateFromDevtools === void 0 || stateFromDevtools === null) {
return;
}
if (JSON.stringify(api.getState()) !== JSON.stringify(stateFromDevtools)) {
setStateFromDevtools(stateFromDevtools);
}
return;
}
if (!api.dispatchFromDevtools)
return;
if (typeof api.dispatch !== "function")
return;
api.dispatch(action);
}
);
case "DISPATCH":
switch (message.payload.type) {
case "RESET":
setStateFromDevtools(initialState);
if (store === void 0) {
return connection == null ? void 0 : connection.init(api.getState());
}
return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));
case "COMMIT":
if (store === void 0) {
connection == null ? void 0 : connection.init(api.getState());
return;
}
return connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));
case "ROLLBACK":
return parseJsonThen(message.state, (state) => {
if (store === void 0) {
setStateFromDevtools(state);
connection == null ? void 0 : connection.init(api.getState());
return;
}
setStateFromDevtools(state[store]);
connection == null ? void 0 : connection.init(getTrackedConnectionState(options.name));
});
case "JUMP_TO_STATE":
case "JUMP_TO_ACTION":
return parseJsonThen(message.state, (state) => {
if (store === void 0) {
setStateFromDevtools(state);
return;
}
if (JSON.stringify(api.getState()) !== JSON.stringify(state[store])) {
setStateFromDevtools(state[store]);
}
});
case "IMPORT_STATE": {
const { nextLiftedState } = message.payload;
const lastComputedState = (_a = nextLiftedState.computedStates.slice(-1)[0]) == null ? void 0 : _a.state;
if (!lastComputedState)
return;
if (store === void 0) {
setStateFromDevtools(lastComputedState);
} else {
setStateFromDevtools(lastComputedState[store]);
}
connection == null ? void 0 : connection.send(
null,
// FIXME no-any
nextLiftedState
);
return;
}
case "PAUSE_RECORDING":
return isRecording = !isRecording;
}
return;
}
});
return initialState;
};
var devtools = devtoolsImpl;
var parseJsonThen = (stringified, fn) => {
let parsed;
try {
parsed = JSON.parse(stringified);
} catch (e) {
console.error(
"[zustand devtools middleware] Could not parse the received json",
e
);
}
if (parsed !== void 0)
fn(parsed);
};
var subscribeWithSelectorImpl = (fn) => (set, get, api) => {
const origSubscribe = api.subscribe;
api.subscribe = (selector, optListener, options) => {
let listener = selector;
if (optListener) {
const equalityFn = (options == null ? void 0 : options.equalityFn) || Object.is;
let currentSlice = selector(api.getState());
listener = (state) => {
const nextSlice = selector(state);
if (!equalityFn(currentSlice, nextSlice)) {
const previousSlice = currentSlice;
optListener(currentSlice = nextSlice, previousSlice);
}
};
if (options == null ? void 0 : options.fireImmediately) {
optListener(currentSlice, currentSlice);
}
}
return origSubscribe(listener);
};
const initialState = fn(set, get, api);
return initialState;
};
var subscribeWithSelector = subscribeWithSelectorImpl;
function combine(initialState, create) {
return (...args) => Object.assign({}, initialState, create(...args));
}
function createJSONStorage(getStorage, options) {
let storage;
try {
storage = getStorage();
} catch (e) {
return;
}
const persistStorage = {
getItem: (name) => {
var _a;
const parse = (str2) => {
if (str2 === null) {
return null;
}
return JSON.parse(str2, options == null ? void 0 : options.reviver);
};
const str = (_a = storage.getItem(name)) != null ? _a : null;
if (str instanceof Promise) {
return str.then(parse);
}
return parse(str);
},
setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),
removeItem: (name) => storage.removeItem(name)
};
return persistStorage;
}
var toThenable = (fn) => (input) => {
try {
const result = fn(input);
if (result instanceof Promise) {
return result;
}
return {
then(onFulfilled) {
return toThenable(onFulfilled)(result);
},
catch(_onRejected) {
return this;
}
};
} catch (e) {
return {
then(_onFulfilled) {
return this;
},
catch(onRejected) {
return toThenable(onRejected)(e);
}
};
}
};
var persistImpl = (config, baseOptions) => (set, get, api) => {
let options = {
storage: createJSONStorage(() => localStorage),
partialize: (state) => state,
version: 0,
merge: (persistedState, currentState) => ({
...currentState,
...persistedState
}),
...baseOptions
};
let hasHydrated = false;
const hydrationListeners = /* @__PURE__ */ new Set();
const finishHydrationListeners = /* @__PURE__ */ new Set();
let storage = options.storage;
if (!storage) {
return config(
(...args) => {
console.warn(
`[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`
);
set(...args);
},
get,
api
);
}
const setItem = () => {
const state = options.partialize({ ...get() });
return storage.setItem(options.name, {
state,
version: options.version
});
};
const savedSetState = api.setState;
api.setState = (state, replace) => {
savedSetState(state, replace);
return setItem();
};
const configResult = config(
(...args) => {
set(...args);
return setItem();
},
get,
api
);
api.getInitialState = () => configResult;
let stateFromStorage;
const hydrate = () => {
var _a, _b;
if (!storage)
return;
hasHydrated = false;
hydrationListeners.forEach((cb) => {
var _a2;
return cb((_a2 = get()) != null ? _a2 : configResult);
});
const postRehydrationCallback = ((_b = options.onRehydrateStorage) == null ? void 0 : _b.call(options, (_a = get()) != null ? _a : configResult)) || void 0;
return toThenable(storage.getItem.bind(storage))(options.name).then((deserializedStorageValue) => {
if (deserializedStorageValue) {
if (typeof deserializedStorageValue.version === "number" && deserializedStorageValue.version !== options.version) {
if (options.migrate) {
const migration = options.migrate(
deserializedStorageValue.state,
deserializedStorageValue.version
);
if (migration instanceof Promise) {
return migration.then((result) => [true, result]);
}
return [true, migration];
}
console.error(
`State loaded from storage couldn't be migrated since no migrate function was provided`
);
} else {
return [false, deserializedStorageValue.state];
}
}
return [false, void 0];
}).then((migrationResult) => {
var _a2;
const [migrated, migratedState] = migrationResult;
stateFromStorage = options.merge(
migratedState,
(_a2 = get()) != null ? _a2 : configResult
);
set(stateFromStorage, true);
if (migrated) {
return setItem();
}
}).then(() => {
postRehydrationCallback == null ? void 0 : postRehydrationCallback(stateFromStorage, void 0);
stateFromStorage = get();
hasHydrated = true;
finishHydrationListeners.forEach((cb) => cb(stateFromStorage));
}).catch((e) => {
postRehydrationCallback == null ? void 0 : postRehydrationCallback(void 0, e);
});
};
api.persist = {
setOptions: (newOptions) => {
options = {
...options,
...newOptions
};
if (newOptions.storage) {
storage = newOptions.storage;
}
},
clearStorage: () => {
storage == null ? void 0 : storage.removeItem(options.name);
},
getOptions: () => options,
rehydrate: () => hydrate(),
hasHydrated: () => hasHydrated,
onHydrate: (cb) => {
hydrationListeners.add(cb);
return () => {
hydrationListeners.delete(cb);
};
},
onFinishHydration: (cb) => {
finishHydrationListeners.add(cb);
return () => {
finishHydrationListeners.delete(cb);
};
}
};
if (!options.skipHydration) {
hydrate();
}
return stateFromStorage || configResult;
};
var persist = persistImpl;
export {
combine,
createJSONStorage,
devtools,
persist,
redux,
subscribeWithSelector
};
//# sourceMappingURL=zustand_middleware.js.map

7
node_modules/.vite/deps/zustand_middleware.js.map generated vendored Normal file

File diff suppressed because one or more lines are too long

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 },
];

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 }),
}
)
);