fix(Issuer): correct AllowedCurrencies and Capabilities structure

This commit is contained in:
ghazall-ag
2025-12-04 22:00:53 +03:30
parent 9d2e2c223c
commit fd2b6537cd
17 changed files with 1300 additions and 206 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import DataTable from '../components/DataTable';
import { countryAPI, provinceAPI, cityAPI, currencyAPI } from '../services/api';
import { countryAPI, provinceAPI, cityAPI, generalAPI } from '../services/api';
import { Plus, Search, Pencil, Trash2, MapPin, Globe, Building2 } from 'lucide-react';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
@@ -17,11 +17,13 @@ const Location = () => {
const [editingCountry, setEditingCountry] = useState(null);
const [countryForm, setCountryForm] = useState({ name: '', phoneCode: '', currencyCode: '', timeZoneName: '' });
const [currencies, setCurrencies] = useState([]);
const [timeZones, setTimeZones] = useState([]);
// Province states
const [provinces, setProvinces] = useState([]);
const [provincesLoading, setProvincesLoading] = useState(true);
const [provinceFilter, setProvinceFilter] = useState('');
const [selectedCountryFilter, setSelectedCountryFilter] = useState('');
const [isProvinceModalOpen, setIsProvinceModalOpen] = useState(false);
const [editingProvince, setEditingProvince] = useState(null);
const [provinceForm, setProvinceForm] = useState({ countryId: '', provinceName: '' });
@@ -31,23 +33,38 @@ const Location = () => {
const [cities, setCities] = useState([]);
const [citiesLoading, setCitiesLoading] = useState(true);
const [cityFilter, setCityFilter] = useState('');
const [selectedCountryFilterForCity, setSelectedCountryFilterForCity] = useState('');
const [selectedProvinceFilterForCity, setSelectedProvinceFilterForCity] = useState('');
const [isCityModalOpen, setIsCityModalOpen] = useState(false);
const [editingCity, setEditingCity] = useState(null);
const [cityForm, setCityForm] = useState({ provinceId: '', cityName: '' });
const [selectedProvinceForCity, setSelectedProvinceForCity] = useState(null);
const [provincesForCity, setProvincesForCity] = useState([]);
// دریافت ارزها برای dropdown
// دریافت ارزها و timezone ها برای dropdown
useEffect(() => {
const fetchCurrencies = async () => {
try {
const list = await currencyAPI.list();
const list = await generalAPI.getCurrencies();
setCurrencies(Array.isArray(list) ? list : []);
} catch (err) {
console.error('Failed to load currencies:', err);
toast.error('Failed to load currencies');
}
};
const fetchTimeZones = async () => {
try {
const list = await generalAPI.getTimeZones();
setTimeZones(Array.isArray(list) ? list : []);
} catch (err) {
console.error('Failed to load timezones:', err);
toast.error('Failed to load timezones');
}
};
fetchCurrencies();
fetchTimeZones();
}, []);
// دریافت کشورها
@@ -64,10 +81,14 @@ const Location = () => {
}, []);
// دریافت استان‌ها
const fetchProvinces = useCallback(async () => {
const fetchProvinces = useCallback(async (countryId = null) => {
try {
setProvincesLoading(true);
const list = await provinceAPI.list();
const params = {};
if (countryId) {
params.countryId = countryId;
}
const list = await provinceAPI.list(params);
setProvinces(Array.isArray(list) ? list : []);
} catch (err) {
toast.error(getErrorMessage(err));
@@ -77,10 +98,17 @@ const Location = () => {
}, []);
// دریافت شهرها
const fetchCities = useCallback(async () => {
const fetchCities = useCallback(async (countryId = null, provinceId = null) => {
try {
setCitiesLoading(true);
const list = await cityAPI.list();
const params = {};
if (countryId) {
params.countryId = countryId;
}
if (provinceId) {
params.provinceId = provinceId;
}
const list = await cityAPI.list(params);
setCities(Array.isArray(list) ? list : []);
} catch (err) {
toast.error(getErrorMessage(err));
@@ -89,17 +117,62 @@ const Location = () => {
}
}, []);
// دریافت کشورها برای dropdown ها
useEffect(() => {
if (activeTab === 'country') {
if (activeTab === 'province' || activeTab === 'city') {
fetchCountries();
} else if (activeTab === 'province') {
fetchProvinces();
fetchCountries(); // برای dropdown
} else if (activeTab === 'city') {
fetchCities();
fetchProvinces(); // برای dropdown
}
}, [activeTab, fetchCountries, fetchProvinces, fetchCities]);
}, [activeTab, fetchCountries]);
// دریافت provinces برای province tab - وقتی tab تغییر می‌کند یا فیلتر country تغییر می‌کند
useEffect(() => {
if (activeTab === 'province') {
const countryId = selectedCountryFilter || null;
console.log('🔵 Fetching provinces for province tab with countryId:', countryId);
fetchProvinces(countryId);
}
}, [activeTab, selectedCountryFilter, fetchProvinces]);
// دریافت provinces برای city tab (برای dropdown)
useEffect(() => {
if (activeTab === 'city') {
if (selectedCountryFilterForCity) {
// دریافت provinces برای country انتخاب شده
fetchProvinces(selectedCountryFilterForCity);
} else {
// دریافت همه provinces
fetchProvinces();
}
}
}, [activeTab, selectedCountryFilterForCity, fetchProvinces]);
// دریافت cities - وقتی tab تغییر می‌کند یا فیلترها تغییر می‌کنند
useEffect(() => {
if (activeTab === 'city') {
const countryId = selectedCountryFilterForCity || null;
const provinceId = selectedProvinceFilterForCity || null;
console.log('🔵 Fetching cities with filters:', { countryId, provinceId });
fetchCities(countryId, provinceId);
}
}, [activeTab, selectedCountryFilterForCity, selectedProvinceFilterForCity, fetchCities]);
// Reset filters when switching tabs
useEffect(() => {
if (activeTab !== 'province') {
setSelectedCountryFilter('');
}
if (activeTab !== 'city') {
setSelectedCountryFilterForCity('');
setSelectedProvinceFilterForCity('');
}
}, [activeTab]);
// وقتی country برای city تغییر می‌کند، province filter را reset کن
useEffect(() => {
if (activeTab === 'city' && selectedCountryFilterForCity) {
setSelectedProvinceFilterForCity('');
}
}, [selectedCountryFilterForCity, activeTab]);
// ========== Country Functions ==========
const openCountryModal = (country = null) => {
@@ -120,6 +193,25 @@ const Location = () => {
const onSaveCountry = async (e) => {
e.preventDefault();
// Validation
if (!countryForm.name?.trim()) {
toast.error('Name is required');
return;
}
if (!countryForm.phoneCode?.trim()) {
toast.error('Phone Code is required');
return;
}
if (!countryForm.currencyCode?.trim()) {
toast.error('Currency Code is required');
return;
}
if (!countryForm.timeZoneName?.trim()) {
toast.error('Time Zone is required');
return;
}
try {
if (editingCountry) {
const countryId = editingCountry.id || editingCountry.countryId;
@@ -132,6 +224,7 @@ const Location = () => {
await countryAPI.update(countryId, countryForm);
toast.success(getSuccessMessage({ data: { message: 'Country updated successfully' } }) || 'Country updated successfully');
} else {
console.log('Creating country with payload:', countryForm);
await countryAPI.create(countryForm);
toast.success(getSuccessMessage({ data: { message: 'Country added successfully' } }) || 'Country added successfully');
}
@@ -248,7 +341,7 @@ const Location = () => {
{ key: 'name', header: 'Name' },
{ key: 'phoneCode', header: 'Phone Code' },
{ key: 'currencyCode', header: 'Currency' },
{ key: 'timeZoneName', header: 'Time Zone', render: (val, row) => val || row.timeZone || '—' },
{ key: 'timeZone', header: 'Time Zone', render: (val, row) => val || row.timeZoneName || '—' },
{
key: 'actions',
header: 'Actions',
@@ -413,19 +506,38 @@ const Location = () => {
{/* Province Tab */}
{activeTab === 'province' && (
<>
<div className="mb-4 flex justify-between items-center">
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
value={provinceFilter}
onChange={(e) => setProvinceFilter(e.target.value)}
placeholder="Filter provinces..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
<div className="mb-4 flex justify-between items-center gap-4">
<div className="flex gap-4 flex-1">
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
value={provinceFilter}
onChange={(e) => setProvinceFilter(e.target.value)}
placeholder="Filter provinces..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<div className="min-w-[200px]">
<select
value={selectedCountryFilter}
onChange={(e) => {
setSelectedCountryFilter(e.target.value);
setProvinceFilter(''); // Reset text filter when country changes
}}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="">All Countries</option>
{countries.map((country) => (
<option key={country.id} value={country.id}>
{country.name}
</option>
))}
</select>
</div>
</div>
<button
onClick={() => openProvinceModal()}
className="btn-primary inline-flex items-center ml-4"
className="btn-primary inline-flex items-center"
>
<Plus className="h-4 w-4 mr-2" /> Add Province
</button>
@@ -443,19 +555,59 @@ const Location = () => {
{/* City Tab */}
{activeTab === 'city' && (
<>
<div className="mb-4 flex justify-between items-center">
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)}
placeholder="Filter cities..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
<div className="mb-4 flex justify-between items-center gap-4">
<div className="flex gap-4 flex-1">
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)}
placeholder="Filter cities..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<div className="min-w-[200px]">
<select
value={selectedCountryFilterForCity}
onChange={(e) => {
setSelectedCountryFilterForCity(e.target.value);
setSelectedProvinceFilterForCity(''); // Reset province filter when country changes
setCityFilter(''); // Reset text filter
}}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="">All Countries</option>
{countries.map((country) => (
<option key={country.id} value={country.id}>
{country.name}
</option>
))}
</select>
</div>
<div className="min-w-[200px]">
<select
value={selectedProvinceFilterForCity}
onChange={(e) => {
setSelectedProvinceFilterForCity(e.target.value);
setCityFilter(''); // Reset text filter
}}
className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
disabled={!selectedCountryFilterForCity}
>
<option value="">All Provinces</option>
{provinces
.filter(p => !selectedCountryFilterForCity || p.countryId === selectedCountryFilterForCity)
.map((province) => (
<option key={province.id} value={province.id}>
{province.name}
</option>
))}
</select>
</div>
</div>
<button
onClick={() => openCityModal()}
className="btn-primary inline-flex items-center ml-4"
className="btn-primary inline-flex items-center"
>
<Plus className="h-4 w-4 mr-2" /> Add City
</button>
@@ -508,21 +660,27 @@ const Location = () => {
>
<option value="">Select Currency</option>
{currencies.map((curr) => (
<option key={curr.currencyCode?.code} value={curr.currencyCode?.code}>
{curr.currencyCode?.code} - {curr.currencyCode?.name}
<option key={curr.code} value={curr.code}>
{curr.code} - {curr.name || curr.nativeName || curr.symbol}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Time Zone *</label>
<input
<select
value={countryForm.timeZoneName}
onChange={(e) => setCountryForm({ ...countryForm, timeZoneName: e.target.value })}
className="w-full p-2 border rounded-lg"
placeholder="e.g., UTC+3:30"
required
/>
>
<option value="">Select Time Zone</option>
{timeZones.map((tz) => (
<option key={tz.name} value={tz.name}>
{tz.name}
</option>
))}
</select>
</div>
<div className="flex justify-end gap-x-2">
<button type="button" onClick={() => setIsCountryModalOpen(false)} className="px-4 py-2 border rounded-lg">