feat(TopUpAgent): add , edit, delete , toggle ,...
This commit is contained in:
144
README.md
144
README.md
@@ -1,26 +1,146 @@
|
||||
For testing purposes, you can use these demo credentials:
|
||||
- **Email**: admin@example.com
|
||||
- **Password**: password
|
||||
# Payment Admin Panel Dashboard
|
||||
|
||||
## Project Structure
|
||||
یک داشبورد مدیریتی مدرن برای مدیریت سیستم پرداخت با رابط کاربری زیبا و کاربردی.
|
||||
|
||||
## ویژگیها
|
||||
|
||||
- 🔐 سیستم احراز هویت کامل (ورود و بازیابی رمز عبور)
|
||||
- 📊 داشبورد با نمودارها و آمار
|
||||
- 📍 مدیریت موقعیتهای جغرافیایی (کشور، استان، شهر)
|
||||
- 🏢 مدیریت صادرکنندگان
|
||||
- 🌙 پشتیبانی از حالت تاریک
|
||||
- 📱 طراحی واکنشگرا (Responsive)
|
||||
|
||||
## تکنولوژیهای استفاده شده
|
||||
|
||||
- **React 18** - کتابخانه UI
|
||||
- **Vite** - ابزار ساخت و توسعه
|
||||
- **React Router DOM** - مسیریابی
|
||||
- **Zustand** - مدیریت state
|
||||
- **Axios** - درخواستهای HTTP
|
||||
- **Tailwind CSS** - استایلدهی
|
||||
- **Recharts** - نمودارها
|
||||
- **Lucide React** - آیکونها
|
||||
- **React Toastify** - اعلانها
|
||||
|
||||
## نصب و راهاندازی
|
||||
|
||||
### پیشنیازها
|
||||
|
||||
- Node.js (نسخه 16 یا بالاتر)
|
||||
- npm یا yarn
|
||||
|
||||
### مراحل نصب
|
||||
|
||||
1. کلون کردن پروژه:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd dashboard-issuer
|
||||
```
|
||||
|
||||
2. نصب وابستگیها:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. اجرای پروژه در حالت توسعه:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. ساخت نسخه production:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. پیشنمایش نسخه production:
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## دستورات موجود
|
||||
|
||||
- `npm run dev` - اجرای سرور توسعه
|
||||
- `npm run build` - ساخت نسخه production
|
||||
- `npm run preview` - پیشنمایش نسخه production
|
||||
- `npm run lint` - بررسی کد با ESLint
|
||||
|
||||
## اطلاعات ورود تست
|
||||
|
||||
برای تست سیستم میتوانید از این اطلاعات استفاده کنید:
|
||||
- **ایمیل**: admin@example.com
|
||||
- **رمز عبور**: password
|
||||
|
||||
## ساختار پروژه
|
||||
|
||||
```
|
||||
src//
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Navbar.jsx
|
||||
│ ├── Sidebar.jsx
|
||||
│ └── DataTable.jsx
|
||||
├── pages/
|
||||
│ ├── Dashboard.jsx
|
||||
│ ├── Transactions.jsx
|
||||
│ ├── Settings.jsx
|
||||
│ └── Login.jsx
|
||||
│ ├── Login.jsx
|
||||
│ ├── ForgotPassword.jsx
|
||||
│ ├── Location.jsx
|
||||
│ └── Issuer.jsx
|
||||
├── context/
|
||||
│ └── AuthContext.jsx
|
||||
├── store/
|
||||
│ └── authStore.js
|
||||
├── services/
|
||||
│ └── api.js
|
||||
│ ├── api.js
|
||||
│ ├── apiClient.js
|
||||
│ ├── authAPI.js
|
||||
│ ├── issuerAPI.js
|
||||
│ ├── countryAPI.js
|
||||
│ ├── provinceAPI.js
|
||||
│ ├── cityAPI.js
|
||||
│ ├── currencyAPI.js
|
||||
│ ├── permissionsAPI.js
|
||||
│ ├── generalAPI.js
|
||||
│ └── paymentsAPI.js
|
||||
├── utils/
|
||||
│ └── errorHandler.js
|
||||
├── App.jsx
|
||||
├── main.jsx
|
||||
└── index.css
|
||||
├── main.jsx
|
||||
├── index.css
|
||||
└── layout.css
|
||||
```
|
||||
.
|
||||
|
||||
## صفحات و مسیرها
|
||||
|
||||
- `/` - داشبورد اصلی
|
||||
- `/login` - صفحه ورود
|
||||
- `/forgot-password` - بازیابی رمز عبور
|
||||
- `/location` - مدیریت موقعیتها (کشور، استان، شهر)
|
||||
- `/issuer` - مدیریت صادرکنندگان
|
||||
|
||||
## API Services
|
||||
|
||||
پروژه از سرویسهای API زیر استفاده میکند:
|
||||
|
||||
- **authAPI.js** - احراز هویت و مدیریت کاربر
|
||||
- **issuerAPI.js** - مدیریت صادرکنندگان
|
||||
- **countryAPI.js** - مدیریت کشورها
|
||||
- **provinceAPI.js** - مدیریت استانها
|
||||
- **cityAPI.js** - مدیریت شهرها
|
||||
- **currencyAPI.js** - مدیریت ارزها
|
||||
- **permissionsAPI.js** - مدیریت دسترسیها
|
||||
- **generalAPI.js** - سرویسهای عمومی
|
||||
- **paymentsAPI.js** - مدیریت پرداختها و آمار داشبورد
|
||||
- **apiClient.js** - کلاینت اصلی API
|
||||
- **api.js** - تنظیمات پایه API
|
||||
|
||||
## توسعه
|
||||
|
||||
پروژه از ESLint برای بررسی کیفیت کد استفاده میکند. قبل از commit کردن تغییرات، حتماً کد را بررسی کنید:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## مجوز
|
||||
|
||||
این پروژه خصوصی است.
|
||||
|
||||
62
node_modules/.vite/deps/_metadata.json
generated
vendored
62
node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,81 +1,81 @@
|
||||
{
|
||||
"hash": "5dc29ded",
|
||||
"browserHash": "8decdfc6",
|
||||
"hash": "0b559729",
|
||||
"browserHash": "99577114",
|
||||
"optimized": {
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "071542c9",
|
||||
"fileHash": "ce1a804a",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "2c0ef184",
|
||||
"fileHash": "e0dd02c9",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "97079daa",
|
||||
"fileHash": "ebfb96cd",
|
||||
"needsInterop": true
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "89ede243",
|
||||
"fileHash": "27c8a3be",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.mjs",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "6ec5bbdf",
|
||||
"fileHash": "afb40087",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "cca5920c",
|
||||
"fileHash": "da86903f",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.js",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "d17c5b44",
|
||||
"needsInterop": false
|
||||
},
|
||||
"recharts": {
|
||||
"src": "../../recharts/es6/index.js",
|
||||
"file": "recharts.js",
|
||||
"fileHash": "93550599",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "80080d68",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand/middleware": {
|
||||
"src": "../../zustand/esm/middleware.mjs",
|
||||
"file": "zustand_middleware.js",
|
||||
"fileHash": "1fdb7da8",
|
||||
"fileHash": "20f961ff",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-toastify": {
|
||||
"src": "../../react-toastify/dist/index.mjs",
|
||||
"file": "react-toastify.js",
|
||||
"fileHash": "af965fd8",
|
||||
"fileHash": "92e7f604",
|
||||
"needsInterop": false
|
||||
},
|
||||
"recharts": {
|
||||
"src": "../../recharts/es6/index.js",
|
||||
"file": "recharts.js",
|
||||
"fileHash": "92750bad",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "815a60b7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand/middleware": {
|
||||
"src": "../../zustand/esm/middleware.mjs",
|
||||
"file": "zustand_middleware.js",
|
||||
"fileHash": "5bd61bb2",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-YV3COZNF": {
|
||||
"file": "chunk-YV3COZNF.js"
|
||||
},
|
||||
"chunk-ZXN67JHZ": {
|
||||
"file": "chunk-ZXN67JHZ.js"
|
||||
},
|
||||
"chunk-YV3COZNF": {
|
||||
"file": "chunk-YV3COZNF.js"
|
||||
},
|
||||
"chunk-WQMOH32Y": {
|
||||
"file": "chunk-WQMOH32Y.js"
|
||||
},
|
||||
|
||||
6
node_modules/.vite/deps/recharts.js
generated
vendored
6
node_modules/.vite/deps/recharts.js
generated
vendored
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
clsx_default
|
||||
} from "./chunk-YV3COZNF.js";
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-ZXN67JHZ.js";
|
||||
import {
|
||||
clsx_default
|
||||
} from "./chunk-YV3COZNF.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-WQMOH32Y.js";
|
||||
|
||||
68
src/App.jsx
68
src/App.jsx
@@ -5,14 +5,10 @@ import Sidebar from './components/Sidebar';
|
||||
import Navbar from './components/Navbar';
|
||||
import Login from './pages/Login';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Transactions from './pages/Transactions';
|
||||
import Settings from './pages/Settings';
|
||||
|
||||
import ForgotPassword from './pages/ForgotPassword';
|
||||
import Roles from './pages/Roles';
|
||||
import Users from './pages/Users';
|
||||
import Currency from './pages/Currency';
|
||||
import Location from './pages/Location';
|
||||
import Issuer from './pages/Issuer';
|
||||
import TopUpAgent from './pages/TopUpAgent';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
|
||||
// Protected Route Component
|
||||
@@ -57,7 +53,6 @@ const Layout = ({ children }) => {
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,57 +72,6 @@ const AppRoutes = () => {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/transactions"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Transactions />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Settings />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/roles"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Roles />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/users"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Users/>
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/currency"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Currency/>
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/location"
|
||||
element={
|
||||
@@ -139,17 +83,15 @@ const AppRoutes = () => {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/issuer"
|
||||
path="/topup-agent"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Issuer/>
|
||||
<TopUpAgent/>
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -102,6 +102,11 @@ const Navbar = ({ onSidebarToggle }) => {
|
||||
{userEmail}
|
||||
</p>
|
||||
)}
|
||||
{isLoggedIn && userData.issuerName && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
Issuer: {userData.issuerName}
|
||||
</p>
|
||||
)}
|
||||
{isLoggedIn && userData.role && (
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||
نقش: {userData.role}
|
||||
|
||||
@@ -2,27 +2,16 @@ import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
CreditCard,
|
||||
Settings,
|
||||
Menu,
|
||||
X,
|
||||
Users,
|
||||
Shield,
|
||||
DollarSign,
|
||||
MapPin,
|
||||
Building2
|
||||
Wallet,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
|
||||
const Sidebar = ({ isOpen, onToggle }) => {
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||
// { name: 'Transactions', href: '/transactions', icon: CreditCard },
|
||||
{ name: 'Roles', href: '/roles', icon: Shield },
|
||||
{ name: 'Users', href: '/users', icon: Users },
|
||||
{ name: 'Currency', href: '/currency', icon: DollarSign },
|
||||
{ name: 'Location', href: '/location', icon: MapPin },
|
||||
{ name: 'Issuer', href: '/issuer', icon: Building2 },
|
||||
// { name: 'Settings', href: '/settings', icon: Settings },
|
||||
{ name: 'TopUp Agent', href: '/topup-agent', icon: Wallet },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,658 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { currencyAPI } from '../services/api';
|
||||
import { Plus, Search, Power, Wrench, Settings, DollarSign, Shield } from 'lucide-react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
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 Currency = () => {
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const [addForm, setAddForm] = useState({ currencyCode: '' });
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// Modals for editing
|
||||
const [permissionsModal, setPermissionsModal] = useState(null);
|
||||
const [limitsModal, setLimitsModal] = useState(null);
|
||||
const [feesModal, setFeesModal] = useState(null);
|
||||
const [maintenanceModal, setMaintenanceModal] = useState(null);
|
||||
const [maintenanceMessage, setMaintenanceMessage] = useState('');
|
||||
|
||||
// Form states for modals
|
||||
const [permissionsForm, setPermissionsForm] = useState({
|
||||
depositEnabled: false,
|
||||
cashOutEnabled: false,
|
||||
exchangeEnabled: false,
|
||||
purchaseEnabled: false,
|
||||
transferFromEnabled: false,
|
||||
transferToEnabled: false,
|
||||
voucherCreationEnabled: false,
|
||||
voucherRedeemEnabled: false,
|
||||
});
|
||||
const [limitsForm, setLimitsForm] = useState({
|
||||
depositLimits: { min: null, max: null },
|
||||
cashOutLimits: { min: null, max: null },
|
||||
transferLimits: { min: null, max: null },
|
||||
voucherLimits: { min: null, max: null },
|
||||
});
|
||||
const [feesForm, setFeesForm] = useState({
|
||||
depositFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
cashOutFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
transferFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
exchangeFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
generateVoucherFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
expireVoucherSystemFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
});
|
||||
|
||||
// دریافت ارزها
|
||||
const fetchCurrencies = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const list = await currencyAPI.list();
|
||||
setCurrencies(Array.isArray(list) ? list : []);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrencies();
|
||||
}, [fetchCurrencies]);
|
||||
|
||||
// --- اضافه کردن ارز ---
|
||||
const onAddCurrency = async (e) => {
|
||||
e.preventDefault();
|
||||
const { currencyCode } = addForm;
|
||||
const code = currencyCode.trim().toUpperCase();
|
||||
|
||||
if (!code) {
|
||||
toast.error('Please enter a currency code');
|
||||
return;
|
||||
}
|
||||
|
||||
// اعتبارسنجی: کد ارز باید 3 حرف باشد
|
||||
if (code.length !== 3) {
|
||||
toast.error('Currency code must be exactly 3 characters (e.g., USD, EUR)');
|
||||
return;
|
||||
}
|
||||
|
||||
// بررسی وجود ارز در لیست (برای جلوگیری از درخواست غیرضروری)
|
||||
const exists = currencies.some(c => c.currencyCode?.code?.toUpperCase() === code);
|
||||
if (exists) {
|
||||
toast.error(`Currency ${code} already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await currencyAPI.create(code);
|
||||
await fetchCurrencies();
|
||||
setIsModalOpen(false);
|
||||
setAddForm({ currencyCode: '' });
|
||||
toast.success(getSuccessMessage(result) || 'Currency added successfully');
|
||||
} catch (err) {
|
||||
console.error('Error creating currency:', err);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- تغییر وضعیت فعال بودن ---
|
||||
const onToggleActivation = async (currencyCode) => {
|
||||
try {
|
||||
const result = await currencyAPI.toggleActivation(currencyCode);
|
||||
await fetchCurrencies();
|
||||
toast.success(getSuccessMessage(result) || 'Currency activation status updated');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- فعال/غیرفعال کردن Maintenance ---
|
||||
const onEnableMaintenance = async (currencyCode, message = '') => {
|
||||
try {
|
||||
const result = await currencyAPI.enableMaintenance(currencyCode, message || null);
|
||||
await fetchCurrencies();
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
toast.success(getSuccessMessage(result) || 'Maintenance enabled');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
const onDisableMaintenance = async (currencyCode) => {
|
||||
try {
|
||||
const result = await currencyAPI.disableMaintenance(currencyCode);
|
||||
await fetchCurrencies();
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
toast.success(getSuccessMessage(result) || 'Maintenance disabled');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- بهروزرسانی Permissions ---
|
||||
const onUpdatePermissions = async (currencyCode, permissions) => {
|
||||
try {
|
||||
const result = await currencyAPI.updatePermissions(currencyCode, permissions);
|
||||
await fetchCurrencies();
|
||||
setPermissionsModal(null);
|
||||
toast.success(getSuccessMessage(result) || 'Permissions updated successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- بهروزرسانی Limits ---
|
||||
const onUpdateLimits = async (currencyCode, limits) => {
|
||||
try {
|
||||
const result = await currencyAPI.updateLimits(currencyCode, limits);
|
||||
await fetchCurrencies();
|
||||
setLimitsModal(null);
|
||||
toast.success(getSuccessMessage(result) || 'Limits updated successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- بهروزرسانی Fees ---
|
||||
const onUpdateFees = async (currencyCode, fees) => {
|
||||
try {
|
||||
const result = await currencyAPI.updateFees(currencyCode, fees);
|
||||
await fetchCurrencies();
|
||||
setFeesModal(null);
|
||||
toast.success(getSuccessMessage(result) || 'Fees updated successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- ستونهای جدول ---
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'currencyCode',
|
||||
header: 'Currency',
|
||||
render: (val) => (
|
||||
<div>
|
||||
<div className="font-semibold">{val?.code || '—'}</div>
|
||||
<div className="text-xs text-gray-500">{val?.name || ''}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'currencyCode',
|
||||
header: 'Symbol',
|
||||
render: (val) => <span className="font-mono">{val?.symbol || '—'}</span>,
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
header: 'Active',
|
||||
render: (val) => (
|
||||
<span className={`px-2 py-1 rounded text-xs ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? '✅ Yes' : '❌ No'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'isUnderMaintenance',
|
||||
header: 'Maintenance',
|
||||
render: (val, row) => (
|
||||
<span className={`px-2 py-1 rounded text-xs ${val ? 'bg-yellow-100 text-yellow-800' : 'bg-gray-100 text-gray-800'}`}>
|
||||
{val ? '⚠️ Yes' : '✓ No'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'permissions',
|
||||
header: 'Permissions',
|
||||
render: (val) => {
|
||||
if (!val) return '—';
|
||||
const enabled = Object.values(val).filter(Boolean).length;
|
||||
return <span className="text-sm">{enabled} enabled</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'Actions',
|
||||
render: (_val, row) => {
|
||||
const code = row.currencyCode?.code;
|
||||
if (!code) return '—';
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={() => onToggleActivation(code)}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-blue-100 text-blue-700 hover:opacity-90"
|
||||
title="Toggle Activation"
|
||||
>
|
||||
<Power className="h-3 w-3 mr-1" /> Toggle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setMaintenanceModal(row);
|
||||
setMaintenanceMessage(row.maintenanceMessage || '');
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-yellow-100 text-yellow-700 hover:opacity-90"
|
||||
title="Maintenance"
|
||||
>
|
||||
<Wrench className="h-3 w-3 mr-1" /> Maintenance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setPermissionsModal(row);
|
||||
setPermissionsForm(row.permissions || {
|
||||
depositEnabled: false,
|
||||
cashOutEnabled: false,
|
||||
exchangeEnabled: false,
|
||||
purchaseEnabled: false,
|
||||
transferFromEnabled: false,
|
||||
transferToEnabled: false,
|
||||
voucherCreationEnabled: false,
|
||||
voucherRedeemEnabled: false,
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-purple-100 text-purple-700 hover:opacity-90"
|
||||
title="Permissions"
|
||||
>
|
||||
<Shield className="h-3 w-3 mr-1" /> Permissions
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLimitsModal(row);
|
||||
setLimitsForm(row.limits || {
|
||||
depositLimits: { min: null, max: null },
|
||||
cashOutLimits: { min: null, max: null },
|
||||
transferLimits: { min: null, max: null },
|
||||
voucherLimits: { min: null, max: null },
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-green-100 text-green-700 hover:opacity-90"
|
||||
title="Limits"
|
||||
>
|
||||
<Settings className="h-3 w-3 mr-1" /> Limits
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFeesModal(row);
|
||||
setFeesForm(row.fees || {
|
||||
depositFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
cashOutFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
transferFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
exchangeFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
generateVoucherFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
expireVoucherSystemFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-indigo-100 text-indigo-700 hover:opacity-90"
|
||||
title="Fees"
|
||||
>
|
||||
<DollarSign className="h-3 w-3 mr-1" /> Fees
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
], []);
|
||||
|
||||
const handleFilterChange = useMemo(() => debounce(async (v) => {
|
||||
// Filter is handled by DataTable component
|
||||
}, 400), []);
|
||||
|
||||
// --- مودال اضافه کردن ارز ---
|
||||
const renderAddModal = () => {
|
||||
if (!isModalOpen) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsModalOpen(false)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md card p-6 relative bg-white rounded-2xl shadow-lg">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Add Currency</h3>
|
||||
<form className="space-y-4" onSubmit={onAddCurrency}>
|
||||
<div>
|
||||
<input
|
||||
value={addForm.currencyCode}
|
||||
onChange={(e) => setAddForm({ currencyCode: e.target.value.toUpperCase() })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
placeholder="Currency Code (e.g., USD, EUR)"
|
||||
maxLength={3}
|
||||
pattern="[A-Z]{3}"
|
||||
title="Enter a 3-letter currency code"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Enter a 3-letter ISO currency code</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Maintenance ---
|
||||
const renderMaintenanceModal = () => {
|
||||
if (!maintenanceModal) return null;
|
||||
const code = maintenanceModal.currencyCode?.code;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => {
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
}} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md card p-6 relative bg-white rounded-2xl shadow-lg">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Maintenance: {code}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Maintenance Message (optional)</label>
|
||||
<textarea
|
||||
value={maintenanceMessage}
|
||||
onChange={(e) => setMaintenanceMessage(e.target.value)}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
rows={3}
|
||||
placeholder="Enter maintenance message..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => onEnableMaintenance(code, maintenanceMessage)}
|
||||
className="flex-1 px-4 py-2 bg-yellow-500 text-white rounded-lg hover:opacity-90"
|
||||
>
|
||||
Enable Maintenance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDisableMaintenance(code)}
|
||||
className="flex-1 px-4 py-2 bg-green-500 text-white rounded-lg hover:opacity-90"
|
||||
>
|
||||
Disable Maintenance
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => {
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
}} className="w-full px-4 py-2 border rounded-lg">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Permissions ---
|
||||
const renderPermissionsModal = () => {
|
||||
if (!permissionsModal) return null;
|
||||
const code = permissionsModal.currencyCode?.code;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onUpdatePermissions(code, permissionsForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setPermissionsModal(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-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Permissions: {code}</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
{Object.keys(permissionsForm).map((key) => (
|
||||
<label key={key} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={permissionsForm[key]}
|
||||
onChange={(e) => setPermissionsForm({ ...permissionsForm, [key]: e.target.checked })}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</span>
|
||||
</label>
|
||||
))}
|
||||
<div className="flex justify-end gap-x-2 mt-4">
|
||||
<button type="button" onClick={() => setPermissionsModal(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Limits ---
|
||||
const renderLimitsModal = () => {
|
||||
if (!limitsModal) return null;
|
||||
const code = limitsModal.currencyCode?.code;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onUpdateLimits(code, limitsForm);
|
||||
};
|
||||
|
||||
const updateLimit = (category, type, value) => {
|
||||
setLimitsForm({
|
||||
...limitsForm,
|
||||
[category]: {
|
||||
...limitsForm[category],
|
||||
[type]: value === '' ? null : parseFloat(value) || null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setLimitsModal(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-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Limits: {code}</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{Object.keys(limitsForm).map((category) => (
|
||||
<div key={category} className="border rounded-lg p-3">
|
||||
<h4 className="font-medium mb-2">{category.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Min</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limitsForm[category].min ?? ''}
|
||||
onChange={(e) => updateLimit(category, 'min', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Min"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Max</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limitsForm[category].max ?? ''}
|
||||
onChange={(e) => updateLimit(category, 'max', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Max"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end gap-x-2 mt-4">
|
||||
<button type="button" onClick={() => setLimitsModal(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Fees ---
|
||||
const renderFeesModal = () => {
|
||||
if (!feesModal) return null;
|
||||
const code = feesModal.currencyCode?.code;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onUpdateFees(code, feesForm);
|
||||
};
|
||||
|
||||
const updateFee = (category, field, value) => {
|
||||
setFeesForm({
|
||||
...feesForm,
|
||||
[category]: {
|
||||
...feesForm[category],
|
||||
[field]: value === '' ? null : parseFloat(value) || null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setFeesModal(null)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-3xl card p-6 relative bg-white rounded-2xl shadow-lg max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fees: {code}</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{Object.keys(feesForm).map((category) => (
|
||||
<div key={category} className="border rounded-lg p-3">
|
||||
<h4 className="font-medium mb-2">{category.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Percent</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].percent ?? ''}
|
||||
onChange={(e) => updateFee(category, 'percent', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Percent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Fixed</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].fixed ?? ''}
|
||||
onChange={(e) => updateFee(category, 'fixed', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Fixed"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Min Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].minAmount ?? ''}
|
||||
onChange={(e) => updateFee(category, 'minAmount', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Min Amount"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Max Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].maxAmount ?? ''}
|
||||
onChange={(e) => updateFee(category, 'maxAmount', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Max Amount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end gap-x-2 mt-4">
|
||||
<button type="button" onClick={() => setFeesModal(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Save</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">Currencies</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Manage currencies: add, activate/deactivate, configure maintenance, permissions, limits, and fees
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(true)} className="btn-primary inline-flex items-center">
|
||||
<Plus className="h-4 w-4 mr-2" /> Add Currency
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{renderAddModal()}
|
||||
{renderMaintenanceModal()}
|
||||
{renderPermissionsModal()}
|
||||
{renderLimitsModal()}
|
||||
{renderFeesModal()}
|
||||
|
||||
<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);
|
||||
handleFilterChange(e.target.value);
|
||||
}}
|
||||
placeholder="Filter by currency code or name"
|
||||
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={currencies.filter(c => {
|
||||
if (!filter) return true;
|
||||
const search = filter.toLowerCase();
|
||||
const code = c.currencyCode?.code?.toLowerCase() || '';
|
||||
const name = c.currencyCode?.name?.toLowerCase() || '';
|
||||
return code.includes(search) || name.includes(search);
|
||||
})}
|
||||
columns={columns}
|
||||
searchable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && <div className="mt-4 text-sm text-red-600">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Currency;
|
||||
|
||||
1788
src/pages/Issuer.jsx
1788
src/pages/Issuer.jsx
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { CreditCard, Eye, EyeOff } from 'lucide-react';
|
||||
import { login as apiLogin } from '../services/api';
|
||||
import { CreditCard, Eye, EyeOff, Building2 } from 'lucide-react';
|
||||
import { getUserIssuers, login as apiLogin } from '../services/api';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { getErrorMessage } from '../utils/errorHandler';
|
||||
|
||||
@@ -11,6 +11,9 @@ const Login = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [issuers, setIssuers] = useState([]);
|
||||
const [selectedIssuer, setSelectedIssuer] = useState(null);
|
||||
const [showIssuerSelection, setShowIssuerSelection] = useState(false);
|
||||
|
||||
const setLoggedIn = useAuthStore((s) => s.setLoggedIn);
|
||||
const setUser = useAuthStore((s) => s.setUser);
|
||||
@@ -22,18 +25,50 @@ const Login = () => {
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const result = await apiLogin(email, password);
|
||||
// Step 1: Get user issuers
|
||||
const issuerList = await getUserIssuers(email, password);
|
||||
|
||||
if (!issuerList || issuerList.length === 0) {
|
||||
setError('No issuers found for this account. Please contact support.');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If exactly one issuer, automatically sign in
|
||||
if (issuerList.length === 1) {
|
||||
const issuer = issuerList[0];
|
||||
await handleSignIn(issuer.id || issuer.issuerId, issuer.name || issuer.issuerName || issuer.title);
|
||||
return;
|
||||
}
|
||||
|
||||
// If more than one issuer, show selection UI
|
||||
if (issuerList.length > 1) {
|
||||
setIssuers(issuerList);
|
||||
setShowIssuerSelection(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err));
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignIn = async (issuerId, issuerName) => {
|
||||
try {
|
||||
const result = await apiLogin(email, password, issuerId);
|
||||
// بررسی اینکه آیا پاسخ موفقیتآمیز است (پشتیبانی از isSuccess و IsSuccess)
|
||||
const isSuccess = result?.isSuccess ?? result?.IsSuccess;
|
||||
|
||||
if (result && (isSuccess === true || isSuccess === undefined)) {
|
||||
// استخراج اطلاعات کاربر از response
|
||||
// فرمت جدید: { data: { email: "..." }, isSuccess: true, ... }
|
||||
const userEmail = result.data?.email || result.email || email;
|
||||
const userData = {
|
||||
email: userEmail,
|
||||
userName: userEmail,
|
||||
name: userEmail.split('@')[0],
|
||||
issuerId: issuerId,
|
||||
issuerName: issuerName,
|
||||
...result.data,
|
||||
...result
|
||||
};
|
||||
@@ -42,7 +77,6 @@ const Login = () => {
|
||||
setLoggedIn(true);
|
||||
navigate('/');
|
||||
} else {
|
||||
// اگر isSuccess false باشد، خطا نمایش داده میشود (در catch block)
|
||||
setError('Invalid email or password');
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -52,6 +86,13 @@ const Login = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleIssuerSelect = async (issuer) => {
|
||||
setSelectedIssuer(issuer);
|
||||
setLoading(true);
|
||||
setError('');
|
||||
await handleSignIn(issuer.id || issuer.issuerId, issuer.name || issuer.issuerName || issuer.title);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
@@ -61,7 +102,7 @@ const Login = () => {
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold text-gray-900 dark:text-white">
|
||||
Khalij Finance Admin
|
||||
Khalij Finance Issuer Admin
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
||||
Sign in to your account
|
||||
@@ -70,82 +111,146 @@ const Login = () => {
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10 border border-gray-200 dark:border-gray-700">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-md text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{!showIssuerSelection ? (
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-md text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="input-field"
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Email address
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="input-field"
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="input-field pr-10"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Password
|
||||
</label>
|
||||
<div className="mt-1 relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="input-field pr-10"
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-5 w-5 text-gray-400" />
|
||||
) : (
|
||||
<Eye className="h-5 w-5 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-5 w-5 text-gray-400" />
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
<Eye className="h-5 w-5 text-gray-400" />
|
||||
'Sign in'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||
Select an Issuer
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Please select the issuer you want to sign in with:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 text-red-700 dark:text-red-300 px-4 py-3 rounded-md text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{issuers.map((issuer, index) => (
|
||||
<button
|
||||
key={issuer.id || issuer.issuerId || index}
|
||||
type="button"
|
||||
onClick={() => handleIssuerSelect(issuer)}
|
||||
disabled={loading}
|
||||
className="w-full flex items-center p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<Building2 className="h-6 w-6 text-primary-600 dark:text-primary-400" />
|
||||
</div>
|
||||
<div className="ml-4 flex-1 text-left">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{issuer.name || issuer.issuerName || issuer.title || `Issuer ${index + 1}`}
|
||||
</div>
|
||||
{issuer.description && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{issuer.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{loading && selectedIssuer?.id === (issuer.id || issuer.issuerId) && (
|
||||
<svg className="animate-spin h-5 w-5 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowIssuerSelection(false);
|
||||
setIssuers([]);
|
||||
setSelectedIssuer(null);
|
||||
setError('');
|
||||
}}
|
||||
className="w-full text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
'Sign in'
|
||||
)}
|
||||
← Back to login
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { rolesAPI, listPermissions } from '../services/api';
|
||||
import { Plus, Trash2, Search, Pencil } from 'lucide-react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||
|
||||
const Roles = () => {
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [permissionsInput, setPermissionsInput] = 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, setPermissionOptions] = useState([]);
|
||||
|
||||
const fetchRoles = async (q = '') => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const list = await rolesAPI.list(q);
|
||||
setRoles(list);
|
||||
} catch (e) {
|
||||
const errorMsg = getErrorMessage(e);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
(async () => {
|
||||
try {
|
||||
const perms = await listPermissions();
|
||||
setPermissionOptions(Array.isArray(perms) ? perms : []);
|
||||
} catch (_) {
|
||||
setPermissionOptions([]);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onAddRole = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
const perms = selectedPermissions.length > 0
|
||||
? selectedPermissions
|
||||
: permissionsInput.split(',').map(p => p.trim()).filter(Boolean);
|
||||
try {
|
||||
await rolesAPI.create({ name: name.trim(), permissions: perms });
|
||||
setName('');
|
||||
setPermissionsInput('');
|
||||
setSelectedPermissions([]);
|
||||
await fetchRoles(nameFilter);
|
||||
setIsModalOpen(false);
|
||||
toast.success('Role created successfully');
|
||||
} catch (err) {
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (id) => {
|
||||
try {
|
||||
await rolesAPI.remove(id);
|
||||
await fetchRoles(nameFilter);
|
||||
toast.success('Role deleted successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
const openEdit = (role) => {
|
||||
setEditingRole(role);
|
||||
setName(role.name || '');
|
||||
setSelectedPermissions(Array.isArray(role.permissions) ? role.permissions : []);
|
||||
setPermissionsInput('');
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const onUpdateRole = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!editingRole) return;
|
||||
const perms = selectedPermissions.length > 0
|
||||
? selectedPermissions
|
||||
: permissionsInput.split(',').map(p => p.trim()).filter(Boolean);
|
||||
try {
|
||||
await rolesAPI.update(editingRole.id, { name: name.trim(), permissions: perms });
|
||||
await fetchRoles(nameFilter);
|
||||
setIsEditModalOpen(false);
|
||||
setEditingRole(null);
|
||||
setName('');
|
||||
setPermissionsInput('');
|
||||
setSelectedPermissions([]);
|
||||
toast.success('Role updated successfully');
|
||||
} catch (err) {
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'permissions', header: 'Permissions', render: (val) => Array.isArray(val) ? val.join(', ') : '' },
|
||||
{ key: 'userType', header: 'User Type' },
|
||||
{ key: 'actions', header: 'Actions', render: (_val, row) => (
|
||||
<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"
|
||||
>
|
||||
<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 dark:bg-red-900 dark:text-red-300 hover:opacity-90"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" /> Delete
|
||||
</button>
|
||||
</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">Roles</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">Manage roles: add, filter by name, and delete</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="btn-primary inline-flex items-center"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" /> Add Role
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add Role Modal */}
|
||||
{isModalOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
/>
|
||||
<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">
|
||||
<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</p>
|
||||
</div>
|
||||
<form className="space-y-4" onSubmit={onAddRole}>
|
||||
<div>
|
||||
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Role Name</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="e.g., Admin"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-2 text-gray-700 dark:text-gray-300">Permissions</label>
|
||||
<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) => {
|
||||
const permName = typeof perm === 'string' ? perm : perm?.name;
|
||||
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
|
||||
const checked = selectedPermissions.includes(permName);
|
||||
return (
|
||||
<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
|
||||
type="checkbox"
|
||||
className="mt-1 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedPermissions((prev) => [...prev, permName]);
|
||||
} else {
|
||||
setSelectedPermissions((prev) => prev.filter(p => p !== permName));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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}
|
||||
</span>
|
||||
{permDesc && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-400 mt-1 ">{permDesc}</span>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-4 p-3 rounded bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
||||
<label className="block text-xs mb-1 text-gray-500 dark:text-gray-400">Custom permissions (comma separated)</label>
|
||||
<input
|
||||
value={permissionsInput}
|
||||
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-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Administrator, RoleManagement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="inline-flex items-center px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn-primary inline-flex items-center">
|
||||
<Plus className="h-4 w-4 mr-2" /> Create Role
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Edit Role Modal */}
|
||||
{isEditModalOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-40"
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
/>
|
||||
<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">
|
||||
<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 and permissions</p>
|
||||
</div>
|
||||
<form className="space-y-4" onSubmit={onUpdateRole}>
|
||||
<div>
|
||||
<label className="block text-sm mb-1 text-gray-700 dark:text-gray-300">Role Name</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="e.g., Admin"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-2 text-gray-700 dark:text-gray-300">Permissions</label>
|
||||
<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) => {
|
||||
const permName = typeof perm === 'string' ? perm : perm?.name;
|
||||
const permDesc = typeof perm === 'object' ? perm?.description : undefined;
|
||||
const checked = selectedPermissions.includes(permName);
|
||||
return (
|
||||
<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
|
||||
type="checkbox"
|
||||
className="mt-1 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||
checked={checked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedPermissions((prev) => [...prev, permName]);
|
||||
} else {
|
||||
setSelectedPermissions((prev) => prev.filter(p => p !== permName));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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}
|
||||
</span>
|
||||
{permDesc && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-400 mt-1 ">{permDesc}</span>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-4 p-3 rounded bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
||||
<label className="block text-xs mb-1 text-gray-500 dark:text-gray-400">Custom permissions (comma separated)</label>
|
||||
<input
|
||||
value={permissionsInput}
|
||||
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-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Administrator, RoleManagement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
className="inline-flex items-center px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn-primary inline-flex items-center">
|
||||
<Pencil className="h-4 w-4 mr-2" /> Update Role
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Filter by name */}
|
||||
<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={nameFilter}
|
||||
onChange={async (e) => {
|
||||
const v = e.target.value;
|
||||
setNameFilter(v);
|
||||
await fetchRoles(v);
|
||||
}}
|
||||
placeholder="Filter by role name"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={roles}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
searchable={false}
|
||||
className=""
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 text-sm text-red-600 dark:text-red-400">{error}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Roles;
|
||||
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Save, Key, Globe, DollarSign, Bell, Shield, Database } from 'lucide-react';
|
||||
|
||||
const Settings = () => {
|
||||
const [settings, setSettings] = useState({
|
||||
paymentApiKey: '',
|
||||
webhookUrl: '',
|
||||
currency: 'USD',
|
||||
notifications: {
|
||||
email: true,
|
||||
sms: false,
|
||||
webhook: true
|
||||
},
|
||||
security: {
|
||||
twoFactor: false,
|
||||
sessionTimeout: 30
|
||||
},
|
||||
data: {
|
||||
retentionPeriod: 365,
|
||||
autoBackup: true
|
||||
}
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [activeTab, setActiveTab] = useState('payment');
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const loadSettings = () => {
|
||||
const savedSettings = localStorage.getItem('adminSettings');
|
||||
if (savedSettings) {
|
||||
setSettings(JSON.parse(savedSettings));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (section, field, value) => {
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleDirectChange = (field, value) => {
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
localStorage.setItem('adminSettings', JSON.stringify(settings));
|
||||
setMessage('Settings saved successfully!');
|
||||
|
||||
setTimeout(() => setMessage(''), 3000);
|
||||
} catch (error) {
|
||||
setMessage('Failed to save settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'payment', label: 'Payment', icon: DollarSign },
|
||||
{ id: 'notifications', label: 'Notifications', icon: Bell },
|
||||
{ id: 'security', label: 'Security', icon: Shield },
|
||||
{ id: 'data', label: 'Data', icon: Database }
|
||||
];
|
||||
|
||||
const currencies = [
|
||||
{ value: 'USD', label: 'US Dollar (USD)' },
|
||||
{ value: 'EUR', label: 'Euro (EUR)' },
|
||||
{ value: 'GBP', label: 'British Pound (GBP)' },
|
||||
{ value: 'CAD', label: 'Canadian Dollar (CAD)' },
|
||||
{ value: 'AUD', label: 'Australian Dollar (AUD)' },
|
||||
{ value: 'JPY', label: 'Japanese Yen (JPY)' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Settings</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">Configure your payment system preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
{message && (
|
||||
<div className={`mb-6 p-4 rounded-lg ${
|
||||
message.includes('success')
|
||||
? 'bg-green-50 dark:bg-green-900 text-green-700 dark:text-green-300 border border-green-200 dark:border-green-700'
|
||||
: 'bg-red-50 dark:bg-red-900 text-red-700 dark:text-red-300 border border-red-200 dark:border-red-700'
|
||||
}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<nav className="space-y-1">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`w-full flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
|
||||
activeTab === tab.id
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
|
||||
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<Icon className="mr-3 h-5 w-5" />
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="lg:col-span-3">
|
||||
<div className="card p-6">
|
||||
{/* Payment Settings */}
|
||||
{activeTab === 'payment' && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Payment Configuration</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<Key className="inline h-4 w-4 mr-1" />
|
||||
Payment API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={settings.paymentApiKey}
|
||||
onChange={(e) => handleDirectChange('paymentApiKey', e.target.value)}
|
||||
className="input-field"
|
||||
placeholder="Enter your payment API key"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Your secure API key for payment processing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<Globe className="inline h-4 w-4 mr-1" />
|
||||
Webhook URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={settings.webhookUrl}
|
||||
onChange={(e) => handleDirectChange('webhookUrl', e.target.value)}
|
||||
className="input-field"
|
||||
placeholder="https://your-domain.com/webhook"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
URL to receive payment notifications
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<DollarSign className="inline h-4 w-4 mr-1" />
|
||||
Default Currency
|
||||
</label>
|
||||
<select
|
||||
value={settings.currency}
|
||||
onChange={(e) => handleDirectChange('currency', e.target.value)}
|
||||
className="input-field"
|
||||
>
|
||||
{currencies.map(currency => (
|
||||
<option key={currency.value} value={currency.value}>
|
||||
{currency.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Notification Settings */}
|
||||
{activeTab === 'notifications' && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Notification Preferences</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Email Notifications</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Receive notifications via email</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.notifications.email}
|
||||
onChange={(e) => handleInputChange('notifications', 'email', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">SMS Notifications</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Receive notifications via SMS</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.notifications.sms}
|
||||
onChange={(e) => handleInputChange('notifications', 'sms', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Webhook Notifications</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Send notifications to webhook URL</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.notifications.webhook}
|
||||
onChange={(e) => handleInputChange('notifications', 'webhook', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Security Settings */}
|
||||
{activeTab === 'security' && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Security Settings</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Two-Factor Authentication</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Enable 2FA for enhanced security</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.security.twoFactor}
|
||||
onChange={(e) => handleInputChange('security', 'twoFactor', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Session Timeout (minutes)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="5"
|
||||
max="480"
|
||||
value={settings.security.sessionTimeout}
|
||||
onChange={(e) => handleInputChange('security', 'sessionTimeout', parseInt(e.target.value))}
|
||||
className="input-field w-32"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Automatically log out after inactivity
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data Settings */}
|
||||
{activeTab === 'data' && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Data Management</h3>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Data Retention Period (days)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="30"
|
||||
max="2555"
|
||||
value={settings.data.retentionPeriod}
|
||||
onChange={(e) => handleInputChange('data', 'retentionPeriod', parseInt(e.target.value))}
|
||||
className="input-field w-32"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
How long to keep transaction data
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Automatic Backup</label>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Automatically backup data daily</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.data.autoBackup}
|
||||
onChange={(e) => handleInputChange('data', 'autoBackup', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-primary-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={loading}
|
||||
className="btn-primary flex items-center"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{loading ? 'Saving...' : 'Save Settings'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
724
src/pages/TopUpAgent.jsx
Normal file
724
src/pages/TopUpAgent.jsx
Normal file
@@ -0,0 +1,724 @@
|
||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { topUpAgentAPI, cityAPI, generalAPI } from '../services/api';
|
||||
import { Plus, Search, Pencil, Trash2, Power, DollarSign, Wallet, X } from 'lucide-react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||
|
||||
const TopUpAgent = () => {
|
||||
const [agents, setAgents] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [filters, setFilters] = useState({
|
||||
nameQuery: '',
|
||||
supportEmailQuery: '',
|
||||
isActive: null, // null means all, true means active, false means inactive
|
||||
});
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingAgent, setEditingAgent] = useState(null);
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
supportEmail: '',
|
||||
cityId: '',
|
||||
postalCode: '',
|
||||
addressDetails: ''
|
||||
});
|
||||
const [cities, setCities] = useState([]);
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
|
||||
// Currency management states
|
||||
const [selectedAgentForCurrency, setSelectedAgentForCurrency] = useState(null);
|
||||
const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false);
|
||||
const [agentCurrencies, setAgentCurrencies] = useState([]);
|
||||
const [currencyForm, setCurrencyForm] = useState({ currencyCode: '', voucherGenerationCommission: 0 });
|
||||
const [editingCurrency, setEditingCurrency] = useState(null);
|
||||
|
||||
// Wallet states
|
||||
const [selectedAgentForWallet, setSelectedAgentForWallet] = useState(null);
|
||||
const [isWalletModalOpen, setIsWalletModalOpen] = useState(false);
|
||||
const [walletData, setWalletData] = useState(null);
|
||||
const [walletTransactions, setWalletTransactions] = useState([]);
|
||||
const [depositForm, setDepositForm] = useState({ amount: 0, currencyCode: '' });
|
||||
|
||||
// دریافت شهرها و ارزها
|
||||
useEffect(() => {
|
||||
const fetchCities = async () => {
|
||||
try {
|
||||
const list = await cityAPI.list({ pageSize: 1000 });
|
||||
setCities(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load cities:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCurrencies = async () => {
|
||||
try {
|
||||
const list = await generalAPI.getCurrencies();
|
||||
setCurrencies(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load currencies:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCities();
|
||||
fetchCurrencies();
|
||||
}, []);
|
||||
|
||||
// دریافت لیست TopUpAgent ها
|
||||
const fetchAgents = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = { pageSize: 100 };
|
||||
|
||||
// اضافه کردن فیلترها
|
||||
if (filters.nameQuery) {
|
||||
params.nameQuery = filters.nameQuery;
|
||||
}
|
||||
if (filters.supportEmailQuery) {
|
||||
params.supportEmailQuery = filters.supportEmailQuery;
|
||||
}
|
||||
if (filters.isActive !== null) {
|
||||
params.isActive = filters.isActive;
|
||||
}
|
||||
|
||||
const list = await topUpAgentAPI.list(params);
|
||||
console.log('🔵 TopUpAgent list response:', list);
|
||||
setAgents(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
console.error('🔴 TopUpAgent fetch error:', err);
|
||||
toast.error(getErrorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, [fetchAgents]);
|
||||
|
||||
// باز کردن modal برای ایجاد/ویرایش
|
||||
const openModal = (agent = null) => {
|
||||
if (agent) {
|
||||
setEditingAgent(agent);
|
||||
setForm({
|
||||
name: agent.name || '',
|
||||
supportEmail: agent.supportEmail || '',
|
||||
cityId: agent.cityId || '',
|
||||
postalCode: agent.postalCode || '',
|
||||
addressDetails: agent.addressDetails || '',
|
||||
});
|
||||
} else {
|
||||
setEditingAgent(null);
|
||||
setForm({ name: '', supportEmail: '', cityId: '', postalCode: '', addressDetails: '' });
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// ذخیره TopUpAgent
|
||||
const onSave = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!form.name?.trim()) {
|
||||
toast.error('Name is required');
|
||||
return;
|
||||
}
|
||||
if (!form.supportEmail?.trim()) {
|
||||
toast.error('Support Email is required');
|
||||
return;
|
||||
}
|
||||
if (!form.cityId) {
|
||||
toast.error('City is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingAgent) {
|
||||
const agentId = editingAgent.id;
|
||||
await topUpAgentAPI.update(agentId, form);
|
||||
toast.success('TopUpAgent updated successfully');
|
||||
} else {
|
||||
await topUpAgentAPI.create(form);
|
||||
toast.success('TopUpAgent created successfully');
|
||||
}
|
||||
await fetchAgents();
|
||||
setIsModalOpen(false);
|
||||
setEditingAgent(null);
|
||||
setForm({ name: '', supportEmail: '', cityId: '', postalCode: '', addressDetails: '' });
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// حذف TopUpAgent
|
||||
const handleDelete = async (agent) => {
|
||||
if (!window.confirm(`Are you sure you want to delete "${agent.name}"?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await topUpAgentAPI.remove(agent.id);
|
||||
toast.success('TopUpAgent deleted successfully');
|
||||
await fetchAgents();
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle Activation
|
||||
const handleToggleActivation = async (agent) => {
|
||||
try {
|
||||
await topUpAgentAPI.toggleActivation(agent.id);
|
||||
toast.success(`TopUpAgent ${agent.isActive ? 'deactivated' : 'activated'} successfully`);
|
||||
await fetchAgents();
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// باز کردن modal مدیریت Currency
|
||||
const openCurrencyModal = async (agent) => {
|
||||
setSelectedAgentForCurrency(agent);
|
||||
try {
|
||||
const list = await topUpAgentAPI.getCurrencies(agent.id);
|
||||
setAgentCurrencies(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
setIsCurrencyModalOpen(true);
|
||||
};
|
||||
|
||||
// ذخیره Currency
|
||||
const onSaveCurrency = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!currencyForm.currencyCode) {
|
||||
toast.error('Currency is required');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (editingCurrency) {
|
||||
await topUpAgentAPI.updateCurrency(
|
||||
selectedAgentForCurrency.id,
|
||||
editingCurrency.currencyCode,
|
||||
currencyForm
|
||||
);
|
||||
toast.success('Currency updated successfully');
|
||||
} else {
|
||||
await topUpAgentAPI.addCurrency(selectedAgentForCurrency.id, currencyForm);
|
||||
toast.success('Currency added successfully');
|
||||
}
|
||||
const list = await topUpAgentAPI.getCurrencies(selectedAgentForCurrency.id);
|
||||
setAgentCurrencies(Array.isArray(list) ? list : []);
|
||||
setCurrencyForm({ currencyCode: '', voucherGenerationCommission: 0 });
|
||||
setEditingCurrency(null);
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// حذف Currency
|
||||
const handleDeleteCurrency = async (currency) => {
|
||||
if (!window.confirm(`Are you sure you want to remove currency "${currency.currencyCode}"?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await topUpAgentAPI.removeCurrency(selectedAgentForCurrency.id, currency.currencyCode);
|
||||
toast.success('Currency removed successfully');
|
||||
const list = await topUpAgentAPI.getCurrencies(selectedAgentForCurrency.id);
|
||||
setAgentCurrencies(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// باز کردن modal مدیریت Wallet
|
||||
const openWalletModal = async (agent) => {
|
||||
setSelectedAgentForWallet(agent);
|
||||
try {
|
||||
const wallet = await topUpAgentAPI.getWallet(agent.id);
|
||||
setWalletData(wallet);
|
||||
if (wallet?.id) {
|
||||
const transactions = await topUpAgentAPI.getWalletTransactions(agent.id, wallet.id);
|
||||
setWalletTransactions(Array.isArray(transactions) ? transactions : []);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
setIsWalletModalOpen(true);
|
||||
};
|
||||
|
||||
// Deposit به Wallet
|
||||
const handleDeposit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!depositForm.amount || depositForm.amount <= 0) {
|
||||
toast.error('Amount must be greater than 0');
|
||||
return;
|
||||
}
|
||||
if (!depositForm.currencyCode) {
|
||||
toast.error('Currency is required');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await topUpAgentAPI.depositWallet(selectedAgentForWallet.id, walletData.id, depositForm);
|
||||
toast.success('Deposit successful');
|
||||
const wallet = await topUpAgentAPI.getWallet(selectedAgentForWallet.id);
|
||||
setWalletData(wallet);
|
||||
if (wallet?.id) {
|
||||
const transactions = await topUpAgentAPI.getWalletTransactions(selectedAgentForWallet.id, wallet.id);
|
||||
setWalletTransactions(Array.isArray(transactions) ? transactions : []);
|
||||
}
|
||||
setDepositForm({ amount: 0, currencyCode: '' });
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// Reset filters
|
||||
const handleResetFilters = () => {
|
||||
setFilters({
|
||||
nameQuery: '',
|
||||
supportEmailQuery: '',
|
||||
isActive: null,
|
||||
});
|
||||
};
|
||||
|
||||
// تعریف ستونهای جدول
|
||||
const columns = useMemo(() => [
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'supportEmail', header: 'Support Email' },
|
||||
{
|
||||
key: 'fullAddress',
|
||||
header: 'Address',
|
||||
render: (val) => val || '—'
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
header: 'Status',
|
||||
render: (val) => (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
val ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`}>
|
||||
{val ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'Actions',
|
||||
render: (_val, row) => (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => openModal(row)}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-blue-100 text-blue-700 hover:opacity-90"
|
||||
>
|
||||
<Pencil className="h-3 w-3 mr-1" /> Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleToggleActivation(row)}
|
||||
className={`inline-flex items-center px-2 py-1 text-xs rounded-md ${
|
||||
row.isActive
|
||||
? 'bg-yellow-100 text-yellow-700 hover:opacity-90'
|
||||
: 'bg-green-100 text-green-700 hover:opacity-90'
|
||||
}`}
|
||||
>
|
||||
<Power className="h-3 w-3 mr-1" /> {row.isActive ? 'Deactivate' : 'Activate'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openCurrencyModal(row)}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-purple-100 text-purple-700 hover:opacity-90"
|
||||
>
|
||||
<DollarSign className="h-3 w-3 mr-1" /> Currencies
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openWalletModal(row)}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-indigo-100 text-indigo-700 hover:opacity-90"
|
||||
>
|
||||
<Wallet className="h-3 w-3 mr-1" /> Wallet
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(row)}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-red-100 text-red-700 hover:opacity-90"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 mr-1" /> Delete
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], []);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ToastContainer position="top-right" autoClose={3000} />
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">TopUp Agent Management</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">Manage top-up agents and their settings</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 space-y-4">
|
||||
{/* Filters */}
|
||||
<div className="card p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Filters</h3>
|
||||
<button
|
||||
onClick={handleResetFilters}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Name
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={filters.nameQuery}
|
||||
onChange={(e) => setFilters({ ...filters, nameQuery: e.target.value })}
|
||||
placeholder="Search by name..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Support Email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={filters.supportEmailQuery}
|
||||
onChange={(e) => setFilters({ ...filters, supportEmailQuery: e.target.value })}
|
||||
placeholder="Search by email..."
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
value={filters.isActive === null ? '' : filters.isActive.toString()}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setFilters({
|
||||
...filters,
|
||||
isActive: value === '' ? null : value === 'true'
|
||||
});
|
||||
}}
|
||||
className="w-full p-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
<option value="">All</option>
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={() => openModal()}
|
||||
className="btn-primary inline-flex items-center"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" /> Add TopUp Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={agents}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
searchable={false}
|
||||
/>
|
||||
|
||||
{/* Main Modal */}
|
||||
{isModalOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsModalOpen(false)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md card p-6 relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{editingAgent ? 'Edit TopUp Agent' : 'Add TopUp Agent'}
|
||||
</h3>
|
||||
<form onSubmit={onSave} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Name *</label>
|
||||
<input
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Support Email *</label>
|
||||
<input
|
||||
type="email"
|
||||
value={form.supportEmail}
|
||||
onChange={(e) => setForm({ ...form, supportEmail: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">City *</label>
|
||||
<select
|
||||
value={form.cityId}
|
||||
onChange={(e) => setForm({ ...form, cityId: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
required
|
||||
>
|
||||
<option value="">Select City</option>
|
||||
{cities.map((city) => (
|
||||
<option key={city.id} value={city.id}>
|
||||
{city.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Postal Code</label>
|
||||
<input
|
||||
value={form.postalCode}
|
||||
onChange={(e) => setForm({ ...form, postalCode: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Address Details</label>
|
||||
<textarea
|
||||
value={form.addressDetails}
|
||||
onChange={(e) => setForm({ ...form, addressDetails: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 border rounded-lg">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
|
||||
{editingAgent ? 'Update' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Currency Management Modal */}
|
||||
{isCurrencyModalOpen && selectedAgentForCurrency && (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsCurrencyModalOpen(false)} />
|
||||
<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 dark:bg-gray-800 rounded-2xl shadow-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Manage Currencies - {selectedAgentForCurrency.name}
|
||||
</h3>
|
||||
<button onClick={() => setIsCurrencyModalOpen(false)} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSaveCurrency} className="space-y-4 mb-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Currency *</label>
|
||||
<select
|
||||
value={currencyForm.currencyCode}
|
||||
onChange={(e) => setCurrencyForm({ ...currencyForm, currencyCode: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
required
|
||||
disabled={!!editingCurrency}
|
||||
>
|
||||
<option value="">Select Currency</option>
|
||||
{currencies.map((curr) => (
|
||||
<option key={curr.code} value={curr.code}>
|
||||
{curr.code} - {curr.name || curr.nativeName || curr.symbol}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Commission *</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={currencyForm.voucherGenerationCommission}
|
||||
onChange={(e) => setCurrencyForm({ ...currencyForm, voucherGenerationCommission: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-2">
|
||||
{editingCurrency && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingCurrency(null);
|
||||
setCurrencyForm({ currencyCode: '', voucherGenerationCommission: 0 });
|
||||
}}
|
||||
className="px-4 py-2 border rounded-lg"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
|
||||
{editingCurrency ? 'Update' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-medium mb-2">Current Currencies</h4>
|
||||
<div className="space-y-2">
|
||||
{agentCurrencies.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">No currencies added</p>
|
||||
) : (
|
||||
agentCurrencies.map((curr) => (
|
||||
<div key={curr.currencyCode} className="flex justify-between items-center p-2 bg-gray-50 dark:bg-gray-700 rounded">
|
||||
<div>
|
||||
<span className="font-medium">{curr.currencyCode}</span>
|
||||
<span className="text-sm text-gray-500 ml-2">
|
||||
Commission: {curr.voucherGenerationCommission}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingCurrency(curr);
|
||||
setCurrencyForm({
|
||||
currencyCode: curr.currencyCode,
|
||||
voucherGenerationCommission: curr.voucherGenerationCommission || 0
|
||||
});
|
||||
}}
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCurrency(curr)}
|
||||
className="text-red-600 hover:text-red-800"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Wallet Management Modal */}
|
||||
{isWalletModalOpen && selectedAgentForWallet && (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsWalletModalOpen(false)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-3xl card p-6 relative bg-white dark:bg-gray-800 rounded-2xl shadow-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Wallet Management - {selectedAgentForWallet.name}
|
||||
</h3>
|
||||
<button onClick={() => setIsWalletModalOpen(false)} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{walletData && (
|
||||
<>
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
||||
<h4 className="font-medium mb-2">Wallet Balance</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{walletData.balances?.map((balance, idx) => (
|
||||
<div key={idx} className="text-sm">
|
||||
<span className="text-gray-500">Amount:</span>
|
||||
<span className="font-medium ml-2">{balance.amount} {balance.currencyCode}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleDeposit} className="space-y-4 mb-6">
|
||||
<h4 className="font-medium">Deposit</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Amount *</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={depositForm.amount}
|
||||
onChange={(e) => setDepositForm({ ...depositForm, amount: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Currency *</label>
|
||||
<select
|
||||
value={depositForm.currencyCode}
|
||||
onChange={(e) => setDepositForm({ ...depositForm, currencyCode: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
required
|
||||
>
|
||||
<option value="">Select Currency</option>
|
||||
{currencies.map((curr) => (
|
||||
<option key={curr.code} value={curr.code}>
|
||||
{curr.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
|
||||
Deposit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="font-medium mb-2">Transactions</h4>
|
||||
<div className="space-y-2">
|
||||
{walletTransactions.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">No transactions</p>
|
||||
) : (
|
||||
walletTransactions.map((tx, idx) => (
|
||||
<div key={idx} className="p-2 bg-gray-50 dark:bg-gray-700 rounded text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>{tx.type || 'Transaction'}</span>
|
||||
<span className="font-medium">
|
||||
{tx.amount?.amount} {tx.amount?.currencyCode}
|
||||
</span>
|
||||
</div>
|
||||
{tx.date && <div className="text-xs text-gray-500 mt-1">{new Date(tx.date).toLocaleString()}</div>}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopUpAgent;
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { RefreshCw, Download, Filter } from 'lucide-react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { paymentsAPI } from '../services/api';
|
||||
|
||||
const Transactions = () => {
|
||||
const [payments, setPayments] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPayments();
|
||||
}, []);
|
||||
|
||||
const fetchPayments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
const response = await paymentsAPI.getPayments();
|
||||
setPayments(response.data || []);
|
||||
} catch (err) {
|
||||
setError('Failed to fetch transactions');
|
||||
console.error('Transactions fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await fetchPayments();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const handleFilter = (status) => {
|
||||
setFilter(status);
|
||||
};
|
||||
|
||||
const filteredPayments = payments.filter(payment => {
|
||||
if (filter === 'all') return true;
|
||||
return payment.status === filter;
|
||||
});
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const statusClasses = {
|
||||
success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
|
||||
pending: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClasses[status] || statusClasses.pending}`}>
|
||||
{status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const formatCurrency = (amount, currency = 'USD') => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
const csvContent = [
|
||||
['ID', 'User', 'Amount', 'Status', 'Date', 'Currency'],
|
||||
...filteredPayments.map(payment => [
|
||||
payment.id,
|
||||
payment.user,
|
||||
payment.amount,
|
||||
payment.status,
|
||||
formatDate(payment.date),
|
||||
payment.currency
|
||||
])
|
||||
].map(row => row.join(',')).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `transactions-${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: 'id',
|
||||
header: 'Transaction ID',
|
||||
render: (value) => (
|
||||
<span className="font-mono text-sm font-medium text-gray-900 dark:text-white">
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
header: 'User',
|
||||
render: (value) => (
|
||||
<div className="flex items-center">
|
||||
<div className="w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center mr-3">
|
||||
<span className="text-xs font-medium text-primary-600 dark:text-primary-400">
|
||||
{value.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">{value}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
header: 'Amount',
|
||||
render: (value, row) => (
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{formatCurrency(value, row.currency)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: (value) => getStatusBadge(value)
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
header: 'Date',
|
||||
render: (value) => (
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{formatDate(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Transactions</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">View and manage all payment transactions</p>
|
||||
</div>
|
||||
<div className="mt-4 sm:mt-0 flex space-x-3">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="btn-secondary flex items-center"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
onClick={exportToCSV}
|
||||
className="btn-primary flex items-center"
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Filter by status:</span>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{[
|
||||
{ key: 'all', label: 'All', count: payments.length },
|
||||
{ key: 'success', label: 'Success', count: payments.filter(p => p.status === 'success').length },
|
||||
{ key: 'pending', label: 'Pending', count: payments.filter(p => p.status === 'pending').length },
|
||||
{ key: 'failed', label: 'Failed', count: payments.filter(p => p.status === 'failed').length },
|
||||
].map(({ key, label, count }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => handleFilter(key)}
|
||||
className={`px-3 py-1 text-sm rounded-full transition-colors duration-200 ${
|
||||
filter === key
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{label} ({count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<div className="card p-6 text-center mb-6">
|
||||
<p className="text-red-600 dark:text-red-400 mb-4">{error}</p>
|
||||
<button onClick={fetchPayments} className="btn-primary">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transactions Table */}
|
||||
<DataTable
|
||||
data={filteredPayments}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
searchable={true}
|
||||
className="shadow-sm"
|
||||
/>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="card p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Transactions</p>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{filteredPayments.length}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-primary-100 dark:bg-primary-900 rounded-lg">
|
||||
<div className="w-6 h-6 bg-primary-600 dark:bg-primary-400 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Total Amount</p>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{formatCurrency(
|
||||
filteredPayments.reduce((sum, payment) => sum + payment.amount, 0)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-100 dark:bg-green-900 rounded-lg">
|
||||
<div className="w-6 h-6 bg-green-600 dark:bg-green-400 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">Success Rate</p>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{filteredPayments.length > 0
|
||||
? ((filteredPayments.filter(p => p.status === 'success').length / filteredPayments.length) * 100).toFixed(1)
|
||||
: 0}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-100 dark:bg-blue-900 rounded-lg">
|
||||
<div className="w-6 h-6 bg-blue-600 dark:bg-blue-400 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Transactions;
|
||||
@@ -1,332 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { usersAPI, rolesAPI } from '../services/api';
|
||||
import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||
|
||||
const Users = () => {
|
||||
const [users, setUsers] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const [addForm, setAddForm] = useState({ firstName: '', lastName: '', email: '' });
|
||||
const [editForm, setEditForm] = useState({ firstName: '', lastName: '', email: '' });
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [rolesModalUser, setRolesModalUser] = useState(null);
|
||||
const [selectedRoles, setSelectedRoles] = useState([]);
|
||||
|
||||
// دریافت کاربران و رولها
|
||||
const fetchUsers = useCallback(async (q = '') => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const list = await usersAPI.list({ searchQuery: q });
|
||||
const usersWithRoles = await Promise.all(
|
||||
(list || []).map(async (user) => {
|
||||
const roles = await usersAPI.getRoles(user.id); // باید طبق ریسپانس نمونه برگرداند
|
||||
return { ...user, roles };
|
||||
})
|
||||
);
|
||||
setUsers(usersWithRoles);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchRoles = useCallback(async () => {
|
||||
const list = await rolesAPI.list(); // گرفتن تمام رولها
|
||||
setRoles(list);
|
||||
}, []);
|
||||
|
||||
// Initial load - fetch all users once
|
||||
useEffect(() => {
|
||||
fetchUsers('');
|
||||
fetchRoles();
|
||||
}, [fetchUsers, fetchRoles]);
|
||||
|
||||
// --- اضافه کردن کاربر ---
|
||||
const onAddUser = async (e) => {
|
||||
e.preventDefault();
|
||||
const { firstName, lastName, email } = addForm;
|
||||
if (!firstName.trim() || !lastName.trim() || !email.trim()) return;
|
||||
try {
|
||||
const newUser = await usersAPI.create(addForm);
|
||||
await fetchUsers(filter);
|
||||
setIsModalOpen(false);
|
||||
setAddForm({ firstName: '', lastName: '', email: '' });
|
||||
toast.success(getSuccessMessage(newUser) || 'User added successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- ویرایش کاربر ---
|
||||
const openEdit = (user) => {
|
||||
setEditingUser(user);
|
||||
setEditForm({
|
||||
firstName: user.firstName || '',
|
||||
lastName: user.lastName || '',
|
||||
email: user.email || '',
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const onUpdateUser = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!editingUser) return;
|
||||
try {
|
||||
const updatedUser = await usersAPI.update(editingUser.id, editForm);
|
||||
await fetchUsers(filter);
|
||||
setIsEditModalOpen(false);
|
||||
setEditingUser(null);
|
||||
toast.success(getSuccessMessage(updatedUser) || 'User updated successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- حذف کاربر ---
|
||||
const onDelete = async (id) => {
|
||||
if (!window.confirm('Are you sure you want to delete this user?')) return;
|
||||
try {
|
||||
const result = await usersAPI.remove(id);
|
||||
await fetchUsers(filter);
|
||||
toast.success(getSuccessMessage(result) || 'User deleted successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- تغییر وضعیت فعال بودن ---
|
||||
const onToggleActivation = async (id) => {
|
||||
try {
|
||||
const updated = await usersAPI.toggleActivation(id);
|
||||
await fetchUsers(filter);
|
||||
toast.success(getSuccessMessage(updated) || 'User status updated');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- ریست پسورد ---
|
||||
const onResetPassword = async (id) => {
|
||||
try {
|
||||
const res = await usersAPI.resetPassword(id);
|
||||
toast.success(getSuccessMessage(res) || 'Password reset successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- مدیریت رولها ---
|
||||
const openRolesModal = async (user) => {
|
||||
setRolesModalUser(user);
|
||||
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 () => {
|
||||
if (!rolesModalUser) return;
|
||||
try {
|
||||
await usersAPI.updateRoles(rolesModalUser.id, selectedRoles);
|
||||
toast.success('Roles updated successfully');
|
||||
setRolesModalUser(null);
|
||||
await fetchUsers(filter);
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- ستونهای جدول ---
|
||||
const columns = useMemo(() => [
|
||||
{ key: 'firstName', header: 'First Name' },
|
||||
{ key: 'lastName', header: 'Last Name' },
|
||||
{ key: 'email', header: 'Email' },
|
||||
{
|
||||
key: 'roles',
|
||||
header: 'Roles',
|
||||
render: (_val, row) => (
|
||||
<span>
|
||||
{row.roles?.length
|
||||
? row.roles.map(r => r.name).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">
|
||||
<ShieldOff className="h-4 w-4 mr-1" /> Toggle
|
||||
</button>
|
||||
<button onClick={() => onResetPassword(row.id)} className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-green-100 text-green-700 hover:opacity-90">
|
||||
<RefreshCcw className="h-4 w-4 mr-1" /> Reset
|
||||
</button>
|
||||
<button onClick={() => openRolesModal(row)} className="inline-flex items-center px-3 py-1 text-sm rounded-md bg-purple-100 text-purple-700 hover:opacity-90">
|
||||
Manage Roles
|
||||
</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-md 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}>
|
||||
{['firstName', 'lastName', 'email'].map((key) => (
|
||||
<input
|
||||
key={key}
|
||||
value={formState[key]}
|
||||
onChange={(e) => setFormState({ ...formState, [key]: e.target.value })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
placeholder={key.replace(/^\w/, (c) => c.toUpperCase())}
|
||||
/>
|
||||
))}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال مدیریت رولها ---
|
||||
const renderRolesModal = () => {
|
||||
if (!rolesModalUser) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setRolesModalUser(null)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md card p-6 relative bg-white rounded-2xl shadow-lg">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Manage Roles for {rolesModalUser.firstName}
|
||||
</h3>
|
||||
<div className="space-y-2 max-h-80 overflow-y-auto">
|
||||
{roles.map(role => (
|
||||
<label key={role.id} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={role.id}
|
||||
checked={selectedRoles.includes(role.id)}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
setSelectedRoles(prev => checked ? [...prev, role.id] : prev.filter(id => id !== role.id));
|
||||
}}
|
||||
/>
|
||||
{role.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-2 mt-4">
|
||||
<button onClick={() => setRolesModalUser(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button onClick={onUpdateRoles} className="btn-primary px-4 py-2 rounded-lg">Save</button>
|
||||
</div>
|
||||
</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">Users</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Manage users: add, edit, activate/deactivate, reset password, and assign roles
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(true)} className="btn-primary inline-flex items-center">
|
||||
<Plus className="h-4 w-4 mr-2" /> Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{renderModal(isModalOpen, 'Add User', onAddUser, addForm, setAddForm, () => setIsModalOpen(false))}
|
||||
{renderModal(isEditModalOpen, 'Edit User', onUpdateUser, editForm, setEditForm, () => setIsEditModalOpen(false))}
|
||||
{renderRolesModal()}
|
||||
|
||||
<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 role"
|
||||
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={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>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Users;
|
||||
@@ -5,13 +5,12 @@
|
||||
// Individual APIs are now in separate files for better organization
|
||||
|
||||
export * from './authAPI';
|
||||
export { paymentsAPI } from './paymentsAPI';
|
||||
export { rolesAPI } from './rolesAPI';
|
||||
export { usersAPI } from './usersAPI';
|
||||
export { currencyAPI } from './currencyAPI';
|
||||
|
||||
export { countryAPI } from './countryAPI';
|
||||
export { provinceAPI } from './provinceAPI';
|
||||
export { cityAPI } from './cityAPI';
|
||||
export { issuerAPI } from './issuerAPI';
|
||||
export { listPermissions } from './permissionsAPI';
|
||||
export { generalAPI } from './generalAPI';
|
||||
export { paymentsAPI } from './paymentsAPI';
|
||||
export { currencyAPI } from './currencyAPI';
|
||||
export { topUpAgentAPI } from './topUpAgentAPI';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAuthStore } from "../store/authStore";
|
||||
// تنظیم BASE_URL
|
||||
// -----------------------------
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ||
|
||||
(import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir");
|
||||
(import.meta.env.DEV ? "/" : "https://khalijpay-issuer.qaserver.ir");
|
||||
|
||||
// -----------------------------
|
||||
// ایجاد instance از axios
|
||||
@@ -43,38 +43,27 @@ if (typeof window !== 'undefined') {
|
||||
// Request interceptor
|
||||
// -----------------------------
|
||||
api.interceptors.request.use(config => {
|
||||
console.log("🔵 Request interceptor:", {
|
||||
url: config.url,
|
||||
fullUrl: config.baseURL + config.url,
|
||||
skipAuthRedirect: config?.skipAuthRedirect,
|
||||
method: config.method,
|
||||
data: config.data,
|
||||
headers: config.headers
|
||||
});
|
||||
|
||||
const skipAuthRedirect = config?.skipAuthRedirect === true;
|
||||
if (skipAuthRedirect) {
|
||||
// بررسی اینکه آیا درخواست به endpoint احراز هویت است یا نه
|
||||
const isAuthEndpoint = config.url?.includes('/Auth/SignIn') ||
|
||||
config.url?.includes('/Auth/SignOut') ||
|
||||
config.url?.includes('/Auth/ForgotPassword');
|
||||
|
||||
console.log("🔵 Auth endpoint check:", { isAuthEndpoint, url: config.url });
|
||||
config.url?.includes('/Auth/ForgotPassword') ||
|
||||
config.url?.includes('/Auth/User/Issuers') ||
|
||||
config.url?.includes('/Auth/Issuers');
|
||||
|
||||
// اگر endpoint احراز هویت است، اجازه ارسال درخواست را بدهیم (حتی اگر کاربر لاگین نباشد)
|
||||
// برای سایر endpointها با skipAuthRedirect، اگر کاربر لاگین نباشد، درخواست را cancel میکنیم
|
||||
// چون این endpointها نیاز به احراز هویت دارند
|
||||
if (!isAuthEndpoint) {
|
||||
const authState = useAuthStore.getState();
|
||||
console.log("🔵 Auth state:", { isLoggedIn: authState?.isLoggedIn });
|
||||
if (!authState?.isLoggedIn) {
|
||||
console.warn("⚠️ Canceling request - user not logged in");
|
||||
const CancelToken = axios.CancelToken;
|
||||
const source = CancelToken.source();
|
||||
source.cancel('User not logged in');
|
||||
config.cancelToken = source.token;
|
||||
config._skipRequest = true;
|
||||
}
|
||||
} else {
|
||||
console.log("✅ Allowing auth endpoint request");
|
||||
}
|
||||
}
|
||||
return config;
|
||||
@@ -84,57 +73,10 @@ api.interceptors.request.use(config => {
|
||||
// Response interceptor
|
||||
// -----------------------------
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
console.log("🟢 Response interceptor - Success:", {
|
||||
url: response.config?.url,
|
||||
status: response.status,
|
||||
data: response.data
|
||||
});
|
||||
return response;
|
||||
},
|
||||
response => response,
|
||||
error => {
|
||||
console.log("🔴 Response interceptor - Error:", {
|
||||
url: error.config?.url,
|
||||
isSilent: error?.isSilent,
|
||||
isCancel: axios.isCancel(error),
|
||||
status: error?.response?.status,
|
||||
message: error?.message
|
||||
});
|
||||
|
||||
// لاگ کامل error.response
|
||||
console.log("🔴 Full error.response:", error.response);
|
||||
console.log("🔴 error.response exists:", !!error.response);
|
||||
|
||||
if (error.response) {
|
||||
console.log("🔴 error.response.status:", error.response.status);
|
||||
console.log("🔴 error.response.statusText:", error.response.statusText);
|
||||
console.log("🔴 error.response.headers:", error.response.headers);
|
||||
console.log("🔴 error.response.data exists:", !!error.response.data);
|
||||
console.log("🔴 error.response.data:", error.response.data);
|
||||
console.log("🔴 error.response.data type:", typeof error.response.data);
|
||||
|
||||
// بررسی اینکه آیا data یک string است
|
||||
if (typeof error.response.data === 'string') {
|
||||
console.log("🔴 Response data is string:", error.response.data);
|
||||
}
|
||||
|
||||
// بررسی اینکه آیا data یک object است
|
||||
if (error.response.data && typeof error.response.data === 'object') {
|
||||
console.log("🔴 Response data keys:", Object.keys(error.response.data));
|
||||
try {
|
||||
console.log("🔴 Response data (JSON):", JSON.stringify(error.response.data, null, 2));
|
||||
} catch (e) {
|
||||
console.log("🔴 Cannot stringify response.data:", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("🔴 No error.response object");
|
||||
console.log("🔴 Full error object:", error);
|
||||
}
|
||||
|
||||
if (error?.isSilent) return Promise.reject(error);
|
||||
if (axios.isCancel(error)) {
|
||||
console.warn("⚠️ Request was canceled");
|
||||
return Promise.reject({ isSilent: true, isCancel: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config || {} });
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,60 @@ import { useAuthStore } from "../store/authStore";
|
||||
// -----------------------------
|
||||
// Auth API
|
||||
// -----------------------------
|
||||
export async function login(email, password) {
|
||||
try {
|
||||
const res = await api.post("/api/v1/Auth/SignIn", { email: email, password }, { skipAuthRedirect: true });
|
||||
const data = res.data;
|
||||
|
||||
console.log("data login", data);
|
||||
// Get user issuers
|
||||
export async function getUserIssuers(email, password) {
|
||||
try {
|
||||
console.log("🔵 Requesting issuers from:", "/api/v1/Auth/User/Issuers");
|
||||
const res = await api.post("/api/v1/Auth/User/Issuers", { email, password }, { skipAuthRedirect: true });
|
||||
console.log("🟢 Response received:", res);
|
||||
const data = res.data;
|
||||
|
||||
// بررسی اینکه آیا پاسخ موفقیتآمیز است یا نه (پشتیبانی از isSuccess و IsSuccess)
|
||||
if (data && (data.isSuccess === false || data.IsSuccess === false)) {
|
||||
const errorMessage = data.message || data.Message || 'Failed to get issuers';
|
||||
const error = new Error(errorMessage);
|
||||
error.response = {
|
||||
...res,
|
||||
data: data,
|
||||
status: data.statusCode || data.StatusCode || 409
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
// استخراج لیست issuers از response
|
||||
// ممکن است پاسخ به صورت مستقیم array باشد یا در data باشد
|
||||
const issuers = Array.isArray(data) ? data : (data?.data || data || []);
|
||||
return Array.isArray(issuers) ? issuers : [];
|
||||
} catch (error) {
|
||||
// اگر خطای 404 است، پیام مناسب نمایش دهیم
|
||||
if (error?.response?.status === 404) {
|
||||
const customError = new Error('Endpoint not found. Please check the API configuration.');
|
||||
customError.response = error.response;
|
||||
throw customError;
|
||||
}
|
||||
|
||||
if (error.response) {
|
||||
const errorData = error.response.data || {};
|
||||
const errorMessage = errorData.message || errorData.Message || error.message || 'خطا در ارتباط با سرور';
|
||||
const customError = new Error(errorMessage);
|
||||
customError.response = error.response;
|
||||
throw customError;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Sign in with issuer
|
||||
export async function login(email, password, issuerId) {
|
||||
try {
|
||||
const requestBody = { email, password };
|
||||
if (issuerId) {
|
||||
requestBody.issuerId = issuerId;
|
||||
}
|
||||
|
||||
const res = await api.post("/api/v1/Auth/SignIn", requestBody, { skipAuthRedirect: true });
|
||||
const data = res.data;
|
||||
|
||||
// بررسی اینکه آیا پاسخ موفقیتآمیز است یا نه (پشتیبانی از isSuccess و IsSuccess)
|
||||
if (data && (data.isSuccess === false || data.IsSuccess === false)) {
|
||||
@@ -26,8 +74,6 @@ export async function login(email, password) {
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
|
||||
|
||||
// اگر خطا از سمت سرور است (500, 400, etc.)، آن را throw کنیم
|
||||
if (error.response) {
|
||||
const errorData = error.response.data || {};
|
||||
|
||||
@@ -17,15 +17,11 @@ export const cityAPI = {
|
||||
requestParams.provinceId = provinceId;
|
||||
}
|
||||
|
||||
console.log('🔵 City API - list request:', { params: requestParams });
|
||||
|
||||
const res = await api.get('/api/v1/City', {
|
||||
params: requestParams,
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
|
||||
console.log('🟢 City API - list response:', res?.data);
|
||||
|
||||
// پاسخ به صورت { data: { data: [...], filterSummary: {...} } } است
|
||||
return res?.data?.data?.data || [];
|
||||
},
|
||||
@@ -49,7 +45,6 @@ export const cityAPI = {
|
||||
ProvinceId: city?.provinceId || '',
|
||||
CityName: String(city?.cityName || '').trim(),
|
||||
};
|
||||
console.log('City API Update:', { cityId, payload });
|
||||
try {
|
||||
const res = await api.put(`/api/v1/City/${encodeURIComponent(cityId)}`, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
|
||||
@@ -19,7 +19,6 @@ export const countryAPI = {
|
||||
try {
|
||||
const res = await api.get('/api/v1/Country/All', { skipAuthRedirect: true });
|
||||
// پاسخ به صورت { data: [...], statusCode: 200, isSuccess: true, ... } است
|
||||
console.log('🔵 Country API - listAll response:', res?.data);
|
||||
return res?.data?.data || [];
|
||||
} catch (error) {
|
||||
console.error('🔴 Country API - listAll error:', error);
|
||||
@@ -35,7 +34,6 @@ export const countryAPI = {
|
||||
CurrencyCode: String(country?.currencyCode || ''),
|
||||
TimeZone: String(country?.timeZoneName || country?.timeZone || ''),
|
||||
};
|
||||
console.log('🔵 Country API - create payload:', payload);
|
||||
const res = await api.post('/api/v1/Country', payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
@@ -54,7 +52,6 @@ export const countryAPI = {
|
||||
};
|
||||
|
||||
const url = `/api/v1/Country/${encodeURIComponent(countryId)}`;
|
||||
console.log('🔵 Country API - update payload:', { url, countryId, payload });
|
||||
|
||||
try {
|
||||
const res = await api.put(url, payload, { skipAuthRedirect: true });
|
||||
|
||||
@@ -4,54 +4,84 @@ import api from './apiClient';
|
||||
// Currency API
|
||||
// -----------------------------
|
||||
export const currencyAPI = {
|
||||
// GET /api/v1/Currency
|
||||
async list() {
|
||||
const res = await api.get('/api/v1/Currency', { skipAuthRedirect: true });
|
||||
return res?.data?.data || [];
|
||||
// GET /api/v1/Currency (with pagination)
|
||||
async list(params = {}) {
|
||||
try {
|
||||
const queryParams = {
|
||||
currentPage: params.currentPage || 1,
|
||||
pageSize: params.pageSize || 1000,
|
||||
...params
|
||||
};
|
||||
|
||||
const res = await api.get('/api/v1/Currency', {
|
||||
params: queryParams,
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
|
||||
// Response might be: { data: [...], statusCode: 200, ... }
|
||||
// Or direct array: [...]
|
||||
const currencies = Array.isArray(res?.data)
|
||||
? res.data
|
||||
: (res?.data?.data || []);
|
||||
|
||||
return currencies;
|
||||
} catch (error) {
|
||||
console.error('🔴 Currency API - list error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/Currency/{id}
|
||||
async getById(id) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/Currency/${encodeURIComponent(id)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 Currency API - getById error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/Currency
|
||||
async create(currencyCode) {
|
||||
const payload = { currencyCode: String(currencyCode || '') };
|
||||
const res = await api.post('/api/v1/Currency', payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
async create(data) {
|
||||
try {
|
||||
const res = await api.post('/api/v1/Currency', data, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 Currency API - create error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// PATCH /api/v1/Currency/{currencyCode}/ToggleActivation
|
||||
async toggleActivation(currencyCode) {
|
||||
const res = await api.patch(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/ToggleActivation`, null, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
// PUT /api/v1/Currency/{id}
|
||||
async update(id, data) {
|
||||
try {
|
||||
const res = await api.put(`/api/v1/Currency/${encodeURIComponent(id)}`, data, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 Currency API - update error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// PATCH /api/v1/Currency/{currencyCode}/Maintenance/Enable
|
||||
async enableMaintenance(currencyCode, maintenanceMessage = null) {
|
||||
const payload = maintenanceMessage ? { maintenanceMessage } : {};
|
||||
const res = await api.patch(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Maintenance/Enable`, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PATCH /api/v1/Currency/{currencyCode}/Maintenance/Disable
|
||||
async disableMaintenance(currencyCode) {
|
||||
const res = await api.patch(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Maintenance/Disable`, null, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/Currency/{currencyCode}/Permissions
|
||||
async updatePermissions(currencyCode, permissions) {
|
||||
const res = await api.put(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Permissions`, permissions, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/Currency/{currencyCode}/Limits
|
||||
async updateLimits(currencyCode, limits) {
|
||||
const res = await api.put(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Limits`, limits, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/Currency/{currencyCode}/Fees
|
||||
async updateFees(currencyCode, fees) {
|
||||
const res = await api.put(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Fees`, fees, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
// DELETE /api/v1/Currency/{id}
|
||||
async remove(id) {
|
||||
try {
|
||||
const res = await api.delete(`/api/v1/Currency/${encodeURIComponent(id)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 Currency API - remove error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,582 +0,0 @@
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// Issuer API
|
||||
// -----------------------------
|
||||
export const issuerAPI = {
|
||||
// GET /api/v1/Issuer (with pagination and filters)
|
||||
async list(params = {}) {
|
||||
const { currentPage = 1, pageSize = 100, isActive, supportCurrencyCode, supportCapabilities, ...otherParams } = params;
|
||||
const requestParams = { currentPage, pageSize, ...otherParams };
|
||||
|
||||
// اگر فیلترها وجود دارند، آنها را به params اضافه کن
|
||||
if (isActive !== undefined && isActive !== null && isActive !== '') {
|
||||
requestParams.isActive = isActive;
|
||||
}
|
||||
if (supportCurrencyCode) {
|
||||
requestParams.supportCurrencyCode = supportCurrencyCode;
|
||||
}
|
||||
if (supportCapabilities) {
|
||||
requestParams.supportCapabilities = supportCapabilities;
|
||||
}
|
||||
|
||||
console.log('🔵 Issuer API - list request:', { params: requestParams });
|
||||
|
||||
const res = await api.get('/api/v1/Issuer', {
|
||||
params: requestParams,
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
|
||||
console.log('🟢 Issuer API - list response:', res?.data);
|
||||
|
||||
// پاسخ به صورت { data: { data: [...], filterSummary: {...} } } است
|
||||
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) {
|
||||
// Build payload according to AddIssuerCommand structure:
|
||||
// name (required), supportEmail (required), title (nullable), cityId (Guid nullable), postalCode, addressDetails (nullable)
|
||||
const payload = {
|
||||
name: String(issuer?.name || '').trim(),
|
||||
supportEmail: String(issuer?.supportEmail || '').trim(),
|
||||
postalCode: String(issuer?.postalCode || '').trim()
|
||||
};
|
||||
|
||||
// Add title only if provided (nullable field - don't send if empty)
|
||||
if (issuer?.title && String(issuer.title).trim()) {
|
||||
payload.title = String(issuer.title).trim();
|
||||
}
|
||||
|
||||
// Add addressDetails only if provided (nullable field - don't send if empty)
|
||||
const addressValue = String(issuer?.addressDetails || issuer?.address || '').trim();
|
||||
if (addressValue) {
|
||||
payload.addressDetails = addressValue;
|
||||
}
|
||||
|
||||
// Include cityId only if valid GUID (nullable Guid - don't send if null/empty)
|
||||
if (issuer?.cityId && issuer.cityId !== '' && issuer.cityId !== 'null' && issuer.cityId !== null) {
|
||||
// Validate GUID format (basic check)
|
||||
const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
if (guidPattern.test(issuer.cityId)) {
|
||||
payload.cityId = issuer.cityId;
|
||||
} else {
|
||||
console.warn('⚠️ Invalid cityId GUID format:', issuer.cityId);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔵 Issuer Create Payload:', JSON.stringify(payload, null, 2));
|
||||
console.log('🔵 Issuer Create Payload (original):', issuer);
|
||||
|
||||
try {
|
||||
const res = await api.post('/api/v1/Issuer', payload, { skipAuthRedirect: true });
|
||||
console.log('🟢 Issuer Create Response:', res?.data);
|
||||
return res?.data;
|
||||
} catch (err) {
|
||||
console.error('🔴 Issuer Create Error:', {
|
||||
status: err?.response?.status,
|
||||
statusText: err?.response?.statusText,
|
||||
data: err?.response?.data,
|
||||
errors: err?.response?.data?.errors,
|
||||
message: err?.response?.data?.message,
|
||||
payload: payload
|
||||
});
|
||||
console.error('🔴 Full error.response:', err?.response);
|
||||
console.error('🔴 Full error.response.data:', err?.response?.data);
|
||||
console.error('🔴 Error response data type:', typeof err?.response?.data);
|
||||
if (err?.response?.data) {
|
||||
try {
|
||||
console.error('🔴 Error response data stringified:', JSON.stringify(err?.response?.data, null, 2));
|
||||
} catch (e) {
|
||||
console.error('🔴 Error response data (could not stringify):', err?.response?.data);
|
||||
console.error('🔴 Error response data toString:', String(err?.response?.data));
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /api/v1/Issuer/{id}
|
||||
async update(id, issuer) {
|
||||
// Build payload according to UpdateIssuerCommand structure (similar to AddIssuerCommand)
|
||||
const payload = {
|
||||
name: String(issuer?.name || '').trim(),
|
||||
supportEmail: String(issuer?.supportEmail || '').trim(),
|
||||
postalCode: String(issuer?.postalCode || '').trim()
|
||||
};
|
||||
|
||||
// Add title if provided (nullable field)
|
||||
if (issuer?.title && String(issuer.title).trim()) {
|
||||
payload.title = String(issuer.title).trim();
|
||||
}
|
||||
|
||||
// Add addressDetails if provided (nullable field)
|
||||
const addressValue = String(issuer?.addressDetails || issuer?.address || '').trim();
|
||||
if (addressValue) {
|
||||
payload.addressDetails = addressValue;
|
||||
}
|
||||
|
||||
// Include cityId - validate GUID format
|
||||
if (issuer?.cityId && issuer.cityId !== '' && issuer.cityId !== 'null' && issuer.cityId !== null) {
|
||||
// Validate GUID format (basic check)
|
||||
const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
if (guidPattern.test(issuer.cityId)) {
|
||||
payload.cityId = issuer.cityId;
|
||||
} else {
|
||||
console.warn('⚠️ Invalid cityId GUID format:', issuer.cityId);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔵 Issuer Update Payload:', JSON.stringify(payload, null, 2));
|
||||
|
||||
try {
|
||||
const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(id)}`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
console.log('🟢 Issuer Update Response:', res?.data);
|
||||
return res?.data;
|
||||
} catch (err) {
|
||||
console.error('🔴 Issuer Update Error:', {
|
||||
status: err?.response?.status,
|
||||
statusText: err?.response?.statusText,
|
||||
data: err?.response?.data,
|
||||
errors: err?.response?.data?.errors,
|
||||
message: err?.response?.data?.message,
|
||||
payload: payload
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// 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/{issuerId}/Capabilities
|
||||
async getCapabilities(issuerId) {
|
||||
try {
|
||||
console.log('🔵 Getting capabilities for issuer:', issuerId);
|
||||
const res = await api.get(`/api/v1/Issuer/${encodeURIComponent(issuerId)}/Capabilities`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
// Response structure: { data: [...], statusCode, isSuccess, ... }
|
||||
// Or direct array: [...]
|
||||
const capabilities = Array.isArray(res?.data) ? res.data : (res?.data?.data || []);
|
||||
console.log('🟢 Capabilities from API:', capabilities);
|
||||
return capabilities;
|
||||
} catch (err) {
|
||||
console.error('🔴 Error getting capabilities:', {
|
||||
status: err?.response?.status,
|
||||
data: err?.response?.data
|
||||
});
|
||||
// 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/{issuerId}/Capabilities
|
||||
// Payload: IssuerManageCapabilitiesCommand { capabilities: List<IssuerCapabilityDto> }
|
||||
async updateCapabilities(issuerId, capabilities) {
|
||||
// Build IssuerCapabilityDto array from input
|
||||
// Each IssuerCapabilityDto should have: capability, capabilityName, hasCapability (or similar fields)
|
||||
const validCapabilities = Array.isArray(capabilities) ? capabilities
|
||||
.filter(cap => {
|
||||
// Filter out invalid capabilities
|
||||
if (typeof cap === 'string') {
|
||||
return cap && String(cap).trim() !== '';
|
||||
}
|
||||
if (typeof cap === 'object' && cap !== null) {
|
||||
const capabilityValue = cap.capability || cap.capabilityName || '';
|
||||
return capabilityValue && String(capabilityValue).trim() !== '';
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(cap => {
|
||||
// Convert to IssuerCapabilityDto format
|
||||
if (typeof cap === 'string') {
|
||||
return {
|
||||
capability: String(cap).trim(),
|
||||
capabilityName: String(cap).trim(),
|
||||
hasCapability: true
|
||||
};
|
||||
}
|
||||
|
||||
// If it's an object, ensure it has the required fields
|
||||
const capabilityValue = cap.capability || cap.capabilityName || '';
|
||||
const capabilityName = cap.capabilityName || cap.capability || String(capabilityValue).trim();
|
||||
const hasCapability = cap.hasCapability !== undefined ? Boolean(cap.hasCapability) : true;
|
||||
|
||||
return {
|
||||
capability: String(capabilityValue).trim(),
|
||||
capabilityName: String(capabilityName).trim() || String(capabilityValue).trim(),
|
||||
hasCapability: hasCapability
|
||||
};
|
||||
})
|
||||
.filter(cap => cap.capability && cap.capability.trim() !== '') : [];
|
||||
|
||||
// Wrap in IssuerManageCapabilitiesCommand structure
|
||||
const payload = {
|
||||
capabilities: validCapabilities
|
||||
};
|
||||
|
||||
console.log('🔵 Update Capabilities Payload:', JSON.stringify(payload, null, 2));
|
||||
console.log('🔵 Issuer ID:', issuerId);
|
||||
console.log('🔵 Valid capabilities count:', validCapabilities.length);
|
||||
|
||||
try {
|
||||
const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(issuerId)}/Capabilities`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
console.log('🟢 Capabilities Update Response:', res?.data);
|
||||
return res?.data;
|
||||
} catch (err) {
|
||||
console.error('🔴 Capabilities update error:', {
|
||||
status: err?.response?.status,
|
||||
statusText: err?.response?.statusText,
|
||||
data: err?.response?.data,
|
||||
errors: err?.response?.data?.errors,
|
||||
message: err?.response?.data?.message,
|
||||
payload: payload
|
||||
});
|
||||
if (err?.response?.data) {
|
||||
try {
|
||||
console.error('🔴 Error response data stringified:', JSON.stringify(err?.response?.data, null, 2));
|
||||
} catch (e) {
|
||||
console.error('🔴 Error response data (could not stringify):', err?.response?.data);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/Issuer/{issuerId}/AllowedCurrencies
|
||||
// Response: { data: [{ currencyCode, currencyEnglishName, allowed }, ...], statusCode, ... }
|
||||
async getAllowedCurrencies(issuerId) {
|
||||
try {
|
||||
console.log('🔵 Getting allowed currencies for issuer:', issuerId);
|
||||
const res = await api.get(`/api/v1/Issuer/${encodeURIComponent(issuerId)}/AllowedCurrencies`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
// Response structure: { data: [{ currencyCode, currencyEnglishName, allowed }, ...], statusCode, ... }
|
||||
// Or direct array: [{ currencyCode, currencyEnglishName, allowed }, ...]
|
||||
const currencies = Array.isArray(res?.data) ? res.data : (res?.data?.data || []);
|
||||
console.log('🟢 Allowed Currencies from API:', currencies);
|
||||
return currencies;
|
||||
} catch (err) {
|
||||
console.error('🔴 Error getting allowed currencies:', {
|
||||
status: err?.response?.status,
|
||||
data: err?.response?.data
|
||||
});
|
||||
// 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/{issuerId}/AllowedCurrencies
|
||||
// Payload: { allowedCurrencies: List<IssuerAllowedCurrencyDto> }
|
||||
async updateAllowedCurrencies(issuerId, allowedCurrencies) {
|
||||
// Build IssuerAllowedCurrencyDto array from input
|
||||
// Each should have: currencyCode, currencyEnglishName, allowed
|
||||
const currencyMap = new Map(); // Use Map to avoid duplicates
|
||||
|
||||
if (Array.isArray(allowedCurrencies)) {
|
||||
allowedCurrencies.forEach(curr => {
|
||||
const currencyCode = String(curr.currencyCode || curr.code || '').trim();
|
||||
if (!currencyCode) return; // Skip invalid currencies
|
||||
|
||||
// Use currencyCode as key to avoid duplicates
|
||||
if (!currencyMap.has(currencyCode)) {
|
||||
const currencyEnglishName = String(curr.currencyEnglishName || curr.name || '').trim();
|
||||
const allowed = curr.allowed !== undefined ? Boolean(curr.allowed) : true;
|
||||
|
||||
currencyMap.set(currencyCode, {
|
||||
currencyCode: currencyCode,
|
||||
currencyEnglishName: currencyEnglishName || currencyCode,
|
||||
allowed: allowed
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Convert Map to array
|
||||
const validCurrencies = Array.from(currencyMap.values());
|
||||
|
||||
// Wrap in command structure
|
||||
const payload = {
|
||||
allowedCurrencies: validCurrencies
|
||||
};
|
||||
|
||||
console.log('🔵 Update Allowed Currencies Payload:', JSON.stringify(payload, null, 2));
|
||||
console.log('🔵 Issuer ID:', issuerId);
|
||||
console.log('🔵 Valid currencies count:', validCurrencies.length);
|
||||
console.log('🔵 Currencies with allowed=true:', validCurrencies.filter(c => c.allowed).length);
|
||||
console.log('🔵 Currencies with allowed=false:', validCurrencies.filter(c => !c.allowed).length);
|
||||
console.log('🔵 Duplicate check - unique currency codes:', [...currencyMap.keys()]);
|
||||
|
||||
try {
|
||||
const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(issuerId)}/AllowedCurrencies`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
console.log('🟢 Allowed Currencies Update Response:', res?.data);
|
||||
return res?.data;
|
||||
} catch (err) {
|
||||
console.error('🔴 Allowed Currencies update error:', {
|
||||
status: err?.response?.status,
|
||||
statusText: err?.response?.statusText,
|
||||
data: err?.response?.data,
|
||||
errors: err?.response?.data?.errors,
|
||||
message: err?.response?.data?.message,
|
||||
code: err?.response?.data?.code,
|
||||
payload: payload
|
||||
});
|
||||
console.error('🔴 Full error.response:', err?.response);
|
||||
if (err?.response?.data) {
|
||||
try {
|
||||
console.error('🔴 Error response data stringified:', JSON.stringify(err?.response?.data, null, 2));
|
||||
} catch (e) {
|
||||
console.error('🔴 Error response data (could not stringify):', err?.response?.data);
|
||||
console.error('🔴 Error response data toString:', String(err?.response?.data));
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// ========== TopUpAgentManagement API ==========
|
||||
|
||||
// GET /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement
|
||||
async getTopUpAgentManagement(issuerId) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || { maxAgents: 0, isActive: false };
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement GET error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement
|
||||
async updateTopUpAgentManagement(issuerId, maxAgents) {
|
||||
try {
|
||||
const payload = { maxAgents: Number(maxAgents) || 0 };
|
||||
const res = await api.put(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement PUT error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency
|
||||
async getTopUpAgentManagementCurrencies(issuerId) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return Array.isArray(res?.data?.data) ? res?.data?.data : [];
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Currencies GET error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency/{currencyCode}
|
||||
async getTopUpAgentManagementCurrency(issuerId, currencyCode) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency/${encodeURIComponent(currencyCode)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || null;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Currency GET error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency/{currencyCode}
|
||||
async createTopUpAgentManagementCurrency(issuerId, currencyCode, currencyData) {
|
||||
try {
|
||||
const payload = {
|
||||
voucherGeneratingFee: {
|
||||
percent: Number(currencyData.voucherGeneratingFee?.percent) || 0,
|
||||
fixed: Number(currencyData.voucherGeneratingFee?.fixed) || 0,
|
||||
minAmount: Number(currencyData.voucherGeneratingFee?.minAmount) || 0,
|
||||
maxAmount: Number(currencyData.voucherGeneratingFee?.maxAmount) || 0
|
||||
},
|
||||
agentMarketingFee: {
|
||||
percent: Number(currencyData.agentMarketingFee?.percent) || 0,
|
||||
fixed: Number(currencyData.agentMarketingFee?.fixed) || 0,
|
||||
minAmount: Number(currencyData.agentMarketingFee?.minAmount) || 0,
|
||||
maxAmount: Number(currencyData.agentMarketingFee?.maxAmount) || 0
|
||||
},
|
||||
agentWalletMaxBalanceLimit: {
|
||||
min: Number(currencyData.agentWalletMaxBalanceLimit?.min) || 0,
|
||||
max: Number(currencyData.agentWalletMaxBalanceLimit?.max) || 0
|
||||
},
|
||||
agentWalletDepositMonthlyLimit: {
|
||||
amount: Number(currencyData.agentWalletDepositMonthlyLimit?.amount) || 0
|
||||
}
|
||||
};
|
||||
const res = await api.post(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency/${encodeURIComponent(currencyCode)}`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Currency POST error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency/{currencyCode}
|
||||
async updateTopUpAgentManagementCurrency(issuerId, currencyCode, currencyData) {
|
||||
try {
|
||||
const payload = {
|
||||
voucherGeneratingFee: {
|
||||
percent: Number(currencyData.voucherGeneratingFee?.percent) || 0,
|
||||
fixed: Number(currencyData.voucherGeneratingFee?.fixed) || 0,
|
||||
minAmount: Number(currencyData.voucherGeneratingFee?.minAmount) || 0,
|
||||
maxAmount: Number(currencyData.voucherGeneratingFee?.maxAmount) || 0
|
||||
},
|
||||
agentMarketingFee: {
|
||||
percent: Number(currencyData.agentMarketingFee?.percent) || 0,
|
||||
fixed: Number(currencyData.agentMarketingFee?.fixed) || 0,
|
||||
minAmount: Number(currencyData.agentMarketingFee?.minAmount) || 0,
|
||||
maxAmount: Number(currencyData.agentMarketingFee?.maxAmount) || 0
|
||||
},
|
||||
agentWalletMaxBalanceLimit: {
|
||||
min: Number(currencyData.agentWalletMaxBalanceLimit?.min) || 0,
|
||||
max: Number(currencyData.agentWalletMaxBalanceLimit?.max) || 0
|
||||
},
|
||||
agentWalletDepositMonthlyLimit: {
|
||||
amount: Number(currencyData.agentWalletDepositMonthlyLimit?.amount) || 0
|
||||
}
|
||||
};
|
||||
const res = await api.put(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency/${encodeURIComponent(currencyCode)}`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Currency PUT error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency/{currencyCode}
|
||||
async deleteTopUpAgentManagementCurrency(issuerId, currencyCode) {
|
||||
try {
|
||||
const res = await api.delete(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency/${encodeURIComponent(currencyCode)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Currency DELETE error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// ========== TopUpAgentManagement Wallet API ==========
|
||||
|
||||
// GET /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency/{currencyCode}/Wallet/Balance
|
||||
async getTopUpAgentManagementWalletBalance(issuerId, currencyCode) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency/${encodeURIComponent(currencyCode)}/Wallet/Balance`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || null;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Wallet Balance GET error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency/{currencyCode}/Wallet/Deposit
|
||||
async depositTopUpAgentManagementWallet(issuerId, currencyCode, amount) {
|
||||
try {
|
||||
const payload = { amount: Number(amount) || 0 };
|
||||
const res = await api.post(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency/${encodeURIComponent(currencyCode)}/Wallet/Deposit`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Wallet Deposit POST error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/issuer/{issuerId}/Capability/TopUpAgentManagement/Currency/{currencyCode}/Wallet/Transactions
|
||||
async getTopUpAgentManagementWalletTransactions(issuerId, currencyCode) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/issuer/${encodeURIComponent(issuerId)}/Capability/TopUpAgentManagement/Currency/${encodeURIComponent(currencyCode)}/Wallet/Transactions`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return Array.isArray(res?.data?.data) ? res?.data?.data : [];
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgentManagement Wallet Transactions GET error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// ========== Issuer Admin API ==========
|
||||
|
||||
// GET /api/v1/Issuer/{issuerId}/Admin
|
||||
async getAdmins(issuerId) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/Issuer/${encodeURIComponent(issuerId)}/Admin`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
// Response structure: { data: { data: [...], filterSummary: {...} }, statusCode, ... }
|
||||
return res?.data?.data?.data || [];
|
||||
} catch (error) {
|
||||
console.error('🔴 Issuer Admin GET error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/Issuer/{issuerId}/Admin
|
||||
async addAdmin(issuerId, adminData) {
|
||||
try {
|
||||
const res = await api.post(`/api/v1/Issuer/${encodeURIComponent(issuerId)}/Admin`, adminData, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 Issuer Admin POST error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,28 +1,44 @@
|
||||
// -----------------------------
|
||||
// Payments API (mock data)
|
||||
// -----------------------------
|
||||
const mockData = {
|
||||
stats: { total: 1247, success: 1189, failed: 58 },
|
||||
payments: [
|
||||
{ id: 'TXN-001', user: 'John Doe', amount: 299.99, status: 'success', date: '2024-01-15T10:30:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-002', user: 'Jane Smith', amount: 150.00, status: 'pending', date: '2024-01-15T11:45:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-003', user: 'Bob Johnson', amount: 75.50, status: 'failed', date: '2024-01-15T12:15:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-004', user: 'Alice Brown', amount: 450.00, status: 'success', date: '2024-01-15T13:20:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-005', user: 'Charlie Wilson', amount: 89.99, status: 'success', date: '2024-01-15T14:30:00Z', currency: 'USD' }
|
||||
],
|
||||
chartData: [
|
||||
{ date: '2024-01-09', amount: 1200 },
|
||||
{ date: '2024-01-10', amount: 1900 },
|
||||
{ date: '2024-01-11', amount: 3000 },
|
||||
{ date: '2024-01-12', amount: 2800 },
|
||||
{ date: '2024-01-13', amount: 1890 },
|
||||
{ date: '2024-01-14', amount: 2390 },
|
||||
{ date: '2024-01-15', amount: 3490 }
|
||||
]
|
||||
};
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// Payments API
|
||||
// -----------------------------
|
||||
export const paymentsAPI = {
|
||||
async getStats() { return mockData.stats; },
|
||||
async getChartData() { return mockData.chartData; },
|
||||
// GET /api/v1/Payments/Stats
|
||||
async getStats() {
|
||||
try {
|
||||
const res = await api.get('/api/v1/Payments/Stats', { skipAuthRedirect: true });
|
||||
// پاسخ ممکن است به صورت { data: {...}, statusCode: 200 } باشد
|
||||
return res?.data?.data || res?.data || {
|
||||
total: 0,
|
||||
success: 0,
|
||||
failed: 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('🔴 Payments API - getStats error:', error);
|
||||
// در صورت خطا، دادههای پیشفرض برگردان
|
||||
return {
|
||||
total: 0,
|
||||
success: 0,
|
||||
failed: 0
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/Payments/ChartData
|
||||
async getChartData() {
|
||||
try {
|
||||
const res = await api.get('/api/v1/Payments/ChartData', { skipAuthRedirect: true });
|
||||
// پاسخ ممکن است به صورت { data: [...], statusCode: 200 } باشد
|
||||
const data = res?.data?.data || res?.data || [];
|
||||
// اطمینان از اینکه دادهها به فرمت مورد نیاز هستند
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
console.error('🔴 Payments API - getChartData error:', error);
|
||||
// در صورت خطا، آرایه خالی برگردان
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -14,15 +14,11 @@ export const provinceAPI = {
|
||||
requestParams.countryId = countryId;
|
||||
}
|
||||
|
||||
console.log('🔵 Province API - list request:', { params: requestParams });
|
||||
|
||||
const res = await api.get('/api/v1/Province', {
|
||||
params: requestParams,
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
|
||||
console.log('🟢 Province API - list response:', res?.data);
|
||||
|
||||
// پاسخ به صورت { data: { data: [...], filterSummary: {...} } } است
|
||||
return res?.data?.data?.data || [];
|
||||
},
|
||||
@@ -46,7 +42,6 @@ export const provinceAPI = {
|
||||
CountryId: province?.countryId || '',
|
||||
ProvinceName: String(province?.provinceName || '').trim(),
|
||||
};
|
||||
console.log('Province API Update:', { provinceId, payload });
|
||||
try {
|
||||
const res = await api.put(`/api/v1/Province/${encodeURIComponent(provinceId)}`, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import api from './apiClient';
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
// -----------------------------
|
||||
// Roles API
|
||||
// -----------------------------
|
||||
const ROLES_STORAGE_KEY = 'app_roles_v1';
|
||||
function readRolesFromStorage() { try { const raw = localStorage.getItem(ROLES_STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } }
|
||||
function writeRolesToStorage(roles) { localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles)); }
|
||||
function ensureSeedRoles() { if (readRolesFromStorage().length === 0) writeRolesToStorage([
|
||||
{ id: crypto.randomUUID(), name: 'Admin', permissions: ['read','write','delete'], userType: 'internal' },
|
||||
{ id: crypto.randomUUID(), name: 'Editor', permissions: ['read','write'], userType: 'internal' },
|
||||
{ id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' },
|
||||
]); }
|
||||
|
||||
export const rolesAPI = {
|
||||
async list(queryOrOptions='') {
|
||||
const opts = typeof queryOrOptions === 'string' ? { nameQuery: queryOrOptions, currentPage:1, pageSize:100 } : { nameQuery: queryOrOptions?.nameQuery||'', currentPage: queryOrOptions?.currentPage||1, pageSize: queryOrOptions?.pageSize||100 };
|
||||
if (!useAuthStore.getState()?.isLoggedIn) { ensureSeedRoles(); return readRolesFromStorage().filter(r=>r.name.toLowerCase().includes((opts.nameQuery||'').toLowerCase())); }
|
||||
try {
|
||||
const res = await api.get('/api/v1/Role', { params: opts, skipAuthRedirect: true });
|
||||
const items = Array.isArray(res?.data?.data?.data) ? res.data.data.data : [];
|
||||
writeRolesToStorage(items.map(r => ({ id: r.id||crypto.randomUUID(), ...r })));
|
||||
return items;
|
||||
} catch { ensureSeedRoles(); return readRolesFromStorage(); }
|
||||
},
|
||||
|
||||
async create(role) {
|
||||
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[] };
|
||||
let created = payload;
|
||||
if (useAuthStore.getState()?.isLoggedIn) {
|
||||
try { const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect:true }); created = res?.data||payload; } catch {}
|
||||
}
|
||||
const roles = readRolesFromStorage(); const newRole = { id: crypto.randomUUID(), ...created }; roles.push(newRole); writeRolesToStorage(roles); return newRole;
|
||||
},
|
||||
|
||||
async remove(id) {
|
||||
if (useAuthStore.getState()?.isLoggedIn) { try { await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect:true }); } catch {} }
|
||||
const roles = readRolesFromStorage(); writeRolesToStorage(roles.filter(r=>r.id!==id)); return { success:true };
|
||||
},
|
||||
|
||||
async update(id, role) {
|
||||
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[], userType: role?.userType||undefined };
|
||||
if (useAuthStore.getState()?.isLoggedIn) { try { await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect:true }); } catch {} }
|
||||
const roles = readRolesFromStorage(); const idx = roles.findIndex(r=>r.id===id); if(idx!==-1){ roles[idx]={...roles[idx], ...payload}; writeRolesToStorage(roles); return roles[idx]; }
|
||||
const updated={id,...payload}; roles.push(updated); writeRolesToStorage(roles); return updated;
|
||||
},
|
||||
};
|
||||
|
||||
209
src/services/topUpAgentAPI.js
Normal file
209
src/services/topUpAgentAPI.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// TopUpAgent API
|
||||
// -----------------------------
|
||||
export const topUpAgentAPI = {
|
||||
// GET /api/v1/TopUpAgent (with pagination)
|
||||
async list(params = {}) {
|
||||
const { currentPage = 1, pageSize = 10, ...otherParams } = params;
|
||||
const res = await api.get('/api/v1/TopUpAgent', {
|
||||
params: { currentPage, pageSize, ...otherParams },
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
|
||||
// بررسی ساختار response
|
||||
// ممکن است به صورت { data: { data: { data: [...] } } } باشد
|
||||
// یا { data: { data: [...] } } باشد
|
||||
// یا { data: [...] } باشد
|
||||
if (res?.data?.data?.data) {
|
||||
return Array.isArray(res.data.data.data) ? res.data.data.data : [];
|
||||
}
|
||||
if (res?.data?.data) {
|
||||
return Array.isArray(res.data.data) ? res.data.data : [];
|
||||
}
|
||||
if (res?.data) {
|
||||
return Array.isArray(res.data) ? res.data : [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
// GET /api/v1/TopUpAgent/{topUpAgentId}
|
||||
async getById(topUpAgentId) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - getById error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/TopUpAgent
|
||||
async create(topUpAgent) {
|
||||
const payload = {
|
||||
name: String(topUpAgent?.name || ''),
|
||||
supportEmail: String(topUpAgent?.supportEmail || ''),
|
||||
cityId: topUpAgent?.cityId || '',
|
||||
postalCode: String(topUpAgent?.postalCode || ''),
|
||||
addressDetails: String(topUpAgent?.addressDetails || ''),
|
||||
};
|
||||
const res = await api.post('/api/v1/TopUpAgent', payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/TopUpAgent/{topUpAgentId}
|
||||
async update(topUpAgentId, topUpAgent) {
|
||||
if (!topUpAgentId) {
|
||||
throw new Error('TopUpAgent ID is required');
|
||||
}
|
||||
const payload = {
|
||||
name: String(topUpAgent?.name || '').trim(),
|
||||
supportEmail: String(topUpAgent?.supportEmail || '').trim(),
|
||||
cityId: topUpAgent?.cityId || '',
|
||||
postalCode: String(topUpAgent?.postalCode || '').trim(),
|
||||
addressDetails: String(topUpAgent?.addressDetails || '').trim(),
|
||||
};
|
||||
const url = `/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}`;
|
||||
try {
|
||||
const res = await api.put(url, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - update error:', {
|
||||
url,
|
||||
topUpAgentId,
|
||||
payload,
|
||||
error: error?.response?.data || error?.message || error
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /api/v1/TopUpAgent/{topUpAgentId}
|
||||
async remove(topUpAgentId) {
|
||||
try {
|
||||
const res = await api.delete(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - remove error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// PATCH /api/v1/TopUpAgent/{topUpAgentId}/ToggleActivation
|
||||
async toggleActivation(topUpAgentId) {
|
||||
try {
|
||||
const res = await api.patch(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/ToggleActivation`, null, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - toggleActivation error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/TopUpAgent/{topUpAgentId}/Currency
|
||||
async addCurrency(topUpAgentId, currency) {
|
||||
const payload = {
|
||||
currencyCode: String(currency?.currencyCode || ''),
|
||||
voucherGenerationCommission: Number(currency?.voucherGenerationCommission || 0),
|
||||
};
|
||||
const res = await api.post(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Currency`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// GET /api/v1/TopUpAgent/{topUpAgentId}/Currency
|
||||
async getCurrencies(topUpAgentId) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Currency`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || [];
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - getCurrencies error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/TopUpAgent/{topUpAgentId}/Currency/{currencyCode}
|
||||
async getCurrency(topUpAgentId, currencyCode) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Currency/${encodeURIComponent(currencyCode)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - getCurrency error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// PUT /api/v1/TopUpAgent/{topUpAgentId}/Currency/{currencyCode}
|
||||
async updateCurrency(topUpAgentId, currencyCode, currency) {
|
||||
const payload = {
|
||||
voucherGenerationCommission: Number(currency?.voucherGenerationCommission || 0),
|
||||
};
|
||||
const res = await api.put(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Currency/${encodeURIComponent(currencyCode)}`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// DELETE /api/v1/TopUpAgent/{topUpAgentId}/Currency/{currencyCode}
|
||||
async removeCurrency(topUpAgentId, currencyCode) {
|
||||
try {
|
||||
const res = await api.delete(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Currency/${encodeURIComponent(currencyCode)}`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - removeCurrency error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/v1/TopUpAgent/{topUpAgentId}/Wallet
|
||||
async getWallet(topUpAgentId) {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Wallet`, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data || res?.data;
|
||||
} catch (error) {
|
||||
console.error('🔴 TopUpAgent API - getWallet error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/TopUpAgent/{topUpAgentId}/Wallet/{walletId}/Deposit
|
||||
async depositWallet(topUpAgentId, walletId, amount) {
|
||||
const payload = {
|
||||
amount: {
|
||||
amount: Number(amount?.amount || 0),
|
||||
currencyCode: String(amount?.currencyCode || ''),
|
||||
},
|
||||
};
|
||||
const res = await api.post(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Wallet/${encodeURIComponent(walletId)}/Deposit`, payload, {
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// GET /api/v1/TopUpAgent/{topUpAgentId}/Wallet/{walletId}/Transaction
|
||||
async getWalletTransactions(topUpAgentId, walletId, params = {}) {
|
||||
const { currentPage = 1, pageSize = 10, ...otherParams } = params;
|
||||
const res = await api.get(`/api/v1/TopUpAgent/${encodeURIComponent(topUpAgentId)}/Wallet/${encodeURIComponent(walletId)}/Transaction`, {
|
||||
params: { currentPage, pageSize, ...otherParams },
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data?.data || [];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// Users API با رولها
|
||||
// -----------------------------
|
||||
const USERS_STORAGE_KEY = 'app_users_v1';
|
||||
function readUsersFromStorage(){ try{ const raw = localStorage.getItem(USERS_STORAGE_KEY); return raw?JSON.parse(raw):[]; } catch{return [];} }
|
||||
function writeUsersToStorage(users){ localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users)); }
|
||||
|
||||
export const usersAPI = {
|
||||
async list({searchQuery='',currentPage=1,pageSize=100}={}) {
|
||||
try {
|
||||
// اطمینان از اینکه params همیشه object است
|
||||
const params = {
|
||||
searchQuery: searchQuery || '',
|
||||
currentPage: currentPage || 1,
|
||||
pageSize: pageSize || 100
|
||||
};
|
||||
|
||||
console.log('🔵 Users API - list request:', {
|
||||
url: '/api/v1/User',
|
||||
method: 'GET',
|
||||
params
|
||||
});
|
||||
|
||||
const res = await api.get('/api/v1/User',{
|
||||
params,
|
||||
skipAuthRedirect:true
|
||||
});
|
||||
|
||||
console.log('🟢 Users API - list response:', res?.data);
|
||||
|
||||
return res?.data?.data?.data||[];
|
||||
} catch (error) {
|
||||
console.error('🔴 Users API - list error:', error);
|
||||
console.error('🔴 Error details:', {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
params: error.config?.params,
|
||||
data: error.config?.data,
|
||||
response: error.response?.data
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async get(id){
|
||||
if (!id) throw new Error('User ID is required');
|
||||
const res = await api.get(`/api/v1/User/${encodeURIComponent(id)}`, { skipAuthRedirect: true });
|
||||
return res?.data?.data || res?.data;
|
||||
},
|
||||
async create(user){
|
||||
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||
const res = await api.post('/api/v1/User',payload,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async update(id,user){
|
||||
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||
const res = await api.put(`/api/v1/User/${encodeURIComponent(id)}`,payload,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async remove(id){
|
||||
const res = await api.delete(`/api/v1/User/Delete/${encodeURIComponent(id)}/Role`,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async toggleActivation(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ToggleActivation`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||
async resetPassword(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ResetPassword`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||
async getRoles(userId){
|
||||
try {
|
||||
const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`, { skipAuthRedirect: true });
|
||||
return res?.data?.data || [];
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
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.put(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{roleIds},{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
} catch(err){
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,9 +6,9 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "https://khalijpay-core.qaserver.ir",
|
||||
target: "https://khalijpay-issuer.qaserver.ir",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
secure: true,
|
||||
// اجازه ارسال کوکیها
|
||||
configure: (proxy) => {
|
||||
proxy.on("proxyReq", (proxyReq, req) => {
|
||||
|
||||
Reference in New Issue
Block a user