feat(Issuers): add , edit, delete , toggle ,capability and currencies functionality
This commit is contained in:
14
src/App.jsx
14
src/App.jsx
@@ -12,6 +12,8 @@ import Roles from './pages/Roles';
|
|||||||
import Users from './pages/Users';
|
import Users from './pages/Users';
|
||||||
import Currency from './pages/Currency';
|
import Currency from './pages/Currency';
|
||||||
import Location from './pages/Location';
|
import Location from './pages/Location';
|
||||||
|
import Issuer from './pages/Issuer';
|
||||||
|
import { AuthProvider } from './context/AuthContext';
|
||||||
|
|
||||||
// Protected Route Component
|
// Protected Route Component
|
||||||
const ProtectedRoute = ({ children }) => {
|
const ProtectedRoute = ({ children }) => {
|
||||||
@@ -136,6 +138,16 @@ const AppRoutes = () => {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/issuer"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Layout>
|
||||||
|
<Issuer/>
|
||||||
|
</Layout>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
<Route path="*" element={<Navigate to="/" />} />
|
<Route path="*" element={<Navigate to="/" />} />
|
||||||
@@ -147,9 +159,11 @@ const AppRoutes = () => {
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
|
<AuthProvider>
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</div>
|
</div>
|
||||||
|
</AuthProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,19 @@ import React, { useState } from 'react';
|
|||||||
import { Menu, Sun, Moon, User, LogOut, ChevronDown } from 'lucide-react';
|
import { Menu, Sun, Moon, User, LogOut, ChevronDown } from 'lucide-react';
|
||||||
import { signOut } from '../services/api';
|
import { signOut } from '../services/api';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
const Navbar = ({ onSidebarToggle }) => {
|
const Navbar = ({ onSidebarToggle }) => {
|
||||||
|
let user = null;
|
||||||
|
try {
|
||||||
|
const authContext = useAuth();
|
||||||
|
user = authContext?.user || null;
|
||||||
|
} catch (e) {
|
||||||
|
// useAuth not available, will use localStorage
|
||||||
|
}
|
||||||
|
const userData = user || JSON.parse(localStorage.getItem('user') || '{}') || {};
|
||||||
|
const userName = userData.name && userData.name !== '...' ? userData.name : (userData.email || '...');
|
||||||
|
const userEmail = userData.email || '';
|
||||||
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
|
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
|
||||||
const [isDarkMode, setIsDarkMode] = useState(() => {
|
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||||
return localStorage.getItem('theme') === 'dark' ||
|
return localStorage.getItem('theme') === 'dark' ||
|
||||||
@@ -77,7 +88,7 @@ const Navbar = ({ onSidebarToggle }) => {
|
|||||||
<User className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
<User className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden sm:block text-sm font-medium">
|
<span className="hidden sm:block text-sm font-medium">
|
||||||
{isLoggedIn ? 'Admin' : 'Guest'}
|
{isLoggedIn ? userName : 'Guest'}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -87,10 +98,10 @@ const Navbar = ({ onSidebarToggle }) => {
|
|||||||
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
|
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
|
||||||
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{isLoggedIn ? 'Admin User' : 'Guest'}
|
{isLoggedIn ? userName : 'Guest'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{isLoggedIn ? 'admin@example.com' : ''}
|
{isLoggedIn ? userEmail : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
Shield,
|
Shield,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
MapPin
|
MapPin,
|
||||||
|
Building2
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const Sidebar = ({ isOpen, onToggle }) => {
|
const Sidebar = ({ isOpen, onToggle }) => {
|
||||||
@@ -20,6 +21,7 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
|||||||
{ name: 'Users', href: '/users', icon: Users },
|
{ name: 'Users', href: '/users', icon: Users },
|
||||||
{ name: 'Currency', href: '/currency', icon: DollarSign },
|
{ name: 'Currency', href: '/currency', icon: DollarSign },
|
||||||
{ name: 'Location', href: '/location', icon: MapPin },
|
{ name: 'Location', href: '/location', icon: MapPin },
|
||||||
|
{ name: 'Issuer', href: '/issuer', icon: Building2 },
|
||||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
667
src/pages/Issuer.jsx
Normal file
667
src/pages/Issuer.jsx
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||||
|
import DataTable from '../components/DataTable';
|
||||||
|
import { issuerAPI, cityAPI, currencyAPI } from '../services/api';
|
||||||
|
import { Plus, Trash2, Search, Pencil, Power, Settings, DollarSign } from 'lucide-react';
|
||||||
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||||
|
|
||||||
|
const Issuer = () => {
|
||||||
|
const [issuers, setIssuers] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
|
const [addForm, setAddForm] = useState({
|
||||||
|
name: '',
|
||||||
|
supportEmail: '',
|
||||||
|
cityId: null,
|
||||||
|
postalCode: '',
|
||||||
|
addressDetails: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
name: '',
|
||||||
|
supportEmail: '',
|
||||||
|
cityId: null,
|
||||||
|
postalCode: '',
|
||||||
|
addressDetails: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [editingIssuer, setEditingIssuer] = useState(null);
|
||||||
|
const [capabilitiesModal, setCapabilitiesModal] = useState(null);
|
||||||
|
const [currenciesModal, setCurrenciesModal] = useState(null);
|
||||||
|
const [cities, setCities] = useState([]);
|
||||||
|
const [currencies, setCurrencies] = useState([]);
|
||||||
|
const [selectedCapabilities, setSelectedCapabilities] = useState([]);
|
||||||
|
const [selectedCurrencies, setSelectedCurrencies] = useState([]);
|
||||||
|
const [capabilitiesData, setCapabilitiesData] = useState([]); // Store full capabilities data from API
|
||||||
|
|
||||||
|
// Capabilities options
|
||||||
|
const capabilityOptions = ['TopUpAgent', 'VoucherIssuer', 'PaymentProcessor', 'WalletProvider'];
|
||||||
|
|
||||||
|
// دریافت Issuers
|
||||||
|
const fetchIssuers = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const list = await issuerAPI.list();
|
||||||
|
setIssuers(Array.isArray(list) ? list : []);
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching issuers:', err);
|
||||||
|
const status = err?.response?.status;
|
||||||
|
const errorMsg = getErrorMessage(err);
|
||||||
|
setError(errorMsg);
|
||||||
|
// Only show toast for non-server errors
|
||||||
|
if (status !== 500) {
|
||||||
|
toast.error(errorMsg);
|
||||||
|
} else {
|
||||||
|
toast.error('Server error. Please try again later.');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchIssuers();
|
||||||
|
// Load cities from /api/v1/City
|
||||||
|
const loadCities = async () => {
|
||||||
|
try {
|
||||||
|
const citiesList = await cityAPI.list({ currentPage: 1, pageSize: 1000 });
|
||||||
|
console.log('Cities loaded from /api/v1/City:', citiesList);
|
||||||
|
console.log('Cities count:', citiesList?.length || 0);
|
||||||
|
if (citiesList && citiesList.length > 0) {
|
||||||
|
console.log('First city sample:', citiesList[0]);
|
||||||
|
}
|
||||||
|
setCities(Array.isArray(citiesList) ? citiesList : []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load cities from /api/v1/City:', err);
|
||||||
|
const status = err?.response?.status;
|
||||||
|
// Don't show toast for server errors (500) - it's a server issue
|
||||||
|
if (status !== 500) {
|
||||||
|
toast.error('Failed to load cities');
|
||||||
|
}
|
||||||
|
setCities([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Load currencies
|
||||||
|
const loadCurrencies = async () => {
|
||||||
|
try {
|
||||||
|
const currenciesList = await currencyAPI.list();
|
||||||
|
setCurrencies(Array.isArray(currenciesList) ? currenciesList : []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load currencies:', err);
|
||||||
|
const status = err?.response?.status;
|
||||||
|
// Don't show toast for server errors (500) - it's a server issue
|
||||||
|
if (status !== 500) {
|
||||||
|
console.warn('Currency loading failed but continuing...');
|
||||||
|
}
|
||||||
|
setCurrencies([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadCities();
|
||||||
|
loadCurrencies();
|
||||||
|
}, [fetchIssuers]);
|
||||||
|
|
||||||
|
// --- اضافه کردن Issuer ---
|
||||||
|
const onAddIssuer = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!addForm.name.trim() || !addForm.supportEmail.trim()) {
|
||||||
|
toast.error('Name and Support Email are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const newIssuer = await issuerAPI.create(addForm);
|
||||||
|
await fetchIssuers();
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setAddForm({
|
||||||
|
name: '',
|
||||||
|
supportEmail: '',
|
||||||
|
cityId: null,
|
||||||
|
postalCode: '',
|
||||||
|
addressDetails: ''
|
||||||
|
});
|
||||||
|
toast.success(getSuccessMessage(newIssuer) || 'Issuer added successfully');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(getErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ویرایش Issuer ---
|
||||||
|
const openEdit = async (issuer) => {
|
||||||
|
setEditingIssuer(issuer);
|
||||||
|
try {
|
||||||
|
const issuerDetails = await issuerAPI.getById(issuer.id);
|
||||||
|
console.log('Issuer details from API:', issuerDetails);
|
||||||
|
|
||||||
|
// API returns: name, supportEmail, address (not addressDetails), and may have cityId, postalCode
|
||||||
|
// Use address as addressDetails if addressDetails doesn't exist
|
||||||
|
setEditForm({
|
||||||
|
name: issuerDetails?.name || '',
|
||||||
|
supportEmail: issuerDetails?.supportEmail || '',
|
||||||
|
cityId: issuerDetails?.cityId || null,
|
||||||
|
postalCode: issuerDetails?.postalCode || '',
|
||||||
|
addressDetails: issuerDetails?.addressDetails || issuerDetails?.address || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Edit form set to:', {
|
||||||
|
name: issuerDetails?.name || '',
|
||||||
|
supportEmail: issuerDetails?.supportEmail || '',
|
||||||
|
cityId: issuerDetails?.cityId || null,
|
||||||
|
postalCode: issuerDetails?.postalCode || '',
|
||||||
|
addressDetails: issuerDetails?.addressDetails || issuerDetails?.address || ''
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading issuer details:', err);
|
||||||
|
toast.error('Failed to load issuer details');
|
||||||
|
// Fallback to issuer data from list
|
||||||
|
setEditForm({
|
||||||
|
name: issuer?.name || '',
|
||||||
|
supportEmail: issuer?.supportEmail || '',
|
||||||
|
cityId: issuer?.cityId || null,
|
||||||
|
postalCode: issuer?.postalCode || '',
|
||||||
|
addressDetails: issuer?.addressDetails || issuer?.address || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateIssuer = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!editingIssuer) return;
|
||||||
|
if (!editForm.name.trim() || !editForm.supportEmail.trim()) {
|
||||||
|
toast.error('Name and Support Email are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const updatedIssuer = await issuerAPI.update(editingIssuer.id, editForm);
|
||||||
|
await fetchIssuers();
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
setEditingIssuer(null);
|
||||||
|
toast.success(getSuccessMessage(updatedIssuer) || 'Issuer updated successfully');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(getErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- حذف Issuer ---
|
||||||
|
const onDelete = async (id) => {
|
||||||
|
if (!window.confirm('Are you sure you want to delete this issuer?')) return;
|
||||||
|
try {
|
||||||
|
const result = await issuerAPI.remove(id);
|
||||||
|
await fetchIssuers();
|
||||||
|
toast.success(getSuccessMessage(result) || 'Issuer deleted successfully');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(getErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- تغییر وضعیت فعال بودن ---
|
||||||
|
const onToggleActivation = async (id) => {
|
||||||
|
try {
|
||||||
|
const updated = await issuerAPI.toggleActivation(id);
|
||||||
|
await fetchIssuers();
|
||||||
|
toast.success(getSuccessMessage(updated) || 'Issuer status updated');
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(getErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- مدیریت Capabilities ---
|
||||||
|
const openCapabilitiesModal = async (issuer) => {
|
||||||
|
setCapabilitiesModal(issuer);
|
||||||
|
try {
|
||||||
|
const caps = await issuerAPI.getCapabilities(issuer.id);
|
||||||
|
if (Array.isArray(caps) && caps.length > 0) {
|
||||||
|
setCapabilitiesData(caps);
|
||||||
|
setSelectedCapabilities(caps.filter(c => c.hasCapability).map(c => c.capability));
|
||||||
|
} else {
|
||||||
|
// If no capabilities from API, create default structure
|
||||||
|
const defaultCaps = capabilityOptions.map(cap => ({
|
||||||
|
capability: cap,
|
||||||
|
capabilityName: cap,
|
||||||
|
hasCapability: false
|
||||||
|
}));
|
||||||
|
setCapabilitiesData(defaultCaps);
|
||||||
|
setSelectedCapabilities(Array.isArray(issuer.capabilities) ? issuer.capabilities : []);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback: create default structure
|
||||||
|
const defaultCaps = capabilityOptions.map(cap => ({
|
||||||
|
capability: cap,
|
||||||
|
capabilityName: cap,
|
||||||
|
hasCapability: false
|
||||||
|
}));
|
||||||
|
setCapabilitiesData(defaultCaps);
|
||||||
|
setSelectedCapabilities(Array.isArray(issuer.capabilities) ? issuer.capabilities : []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateCapabilities = async () => {
|
||||||
|
if (!capabilitiesModal) return;
|
||||||
|
try {
|
||||||
|
// Build capabilities array - only include selected capabilities
|
||||||
|
const capabilities = selectedCapabilities
|
||||||
|
.filter(cap => cap && String(cap).trim() !== '') // Filter out empty/invalid capabilities
|
||||||
|
.map(cap => {
|
||||||
|
const capStr = String(cap).trim();
|
||||||
|
const existingCap = capabilitiesData.find(c => c.capability === capStr);
|
||||||
|
|
||||||
|
return {
|
||||||
|
capability: capStr,
|
||||||
|
capabilityName: existingCap?.capabilityName || capStr,
|
||||||
|
hasCapability: true
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(cap => cap.capability); // Final filter to ensure capability is not empty
|
||||||
|
|
||||||
|
console.log('Sending capabilities (selected only):', JSON.stringify(capabilities, null, 2));
|
||||||
|
console.log('Selected capabilities:', selectedCapabilities);
|
||||||
|
|
||||||
|
await issuerAPI.updateCapabilities(capabilitiesModal.id, capabilities);
|
||||||
|
toast.success('Capabilities updated successfully');
|
||||||
|
setCapabilitiesModal(null);
|
||||||
|
await fetchIssuers();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating capabilities:', err);
|
||||||
|
console.error('Error response:', err?.response?.data);
|
||||||
|
console.error('Error details:', err?.response?.data?.errors);
|
||||||
|
toast.error(getErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- مدیریت Allowed Currencies ---
|
||||||
|
const openCurrenciesModal = async (issuer) => {
|
||||||
|
setCurrenciesModal(issuer);
|
||||||
|
try {
|
||||||
|
const allowed = await issuerAPI.getAllowedCurrencies(issuer.id);
|
||||||
|
console.log('Allowed currencies from API:', allowed);
|
||||||
|
if (Array.isArray(allowed) && allowed.length > 0) {
|
||||||
|
// Extract currency codes from the response
|
||||||
|
const codes = allowed.map(c => c.code).filter(code => code);
|
||||||
|
console.log('Extracted currency codes:', codes);
|
||||||
|
setSelectedCurrencies(codes);
|
||||||
|
} else {
|
||||||
|
setSelectedCurrencies([]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading allowed currencies:', err);
|
||||||
|
setSelectedCurrencies([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateCurrencies = async () => {
|
||||||
|
if (!currenciesModal) return;
|
||||||
|
try {
|
||||||
|
const allowedCurrencies = currencies.map(curr => ({
|
||||||
|
currencyCode: curr.currencyCode?.code || curr.code || '',
|
||||||
|
currencyEnglishName: curr.currencyCode?.name || curr.name || '',
|
||||||
|
allowed: selectedCurrencies.includes(curr.currencyCode?.code || curr.code || '')
|
||||||
|
}));
|
||||||
|
await issuerAPI.updateAllowedCurrencies(currenciesModal.id, allowedCurrencies);
|
||||||
|
toast.success('Allowed currencies updated successfully');
|
||||||
|
setCurrenciesModal(null);
|
||||||
|
await fetchIssuers();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(getErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ستونهای جدول ---
|
||||||
|
const columns = useMemo(() => [
|
||||||
|
{ key: 'name', header: 'Name' },
|
||||||
|
{ key: 'supportEmail', header: 'Support Email' },
|
||||||
|
{ key: 'address', header: 'Address' },
|
||||||
|
{ key: 'supportCurrencies', header: 'Support Currencies' },
|
||||||
|
{
|
||||||
|
key: 'capabilities',
|
||||||
|
header: 'Capabilities',
|
||||||
|
render: (val) => (
|
||||||
|
<span>
|
||||||
|
{Array.isArray(val) && val.length > 0
|
||||||
|
? val.join(', ')
|
||||||
|
: '—'}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'isActive',
|
||||||
|
header: 'Active',
|
||||||
|
render: (val) => (val ? '✅' : '❌')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
header: 'Actions',
|
||||||
|
render: (_val, row) => (
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => openEdit(row)}
|
||||||
|
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-blue-100 text-blue-700 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 hover:opacity-90"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" /> Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onToggleActivation(row.id)}
|
||||||
|
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-gray-100 text-gray-700 hover:opacity-90"
|
||||||
|
>
|
||||||
|
<Power className="h-4 w-4 mr-1" /> Toggle
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openCapabilitiesModal(row)}
|
||||||
|
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-purple-100 text-purple-700 hover:opacity-90"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 mr-1" /> Capabilities
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => openCurrenciesModal(row)}
|
||||||
|
className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-green-100 text-green-700 hover:opacity-90"
|
||||||
|
>
|
||||||
|
<DollarSign className="h-4 w-4 mr-1" /> Currencies
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
|
// --- مودال اضافه و ویرایش ---
|
||||||
|
const renderModal = (isOpen, title, onSubmit, formState, setFormState, onClose) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={onClose} />
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-2xl card p-6 relative bg-white rounded-2xl shadow-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">{title}</h3>
|
||||||
|
<form className="space-y-4" onSubmit={onSubmit}>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Name *</label>
|
||||||
|
<input
|
||||||
|
value={formState.name}
|
||||||
|
onChange={(e) => setFormState({ ...formState, name: e.target.value })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
placeholder="Issuer name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Support Email *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={formState.supportEmail}
|
||||||
|
onChange={(e) => setFormState({ ...formState, supportEmail: e.target.value })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
placeholder="support@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{title.includes('Edit') && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">City</label>
|
||||||
|
<select
|
||||||
|
value={formState.cityId ? String(formState.cityId) : ''}
|
||||||
|
onChange={(e) => setFormState({ ...formState, cityId: e.target.value || null })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select City</option>
|
||||||
|
{cities.length > 0 ? (
|
||||||
|
cities.map(city => (
|
||||||
|
<option key={city.id || city.cityId} value={city.id || city.cityId}>
|
||||||
|
{city.cityName || city.name || 'Unknown City'}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<option value="" disabled>Loading cities...</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Postal Code</label>
|
||||||
|
<input
|
||||||
|
value={formState.postalCode}
|
||||||
|
onChange={(e) => setFormState({ ...formState, postalCode: e.target.value })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
placeholder="Postal code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Address Details</label>
|
||||||
|
<textarea
|
||||||
|
value={formState.addressDetails}
|
||||||
|
onChange={(e) => setFormState({ ...formState, addressDetails: e.target.value })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
placeholder="Address details"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!title.includes('Edit') && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">City</label>
|
||||||
|
<select
|
||||||
|
value={formState.cityId ? String(formState.cityId) : ''}
|
||||||
|
onChange={(e) => setFormState({ ...formState, cityId: e.target.value || null })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select City</option>
|
||||||
|
{cities.length > 0 ? (
|
||||||
|
cities.map(city => (
|
||||||
|
<option key={city.id || city.cityId} value={city.id || city.cityId}>
|
||||||
|
{city.cityName || city.name || 'Unknown City'}
|
||||||
|
</option>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<option value="" disabled>Loading cities...</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Postal Code</label>
|
||||||
|
<input
|
||||||
|
value={formState.postalCode}
|
||||||
|
onChange={(e) => setFormState({ ...formState, postalCode: e.target.value })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
placeholder="Postal code"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Address Details</label>
|
||||||
|
<textarea
|
||||||
|
value={formState.addressDetails}
|
||||||
|
onChange={(e) => setFormState({ ...formState, addressDetails: e.target.value })}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
placeholder="Address details"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end gap-x-2">
|
||||||
|
<button type="button" onClick={onClose} className="px-4 py-2 border rounded-lg">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
|
||||||
|
{title.includes('Edit') ? 'Update' : 'Add'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<ToastContainer position="top-right" autoClose={3000} />
|
||||||
|
|
||||||
|
<div className="mb-6 flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Issuers</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Manage issuers: add, edit, activate/deactivate, and delete
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setIsModalOpen(true)} className="btn-primary inline-flex items-center">
|
||||||
|
<Plus className="h-4 w-4 mr-2" /> Add Issuer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderModal(isModalOpen, 'Add Issuer', onAddIssuer, addForm, setAddForm, () => setIsModalOpen(false))}
|
||||||
|
{renderModal(isEditModalOpen, 'Edit Issuer', onUpdateIssuer, editForm, setEditForm, () => setIsEditModalOpen(false))}
|
||||||
|
|
||||||
|
{/* Capabilities Modal */}
|
||||||
|
{capabilitiesModal && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setCapabilitiesModal(null)} />
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-2xl card p-6 relative bg-white rounded-2xl shadow-lg">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Manage Capabilities for {capabilitiesModal.name}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{capabilityOptions.map((cap) => {
|
||||||
|
const checked = selectedCapabilities.includes(cap);
|
||||||
|
return (
|
||||||
|
<label key={cap} className="flex items-center gap-2 p-2 border rounded hover:bg-gray-50">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedCapabilities([...selectedCapabilities, cap]);
|
||||||
|
} else {
|
||||||
|
setSelectedCapabilities(selectedCapabilities.filter(c => c !== cap));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{cap}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-x-2 mt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCapabilitiesModal(null)}
|
||||||
|
className="px-4 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onUpdateCapabilities}
|
||||||
|
className="btn-primary px-4 py-2 rounded-lg"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Allowed Currencies Modal */}
|
||||||
|
{currenciesModal && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setCurrenciesModal(null)} />
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-2xl card p-6 relative bg-white rounded-2xl shadow-lg max-h-[80vh] overflow-y-auto">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Manage Allowed Currencies for {currenciesModal.name}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{currencies.map((curr) => {
|
||||||
|
const code = curr.currencyCode?.code || curr.code || '';
|
||||||
|
const name = curr.currencyCode?.name || curr.name || '';
|
||||||
|
const checked = selectedCurrencies.includes(code);
|
||||||
|
return (
|
||||||
|
<label key={code} className="flex items-center gap-2 p-2 border rounded hover:bg-gray-50">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedCurrencies([...selectedCurrencies, code]);
|
||||||
|
} else {
|
||||||
|
setSelectedCurrencies(selectedCurrencies.filter(c => c !== code));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{code} - {name}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-x-2 mt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCurrenciesModal(null)}
|
||||||
|
className="px-4 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onUpdateCurrencies}
|
||||||
|
className="btn-primary px-4 py-2 rounded-lg"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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={filter}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFilter(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Filter by name, email or address"
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-10">
|
||||||
|
<div className="w-8 h-8 border-4 border-t-transparent border-primary-500 rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DataTable
|
||||||
|
data={issuers.filter(issuer => {
|
||||||
|
if (!filter.trim()) return true;
|
||||||
|
const searchTerm = filter.toLowerCase();
|
||||||
|
const name = (issuer.name || '').toLowerCase();
|
||||||
|
const email = (issuer.supportEmail || '').toLowerCase();
|
||||||
|
const address = (issuer.address || '').toLowerCase();
|
||||||
|
return name.includes(searchTerm) ||
|
||||||
|
email.includes(searchTerm) ||
|
||||||
|
address.includes(searchTerm);
|
||||||
|
})}
|
||||||
|
columns={columns}
|
||||||
|
searchable={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && <div className="mt-4 text-sm text-red-600">{error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Issuer;
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ const Roles = () => {
|
|||||||
onClick={() => setIsModalOpen(false)}
|
onClick={() => setIsModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<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="w-full max-w-2xl card p-6 relative">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Add Role</h3>
|
<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</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">Create a new role with permissions</p>
|
||||||
@@ -170,16 +170,34 @@ const Roles = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-2 text-gray-700 dark:text-gray-300">Permissions</label>
|
<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">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPermissions(permissionOptions.map(p => typeof p === 'string' ? p : p?.name).filter(Boolean))}
|
||||||
|
className="text-xs border px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-primary-50 dark:hover:bg-gray-600 transition"
|
||||||
|
>
|
||||||
|
Select All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPermissions([])}
|
||||||
|
className="text-xs border px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-red-50 dark:hover:bg-gray-600 transition"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 max-h-48 overflow-y-auto border rounded-lg p-3 bg-gray-50 dark:bg-gray-800">
|
||||||
{permissionOptions.map((perm) => {
|
{permissionOptions.map((perm) => {
|
||||||
const permName = typeof perm === 'string' ? perm : perm?.name;
|
const permName = typeof perm === 'string' ? perm : perm?.name;
|
||||||
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
|
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
|
||||||
const checked = selectedPermissions.includes(permName);
|
const checked = selectedPermissions.includes(permName);
|
||||||
return (
|
return (
|
||||||
<label key={permName} className="inline-flex items-center gap-x-2 space-x-reverse justify-start">
|
<label key={permName}
|
||||||
|
className="flex items-start gap-x-2 cursor-pointer border border-gray-200 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 px-2 py-2 shadow-sm hover:border-primary-400 dark:hover:border-primary-600 relative transition"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-4 w-4 text-primary-600 border-gray-300 rounded"
|
className="mt-1 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
@@ -189,37 +207,32 @@ const Roles = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300 mx-2">
|
<span className="text-sm text-gray-700 dark:text-gray-200 flex flex-col">
|
||||||
|
<span
|
||||||
|
title={permDesc || ''}
|
||||||
|
className={permDesc ? 'underline decoration-dotted decoration-gray-400 cursor-help' : ''}
|
||||||
|
>
|
||||||
{permName}
|
{permName}
|
||||||
{permDesc ? (
|
</span>
|
||||||
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 block">{permDesc}</span>
|
{permDesc && (
|
||||||
) : null}
|
<span className="text-xs text-gray-400 dark:text-gray-400 mt-1 ">{permDesc}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-4 p-3 rounded bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
||||||
<button
|
<label className="block text-xs mb-1 text-gray-500 dark:text-gray-400">Custom permissions (comma separated)</label>
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedPermissions([])}
|
|
||||||
className="text-xs text-blue-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
|
<input
|
||||||
value={permissionsInput}
|
value={permissionsInput}
|
||||||
onChange={(e) => setPermissionsInput(e.target.value)}
|
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"
|
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
placeholder="Administrator, RoleManagement"
|
placeholder="Administrator, RoleManagement"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsModalOpen(false)}
|
onClick={() => setIsModalOpen(false)}
|
||||||
@@ -245,7 +258,7 @@ const Roles = () => {
|
|||||||
onClick={() => setIsEditModalOpen(false)}
|
onClick={() => setIsEditModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<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="w-full max-w-2xl card p-6 relative">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Edit Role</h3>
|
<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 and permissions</p>
|
<p className="text-sm text-gray-600 dark:text-gray-400">Update role name and permissions</p>
|
||||||
@@ -262,16 +275,34 @@ const Roles = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm mb-2 text-gray-700 dark:text-gray-300">Permissions</label>
|
<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">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPermissions(permissionOptions.map(p => typeof p === 'string' ? p : p?.name).filter(Boolean))}
|
||||||
|
className="text-xs border px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-primary-50 dark:hover:bg-gray-600 transition"
|
||||||
|
>
|
||||||
|
Select All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPermissions([])}
|
||||||
|
className="text-xs border px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 hover:bg-red-50 dark:hover:bg-gray-600 transition"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 max-h-48 overflow-y-auto border rounded-lg p-3 bg-gray-50 dark:bg-gray-800">
|
||||||
{permissionOptions.map((perm) => {
|
{permissionOptions.map((perm) => {
|
||||||
const permName = typeof perm === 'string' ? perm : perm?.name;
|
const permName = typeof perm === 'string' ? perm : perm?.name;
|
||||||
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
|
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
|
||||||
const checked = selectedPermissions.includes(permName);
|
const checked = selectedPermissions.includes(permName);
|
||||||
return (
|
return (
|
||||||
<label key={permName} className="inline-flex items-center space-x-reverse justify-start">
|
<label key={permName}
|
||||||
|
className="flex items-start gap-x-2 cursor-pointer border border-gray-200 dark:border-gray-700 rounded-md bg-white dark:bg-gray-900 px-2 py-2 shadow-sm hover:border-primary-400 dark:hover:border-primary-600 relative transition"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-4 w-4 text-primary-600 border-gray-300 rounded"
|
className="mt-1 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
@@ -281,37 +312,32 @@ const Roles = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300 mx-2">
|
<span className="text-sm text-gray-700 dark:text-gray-200 flex flex-col">
|
||||||
|
<span
|
||||||
|
title={permDesc || ''}
|
||||||
|
className={permDesc ? 'underline decoration-dotted decoration-gray-400 cursor-help' : ''}
|
||||||
|
>
|
||||||
{permName}
|
{permName}
|
||||||
{permDesc ? (
|
</span>
|
||||||
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 block">{permDesc}</span>
|
{permDesc && (
|
||||||
) : null}
|
<span className="text-xs text-gray-400 dark:text-gray-400 mt-1 ">{permDesc}</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-4 p-3 rounded bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
||||||
<button
|
<label className="block text-xs mb-1 text-gray-500 dark:text-gray-400">Custom permissions (comma separated)</label>
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedPermissions([])}
|
|
||||||
className="text-xs text-blue-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
|
<input
|
||||||
value={permissionsInput}
|
value={permissionsInput}
|
||||||
onChange={(e) => setPermissionsInput(e.target.value)}
|
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"
|
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
placeholder="Administrator, RoleManagement"
|
placeholder="Administrator, RoleManagement"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsEditModalOpen(false)}
|
onClick={() => setIsEditModalOpen(false)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
import React, { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||||
import DataTable from '../components/DataTable';
|
import DataTable from '../components/DataTable';
|
||||||
import { usersAPI, rolesAPI } from '../services/api';
|
import { usersAPI, rolesAPI } from '../services/api';
|
||||||
import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react';
|
import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react';
|
||||||
@@ -6,15 +6,6 @@ import { ToastContainer, toast } from 'react-toastify';
|
|||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||||
|
|
||||||
// debounce ساده
|
|
||||||
function debounce(func, delay) {
|
|
||||||
let timer;
|
|
||||||
return function (...args) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => func.apply(this, args), delay);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Users = () => {
|
const Users = () => {
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -60,8 +51,9 @@ const Users = () => {
|
|||||||
setRoles(list);
|
setRoles(list);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Initial load - fetch all users once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers('');
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}, [fetchUsers, fetchRoles]);
|
}, [fetchUsers, fetchRoles]);
|
||||||
|
|
||||||
@@ -140,9 +132,18 @@ const Users = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- مدیریت رولها ---
|
// --- مدیریت رولها ---
|
||||||
const openRolesModal = (user) => {
|
const openRolesModal = async (user) => {
|
||||||
setRolesModalUser(user);
|
setRolesModalUser(user);
|
||||||
setSelectedRoles(user.roles?.map(r => r.id) || []);
|
try {
|
||||||
|
// دریافت roles کاربر از API
|
||||||
|
const userRoles = await usersAPI.getRoles(user.id);
|
||||||
|
setSelectedRoles(userRoles?.map(r => r.id) || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching user roles:', err);
|
||||||
|
// Fallback to roles from user object
|
||||||
|
setSelectedRoles(user.roles?.map(r => r.id) || []);
|
||||||
|
toast.error('Failed to load user roles');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUpdateRoles = async () => {
|
const onUpdateRoles = async () => {
|
||||||
@@ -151,7 +152,7 @@ const Users = () => {
|
|||||||
await usersAPI.updateRoles(rolesModalUser.id, selectedRoles);
|
await usersAPI.updateRoles(rolesModalUser.id, selectedRoles);
|
||||||
toast.success('Roles updated successfully');
|
toast.success('Roles updated successfully');
|
||||||
setRolesModalUser(null);
|
setRolesModalUser(null);
|
||||||
fetchUsers(filter);
|
await fetchUsers(filter);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(getErrorMessage(err));
|
toast.error(getErrorMessage(err));
|
||||||
}
|
}
|
||||||
@@ -197,9 +198,7 @@ const Users = () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
], [filter]);
|
], []);
|
||||||
|
|
||||||
const handleFilterChange = useMemo(() => debounce(async (v) => fetchUsers(v), 400), [fetchUsers]);
|
|
||||||
|
|
||||||
// --- مودال اضافه و ویرایش ---
|
// --- مودال اضافه و ویرایش ---
|
||||||
const renderModal = (isOpen, title, onSubmit, formState, setFormState, onClose) => {
|
const renderModal = (isOpen, title, onSubmit, formState, setFormState, onClose) => {
|
||||||
@@ -295,7 +294,6 @@ const Users = () => {
|
|||||||
value={filter}
|
value={filter}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFilter(e.target.value);
|
setFilter(e.target.value);
|
||||||
handleFilterChange(e.target.value);
|
|
||||||
}}
|
}}
|
||||||
placeholder="Filter by name, email or role"
|
placeholder="Filter by name, email or role"
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||||
@@ -308,7 +306,22 @@ const Users = () => {
|
|||||||
<div className="w-8 h-8 border-4 border-t-transparent border-primary-500 rounded-full animate-spin" />
|
<div className="w-8 h-8 border-4 border-t-transparent border-primary-500 rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<DataTable data={users} columns={columns} searchable={false} />
|
<DataTable
|
||||||
|
data={users.filter(user => {
|
||||||
|
if (!filter.trim()) return true;
|
||||||
|
const searchTerm = filter.toLowerCase();
|
||||||
|
const firstName = (user.firstName || '').toLowerCase();
|
||||||
|
const lastName = (user.lastName || '').toLowerCase();
|
||||||
|
const email = (user.email || '').toLowerCase();
|
||||||
|
const rolesText = (user.roles || []).map(r => r.name).join(' ').toLowerCase();
|
||||||
|
return firstName.includes(searchTerm) ||
|
||||||
|
lastName.includes(searchTerm) ||
|
||||||
|
email.includes(searchTerm) ||
|
||||||
|
rolesText.includes(searchTerm);
|
||||||
|
})}
|
||||||
|
columns={columns}
|
||||||
|
searchable={false}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <div className="mt-4 text-sm text-red-600">{error}</div>}
|
{error && <div className="mt-4 text-sm text-red-600">{error}</div>}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ export { currencyAPI } from './currencyAPI';
|
|||||||
export { countryAPI } from './countryAPI';
|
export { countryAPI } from './countryAPI';
|
||||||
export { provinceAPI } from './provinceAPI';
|
export { provinceAPI } from './provinceAPI';
|
||||||
export { cityAPI } from './cityAPI';
|
export { cityAPI } from './cityAPI';
|
||||||
|
export { issuerAPI } from './issuerAPI';
|
||||||
export { listPermissions } from './permissionsAPI';
|
export { listPermissions } from './permissionsAPI';
|
||||||
|
|||||||
194
src/services/issuerAPI.js
Normal file
194
src/services/issuerAPI.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import api from './apiClient';
|
||||||
|
|
||||||
|
// -----------------------------
|
||||||
|
// Issuer API
|
||||||
|
// -----------------------------
|
||||||
|
export const issuerAPI = {
|
||||||
|
// GET /api/v1/Issuer (with pagination)
|
||||||
|
async list(params = {}) {
|
||||||
|
const { currentPage = 1, pageSize = 100, ...otherParams } = params;
|
||||||
|
const res = await api.get('/api/v1/Issuer', {
|
||||||
|
params: { currentPage, pageSize, ...otherParams },
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
return res?.data?.data?.data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
// GET /api/v1/Issuer/{id}
|
||||||
|
async getById(id) {
|
||||||
|
const res = await api.get(`/api/v1/Issuer/${encodeURIComponent(id)}`, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
console.log('Full API response:', res?.data);
|
||||||
|
console.log('API response data:', res?.data?.data);
|
||||||
|
return res?.data?.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// POST /api/v1/Issuer
|
||||||
|
async create(issuer) {
|
||||||
|
const payload = {
|
||||||
|
name: String(issuer?.name || ''),
|
||||||
|
supportEmail: String(issuer?.supportEmail || ''),
|
||||||
|
postalCode: String(issuer?.postalCode || ''),
|
||||||
|
addressDetails: String(issuer?.addressDetails || '')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include cityId - use null if empty or invalid
|
||||||
|
if (issuer?.cityId && issuer.cityId !== '' && issuer.cityId !== 'null' && issuer.cityId !== null) {
|
||||||
|
payload.cityId = issuer.cityId;
|
||||||
|
} else {
|
||||||
|
payload.cityId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Issuer Create Payload:', payload);
|
||||||
|
const res = await api.post('/api/v1/Issuer', payload, { skipAuthRedirect: true });
|
||||||
|
return res?.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// PUT /api/v1/Issuer/{id}
|
||||||
|
async update(id, issuer) {
|
||||||
|
const payload = {
|
||||||
|
name: String(issuer?.name || ''),
|
||||||
|
supportEmail: String(issuer?.supportEmail || ''),
|
||||||
|
postalCode: String(issuer?.postalCode || ''),
|
||||||
|
addressDetails: String(issuer?.addressDetails || '')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include cityId - use null if empty or invalid
|
||||||
|
if (issuer?.cityId && issuer.cityId !== '' && issuer.cityId !== 'null' && issuer.cityId !== null) {
|
||||||
|
payload.cityId = issuer.cityId;
|
||||||
|
} else {
|
||||||
|
payload.cityId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Issuer Update Payload:', payload);
|
||||||
|
const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(id)}`, payload, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
return res?.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// DELETE /api/v1/Issuer/{id}
|
||||||
|
async remove(id) {
|
||||||
|
const res = await api.delete(`/api/v1/Issuer/${encodeURIComponent(id)}`, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
return res?.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// PATCH /api/v1/Issuer/{id}/ToggleActivation
|
||||||
|
async toggleActivation(id) {
|
||||||
|
const res = await api.patch(`/api/v1/Issuer/${encodeURIComponent(id)}/ToggleActivation`, null, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
return res?.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// GET /api/v1/Issuer/{id}/Capabilities
|
||||||
|
async getCapabilities(id) {
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/api/v1/Issuer/${encodeURIComponent(id)}/Capabilities`, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
// Response structure: { data: [...], statusCode, isSuccess, ... }
|
||||||
|
const capabilities = res?.data?.data || [];
|
||||||
|
console.log('Capabilities from API:', capabilities);
|
||||||
|
return capabilities;
|
||||||
|
} catch (err) {
|
||||||
|
// Handle 404 gracefully - endpoint might not exist for some issuers
|
||||||
|
if (err?.response?.status === 404) {
|
||||||
|
console.warn('Capabilities endpoint not found (404), returning empty array');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// PUT /api/v1/Issuer/{id}/Capabilities
|
||||||
|
async updateCapabilities(id, capabilities) {
|
||||||
|
// Filter out invalid capabilities and ensure all required fields are present
|
||||||
|
const validCapabilities = Array.isArray(capabilities) ? capabilities
|
||||||
|
.filter(cap => {
|
||||||
|
const capabilityValue = typeof cap === 'string' ? cap : (cap.capability || '');
|
||||||
|
return capabilityValue && String(capabilityValue).trim() !== '';
|
||||||
|
})
|
||||||
|
.map(cap => {
|
||||||
|
const capabilityValue = typeof cap === 'string' ? cap : (cap.capability || '');
|
||||||
|
const capabilityName = typeof cap === 'object' && cap.capabilityName ? cap.capabilityName : capabilityValue;
|
||||||
|
const hasCapability = typeof cap === 'object' ? (cap.hasCapability !== undefined ? cap.hasCapability : true) : true;
|
||||||
|
|
||||||
|
// Ensure capability is a valid string (not empty, not null, not undefined)
|
||||||
|
const capValue = String(capabilityValue).trim();
|
||||||
|
if (!capValue) {
|
||||||
|
return null; // Will be filtered out
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
capability: capValue,
|
||||||
|
capabilityName: String(capabilityName).trim() || capValue,
|
||||||
|
hasCapability: Boolean(hasCapability)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(cap => cap !== null) : [];
|
||||||
|
|
||||||
|
// Wrap in IssuerManageCapabilitiesCommand structure
|
||||||
|
const payload = {
|
||||||
|
capabilities: validCapabilities
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Update Capabilities Payload:', JSON.stringify(payload, null, 2));
|
||||||
|
console.log('Valid capabilities count:', validCapabilities.length);
|
||||||
|
console.log('Capabilities details:', validCapabilities);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(id)}/Capabilities`, payload, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
return res?.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Capabilities update error details:', {
|
||||||
|
status: err?.response?.status,
|
||||||
|
data: err?.response?.data,
|
||||||
|
errors: err?.response?.data?.errors,
|
||||||
|
payload: payload
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// GET /api/v1/Issuer/{id}/AllowedCurrencies
|
||||||
|
async getAllowedCurrencies(id) {
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/api/v1/Issuer/${encodeURIComponent(id)}/AllowedCurrencies`, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
// Response is a direct array: [{code, name, nativeName, symbol, numericCode, decimalPlaces}, ...]
|
||||||
|
const currencies = Array.isArray(res?.data) ? res.data : (res?.data?.data || []);
|
||||||
|
console.log('Allowed Currencies from API:', currencies);
|
||||||
|
return currencies;
|
||||||
|
} catch (err) {
|
||||||
|
// Handle 404 gracefully - endpoint might not exist for some issuers
|
||||||
|
if (err?.response?.status === 404) {
|
||||||
|
console.warn('AllowedCurrencies endpoint not found (404), returning empty array');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// PUT /api/v1/Issuer/{id}/AllowedCurrencies
|
||||||
|
async updateAllowedCurrencies(id, allowedCurrencies) {
|
||||||
|
const payload = {
|
||||||
|
allowedCurrencies: Array.isArray(allowedCurrencies) ? allowedCurrencies.map(curr => ({
|
||||||
|
currencyCode: curr.currencyCode || curr.code || '',
|
||||||
|
currencyEnglishName: curr.currencyEnglishName || curr.name || '',
|
||||||
|
allowed: curr.allowed !== undefined ? curr.allowed : true
|
||||||
|
})) : []
|
||||||
|
};
|
||||||
|
const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(id)}/AllowedCurrencies`, payload, {
|
||||||
|
skipAuthRedirect: true
|
||||||
|
});
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -38,6 +38,14 @@ export const usersAPI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async removeRole(userId,roleId){ try{ const res = await api.delete(`/api/v1/User/${encodeURIComponent(userId)}/Role/${encodeURIComponent(roleId)}`,{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
async removeRole(userId,roleId){ try{ const res = await api.delete(`/api/v1/User/${encodeURIComponent(userId)}/Role/${encodeURIComponent(roleId)}`,{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
||||||
async updateRoles(userId,roleIds=[]){ try{ const res = await api.post(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{roleIds},{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
async updateRoles(userId,roleIds=[]){
|
||||||
|
try{
|
||||||
|
const res = await api.put(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{roleIds},{skipAuthRedirect:true});
|
||||||
|
return res?.data;
|
||||||
|
} catch(err){
|
||||||
|
console.error(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user