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 React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||||
import DataTable from '../components/DataTable';
|
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 { Plus, Search, Pencil, Trash2, MapPin, Globe, Building2 } from 'lucide-react';
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
@@ -16,8 +16,6 @@ const Location = () => {
|
|||||||
const [isCountryModalOpen, setIsCountryModalOpen] = useState(false);
|
const [isCountryModalOpen, setIsCountryModalOpen] = useState(false);
|
||||||
const [editingCountry, setEditingCountry] = useState(null);
|
const [editingCountry, setEditingCountry] = useState(null);
|
||||||
const [countryForm, setCountryForm] = useState({ name: '', phoneCode: '', currencyCode: '', timeZoneName: '' });
|
const [countryForm, setCountryForm] = useState({ name: '', phoneCode: '', currencyCode: '', timeZoneName: '' });
|
||||||
const [currencies, setCurrencies] = useState([]);
|
|
||||||
const [timeZones, setTimeZones] = useState([]);
|
|
||||||
|
|
||||||
// Province states
|
// Province states
|
||||||
const [provinces, setProvinces] = useState([]);
|
const [provinces, setProvinces] = useState([]);
|
||||||
@@ -41,32 +39,6 @@ const Location = () => {
|
|||||||
const [selectedProvinceForCity, setSelectedProvinceForCity] = useState(null);
|
const [selectedProvinceForCity, setSelectedProvinceForCity] = useState(null);
|
||||||
const [provincesForCity, setProvincesForCity] = useState([]);
|
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 () => {
|
const fetchCountries = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -337,23 +309,6 @@ const Location = () => {
|
|||||||
// ========== Table Columns ==========
|
// ========== Table Columns ==========
|
||||||
const countryColumns = useMemo(() => [
|
const countryColumns = useMemo(() => [
|
||||||
{ key: 'name', header: 'Name' },
|
{ 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(() => [
|
const provinceColumns = useMemo(() => [
|
||||||
@@ -399,9 +354,7 @@ const Location = () => {
|
|||||||
if (!countryFilter) return countries;
|
if (!countryFilter) return countries;
|
||||||
const filter = countryFilter.toLowerCase();
|
const filter = countryFilter.toLowerCase();
|
||||||
return countries.filter(c =>
|
return countries.filter(c =>
|
||||||
c.name?.toLowerCase().includes(filter) ||
|
c.name?.toLowerCase().includes(filter)
|
||||||
c.phoneCode?.toLowerCase().includes(filter) ||
|
|
||||||
c.currencyCode?.toLowerCase().includes(filter)
|
|
||||||
);
|
);
|
||||||
}, [countries, countryFilter]);
|
}, [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"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<DataTable
|
<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 */}
|
{/* Province Modal */}
|
||||||
{isProvinceModalOpen && (
|
{isProvinceModalOpen && (
|
||||||
|
|||||||
@@ -103,35 +103,15 @@ api.interceptors.response.use(
|
|||||||
// اگر errorData خودش یک AxiosError باشد، پیام خطا را از آن استخراج کنیم
|
// اگر errorData خودش یک AxiosError باشد، پیام خطا را از آن استخراج کنیم
|
||||||
if (errorData && typeof errorData === 'object' && errorData.name === 'AxiosError') {
|
if (errorData && typeof errorData === 'object' && errorData.name === 'AxiosError') {
|
||||||
errorData = {
|
errorData = {
|
||||||
message: errorData.message || error.message || 'خطا در ارتباط با سرور',
|
message: errorData.message || error.message,
|
||||||
...(errorData.response?.data || {})
|
...(errorData.response?.data || {})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر errorData وجود نداشت یا خالی است، از status code برای ایجاد پیام مناسب استفاده کنیم
|
// فقط پیامی که از سرویس میآید را استفاده میکنیم
|
||||||
|
// اگر errorData وجود نداشت، از error.response.data اصلی استفاده میکنیم
|
||||||
if (!errorData || (typeof errorData === 'object' && Object.keys(errorData).length === 0)) {
|
if (!errorData || (typeof errorData === 'object' && Object.keys(errorData).length === 0)) {
|
||||||
const status = error.response?.status;
|
errorData = error.response?.data || {};
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
|
|||||||
@@ -4,16 +4,6 @@ import api from './apiClient';
|
|||||||
// Country API
|
// Country API
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
export const countryAPI = {
|
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)
|
// GET /api/v1/Country/All (all countries without pagination)
|
||||||
async listAll() {
|
async listAll() {
|
||||||
try {
|
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
|
// General API
|
||||||
// -----------------------------
|
// -----------------------------
|
||||||
export const generalAPI = {
|
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
|
// GET /api/v1/General/SearchUsersByEmail
|
||||||
async searchUsersByEmail(email) {
|
async searchUsersByEmail(email) {
|
||||||
|
|||||||
Reference in New Issue
Block a user