feat(topup-agent): add agent page with create and filter functionality

This commit is contained in:
ghazall-ag
2025-12-30 11:44:46 +03:30
parent 8e19500f7c
commit 144687c4e2
5 changed files with 34 additions and 248 deletions

24
et --hard 8e19500 Normal file
View File

@@ -0,0 +1,24 @@
212d1ee (HEAD -> main, origin/main) HEAD@{0}: Branch: renamed refs/heads/clean-main to refs/heads/main
212d1ee (HEAD -> main, origin/main) HEAD@{2}: commit (initial): c initial commit
fd2b653 HEAD@{3}: reset: moving to HEAD~2
8e19500 HEAD@{4}: commit: feat(TopUpAgent): add , edit, delete , toggle ,...
ecf6574 HEAD@{5}: reset: moving to HEAD~1
2cebace HEAD@{6}: commit: first commit
ecf6574 HEAD@{7}: commit: feat(Issuer): add admin selection page for issuer
fd2b653 HEAD@{8}: commit: fix(Issuer): correct AllowedCurrencies and Capabilities structure
9d2e2c2 HEAD@{9}: commit: feat(Issuers): add , edit, delete , toggle ,capability and currencies functionality
7cc442b HEAD@{10}: commit: feat(country-province-city): add, edit functionality
77cc753 HEAD@{11}: commit: fix(users): update user role and edit API logic
fc229ca HEAD@{12}: commit: feat(users): add roles page and add, edit, delete , toggle ,reset password functionality
06d430d HEAD@{13}: commit: fix(roles): handle errors properly in edit role API
453cc81 HEAD@{14}: commit: feat(roles): add roles management page with add, edit and delete functionality
5079a5b HEAD@{15}: commit: feat : add signin ,signout, forgot password features
46a1fae HEAD@{16}: rebase (finish): returning to refs/heads/main
46a1fae HEAD@{17}: pull --rebase origin main (pick): move final
43244d4 HEAD@{18}: pull --rebase origin main (start): checkout 43244d4582b5efcbe03c112ce142d32af5d1a87f
247345e HEAD@{19}: commit: test: trigger git hooks refresh
b239399 HEAD@{20}: commit: move final
7633516 HEAD@{21}: pull: Fast-forward
0355898 HEAD@{22}: commit: edit 2 readme
51e9d7e HEAD@{23}: commit: edit readme
d63a5b8 HEAD@{24}: commit (initial): first commit

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import DataTable from '../components/DataTable';
import { countryAPI, provinceAPI, cityAPI, generalAPI } from '../services/api';
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';
@@ -16,8 +16,6 @@ const Location = () => {
const [isCountryModalOpen, setIsCountryModalOpen] = useState(false);
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([]);
@@ -41,32 +39,6 @@ const Location = () => {
const [selectedProvinceForCity, setSelectedProvinceForCity] = useState(null);
const [provincesForCity, setProvincesForCity] = useState([]);
// دریافت ارزها و timezone ها برای dropdown
useEffect(() => {
const fetchCurrencies = async () => {
try {
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();
}, []);
// دریافت کشورها
const fetchCountries = useCallback(async () => {
try {
@@ -337,23 +309,6 @@ const Location = () => {
// ========== Table Columns ==========
const countryColumns = useMemo(() => [
{ key: 'name', header: 'Name' },
{ key: 'phoneCode', header: 'Phone Code' },
{ key: 'currencyCode', header: 'Currency' },
{ key: 'timeZone', header: 'Time Zone', render: (val, row) => val || row.timeZoneName || '—' },
{
key: 'actions',
header: 'Actions',
render: (_val, row) => (
<div className="flex gap-2">
<button
onClick={() => openCountryModal(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 provinceColumns = useMemo(() => [
@@ -399,9 +354,7 @@ const Location = () => {
if (!countryFilter) return countries;
const filter = countryFilter.toLowerCase();
return countries.filter(c =>
c.name?.toLowerCase().includes(filter) ||
c.phoneCode?.toLowerCase().includes(filter) ||
c.currencyCode?.toLowerCase().includes(filter)
c.name?.toLowerCase().includes(filter)
);
}, [countries, countryFilter]);
@@ -484,12 +437,6 @@ const Location = () => {
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<button
onClick={() => openCountryModal()}
className="btn-primary inline-flex items-center ml-4"
>
<Plus className="h-4 w-4 mr-2" /> Add Country
</button>
</div>
<DataTable
@@ -620,79 +567,6 @@ const Location = () => {
</>
)}
{/* Country Modal */}
{isCountryModalOpen && (
<>
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsCountryModalOpen(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">
{editingCountry ? 'Edit Country' : 'Add Country'}
</h3>
<form onSubmit={onSaveCountry} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Name *</label>
<input
value={countryForm.name}
onChange={(e) => setCountryForm({ ...countryForm, name: e.target.value })}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Phone Code *</label>
<input
value={countryForm.phoneCode}
onChange={(e) => setCountryForm({ ...countryForm, phoneCode: e.target.value })}
className="w-full p-2 border rounded-lg"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Currency Code *</label>
<select
value={countryForm.currencyCode}
onChange={(e) => setCountryForm({ ...countryForm, currencyCode: e.target.value })}
className="w-full p-2 border rounded-lg"
required
>
<option value="">Select Currency</option>
{currencies.map((curr) => (
<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>
<select
value={countryForm.timeZoneName}
onChange={(e) => setCountryForm({ ...countryForm, timeZoneName: e.target.value })}
className="w-full p-2 border rounded-lg"
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">
Cancel
</button>
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">
{editingCountry ? 'Update' : 'Add'}
</button>
</div>
</form>
</div>
</div>
</>
)}
{/* Province Modal */}
{isProvinceModalOpen && (

View File

@@ -103,35 +103,15 @@ api.interceptors.response.use(
// اگر errorData خودش یک AxiosError باشد، پیام خطا را از آن استخراج کنیم
if (errorData && typeof errorData === 'object' && errorData.name === 'AxiosError') {
errorData = {
message: errorData.message || error.message || 'خطا در ارتباط با سرور',
message: errorData.message || error.message,
...(errorData.response?.data || {})
};
}
// اگر errorData وجود نداشت یا خالی است، از status code برای ایجاد پیام مناسب استفاده کنیم
// فقط پیامی که از سرویس می‌آید را استفاده می‌کنیم
// اگر errorData وجود نداشت، از error.response.data اصلی استفاده می‌کنیم
if (!errorData || (typeof errorData === 'object' && Object.keys(errorData).length === 0)) {
const status = error.response?.status;
let message = error.message || 'خطا در ارتباط با سرور';
// پیام‌های مناسب بر اساس status code
if (status === 500) {
message = 'خطای سرور. لطفاً با پشتیبانی تماس بگیرید یا دوباره تلاش کنید.';
} else if (status === 400) {
message = 'درخواست نامعتبر. لطفاً اطلاعات را بررسی کنید.';
} else if (status === 401) {
message = 'نام کاربری یا رمز عبور اشتباه است.';
} else if (status === 403) {
message = 'شما دسترسی به این بخش را ندارید.';
} else if (status === 404) {
message = 'منبع مورد نظر یافت نشد.';
} else if (status === 502 || status === 503) {
message = 'سرور در دسترس نیست. لطفاً بعداً تلاش کنید.';
}
errorData = {
message: message,
statusCode: status
};
errorData = error.response?.data || {};
}
if (error.response) {

View File

@@ -4,16 +4,6 @@ import api from './apiClient';
// Country API
// -----------------------------
export const countryAPI = {
// GET /api/v1/Country (with pagination)
async list(params = {}) {
const { currentPage = 1, pageSize = 10, ...otherParams } = params;
const res = await api.get('/api/v1/Country', {
params: { currentPage, pageSize, ...otherParams },
skipAuthRedirect: true
});
return res?.data?.data?.data || [];
},
// GET /api/v1/Country/All (all countries without pagination)
async listAll() {
try {
@@ -26,61 +16,8 @@ export const countryAPI = {
}
},
// POST /api/v1/Country
async create(country) {
const payload = {
Name: String(country?.name || ''),
PhoneCode: String(country?.phoneCode || ''),
CurrencyCode: String(country?.currencyCode || ''),
TimeZone: String(country?.timeZoneName || country?.timeZone || ''),
};
const res = await api.post('/api/v1/Country', payload, { skipAuthRedirect: true });
return res?.data;
},
// PUT /api/v1/Country/{countryId}
async update(countryId, country) {
if (!countryId) {
throw new Error('Country ID is required');
}
// ساخت payload - سرور در PUT انتظار TimeZoneName دارد (نه TimeZone)
const payload = {
Name: country?.name ? String(country.name).trim() : '',
PhoneCode: country?.phoneCode ? String(country.phoneCode).trim() : '',
CurrencyCode: country?.currencyCode ? String(country.currencyCode).trim() : '',
TimeZoneName: country?.timeZoneName || country?.timeZone ? String((country.timeZoneName || country.timeZone).trim()) : '',
};
const url = `/api/v1/Country/${encodeURIComponent(countryId)}`;
try {
const res = await api.put(url, payload, { skipAuthRedirect: true });
return res?.data;
} catch (error) {
console.error('🔴 Country API - update error:', {
url,
countryId,
payload,
error: error?.response?.data || error?.message || error
});
throw error;
}
},
// POST /api/v1/Country/{countryId}/Province
async createProvince(countryId, province) {
const payload = {
CountryId: countryId,
ProvinceName: String(province?.provinceName || ''),
};
const res = await api.post(`/api/v1/Country/${encodeURIComponent(countryId)}/Province`, payload, { skipAuthRedirect: true });
return res?.data;
},
// GET /api/v1/Country/{countryId}/Province
async getProvinces(countryId) {
const res = await api.get(`/api/v1/Country/${encodeURIComponent(countryId)}/Province`, { skipAuthRedirect: true });
return res?.data?.data || [];
},
};

View File

@@ -4,41 +4,12 @@ import api from './apiClient';
// General API
// -----------------------------
export const generalAPI = {
// GET /api/v1/General/Currencies
async getCurrencies() {
try {
const res = await api.get('/api/v1/General/Currencies', { skipAuthRedirect: true });
// پاسخ به صورت array مستقیم است
return Array.isArray(res?.data) ? res?.data : [];
} catch (error) {
console.error('🔴 General API - getCurrencies error:', error);
throw error;
}
},
// GET /api/v1/General/TimeZones
async getTimeZones() {
try {
const res = await api.get('/api/v1/General/TimeZones', { skipAuthRedirect: true });
// پاسخ به صورت { data: [...], statusCode: 200, ... } است
return res?.data?.data || [];
} catch (error) {
console.error('🔴 General API - getTimeZones error:', error);
throw error;
}
},
// GET /api/v1/General/IssuerCapabilities
async getIssuerCapabilities() {
try {
const res = await api.get('/api/v1/General/IssuerCapabilities', { skipAuthRedirect: true });
// پاسخ به صورت { data: [...], statusCode: 200, ... } است
return res?.data?.data || [];
} catch (error) {
console.error('🔴 General API - getIssuerCapabilities error:', error);
throw error;
}
},
// GET /api/v1/General/SearchUsersByEmail
async searchUsersByEmail(email) {