fix(topup-agent-management): fix wallet deposit fields

This commit is contained in:
ghazall-ag
2026-01-06 12:28:42 +03:30
parent 964965f0d1
commit 80d0c675a0
6 changed files with 398 additions and 257 deletions

View File

@@ -49,10 +49,13 @@ const Currency = () => {
voucherLimits: { min: null, max: null },
});
const [feesForm, setFeesForm] = useState({
depositLimits: { min: null, max: null },
cashOutLimits: { min: null, max: null },
transferLimits: { min: null, max: null },
voucherLimits: { min: null, max: null },
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 },
generateVoucherByAgentFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
generateVoucherByUserFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
expireVoucherSystemFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
});
// دریافت ارزها
@@ -180,16 +183,51 @@ const Currency = () => {
const onUpdateFees = async (currencyCode, fees) => {
try {
// API PUT expects: depositFee, cashOutFee, transferFee, exchangeFee, generateVoucherByAgentFee, generateVoucherByUserFee, expireVoucherSystemFee
// But GET returns: depositLimits, cashOutLimits, transferLimits, voucherLimits
// Map our structure to API expected structure
// Each with: { percent, fixed, minAmount, maxAmount }
// Ensure all fields are properly formatted
const payload = {
depositFee: fees.depositLimits || { min: null, max: null },
cashOutFee: fees.cashOutLimits || { min: null, max: null },
transferFee: fees.transferLimits || { min: null, max: null },
exchangeFee: { min: null, max: null }, // Required by API
generateVoucherByAgentFee: fees.voucherLimits || { min: null, max: null }, // Map voucherLimits to generateVoucherByAgentFee
generateVoucherByUserFee: { min: null, max: null }, // Required by API
expireVoucherSystemFee: { min: null, max: null }, // Required by API
depositFee: {
percent: Number(fees.depositFee?.percent) || 0,
fixed: Number(fees.depositFee?.fixed) || 0,
minAmount: fees.depositFee?.minAmount != null ? Number(fees.depositFee.minAmount) : null,
maxAmount: fees.depositFee?.maxAmount != null ? Number(fees.depositFee.maxAmount) : null,
},
cashOutFee: {
percent: Number(fees.cashOutFee?.percent) || 0,
fixed: Number(fees.cashOutFee?.fixed) || 0,
minAmount: fees.cashOutFee?.minAmount != null ? Number(fees.cashOutFee.minAmount) : null,
maxAmount: fees.cashOutFee?.maxAmount != null ? Number(fees.cashOutFee.maxAmount) : null,
},
transferFee: {
percent: Number(fees.transferFee?.percent) || 0,
fixed: Number(fees.transferFee?.fixed) || 0,
minAmount: fees.transferFee?.minAmount != null ? Number(fees.transferFee.minAmount) : null,
maxAmount: fees.transferFee?.maxAmount != null ? Number(fees.transferFee.maxAmount) : null,
},
exchangeFee: {
percent: Number(fees.exchangeFee?.percent) || 0,
fixed: Number(fees.exchangeFee?.fixed) || 0,
minAmount: fees.exchangeFee?.minAmount != null ? Number(fees.exchangeFee.minAmount) : null,
maxAmount: fees.exchangeFee?.maxAmount != null ? Number(fees.exchangeFee.maxAmount) : null,
},
generateVoucherByAgentFee: {
percent: Number(fees.generateVoucherByAgentFee?.percent) || 0,
fixed: Number(fees.generateVoucherByAgentFee?.fixed) || 0,
minAmount: fees.generateVoucherByAgentFee?.minAmount != null ? Number(fees.generateVoucherByAgentFee.minAmount) : null,
maxAmount: fees.generateVoucherByAgentFee?.maxAmount != null ? Number(fees.generateVoucherByAgentFee.maxAmount) : null,
},
generateVoucherByUserFee: {
percent: Number(fees.generateVoucherByUserFee?.percent) || 0,
fixed: Number(fees.generateVoucherByUserFee?.fixed) || 0,
minAmount: fees.generateVoucherByUserFee?.minAmount != null ? Number(fees.generateVoucherByUserFee.minAmount) : null,
maxAmount: fees.generateVoucherByUserFee?.maxAmount != null ? Number(fees.generateVoucherByUserFee.maxAmount) : null,
},
expireVoucherSystemFee: {
percent: Number(fees.expireVoucherSystemFee?.percent) || 0,
fixed: Number(fees.expireVoucherSystemFee?.fixed) || 0,
minAmount: fees.expireVoucherSystemFee?.minAmount != null ? Number(fees.expireVoucherSystemFee.minAmount) : null,
maxAmount: fees.expireVoucherSystemFee?.maxAmount != null ? Number(fees.expireVoucherSystemFee.maxAmount) : null,
},
};
console.log('🔵 Currency - onUpdateFees payload:', payload);
const result = await currencyAPI.updateFees(currencyCode, payload);
@@ -313,17 +351,32 @@ const Currency = () => {
try {
// Fetch fees data from API
const feesData = await currencyAPI.getFees(row.currencyCode);
// API returns: depositLimits, cashOutLimits, transferLimits, voucherLimits
const allowedFields = ['depositLimits', 'cashOutLimits', 'transferLimits', 'voucherLimits'];
const filteredFees = allowedFields.reduce((acc, key) => {
// API returns: depositFee, cashOutFee, transferFee, exchangeFee, generateVoucherByAgentFee, generateVoucherByUserFee, expireVoucherSystemFee
// Each with: { percent, fixed, minAmount, maxAmount }
const defaultFee = { percent: 0, fixed: 0, minAmount: null, maxAmount: null };
const feeFields = [
'depositFee',
'cashOutFee',
'transferFee',
'exchangeFee',
'generateVoucherByAgentFee',
'generateVoucherByUserFee',
'expireVoucherSystemFee'
];
const formattedFees = feeFields.reduce((acc, key) => {
if (feesData && feesData[key]) {
acc[key] = feesData[key];
acc[key] = {
percent: feesData[key].percent ?? 0,
fixed: feesData[key].fixed ?? 0,
minAmount: feesData[key].minAmount ?? null,
maxAmount: feesData[key].maxAmount ?? null,
};
} else {
acc[key] = { min: null, max: null };
acc[key] = { ...defaultFee };
}
return acc;
}, {});
setFeesForm(filteredFees);
setFeesForm(formattedFees);
} catch (err) {
console.error('Error fetching fees:', err);
// Fallback to default structure
@@ -558,45 +611,76 @@ const Currency = () => {
...feesForm,
[category]: {
...feesForm[category],
[type]: value === '' ? null : parseFloat(value) || null,
[type]: value === '' ? (type === 'percent' || type === 'fixed' ? 0 : null) : parseFloat(value) || (type === 'percent' || type === 'fixed' ? 0 : null),
},
});
};
const formatCategoryName = (category) => {
return category
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim();
};
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-2xl card p-6 relative bg-white rounded-2xl shadow-lg max-h-[90vh] overflow-y-auto">
<div className="w-full max-w-4xl 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) => {
return (
<div key={category} className="border rounded-lg p-3">
<h4 className="font-medium mb-2">{category}</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={feesForm[category].min ?? ''}
onChange={(e) => updateFee(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={feesForm[category].max ?? ''}
onChange={(e) => updateFee(category, 'max', e.target.value)}
className="w-full p-2 border rounded"
placeholder="Max"
/>
<div key={category} className="border rounded-lg p-4">
<h4 className="font-medium mb-3 text-gray-800">{formatCategoryName(category)}</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<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="0"
/>
</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="0"
/>
</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"
/>
</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"
/>
</div>
</div>
</div>
</div>
);
})}
<div className="flex justify-end gap-x-2 mt-4">

View File

@@ -57,10 +57,7 @@ const Issuer = () => {
const [topUpAgentCurrencyModal, setTopUpAgentCurrencyModal] = useState(null);
const [topUpAgentCurrencyForm, setTopUpAgentCurrencyForm] = useState({
currencyCode: '',
voucherGeneratingFee: { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentMarketingFee: { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentWalletMaxBalanceLimit: { min: 0, max: 0 },
agentWalletDepositMonthlyLimit: { amount: 0 }
voucherGenerationCommission: 0
});
// Wallet states
@@ -479,19 +476,13 @@ const Issuer = () => {
// Edit mode
setTopUpAgentCurrencyForm({
currencyCode: currency.currencyCode || currency.code || '',
voucherGeneratingFee: currency.voucherGeneratingFee || { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentMarketingFee: currency.agentMarketingFee || { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentWalletMaxBalanceLimit: currency.agentWalletMaxBalanceLimit || { min: 0, max: 0 },
agentWalletDepositMonthlyLimit: currency.agentWalletDepositMonthlyLimit || { amount: 0 }
voucherGenerationCommission: currency.voucherGenerationCommission ?? 0
});
} else {
// Add mode
setTopUpAgentCurrencyForm({
currencyCode: '',
voucherGeneratingFee: { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentMarketingFee: { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentWalletMaxBalanceLimit: { min: 0, max: 0 },
agentWalletDepositMonthlyLimit: { amount: 0 }
voucherGenerationCommission: 0
});
}
setTopUpAgentCurrencyModal(currency);
@@ -504,12 +495,21 @@ const Issuer = () => {
return;
}
try {
// Prepare payload with default values for required fields (not shown in UI)
const currencyData = {
voucherGenerationCommission: Number(topUpAgentCurrencyForm.voucherGenerationCommission) || 0,
voucherGeneratingFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
agentMarketingFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
agentWalletMaxBalanceLimit: { min: null, max: null },
agentWalletDepositMonthlyLimit: { amount: null }
};
if (topUpAgentCurrencyModal) {
// Update existing currency
await issuerAPI.updateTopUpAgentManagementCurrency(
topUpAgentModal.id,
topUpAgentCurrencyForm.currencyCode,
topUpAgentCurrencyForm
currencyData
);
toast.success('Currency updated successfully');
} else {
@@ -517,7 +517,7 @@ const Issuer = () => {
await issuerAPI.createTopUpAgentManagementCurrency(
topUpAgentModal.id,
topUpAgentCurrencyForm.currencyCode,
topUpAgentCurrencyForm
currencyData
);
toast.success('Currency added successfully');
}
@@ -528,10 +528,7 @@ const Issuer = () => {
setTopUpAgentCurrencyModal(null);
setTopUpAgentCurrencyForm({
currencyCode: '',
voucherGeneratingFee: { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentMarketingFee: { percent: 0, fixed: 0, minAmount: 0, maxAmount: 0 },
agentWalletMaxBalanceLimit: { min: 0, max: 0 },
agentWalletDepositMonthlyLimit: { amount: 0 }
voucherGenerationCommission: 0
});
} catch (err) {
toast.error(getErrorMessage(err));
@@ -559,13 +556,38 @@ const Issuer = () => {
setSelectedCurrencyForWallet(currencyCode);
setWalletLoading(true);
try {
// دریافت balance و transactions به صورت همزمان
const [balance, transactions] = await Promise.all([
issuerAPI.getTopUpAgentManagementWalletBalance(topUpAgentModal.id, currencyCode),
issuerAPI.getTopUpAgentManagementWalletTransactions(topUpAgentModal.id, currencyCode)
]);
// دریافت balance و transactions به صورت جداگانه برای handle کردن خطاها
let balance = null;
let transactions = [];
try {
balance = await issuerAPI.getTopUpAgentManagementWalletBalance(topUpAgentModal.id, currencyCode);
} catch (err) {
console.error('Error loading wallet balance:', err);
toast.error('Failed to load wallet balance');
}
try {
transactions = await issuerAPI.getTopUpAgentManagementWalletTransactions(topUpAgentModal.id, currencyCode);
transactions = Array.isArray(transactions) ? transactions : [];
} catch (err) {
console.error('Error loading wallet transactions:', err);
// اگر transactions خطا داد، فقط warning نشان بده و balance را نمایش بده
const errorMessage = err?.response?.status === 500
? 'Unable to load transaction history. Balance information is available.'
: 'Failed to load transaction history';
// Use toast.warning if available, otherwise use toast.error
if (toast.warning) {
toast.warning(errorMessage);
} else {
toast.error(errorMessage);
}
transactions = [];
}
setWalletBalance(balance);
setWalletTransactions(Array.isArray(transactions) ? transactions : []);
setWalletTransactions(transactions);
} catch (err) {
console.error('Error loading wallet data:', err);
toast.error('Failed to load wallet data');
@@ -1152,16 +1174,6 @@ const Issuer = () => {
required
/>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={topUpAgentData.isActive}
onChange={(e) => setTopUpAgentData({ ...topUpAgentData, isActive: e.target.checked })}
disabled
className="w-4 h-4"
/>
<label className="text-sm text-gray-700">Active: {topUpAgentData.isActive ? 'Yes' : 'No'}</label>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<button
@@ -1274,173 +1286,18 @@ const Issuer = () => {
)}
</div>
{/* Voucher Generating Fee */}
<div className="border-t pt-4">
<h4 className="font-medium text-gray-900 mb-3">Voucher Generating Fee</h4>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm mb-1 text-gray-600">Percent</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.voucherGeneratingFee.percent}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, percent: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm mb-1 text-gray-600">Fixed</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.voucherGeneratingFee.fixed}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, fixed: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm mb-1 text-gray-600">Min Amount</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.voucherGeneratingFee.minAmount}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, minAmount: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm mb-1 text-gray-600">Max Amount</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.voucherGeneratingFee.maxAmount}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, maxAmount: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
</div>
</div>
{/* Agent Marketing Fee */}
<div className="border-t pt-4">
<h4 className="font-medium text-gray-900 mb-3">Agent Marketing Fee</h4>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm mb-1 text-gray-600">Percent</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.agentMarketingFee.percent}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, percent: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm mb-1 text-gray-600">Fixed</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.agentMarketingFee.fixed}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, fixed: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm mb-1 text-gray-600">Min Amount</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.agentMarketingFee.minAmount}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, minAmount: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm mb-1 text-gray-600">Max Amount</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.agentMarketingFee.maxAmount}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, maxAmount: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
</div>
</div>
{/* Agent Wallet Max Balance Limit */}
<div className="border-t pt-4">
<h4 className="font-medium text-gray-900 mb-3">Agent Wallet Max Balance Limit</h4>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm mb-1 text-gray-600">Min</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.agentWalletMaxBalanceLimit.min}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
agentWalletMaxBalanceLimit: { ...topUpAgentCurrencyForm.agentWalletMaxBalanceLimit, min: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm mb-1 text-gray-600">Max</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.agentWalletMaxBalanceLimit.max}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
agentWalletMaxBalanceLimit: { ...topUpAgentCurrencyForm.agentWalletMaxBalanceLimit, max: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
</div>
</div>
{/* Agent Wallet Deposit Monthly Limit */}
<div className="border-t pt-4">
<h4 className="font-medium text-gray-900 mb-3">Agent Wallet Deposit Monthly Limit</h4>
<div>
<label className="block text-sm mb-1 text-gray-600">Amount</label>
<input
type="number"
step="0.01"
value={topUpAgentCurrencyForm.agentWalletDepositMonthlyLimit.amount}
onChange={(e) => setTopUpAgentCurrencyForm({
...topUpAgentCurrencyForm,
agentWalletDepositMonthlyLimit: { amount: Number(e.target.value) || 0 }
})}
className="w-full p-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700">Voucher Generation Commission</label>
<input
type="number"
step="0.01"
min="0"
value={topUpAgentCurrencyForm.voucherGenerationCommission}
onChange={(e) => setTopUpAgentCurrencyForm({ ...topUpAgentCurrencyForm, voucherGenerationCommission: Number(e.target.value) || 0 })}
className="w-full p-2 border rounded-lg"
placeholder="0"
/>
<p className="mt-1 text-xs text-gray-500">Commission for voucher generation</p>
</div>
<div className="flex justify-end gap-2 pt-4 border-t">
@@ -1509,7 +1366,9 @@ const Issuer = () => {
<h4 className="text-sm font-medium text-gray-600 mb-2">Current Balance</h4>
{walletBalance !== null ? (
<div className="text-3xl font-bold text-gray-900">
{typeof walletBalance === 'object' && walletBalance.balance !== undefined
{typeof walletBalance === 'object' && walletBalance.amount !== undefined
? walletBalance.amount
: typeof walletBalance === 'object' && walletBalance.balance !== undefined
? walletBalance.balance
: typeof walletBalance === 'number'
? walletBalance

View File

@@ -1,5 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Save, Key, Globe, DollarSign, Bell, Shield, Database } from 'lucide-react';
import { Save, Key, Globe, DollarSign, Bell, Shield, Database, Ticket } from 'lucide-react';
import { generalAPI } from '../services/api';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
const Settings = () => {
const [settings, setSettings] = useState({
@@ -21,12 +25,21 @@ const Settings = () => {
}
});
const [voucherConfig, setVoucherConfig] = useState({
expireInDays: 0,
refundUponVoucherExpiration: true,
canPurchaseByAgentVouchers: true,
canTopUpWithUserVouchers: true
});
const [loading, setLoading] = useState(false);
const [voucherLoading, setVoucherLoading] = useState(false);
const [message, setMessage] = useState('');
const [activeTab, setActiveTab] = useState('payment');
useEffect(() => {
loadSettings();
loadVoucherConfiguration();
}, []);
const loadSettings = () => {
@@ -36,6 +49,26 @@ const Settings = () => {
}
};
const loadVoucherConfiguration = async () => {
try {
setVoucherLoading(true);
const config = await generalAPI.getVoucherConfiguration();
if (config) {
setVoucherConfig({
expireInDays: config.expireInDays ?? 0,
refundUponVoucherExpiration: config.refundUponVoucherExpiration ?? true,
canPurchaseByAgentVouchers: config.canPurchaseByAgentVouchers ?? true,
canTopUpWithUserVouchers: config.canTopUpWithUserVouchers ?? true
});
}
} catch (err) {
console.error('Error loading voucher configuration:', err);
toast.error(getErrorMessage(err));
} finally {
setVoucherLoading(false);
}
};
const handleInputChange = (section, field, value) => {
setSettings(prev => ({
...prev,
@@ -72,11 +105,32 @@ const Settings = () => {
}
};
const handleVoucherSave = async () => {
setVoucherLoading(true);
try {
const result = await generalAPI.updateVoucherConfiguration(voucherConfig);
toast.success(getSuccessMessage(result) || 'Voucher configuration saved successfully');
await loadVoucherConfiguration();
} catch (err) {
toast.error(getErrorMessage(err));
} finally {
setVoucherLoading(false);
}
};
const handleVoucherChange = (field, value) => {
setVoucherConfig(prev => ({
...prev,
[field]: value
}));
};
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 }
{ id: 'data', label: 'Data', icon: Database },
{ id: 'voucher', label: 'Voucher', icon: Ticket }
];
const currencies = [
@@ -90,6 +144,7 @@ const Settings = () => {
return (
<div className="p-6">
<ToastContainer position="top-right" autoClose={3000} />
<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>
@@ -331,16 +386,105 @@ const Settings = () => {
</div>
)}
{/* Voucher Settings */}
{activeTab === 'voucher' && (
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6">Voucher Configuration</h3>
{voucherLoading ? (
<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>
) : (
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Expire In Days
</label>
<input
type="number"
min="0"
value={voucherConfig.expireInDays}
onChange={(e) => handleVoucherChange('expireInDays', Number(e.target.value) || 0)}
className="input-field w-32"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Number of days before vouchers expire
</p>
</div>
<div className="flex items-center justify-between">
<div>
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">Refund Upon Voucher Expiration</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Automatically refund when voucher expires</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={voucherConfig.refundUponVoucherExpiration}
onChange={(e) => handleVoucherChange('refundUponVoucherExpiration', 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">Can Purchase By Agent Vouchers</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Allow purchases using agent-generated vouchers</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={voucherConfig.canPurchaseByAgentVouchers}
onChange={(e) => handleVoucherChange('canPurchaseByAgentVouchers', 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">Can Top Up With User Vouchers</label>
<p className="text-xs text-gray-500 dark:text-gray-400">Allow top-up using user-generated vouchers</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={voucherConfig.canTopUpWithUserVouchers}
onChange={(e) => handleVoucherChange('canTopUpWithUserVouchers', 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>
{activeTab === 'voucher' ? (
<button
onClick={handleVoucherSave}
disabled={voucherLoading}
className="btn-primary flex items-center"
>
<Save className="h-4 w-4 mr-2" />
{voucherLoading ? 'Saving...' : 'Save Voucher Settings'}
</button>
) : (
<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>

View File

@@ -157,7 +157,8 @@ export const currencyAPI = {
skipAuthRedirect: true
});
console.log('🟢 Currency API - getFees response:', res?.data);
// Response structure: { statusCode, isSuccess, code, message, errors, data: { depositLimits, cashOutLimits, transferLimits, voucherLimits } }
// Response structure: { statusCode, isSuccess, code, message, errors, data: { depositFee, cashOutFee, transferFee, exchangeFee, generateVoucherByAgentFee, generateVoucherByUserFee, expireVoucherSystemFee } }
// Each fee has: { percent, fixed, minAmount, maxAmount }
// Return the data field from the response
return res?.data?.data || null;
} catch (error) {

View File

@@ -54,5 +54,56 @@ export const generalAPI = {
throw error;
}
},
// ========== System Configuration Voucher API ==========
// GET /api/v1/SystemConfiguration/Voucher
async getVoucherConfiguration() {
try {
console.log('🔵 General API - getVoucherConfiguration request');
const res = await api.get('/api/v1/SystemConfiguration/Voucher', {
skipAuthRedirect: true
});
console.log('🟢 General API - getVoucherConfiguration response:', res?.data);
// Response structure: { statusCode, isSuccess, code, message, errors, data: { expireInDays, refundUponVoucherExpiration, canPurchaseByAgentVouchers, canTopUpWithUserVouchers } }
return res?.data?.data || null;
} catch (error) {
console.error('🔴 General API - getVoucherConfiguration error:', {
status: error?.response?.status,
data: error?.response?.data,
error: error?.response?.data || error?.message
});
throw error;
}
},
// PUT /api/v1/SystemConfiguration/Voucher
async updateVoucherConfiguration(config) {
if (!config) {
throw new Error('Voucher configuration is required');
}
try {
const payload = {
expireInDays: Number(config.expireInDays) || 0,
refundUponVoucherExpiration: Boolean(config.refundUponVoucherExpiration),
canPurchaseByAgentVouchers: Boolean(config.canPurchaseByAgentVouchers),
canTopUpWithUserVouchers: Boolean(config.canTopUpWithUserVouchers)
};
console.log('🔵 General API - updateVoucherConfiguration request:', { payload });
const res = await api.put('/api/v1/SystemConfiguration/Voucher', payload, {
skipAuthRedirect: true
});
console.log('🟢 General API - updateVoucherConfiguration response:', res?.data);
return res?.data;
} catch (error) {
console.error('🔴 General API - updateVoucherConfiguration error:', {
payload: config,
status: error?.response?.status,
data: error?.response?.data,
error: error?.response?.data || error?.message
});
throw error;
}
},
};

View File

@@ -455,6 +455,7 @@ export const issuerAPI = {
}
try {
const payload = {
voucherGenerationCommission: Number(currencyData.voucherGenerationCommission) || 0,
voucherGeneratingFee: {
percent: Number(currencyData.voucherGeneratingFee?.percent) || 0,
fixed: Number(currencyData.voucherGeneratingFee?.fixed) || 0,
@@ -498,6 +499,7 @@ export const issuerAPI = {
}
try {
const payload = {
voucherGenerationCommission: Number(currencyData.voucherGenerationCommission) || 0,
voucherGeneratingFee: {
percent: Number(currencyData.voucherGeneratingFee?.percent) || 0,
fixed: Number(currencyData.voucherGeneratingFee?.fixed) || 0,