fix(topup-agent-management): fix wallet deposit fields
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user