feat(TopUpAgent): add , edit, delete , toggle ,...

This commit is contained in:
ghazall-ag
2025-12-23 23:29:58 +03:30
parent ecf6574e5d
commit 8e19500f7c
27 changed files with 1462 additions and 4865 deletions

144
README.md
View File

@@ -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
```
## مجوز
این پروژه خصوصی است.

View File

@@ -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"
},

View File

@@ -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";

View File

@@ -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>
);

View File

@@ -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}

View File

@@ -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 (

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -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">

View File

@@ -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;

View File

@@ -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
View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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 || {} });
}

View File

@@ -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 || {};

View File

@@ -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;

View File

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

View File

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

View File

@@ -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;
}
}
};

View File

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

View File

@@ -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;

View File

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

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

View File

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

View File

@@ -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) => {