From ecf6574e5da9c5fe6db4552b0004ed8acca65ae0 Mon Sep 17 00:00:00 2001 From: ghazall-ag Date: Mon, 15 Dec 2025 14:04:15 +0330 Subject: [PATCH] feat(Issuer): add admin selection page for issuer --- src/components/Sidebar.jsx | 4 +- src/pages/Issuer.jsx | 1170 ++++++++++++++++++++++++++++-------- src/pages/Location.jsx | 8 +- src/services/generalAPI.js | 15 + src/services/issuerAPI.js | 234 +++++++- 5 files changed, 1174 insertions(+), 257 deletions(-) diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 8821d572..d3ebac90 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -16,13 +16,13 @@ import { const Sidebar = ({ isOpen, onToggle }) => { const navigation = [ { name: 'Dashboard', href: '/', icon: LayoutDashboard }, - { name: 'Transactions', href: '/transactions', icon: CreditCard }, + // { 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: 'Settings', href: '/settings', icon: Settings }, ]; return ( diff --git a/src/pages/Issuer.jsx b/src/pages/Issuer.jsx index 663d4b1e..a203be8a 100644 --- a/src/pages/Issuer.jsx +++ b/src/pages/Issuer.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import DataTable from '../components/DataTable'; -import { issuerAPI, cityAPI, currencyAPI, countryAPI, provinceAPI, generalAPI } from '../services/api'; -import { Plus, Trash2, Search, Pencil, Power, Settings, DollarSign, Filter } from 'lucide-react'; +import { issuerAPI, cityAPI, currencyAPI, generalAPI } from '../services/api'; +import { Plus, Trash2, Search, Pencil, Power, Settings, DollarSign, Filter, X, Wallet, RefreshCw, Users } from 'lucide-react'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler'; @@ -15,15 +15,10 @@ const Issuer = () => { // Filter states const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); const [filterState, setFilterState] = useState({ - countryId: '', - provinceId: '', - cityId: '', isActive: '', supportCurrencyCode: '', supportCapabilities: '' }); - const [countries, setCountries] = useState([]); - const [provinces, setProvinces] = useState([]); const [issuerCapabilities, setIssuerCapabilities] = useState([]); const [addForm, setAddForm] = useState({ @@ -47,12 +42,41 @@ const Issuer = () => { const [editingIssuer, setEditingIssuer] = useState(null); const [capabilitiesModal, setCapabilitiesModal] = useState(null); const [currenciesModal, setCurrenciesModal] = useState(null); + const [topUpAgentModal, setTopUpAgentModal] = useState(null); + const [adminModal, setAdminModal] = useState(null); const [cities, setCities] = useState([]); const [currencies, setCurrencies] = useState([]); const [selectedCapabilities, setSelectedCapabilities] = useState([]); const [selectedCurrencies, setSelectedCurrencies] = useState([]); const [capabilitiesData, setCapabilitiesData] = useState([]); // Store full capabilities data from API const [currenciesData, setCurrenciesData] = useState([]); // Store full currencies data from API + + // TopUpAgentManagement states + const [topUpAgentData, setTopUpAgentData] = useState({ maxAgents: 0, isActive: false }); + const [topUpAgentCurrencies, setTopUpAgentCurrencies] = useState([]); + 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 } + }); + + // Wallet states + const [walletBalance, setWalletBalance] = useState(null); + const [walletTransactions, setWalletTransactions] = useState([]); + const [walletLoading, setWalletLoading] = useState(false); + const [selectedCurrencyForWallet, setSelectedCurrencyForWallet] = useState(null); + const [depositAmount, setDepositAmount] = useState(0); + const [isDepositModalOpen, setIsDepositModalOpen] = useState(false); + + // Admin states + const [admins, setAdmins] = useState([]); + const [adminLoading, setAdminLoading] = useState(false); + const [adminSearchEmail, setAdminSearchEmail] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [searchLoading, setSearchLoading] = useState(false); // Capabilities options - will be loaded from API const capabilityOptions = issuerCapabilities.map(cap => cap.name); @@ -62,9 +86,6 @@ const Issuer = () => { try { setLoading(true); const params = {}; - if (filters.countryId) params.countryId = filters.countryId; - if (filters.provinceId) params.provinceId = filters.provinceId; - if (filters.cityId) params.cityId = filters.cityId; if (filters.isActive !== undefined && filters.isActive !== null && filters.isActive !== '') { params.isActive = filters.isActive === 'true' || filters.isActive === true; } @@ -91,26 +112,8 @@ const Issuer = () => { } }, []); - // Load countries, provinces, and capabilities for filter modal + // Load capabilities for filter modal useEffect(() => { - const loadCountries = async () => { - try { - const countriesList = await countryAPI.listAll(); - setCountries(Array.isArray(countriesList) ? countriesList : []); - } catch (err) { - console.error('Failed to load countries:', err); - } - }; - - const loadProvinces = async () => { - try { - const provincesList = await provinceAPI.list({ currentPage: 1, pageSize: 1000 }); - setProvinces(Array.isArray(provincesList) ? provincesList : []); - } catch (err) { - console.error('Failed to load provinces:', err); - } - }; - const loadCapabilities = async () => { try { const capabilitiesList = await generalAPI.getIssuerCapabilities(); @@ -120,8 +123,6 @@ const Issuer = () => { } }; - loadCountries(); - loadProvinces(); loadCapabilities(); }, []); @@ -340,111 +341,77 @@ const Issuer = () => { setCurrenciesModal(issuer); try { // Get allowed currencies from /api/v1/Issuer/{issuerId}/AllowedCurrencies + // API returns: { data: [{ currencyCode, currencyEnglishName, allowed }, ...], ... } const allowed = await issuerAPI.getAllowedCurrencies(issuer.id); console.log('🔵 Allowed currencies received from API:', allowed); if (Array.isArray(allowed) && allowed.length > 0) { - // Use currencies from API - setCurrenciesData(allowed); + // Use currencies directly from API response + const allCurrenciesList = allowed.map(curr => { + const code = String(curr.currencyCode || curr.code || '').trim(); + if (!code) return null; + + return { + code: code, + currencyCode: code, + name: curr.currencyEnglishName || curr.name || '', + currencyEnglishName: curr.currencyEnglishName || curr.name || '', + allowed: curr.allowed === true || curr.allowed === 'true' + }; + }).filter(c => c !== null); + + setCurrenciesData(allCurrenciesList); + // Extract selected currencies (those with allowed = true) - const selected = allowed + const selected = allCurrenciesList .filter(c => c.allowed === true || c.allowed === 'true') .map(c => c.code || c.currencyCode); setSelectedCurrencies(selected); console.log('🔵 Selected currencies:', selected); + console.log('🔵 All currencies with allowed status:', allCurrenciesList); } else { - // If no currencies from API, create default structure from available currencies - const defaultCurrencies = currencies.map(curr => { - const code = curr.currencyCode?.code || curr.code || ''; - const name = curr.currencyCode?.name || curr.name || ''; - return { - code: code, - currencyCode: code, - name: name, - currencyEnglishName: name, - allowed: false - }; - }).filter(c => c.code); - setCurrenciesData(defaultCurrencies); + // If API returns empty array, show empty list + setCurrenciesData([]); setSelectedCurrencies([]); - console.log('🔵 Using default currencies structure'); + console.log('🔵 No currencies returned from API'); } } catch (err) { console.error('🔴 Error loading allowed currencies:', err); - // Fallback: create default structure - const defaultCurrencies = currencies.map(curr => { - const code = curr.currencyCode?.code || curr.code || ''; - const name = curr.currencyCode?.name || curr.name || ''; - return { - code: code, - currencyCode: code, - name: name, - currencyEnglishName: name, - allowed: false - }; - }).filter(c => c.code); - setCurrenciesData(defaultCurrencies); + toast.error('Failed to load allowed currencies'); + setCurrenciesData([]); setSelectedCurrencies([]); - toast.error('Failed to load allowed currencies. Using default structure.'); } }; const onUpdateCurrencies = async () => { if (!currenciesModal) return; try { - // Get current allowed currencies from API - let currentAllowedCurrencies = []; - try { - currentAllowedCurrencies = await issuerAPI.getAllowedCurrencies(currenciesModal.id); - } catch (err) { - console.warn('⚠️ Could not fetch current allowed currencies:', err); - } - - // Build map of current currencies (those already configured) - const currentCurrenciesMap = new Map(); - if (Array.isArray(currentAllowedCurrencies)) { - currentAllowedCurrencies.forEach(curr => { - const code = curr.code || curr.currencyCode || ''; - if (code) { - currentCurrenciesMap.set(String(code).trim(), curr); - } - }); - } - - // Build IssuerAllowedCurrencyDto array from SELECTED currencies only - // Only send currencies that are selected (to avoid conflict with already configured ones) + // Get all currencies that should be sent (from currenciesData or currencies) const allCurrencies = currenciesData.length > 0 ? currenciesData : currencies; - const allowedCurrencies = selectedCurrencies - .filter(selectedCode => { - // Filter out invalid currency codes - return selectedCode && String(selectedCode).trim() !== ''; - }) - .map(selectedCode => { - const code = String(selectedCode).trim(); + // Build IssuerAllowedCurrencyDto array for ALL currencies + // Set allowed=true for selected currencies, allowed=false for unselected ones + const allowedCurrencies = allCurrencies + .map(curr => { + const code = String(curr.code || curr.currencyCode || curr.currencyCode?.code || '').trim(); + if (!code) return null; - // Find the currency data - const currencyData = allCurrencies.find(curr => { - const currCode = String(curr.code || curr.currencyCode || curr.currencyCode?.code || '').trim(); - return currCode === code || - currCode === String(curr.currencyCode || '').trim() || - currCode === String(curr.currencyCode?.code || '').trim(); + const name = String(curr.currencyEnglishName || curr.name || curr.currencyCode?.name || '').trim(); + + // Check if this currency is in selectedCurrencies + const isSelected = selectedCurrencies.some(selected => { + const selectedStr = String(selected).trim(); + return selectedStr === code || + selectedStr === String(curr.currencyCode || '').trim(); }); - const name = currencyData - ? String(currencyData.currencyEnglishName || currencyData.name || currencyData.currencyCode?.name || '').trim() - : ''; - - // Use existing currency data if available - const existingCurrency = currentCurrenciesMap.get(code); - return { currencyCode: code, - currencyEnglishName: name || existingCurrency?.currencyEnglishName || existingCurrency?.name || code, - allowed: true // All selected currencies have allowed=true + currencyEnglishName: name || code, + allowed: isSelected // true if selected, false if not }; }) - .filter(curr => curr.currencyCode && curr.currencyCode.trim() !== ''); + .filter(curr => curr !== null && curr.currencyCode && curr.currencyCode.trim() !== ''); console.log('🔵 Sending ALL currencies to update:', JSON.stringify(allowedCurrencies, null, 2)); console.log('🔵 Issuer ID:', currenciesModal.id); @@ -452,7 +419,6 @@ const Issuer = () => { console.log('🔵 Selected currencies:', selectedCurrencies); console.log('🔵 Currencies with allowed=true:', allowedCurrencies.filter(c => c.allowed).length); console.log('🔵 Currencies with allowed=false:', allowedCurrencies.filter(c => !c.allowed).length); - console.log('🔵 Current currencies from API:', currentAllowedCurrencies.length); // Call PUT /api/v1/Issuer/{issuerId}/AllowedCurrencies await issuerAPI.updateAllowedCurrencies(currenciesModal.id, allowedCurrencies); @@ -474,6 +440,229 @@ const Issuer = () => { } }; + // --- مدیریت TopUpAgentManagement --- + const openTopUpAgentModal = async (issuer) => { + setTopUpAgentModal(issuer); + try { + // دریافت تنظیمات TopUpAgentManagement + const data = await issuerAPI.getTopUpAgentManagement(issuer.id); + setTopUpAgentData(data || { maxAgents: 0, isActive: false }); + + // دریافت لیست currencies + const currenciesList = await issuerAPI.getTopUpAgentManagementCurrencies(issuer.id); + setTopUpAgentCurrencies(Array.isArray(currenciesList) ? currenciesList : []); + } catch (err) { + console.error('Error loading TopUpAgentManagement:', err); + toast.error('Failed to load TopUpAgentManagement data'); + setTopUpAgentData({ maxAgents: 0, isActive: false }); + setTopUpAgentCurrencies([]); + } + }; + + const onUpdateTopUpAgentManagement = async (e) => { + e.preventDefault(); + if (!topUpAgentModal) return; + try { + await issuerAPI.updateTopUpAgentManagement(topUpAgentModal.id, topUpAgentData.maxAgents); + toast.success('TopUpAgentManagement updated successfully'); + // Refresh data + const data = await issuerAPI.getTopUpAgentManagement(topUpAgentModal.id); + setTopUpAgentData(data || { maxAgents: 0, isActive: false }); + await fetchIssuers(filterState); + } catch (err) { + toast.error(getErrorMessage(err)); + } + }; + + const openTopUpAgentCurrencyModal = (currency = null) => { + if (currency) { + // 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 } + }); + } 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 } + }); + } + setTopUpAgentCurrencyModal(currency); + }; + + const onSaveTopUpAgentCurrency = async (e) => { + e.preventDefault(); + if (!topUpAgentModal || !topUpAgentCurrencyForm.currencyCode) { + toast.error('Currency code is required'); + return; + } + try { + if (topUpAgentCurrencyModal) { + // Update existing currency + await issuerAPI.updateTopUpAgentManagementCurrency( + topUpAgentModal.id, + topUpAgentCurrencyForm.currencyCode, + topUpAgentCurrencyForm + ); + toast.success('Currency updated successfully'); + } else { + // Create new currency + await issuerAPI.createTopUpAgentManagementCurrency( + topUpAgentModal.id, + topUpAgentCurrencyForm.currencyCode, + topUpAgentCurrencyForm + ); + toast.success('Currency added successfully'); + } + + // Refresh currencies list + const currenciesList = await issuerAPI.getTopUpAgentManagementCurrencies(topUpAgentModal.id); + setTopUpAgentCurrencies(Array.isArray(currenciesList) ? currenciesList : []); + 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 } + }); + } catch (err) { + toast.error(getErrorMessage(err)); + } + }; + + const onDeleteTopUpAgentCurrency = async (currencyCode) => { + if (!topUpAgentModal || !currencyCode) return; + if (!window.confirm(`Are you sure you want to delete currency ${currencyCode}?`)) return; + try { + await issuerAPI.deleteTopUpAgentManagementCurrency(topUpAgentModal.id, currencyCode); + toast.success('Currency deleted successfully'); + + // Refresh currencies list + const currenciesList = await issuerAPI.getTopUpAgentManagementCurrencies(topUpAgentModal.id); + setTopUpAgentCurrencies(Array.isArray(currenciesList) ? currenciesList : []); + } catch (err) { + toast.error(getErrorMessage(err)); + } + }; + + // --- مدیریت Wallet --- + const openWalletView = async (currencyCode) => { + if (!topUpAgentModal || !currencyCode) return; + setSelectedCurrencyForWallet(currencyCode); + setWalletLoading(true); + try { + // دریافت balance و transactions به صورت همزمان + const [balance, transactions] = await Promise.all([ + issuerAPI.getTopUpAgentManagementWalletBalance(topUpAgentModal.id, currencyCode), + issuerAPI.getTopUpAgentManagementWalletTransactions(topUpAgentModal.id, currencyCode) + ]); + setWalletBalance(balance); + setWalletTransactions(Array.isArray(transactions) ? transactions : []); + } catch (err) { + console.error('Error loading wallet data:', err); + toast.error('Failed to load wallet data'); + setWalletBalance(null); + setWalletTransactions([]); + } finally { + setWalletLoading(false); + } + }; + + const onDepositWallet = async (e) => { + e.preventDefault(); + if (!topUpAgentModal || !selectedCurrencyForWallet || !depositAmount || depositAmount <= 0) { + toast.error('Please enter a valid deposit amount'); + return; + } + try { + await issuerAPI.depositTopUpAgentManagementWallet( + topUpAgentModal.id, + selectedCurrencyForWallet, + depositAmount + ); + toast.success('Deposit successful'); + setIsDepositModalOpen(false); + setDepositAmount(0); + + // Refresh wallet data + await openWalletView(selectedCurrencyForWallet); + } catch (err) { + toast.error(getErrorMessage(err)); + } + }; + + const refreshWalletData = async () => { + if (selectedCurrencyForWallet) { + await openWalletView(selectedCurrencyForWallet); + } + }; + + // --- مدیریت Admin --- + const openAdminModal = async (issuer) => { + setAdminModal(issuer); + setAdminLoading(true); + setAdmins([]); + setSearchResults([]); + setAdminSearchEmail(''); + try { + const adminsList = await issuerAPI.getAdmins(issuer.id); + setAdmins(Array.isArray(adminsList) ? adminsList : []); + } catch (err) { + console.error('Error loading admins:', err); + toast.error('Failed to load admins'); + setAdmins([]); + } finally { + setAdminLoading(false); + } + }; + + const searchUsers = async () => { + if (!adminSearchEmail || !adminSearchEmail.trim()) { + toast.error('Please enter an email to search'); + return; + } + setSearchLoading(true); + setSearchResults([]); + try { + const results = await generalAPI.searchUsersByEmail(adminSearchEmail.trim()); + setSearchResults(Array.isArray(results) ? results : []); + if (results.length === 0) { + toast.info('No users found with this email'); + } + } catch (err) { + console.error('Error searching users:', err); + toast.error(getErrorMessage(err)); + setSearchResults([]); + } finally { + setSearchLoading(false); + } + }; + + const onAddAdmin = async (user) => { + if (!adminModal || !user) return; + try { + await issuerAPI.addAdmin(adminModal.id, { userId: user.id || user.userId }); + toast.success('Admin added successfully'); + // Refresh admins list + const adminsList = await issuerAPI.getAdmins(adminModal.id); + setAdmins(Array.isArray(adminsList) ? adminsList : []); + // Clear search + setSearchResults([]); + setAdminSearchEmail(''); + } catch (err) { + console.error('Error adding admin:', err); + toast.error(getErrorMessage(err)); + } + }; + // --- مدیریت فیلترها --- const handleApplyFilter = () => { fetchIssuers(filterState); @@ -482,9 +671,6 @@ const Issuer = () => { const handleResetFilter = () => { setFilterState({ - countryId: '', - provinceId: '', - cityId: '', isActive: '', supportCurrencyCode: '', supportCapabilities: '' @@ -493,74 +679,6 @@ const Issuer = () => { setIsFilterModalOpen(false); }; - // وقتی country در فیلتر تغییر می‌کند، provinces را به‌روزرسانی کن - useEffect(() => { - if (filterState.countryId) { - const loadProvinces = async () => { - try { - const provincesList = await provinceAPI.list({ - currentPage: 1, - pageSize: 1000, - countryId: filterState.countryId - }); - setProvinces(Array.isArray(provincesList) ? provincesList : []); - } catch (err) { - console.error('Failed to load provinces:', err); - } - }; - loadProvinces(); - // Reset province and city when country changes - setFilterState(prev => ({ ...prev, provinceId: '', cityId: '' })); - } else { - // Load all provinces if no country selected - const loadProvinces = async () => { - try { - const provincesList = await provinceAPI.list({ currentPage: 1, pageSize: 1000 }); - setProvinces(Array.isArray(provincesList) ? provincesList : []); - } catch (err) { - console.error('Failed to load provinces:', err); - } - }; - loadProvinces(); - } - }, [filterState.countryId]); - - // وقتی province در فیلتر تغییر می‌کند، cities را به‌روزرسانی کن - useEffect(() => { - if (filterState.provinceId) { - const loadCities = async () => { - try { - const citiesList = await cityAPI.list({ - currentPage: 1, - pageSize: 1000, - provinceId: filterState.provinceId - }); - setCities(Array.isArray(citiesList) ? citiesList : []); - } catch (err) { - console.error('Failed to load cities:', err); - } - }; - loadCities(); - // Reset city when province changes - setFilterState(prev => ({ ...prev, cityId: '' })); - } else if (filterState.countryId) { - // Load cities for selected country if no province selected - const loadCities = async () => { - try { - const citiesList = await cityAPI.list({ - currentPage: 1, - pageSize: 1000, - countryId: filterState.countryId - }); - setCities(Array.isArray(citiesList) ? citiesList : []); - } catch (err) { - console.error('Failed to load cities:', err); - } - }; - loadCities(); - } - }, [filterState.provinceId, filterState.countryId]); - // --- ستون‌های جدول --- const columns = useMemo(() => [ { key: 'name', header: 'Name' }, @@ -617,6 +735,18 @@ const Issuer = () => { > Currencies + + ), }, @@ -787,65 +917,6 @@ const Issuer = () => {

Filter Issuers

-
- - -
-
- - -
-
- - -
setTopUpAgentData({ ...topUpAgentData, maxAgents: Number(e.target.value) || 0 })} + className="w-full p-2 border rounded-lg" + required + /> +
+
+ setTopUpAgentData({ ...topUpAgentData, isActive: e.target.checked })} + disabled + className="w-4 h-4" + /> + +
+
+
+ + +
+
+ + + {/* Currencies Section */} +
+
+

Currencies

+ +
+ + {topUpAgentCurrencies.length > 0 ? ( +
+ {topUpAgentCurrencies.map((curr) => ( +
+ {curr.currencyCode || curr.code} +
+ + + +
+
+ ))} +
+ ) : ( +

No currencies configured

+ )} +
+ + + + )} + + {/* TopUpAgentManagement Currency Modal */} + {topUpAgentModal && topUpAgentCurrencyModal !== undefined && ( + <> +
setTopUpAgentCurrencyModal(undefined)} /> +
+
+ +

+ {topUpAgentCurrencyModal ? 'Edit Currency' : 'Add Currency'} - {topUpAgentModal.name} +

+ +
+
+ + {topUpAgentCurrencyModal ? ( + + ) : ( + + )} +
+ + {/* Voucher Generating Fee */} +
+

Voucher Generating Fee

+
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, percent: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, fixed: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, minAmount: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + voucherGeneratingFee: { ...topUpAgentCurrencyForm.voucherGeneratingFee, maxAmount: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+
+ + {/* Agent Marketing Fee */} +
+

Agent Marketing Fee

+
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, percent: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, fixed: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, minAmount: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + agentMarketingFee: { ...topUpAgentCurrencyForm.agentMarketingFee, maxAmount: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+
+ + {/* Agent Wallet Max Balance Limit */} +
+

Agent Wallet Max Balance Limit

+
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + agentWalletMaxBalanceLimit: { ...topUpAgentCurrencyForm.agentWalletMaxBalanceLimit, min: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + agentWalletMaxBalanceLimit: { ...topUpAgentCurrencyForm.agentWalletMaxBalanceLimit, max: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+
+ + {/* Agent Wallet Deposit Monthly Limit */} +
+

Agent Wallet Deposit Monthly Limit

+
+ + setTopUpAgentCurrencyForm({ + ...topUpAgentCurrencyForm, + agentWalletDepositMonthlyLimit: { amount: Number(e.target.value) || 0 } + })} + className="w-full p-2 border rounded-lg" + /> +
+
+ +
+ + +
+
+
+
+ + )} + + {/* Wallet View Modal */} + {topUpAgentModal && selectedCurrencyForWallet && ( + <> +
setSelectedCurrencyForWallet(null)} /> +
+
+ +
+

+ Wallet - {selectedCurrencyForWallet} - {topUpAgentModal.name} +

+
+ + +
+
+ + {walletLoading ? ( +
+
+
+ ) : ( +
+ {/* Balance Section */} +
+

Current Balance

+ {walletBalance !== null ? ( +
+ {typeof walletBalance === 'object' && walletBalance.balance !== undefined + ? walletBalance.balance + : typeof walletBalance === 'number' + ? walletBalance + : JSON.stringify(walletBalance)} + {selectedCurrencyForWallet} +
+ ) : ( +

No balance data available

+ )} +
+ + {/* Transactions Section */} +
+

Transactions

+ {walletTransactions.length > 0 ? ( +
+
+ + + + + + + + + + + {walletTransactions.map((transaction, index) => ( + + + + + + + ))} + +
DateTypeAmountDescription
+ {transaction.date || transaction.createdAt || transaction.timestamp || '—'} + + + {transaction.type || transaction.transactionType || '—'} + + + {transaction.amount !== undefined ? transaction.amount : '—'} {selectedCurrencyForWallet} + + {transaction.description || transaction.note || transaction.reference || '—'} +
+
+
+ ) : ( +

No transactions found

+ )} +
+
+ )} +
+
+ + )} + + {/* Deposit Modal */} + {isDepositModalOpen && topUpAgentModal && selectedCurrencyForWallet && ( + <> +
setIsDepositModalOpen(false)} /> +
+
+ +

+ Deposit to Wallet - {selectedCurrencyForWallet} +

+
+
+ + setDepositAmount(Number(e.target.value) || 0)} + className="w-full p-2 border rounded-lg" + placeholder="Enter deposit amount" + required + /> +

Currency: {selectedCurrencyForWallet}

+
+
+ + +
+
+
+
+ + )} +
@@ -1083,6 +1664,121 @@ const Issuer = () => { /> )} + {/* Admin Modal */} + {adminModal && ( + <> +
setAdminModal(null)} /> +
+
+ +

+ Manage Admins - {adminModal.name} +

+ + {/* Search Section */} +
+

Add New Admin

+
+
+ setAdminSearchEmail(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + searchUsers(); + } + }} + className="w-full p-2 border rounded-lg" + placeholder="Search users by email..." + /> +
+ +
+ + {/* Search Results */} + {searchResults.length > 0 && ( +
+

Search Results:

+ {searchResults.map((user) => ( +
+
+

+ {user.firstName || ''} {user.lastName || ''} +

+

{user.email || ''}

+
+ +
+ ))} +
+ )} +
+ + {/* Admins List */} +
+

