fix(Issuer): correct AllowedCurrencies and Capabilities structure
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user