feat(topup-agent): add agent page with create and filter functionality
This commit is contained in:
24
et --hard 8e19500
Normal file
24
et --hard 8e19500
Normal file
@@ -0,0 +1,24 @@
|
||||
[33m212d1ee[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m, [m[1;31morigin/main[m[33m)[m HEAD@{0}: Branch: renamed refs/heads/clean-main to refs/heads/main
|
||||
[33m212d1ee[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m, [m[1;31morigin/main[m[33m)[m HEAD@{2}: commit (initial): c initial commit
|
||||
[33mfd2b653[m HEAD@{3}: reset: moving to HEAD~2
|
||||
[33m8e19500[m HEAD@{4}: commit: feat(TopUpAgent): add , edit, delete , toggle ,...
|
||||
[33mecf6574[m HEAD@{5}: reset: moving to HEAD~1
|
||||
[33m2cebace[m HEAD@{6}: commit: first commit
|
||||
[33mecf6574[m HEAD@{7}: commit: feat(Issuer): add admin selection page for issuer
|
||||
[33mfd2b653[m HEAD@{8}: commit: fix(Issuer): correct AllowedCurrencies and Capabilities structure
|
||||
[33m9d2e2c2[m HEAD@{9}: commit: feat(Issuers): add , edit, delete , toggle ,capability and currencies functionality
|
||||
[33m7cc442b[m HEAD@{10}: commit: feat(country-province-city): add, edit functionality
|
||||
[33m77cc753[m HEAD@{11}: commit: fix(users): update user role and edit API logic
|
||||
[33mfc229ca[m HEAD@{12}: commit: feat(users): add roles page and add, edit, delete , toggle ,reset password functionality
|
||||
[33m06d430d[m HEAD@{13}: commit: fix(roles): handle errors properly in edit role API
|
||||
[33m453cc81[m HEAD@{14}: commit: feat(roles): add roles management page with add, edit and delete functionality
|
||||
[33m5079a5b[m HEAD@{15}: commit: feat : add signin ,signout, forgot password features
|
||||
[33m46a1fae[m HEAD@{16}: rebase (finish): returning to refs/heads/main
|
||||
[33m46a1fae[m HEAD@{17}: pull --rebase origin main (pick): move final
|
||||
[33m43244d4[m HEAD@{18}: pull --rebase origin main (start): checkout 43244d4582b5efcbe03c112ce142d32af5d1a87f
|
||||
[33m247345e[m HEAD@{19}: commit: test: trigger git hooks refresh
|
||||
[33mb239399[m HEAD@{20}: commit: move final
|
||||
[33m7633516[m HEAD@{21}: pull: Fast-forward
|
||||
[33m0355898[m HEAD@{22}: commit: edit 2 readme
|
||||
[33m51e9d7e[m HEAD@{23}: commit: edit readme
|
||||
[33md63a5b8[m HEAD@{24}: commit (initial): first commit
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 || [];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user