Current Admins

+ {adminLoading ? ( +
+
+
+ ) : admins.length > 0 ? ( +
+ {admins.map((admin) => ( +
+
+

+ {admin.firstName || admin.name || ''} {admin.lastName || ''} +

+

{admin.email || ''}

+
+
+ ))} +
+ ) : ( +

No admins found

+ )} +
+ +
+ +
+
+
+ + )} + {error &&
{error}
}
); diff --git a/src/pages/Location.jsx b/src/pages/Location.jsx index a17e50d4..658f71b6 100644 --- a/src/pages/Location.jsx +++ b/src/pages/Location.jsx @@ -117,12 +117,10 @@ const Location = () => { } }, []); - // دریافت کشورها برای dropdown ها + // دریافت کشورها - لود اولیه (برای country tab و dropdown ها) useEffect(() => { - if (activeTab === 'province' || activeTab === 'city') { - fetchCountries(); - } - }, [activeTab, fetchCountries]); + fetchCountries(); + }, [fetchCountries]); // دریافت provinces برای province tab - وقتی tab تغییر می‌کند یا فیلتر country تغییر می‌کند useEffect(() => { diff --git a/src/services/generalAPI.js b/src/services/generalAPI.js index ecf60a3d..5bdf8939 100644 --- a/src/services/generalAPI.js +++ b/src/services/generalAPI.js @@ -39,5 +39,20 @@ export const generalAPI = { throw error; } }, + + // GET /api/v1/General/SearchUsersByEmail + async searchUsersByEmail(email) { + try { + const res = await api.get('/api/v1/General/SearchUsersByEmail', { + params: { email: email || '' }, + skipAuthRedirect: true + }); + // پاسخ به صورت { data: [...], statusCode: 200, ... } است + return res?.data?.data || []; + } catch (error) { + console.error('🔴 General API - searchUsersByEmail error:', error); + throw error; + } + }, }; diff --git a/src/services/issuerAPI.js b/src/services/issuerAPI.js index cf1f2793..a96e33fc 100644 --- a/src/services/issuerAPI.js +++ b/src/services/issuerAPI.js @@ -69,8 +69,8 @@ export const issuerAPI = { // 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 { + payload.cityId = issuer.cityId; + } else { console.warn('⚠️ Invalid cityId GUID format:', issuer.cityId); } } @@ -79,9 +79,9 @@ export const issuerAPI = { console.log('🔵 Issuer Create Payload (original):', issuer); try { - const res = await api.post('/api/v1/Issuer', payload, { skipAuthRedirect: true }); + const res = await api.post('/api/v1/Issuer', payload, { skipAuthRedirect: true }); console.log('🟢 Issuer Create Response:', res?.data); - return res?.data; + return res?.data; } catch (err) { console.error('🔴 Issuer Create Error:', { status: err?.response?.status, @@ -131,8 +131,8 @@ export const issuerAPI = { // 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 { + payload.cityId = issuer.cityId; + } else { console.warn('⚠️ Invalid cityId GUID format:', issuer.cityId); } } @@ -140,11 +140,11 @@ export const issuerAPI = { console.log('🔵 Issuer Update Payload:', JSON.stringify(payload, null, 2)); try { - const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(id)}`, payload, { - skipAuthRedirect: true - }); + const res = await api.put(`/api/v1/Issuer/${encodeURIComponent(id)}`, payload, { + skipAuthRedirect: true + }); console.log('🟢 Issuer Update Response:', res?.data); - return res?.data; + return res?.data; } catch (err) { console.error('🔴 Issuer Update Error:', { status: err?.response?.status, @@ -213,7 +213,7 @@ export const issuerAPI = { } if (typeof cap === 'object' && cap !== null) { const capabilityValue = cap.capability || cap.capabilityName || ''; - return capabilityValue && String(capabilityValue).trim() !== ''; + return capabilityValue && String(capabilityValue).trim() !== ''; } return false; }) @@ -276,14 +276,15 @@ export const issuerAPI = { }, // 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 is a direct array: [{code, name, nativeName, symbol, numericCode, decimalPlaces, allowed}, ...] - // Or wrapped: { data: [...] } + // 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; @@ -369,6 +370,213 @@ export const issuerAPI = { } 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; + } } };