Files
khalijpay-issuer/src/pages/Location.jsx

680 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useState, useCallback } from 'react';
import DataTable from '../components/DataTable';
import { countryAPI, provinceAPI, cityAPI } 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';
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
const Location = () => {
const [activeTab, setActiveTab] = useState('country'); // 'country', 'province', 'city'
// Country states
const [countries, setCountries] = useState([]);
const [countriesLoading, setCountriesLoading] = useState(true);
const [countryFilter, setCountryFilter] = useState('');
const [isCountryModalOpen, setIsCountryModalOpen] = useState(false);
const [editingCountry, setEditingCountry] = useState(null);
const [countryForm, setCountryForm] = useState({ name: '', phoneCode: '', currencyCode: '', timeZoneName: '' });
// 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: '' });
const [selectedCountryForProvince, setSelectedCountryForProvince] = useState(null);
// City states
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([]);
// دریافت کشورها
const fetchCountries = useCallback(async () => {
try {
setCountriesLoading(true);
const list = await countryAPI.listAll();
setCountries(Array.isArray(list) ? list : []);
} catch (err) {
toast.error(getErrorMessage(err));
} finally {
setCountriesLoading(false);
}
}, []);
// دریافت استان‌ها
const fetchProvinces = useCallback(async (countryId = null) => {
try {
setProvincesLoading(true);
const params = {};
if (countryId) {
params.countryId = countryId;
}
const list = await provinceAPI.list(params);
setProvinces(Array.isArray(list) ? list : []);
} catch (err) {
toast.error(getErrorMessage(err));
} finally {
setProvincesLoading(false);
}
}, []);
// دریافت شهرها
const fetchCities = useCallback(async (countryId = null, provinceId = null) => {
try {
setCitiesLoading(true);
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));
} finally {
setCitiesLoading(false);
}
}, []);
// دریافت کشورها - لود اولیه (برای country tab و dropdown ها)
useEffect(() => {
fetchCountries();
}, [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) => {
if (country) {
setEditingCountry(country);
setCountryForm({
name: country.name || '',
phoneCode: country.phoneCode || '',
currencyCode: country.currencyCode || '',
timeZoneName: country.timeZoneName || country.timeZone || '',
});
} else {
setEditingCountry(null);
setCountryForm({ name: '', phoneCode: '', currencyCode: '', timeZoneName: '' });
}
setIsCountryModalOpen(true);
};
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;
if (!countryId) {
toast.error('Country ID is missing');
console.error('Editing country:', editingCountry);
return;
}
console.log('Updating country with ID:', countryId, 'Payload:', countryForm);
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');
}
await fetchCountries();
setIsCountryModalOpen(false);
setEditingCountry(null);
setCountryForm({ name: '', phoneCode: '', currencyCode: '', timeZoneName: '' });
} catch (err) {
console.error('Error saving country:', err);
toast.error(getErrorMessage(err));
}
};
// ========== Province Functions ==========
const openProvinceModal = (province = null) => {
if (province) {
setEditingProvince(province);
setProvinceForm({
countryId: province.countryId || '',
provinceName: province.name || '',
});
setSelectedCountryForProvince(province.countryId);
} else {
setEditingProvince(null);
setProvinceForm({ countryId: '', provinceName: '' });
setSelectedCountryForProvince(null);
}
setIsProvinceModalOpen(true);
};
const onSaveProvince = async (e) => {
e.preventDefault();
if (!provinceForm.countryId) {
toast.error('Please select a country');
return;
}
try {
if (editingProvince) {
const provinceId = editingProvince.id || editingProvince.provinceId;
if (!provinceId) {
toast.error('Province ID is missing');
console.error('Editing province:', editingProvince);
return;
}
console.log('Updating province with ID:', provinceId, 'Payload:', provinceForm);
await provinceAPI.update(provinceId, provinceForm);
toast.success(getSuccessMessage({ data: { message: 'Province updated successfully' } }) || 'Province updated successfully');
} else {
await provinceAPI.create(provinceForm);
toast.success(getSuccessMessage({ data: { message: 'Province added successfully' } }) || 'Province added successfully');
}
await fetchProvinces();
setIsProvinceModalOpen(false);
setEditingProvince(null);
setProvinceForm({ countryId: '', provinceName: '' });
setSelectedCountryForProvince(null);
} catch (err) {
console.error('Error saving province:', err);
toast.error(getErrorMessage(err));
}
};
// ========== City Functions ==========
const openCityModal = (city = null) => {
if (city) {
setEditingCity(city);
setCityForm({
provinceId: city.provinceId || '',
cityName: city.name || '',
});
setSelectedProvinceForCity(city.provinceId);
} else {
setEditingCity(null);
setCityForm({ provinceId: '', cityName: '' });
setSelectedProvinceForCity(null);
}
setIsCityModalOpen(true);
};
const onSaveCity = async (e) => {
e.preventDefault();
if (!cityForm.provinceId) {
toast.error('Please select a province');
return;
}
try {
if (editingCity) {
const cityId = editingCity.id || editingCity.cityId;
if (!cityId) {
toast.error('City ID is missing');
console.error('Editing city:', editingCity);
return;
}
console.log('Updating city with ID:', cityId, 'Payload:', cityForm);
await cityAPI.update(cityId, cityForm);
toast.success(getSuccessMessage({ data: { message: 'City updated successfully' } }) || 'City updated successfully');
} else {
await cityAPI.create(cityForm);
toast.success(getSuccessMessage({ data: { message: 'City added successfully' } }) || 'City added successfully');
}
await fetchCities();
setIsCityModalOpen(false);
setEditingCity(null);
setCityForm({ provinceId: '', cityName: '' });
setSelectedProvinceForCity(null);
} catch (err) {
console.error('Error saving city:', err);
toast.error(getErrorMessage(err));
}
};
// ========== Table Columns ==========
const countryColumns = useMemo(() => [
{ key: 'name', header: 'Name' },
], []);
const provinceColumns = useMemo(() => [
{ key: 'name', header: 'Province Name' },
{ key: 'countryName', header: 'Country' },
{
key: 'actions',
header: 'Actions',
render: (_val, row) => (
<div className="flex gap-2">
<button
onClick={() => openProvinceModal(row)}
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-blue-100 text-blue-700 hover:opacity-90"
>
<Pencil className="h-3 w-3 mr-1" /> Edit
</button>
</div>
),
},
], []);
const cityColumns = useMemo(() => [
{ key: 'name', header: 'City Name' },
{ key: 'provinceName', header: 'Province' },
{
key: 'actions',
header: 'Actions',
render: (_val, row) => (
<div className="flex gap-2">
<button
onClick={() => openCityModal(row)}
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-blue-100 text-blue-700 hover:opacity-90"
>
<Pencil className="h-3 w-3 mr-1" /> Edit
</button>
</div>
),
},
], []);
// Filtered data
const filteredCountries = useMemo(() => {
if (!countryFilter) return countries;
const filter = countryFilter.toLowerCase();
return countries.filter(c =>
c.name?.toLowerCase().includes(filter)
);
}, [countries, countryFilter]);
const filteredProvinces = useMemo(() => {
if (!provinceFilter) return provinces;
const filter = provinceFilter.toLowerCase();
return provinces.filter(p =>
p.name?.toLowerCase().includes(filter) ||
p.countryName?.toLowerCase().includes(filter)
);
}, [provinces, provinceFilter]);
const filteredCities = useMemo(() => {
if (!cityFilter) return cities;
const filter = cityFilter.toLowerCase();
return cities.filter(c =>
c.name?.toLowerCase().includes(filter) ||
c.provinceName?.toLowerCase().includes(filter)
);
}, [cities, cityFilter]);
return (
<div className="p-6">
<ToastContainer position="top-right" autoClose={3000} />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Location Management</h1>
<p className="text-gray-600 dark:text-gray-400">Manage countries, provinces, and cities</p>
</div>
{/* Tabs */}
<div className="mb-4 border-b border-gray-200 dark:border-gray-700">
<nav className="flex space-x-8">
<button
onClick={() => setActiveTab('country')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'country'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Globe className="inline h-4 w-4 mr-2" />
Countries
</button>
<button
onClick={() => setActiveTab('province')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'province'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<MapPin className="inline h-4 w-4 mr-2" />
Provinces
</button>
<button
onClick={() => setActiveTab('city')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'city'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Building2 className="inline h-4 w-4 mr-2" />
Cities
</button>
</nav>
</div>
{/* Country Tab */}
{activeTab === 'country' && (
<>
<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={countryFilter}
onChange={(e) => setCountryFilter(e.target.value)}
placeholder="Filter countries..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
</div>
<DataTable
data={filteredCountries}
columns={countryColumns}
loading={countriesLoading}
searchable={false}
/>
</>
)}
{/* Province Tab */}
{activeTab === 'province' && (
<>
<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"
>
<Plus className="h-4 w-4 mr-2" /> Add Province
</button>
</div>
<DataTable
data={filteredProvinces}
columns={provinceColumns}
loading={provincesLoading}
searchable={false}
/>
</>
)}
{/* City Tab */}
{activeTab === 'city' && (
<>
<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"
>
<Plus className="h-4 w-4 mr-2" /> Add City
</button>
</div>
<DataTable
data={filteredCities}
columns={cityColumns}
loading={citiesLoading}
searchable={false}
/>
</>
)}
{/* Province Modal */}
{isProvinceModalOpen && (
<>
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsProvinceModalOpen(false)} />
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="w-full max-w-md card p-6 relative bg-white rounded-2xl shadow-lg">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{editingProvince ? 'Edit Province' : 'Add Province'}
</h3>
<form onSubmit={onSaveProvince} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Country *</label>
<select
value={provinceForm.countryId}
onChange={(e) => {
setProvinceForm({ ...provinceForm, countryId: e.target.value });
setSelectedCountryForProvince(e.target.value);
}}
className="w-full p-2 border rounded-lg"
required
>
<option value="">Select Country</option>
{countries.map((country) => (
<option key={country.id} value={country.id}>
{country.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Province Name *</label>
<input
value={provinceForm.provinceName}
onChange={(e) => setProvinceForm({ ...provinceForm, provinceName: e.target.value })}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div className="flex justify-end gap-x-2">
<button type="button" onClick={() => setIsProvinceModalOpen(false)} className="px-4 py-2 border rounded-lg">
Cancel
</button>
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
{editingProvince ? 'Update' : 'Add'}
</button>
</div>
</form>
</div>
</div>
</>
)}
{/* City Modal */}
{isCityModalOpen && (
<>
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsCityModalOpen(false)} />
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="w-full max-w-md card p-6 relative bg-white rounded-2xl shadow-lg">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{editingCity ? 'Edit City' : 'Add City'}
</h3>
<form onSubmit={onSaveCity} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Province *</label>
<select
value={cityForm.provinceId}
onChange={(e) => {
setCityForm({ ...cityForm, provinceId: e.target.value });
setSelectedProvinceForCity(e.target.value);
}}
className="w-full p-2 border rounded-lg"
required
>
<option value="">Select Province</option>
{provinces.map((province) => (
<option key={province.id} value={province.id}>
{province.name} - {province.countryName}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">City Name *</label>
<input
value={cityForm.cityName}
onChange={(e) => setCityForm({ ...cityForm, cityName: e.target.value })}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div className="flex justify-end gap-x-2">
<button type="button" onClick={() => setIsCityModalOpen(false)} className="px-4 py-2 border rounded-lg">
Cancel
</button>
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
{editingCity ? 'Update' : 'Add'}
</button>
</div>
</form>
</div>
</div>
</>
)}
</div>
);
};
export default Location;