feat(country-province-city): add, edit functionality
This commit is contained in:
22
src/App.jsx
22
src/App.jsx
@@ -10,6 +10,8 @@ import Settings from './pages/Settings';
|
||||
import ForgotPassword from './pages/ForgotPassword';
|
||||
import Roles from './pages/Roles';
|
||||
import Users from './pages/Users';
|
||||
import Currency from './pages/Currency';
|
||||
import Location from './pages/Location';
|
||||
|
||||
// Protected Route Component
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
@@ -113,6 +115,26 @@ const AppRoutes = () => {
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/currency"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Currency/>
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/location"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Layout>
|
||||
<Location/>
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
Menu,
|
||||
X,
|
||||
Users,
|
||||
Shield
|
||||
Shield,
|
||||
DollarSign,
|
||||
MapPin
|
||||
} from 'lucide-react';
|
||||
|
||||
const Sidebar = ({ isOpen, onToggle }) => {
|
||||
@@ -16,6 +18,8 @@ const Sidebar = ({ isOpen, onToggle }) => {
|
||||
{ name: 'Transactions', href: '/transactions', icon: CreditCard },
|
||||
{ name: 'Roles', href: '/roles', icon: Shield },
|
||||
{ name: 'Users', href: '/users', icon: Users },
|
||||
{ name: 'Currency', href: '/currency', icon: DollarSign },
|
||||
{ name: 'Location', href: '/location', icon: MapPin },
|
||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
|
||||
658
src/pages/Currency.jsx
Normal file
658
src/pages/Currency.jsx
Normal file
@@ -0,0 +1,658 @@
|
||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { currencyAPI } from '../services/api';
|
||||
import { Plus, Search, Power, Wrench, Settings, DollarSign, Shield } from 'lucide-react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||
|
||||
// debounce ساده
|
||||
function debounce(func, delay) {
|
||||
let timer;
|
||||
return function (...args) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => func.apply(this, args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
const Currency = () => {
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const [addForm, setAddForm] = useState({ currencyCode: '' });
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// Modals for editing
|
||||
const [permissionsModal, setPermissionsModal] = useState(null);
|
||||
const [limitsModal, setLimitsModal] = useState(null);
|
||||
const [feesModal, setFeesModal] = useState(null);
|
||||
const [maintenanceModal, setMaintenanceModal] = useState(null);
|
||||
const [maintenanceMessage, setMaintenanceMessage] = useState('');
|
||||
|
||||
// Form states for modals
|
||||
const [permissionsForm, setPermissionsForm] = useState({
|
||||
depositEnabled: false,
|
||||
cashOutEnabled: false,
|
||||
exchangeEnabled: false,
|
||||
purchaseEnabled: false,
|
||||
transferFromEnabled: false,
|
||||
transferToEnabled: false,
|
||||
voucherCreationEnabled: false,
|
||||
voucherRedeemEnabled: false,
|
||||
});
|
||||
const [limitsForm, setLimitsForm] = useState({
|
||||
depositLimits: { min: null, max: null },
|
||||
cashOutLimits: { min: null, max: null },
|
||||
transferLimits: { min: null, max: null },
|
||||
voucherLimits: { min: null, max: null },
|
||||
});
|
||||
const [feesForm, setFeesForm] = useState({
|
||||
depositFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
cashOutFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
transferFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
exchangeFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
generateVoucherFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
expireVoucherSystemFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
});
|
||||
|
||||
// دریافت ارزها
|
||||
const fetchCurrencies = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const list = await currencyAPI.list();
|
||||
setCurrencies(Array.isArray(list) ? list : []);
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrencies();
|
||||
}, [fetchCurrencies]);
|
||||
|
||||
// --- اضافه کردن ارز ---
|
||||
const onAddCurrency = async (e) => {
|
||||
e.preventDefault();
|
||||
const { currencyCode } = addForm;
|
||||
const code = currencyCode.trim().toUpperCase();
|
||||
|
||||
if (!code) {
|
||||
toast.error('Please enter a currency code');
|
||||
return;
|
||||
}
|
||||
|
||||
// اعتبارسنجی: کد ارز باید 3 حرف باشد
|
||||
if (code.length !== 3) {
|
||||
toast.error('Currency code must be exactly 3 characters (e.g., USD, EUR)');
|
||||
return;
|
||||
}
|
||||
|
||||
// بررسی وجود ارز در لیست (برای جلوگیری از درخواست غیرضروری)
|
||||
const exists = currencies.some(c => c.currencyCode?.code?.toUpperCase() === code);
|
||||
if (exists) {
|
||||
toast.error(`Currency ${code} already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await currencyAPI.create(code);
|
||||
await fetchCurrencies();
|
||||
setIsModalOpen(false);
|
||||
setAddForm({ currencyCode: '' });
|
||||
toast.success(getSuccessMessage(result) || 'Currency added successfully');
|
||||
} catch (err) {
|
||||
console.error('Error creating currency:', err);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- تغییر وضعیت فعال بودن ---
|
||||
const onToggleActivation = async (currencyCode) => {
|
||||
try {
|
||||
const result = await currencyAPI.toggleActivation(currencyCode);
|
||||
await fetchCurrencies();
|
||||
toast.success(getSuccessMessage(result) || 'Currency activation status updated');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- فعال/غیرفعال کردن Maintenance ---
|
||||
const onEnableMaintenance = async (currencyCode, message = '') => {
|
||||
try {
|
||||
const result = await currencyAPI.enableMaintenance(currencyCode, message || null);
|
||||
await fetchCurrencies();
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
toast.success(getSuccessMessage(result) || 'Maintenance enabled');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
const onDisableMaintenance = async (currencyCode) => {
|
||||
try {
|
||||
const result = await currencyAPI.disableMaintenance(currencyCode);
|
||||
await fetchCurrencies();
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
toast.success(getSuccessMessage(result) || 'Maintenance disabled');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- بهروزرسانی Permissions ---
|
||||
const onUpdatePermissions = async (currencyCode, permissions) => {
|
||||
try {
|
||||
const result = await currencyAPI.updatePermissions(currencyCode, permissions);
|
||||
await fetchCurrencies();
|
||||
setPermissionsModal(null);
|
||||
toast.success(getSuccessMessage(result) || 'Permissions updated successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- بهروزرسانی Limits ---
|
||||
const onUpdateLimits = async (currencyCode, limits) => {
|
||||
try {
|
||||
const result = await currencyAPI.updateLimits(currencyCode, limits);
|
||||
await fetchCurrencies();
|
||||
setLimitsModal(null);
|
||||
toast.success(getSuccessMessage(result) || 'Limits updated successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- بهروزرسانی Fees ---
|
||||
const onUpdateFees = async (currencyCode, fees) => {
|
||||
try {
|
||||
const result = await currencyAPI.updateFees(currencyCode, fees);
|
||||
await fetchCurrencies();
|
||||
setFeesModal(null);
|
||||
toast.success(getSuccessMessage(result) || 'Fees updated successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
// --- ستونهای جدول ---
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'currencyCode',
|
||||
header: 'Currency',
|
||||
render: (val) => (
|
||||
<div>
|
||||
<div className="font-semibold">{val?.code || '—'}</div>
|
||||
<div className="text-xs text-gray-500">{val?.name || ''}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'currencyCode',
|
||||
header: 'Symbol',
|
||||
render: (val) => <span className="font-mono">{val?.symbol || '—'}</span>,
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
header: 'Active',
|
||||
render: (val) => (
|
||||
<span className={`px-2 py-1 rounded text-xs ${val ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{val ? '✅ Yes' : '❌ No'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'isUnderMaintenance',
|
||||
header: 'Maintenance',
|
||||
render: (val, row) => (
|
||||
<span className={`px-2 py-1 rounded text-xs ${val ? 'bg-yellow-100 text-yellow-800' : 'bg-gray-100 text-gray-800'}`}>
|
||||
{val ? '⚠️ Yes' : '✓ No'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'permissions',
|
||||
header: 'Permissions',
|
||||
render: (val) => {
|
||||
if (!val) return '—';
|
||||
const enabled = Object.values(val).filter(Boolean).length;
|
||||
return <span className="text-sm">{enabled} enabled</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: 'Actions',
|
||||
render: (_val, row) => {
|
||||
const code = row.currencyCode?.code;
|
||||
if (!code) return '—';
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={() => onToggleActivation(code)}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-blue-100 text-blue-700 hover:opacity-90"
|
||||
title="Toggle Activation"
|
||||
>
|
||||
<Power className="h-3 w-3 mr-1" /> Toggle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setMaintenanceModal(row);
|
||||
setMaintenanceMessage(row.maintenanceMessage || '');
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-yellow-100 text-yellow-700 hover:opacity-90"
|
||||
title="Maintenance"
|
||||
>
|
||||
<Wrench className="h-3 w-3 mr-1" /> Maintenance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setPermissionsModal(row);
|
||||
setPermissionsForm(row.permissions || {
|
||||
depositEnabled: false,
|
||||
cashOutEnabled: false,
|
||||
exchangeEnabled: false,
|
||||
purchaseEnabled: false,
|
||||
transferFromEnabled: false,
|
||||
transferToEnabled: false,
|
||||
voucherCreationEnabled: false,
|
||||
voucherRedeemEnabled: false,
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-purple-100 text-purple-700 hover:opacity-90"
|
||||
title="Permissions"
|
||||
>
|
||||
<Shield className="h-3 w-3 mr-1" /> Permissions
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setLimitsModal(row);
|
||||
setLimitsForm(row.limits || {
|
||||
depositLimits: { min: null, max: null },
|
||||
cashOutLimits: { min: null, max: null },
|
||||
transferLimits: { min: null, max: null },
|
||||
voucherLimits: { min: null, max: null },
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-green-100 text-green-700 hover:opacity-90"
|
||||
title="Limits"
|
||||
>
|
||||
<Settings className="h-3 w-3 mr-1" /> Limits
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setFeesModal(row);
|
||||
setFeesForm(row.fees || {
|
||||
depositFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
cashOutFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
transferFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
exchangeFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
generateVoucherFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
expireVoucherSystemFee: { percent: 0, fixed: 0, minAmount: null, maxAmount: null },
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-1 text-xs rounded-md bg-indigo-100 text-indigo-700 hover:opacity-90"
|
||||
title="Fees"
|
||||
>
|
||||
<DollarSign className="h-3 w-3 mr-1" /> Fees
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
], []);
|
||||
|
||||
const handleFilterChange = useMemo(() => debounce(async (v) => {
|
||||
// Filter is handled by DataTable component
|
||||
}, 400), []);
|
||||
|
||||
// --- مودال اضافه کردن ارز ---
|
||||
const renderAddModal = () => {
|
||||
if (!isModalOpen) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setIsModalOpen(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">Add Currency</h3>
|
||||
<form className="space-y-4" onSubmit={onAddCurrency}>
|
||||
<div>
|
||||
<input
|
||||
value={addForm.currencyCode}
|
||||
onChange={(e) => setAddForm({ currencyCode: e.target.value.toUpperCase() })}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
placeholder="Currency Code (e.g., USD, EUR)"
|
||||
maxLength={3}
|
||||
pattern="[A-Z]{3}"
|
||||
title="Enter a 3-letter currency code"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Enter a 3-letter ISO currency code</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-2">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Maintenance ---
|
||||
const renderMaintenanceModal = () => {
|
||||
if (!maintenanceModal) return null;
|
||||
const code = maintenanceModal.currencyCode?.code;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => {
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
}} />
|
||||
<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">
|
||||
Maintenance: {code}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Maintenance Message (optional)</label>
|
||||
<textarea
|
||||
value={maintenanceMessage}
|
||||
onChange={(e) => setMaintenanceMessage(e.target.value)}
|
||||
className="w-full p-2 border rounded-lg"
|
||||
rows={3}
|
||||
placeholder="Enter maintenance message..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => onEnableMaintenance(code, maintenanceMessage)}
|
||||
className="flex-1 px-4 py-2 bg-yellow-500 text-white rounded-lg hover:opacity-90"
|
||||
>
|
||||
Enable Maintenance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDisableMaintenance(code)}
|
||||
className="flex-1 px-4 py-2 bg-green-500 text-white rounded-lg hover:opacity-90"
|
||||
>
|
||||
Disable Maintenance
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => {
|
||||
setMaintenanceModal(null);
|
||||
setMaintenanceMessage('');
|
||||
}} className="w-full px-4 py-2 border rounded-lg">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Permissions ---
|
||||
const renderPermissionsModal = () => {
|
||||
if (!permissionsModal) return null;
|
||||
const code = permissionsModal.currencyCode?.code;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onUpdatePermissions(code, permissionsForm);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setPermissionsModal(null)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-2xl card p-6 relative bg-white rounded-2xl shadow-lg max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Permissions: {code}</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
{Object.keys(permissionsForm).map((key) => (
|
||||
<label key={key} className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={permissionsForm[key]}
|
||||
onChange={(e) => setPermissionsForm({ ...permissionsForm, [key]: e.target.checked })}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</span>
|
||||
</label>
|
||||
))}
|
||||
<div className="flex justify-end gap-x-2 mt-4">
|
||||
<button type="button" onClick={() => setPermissionsModal(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Limits ---
|
||||
const renderLimitsModal = () => {
|
||||
if (!limitsModal) return null;
|
||||
const code = limitsModal.currencyCode?.code;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onUpdateLimits(code, limitsForm);
|
||||
};
|
||||
|
||||
const updateLimit = (category, type, value) => {
|
||||
setLimitsForm({
|
||||
...limitsForm,
|
||||
[category]: {
|
||||
...limitsForm[category],
|
||||
[type]: value === '' ? null : parseFloat(value) || null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setLimitsModal(null)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-2xl card p-6 relative bg-white rounded-2xl shadow-lg max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Limits: {code}</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{Object.keys(limitsForm).map((category) => (
|
||||
<div key={category} className="border rounded-lg p-3">
|
||||
<h4 className="font-medium mb-2">{category.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Min</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limitsForm[category].min ?? ''}
|
||||
onChange={(e) => updateLimit(category, 'min', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Min"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Max</label>
|
||||
<input
|
||||
type="number"
|
||||
value={limitsForm[category].max ?? ''}
|
||||
onChange={(e) => updateLimit(category, 'max', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Max"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end gap-x-2 mt-4">
|
||||
<button type="button" onClick={() => setLimitsModal(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// --- مودال Fees ---
|
||||
const renderFeesModal = () => {
|
||||
if (!feesModal) return null;
|
||||
const code = feesModal.currencyCode?.code;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
onUpdateFees(code, feesForm);
|
||||
};
|
||||
|
||||
const updateFee = (category, field, value) => {
|
||||
setFeesForm({
|
||||
...feesForm,
|
||||
[category]: {
|
||||
...feesForm[category],
|
||||
[field]: value === '' ? null : parseFloat(value) || null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={() => setFeesModal(null)} />
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-3xl card p-6 relative bg-white rounded-2xl shadow-lg max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Fees: {code}</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{Object.keys(feesForm).map((category) => (
|
||||
<div key={category} className="border rounded-lg p-3">
|
||||
<h4 className="font-medium mb-2">{category.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Percent</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].percent ?? ''}
|
||||
onChange={(e) => updateFee(category, 'percent', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Percent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Fixed</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].fixed ?? ''}
|
||||
onChange={(e) => updateFee(category, 'fixed', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Fixed"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Min Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].minAmount ?? ''}
|
||||
onChange={(e) => updateFee(category, 'minAmount', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Min Amount"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-600 mb-1">Max Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={feesForm[category].maxAmount ?? ''}
|
||||
onChange={(e) => updateFee(category, 'maxAmount', e.target.value)}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Max Amount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end gap-x-2 mt-4">
|
||||
<button type="button" onClick={() => setFeesModal(null)} className="px-4 py-2 border rounded-lg">Cancel</button>
|
||||
<button type="submit" className="btn-primary px-4 py-2 rounded-lg">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ToastContainer position="top-right" autoClose={3000} />
|
||||
|
||||
<div className="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Currencies</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
Manage currencies: add, activate/deactivate, configure maintenance, permissions, limits, and fees
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setIsModalOpen(true)} className="btn-primary inline-flex items-center">
|
||||
<Plus className="h-4 w-4 mr-2" /> Add Currency
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{renderAddModal()}
|
||||
{renderMaintenanceModal()}
|
||||
{renderPermissionsModal()}
|
||||
{renderLimitsModal()}
|
||||
{renderFeesModal()}
|
||||
|
||||
<div className="card p-4 mb-4">
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
value={filter}
|
||||
onChange={(e) => {
|
||||
setFilter(e.target.value);
|
||||
handleFilterChange(e.target.value);
|
||||
}}
|
||||
placeholder="Filter by currency code or name"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-10">
|
||||
<div className="w-8 h-8 border-4 border-t-transparent border-primary-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
data={currencies.filter(c => {
|
||||
if (!filter) return true;
|
||||
const search = filter.toLowerCase();
|
||||
const code = c.currencyCode?.code?.toLowerCase() || '';
|
||||
const name = c.currencyCode?.name?.toLowerCase() || '';
|
||||
return code.includes(search) || name.includes(search);
|
||||
})}
|
||||
columns={columns}
|
||||
searchable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && <div className="mt-4 text-sm text-red-600">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Currency;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { CreditCard } from 'lucide-react';
|
||||
import { forgotPassword } from '../services/api';
|
||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||
|
||||
const ForgotPassword = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -18,9 +19,9 @@ const ForgotPassword = () => {
|
||||
|
||||
try {
|
||||
const res = await forgotPassword(email);
|
||||
setSuccess(res?.message || 'If the email exists, a reset link was sent.');
|
||||
setSuccess(getSuccessMessage(res) || 'If the email exists, a reset link was sent.');
|
||||
} catch (err) {
|
||||
setError(typeof err === 'string' ? err : 'Request failed. Please try again.');
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
649
src/pages/Location.jsx
Normal file
649
src/pages/Location.jsx
Normal file
@@ -0,0 +1,649 @@
|
||||
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { countryAPI, provinceAPI, cityAPI, currencyAPI } 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: '' });
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
|
||||
// Province states
|
||||
const [provinces, setProvinces] = useState([]);
|
||||
const [provincesLoading, setProvincesLoading] = useState(true);
|
||||
const [provinceFilter, setProvinceFilter] = 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 [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
|
||||
useEffect(() => {
|
||||
const fetchCurrencies = async () => {
|
||||
try {
|
||||
const list = await currencyAPI.list();
|
||||
setCurrencies(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load currencies:', err);
|
||||
}
|
||||
};
|
||||
fetchCurrencies();
|
||||
}, []);
|
||||
|
||||
// دریافت کشورها
|
||||
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 () => {
|
||||
try {
|
||||
setProvincesLoading(true);
|
||||
const list = await provinceAPI.list();
|
||||
setProvinces(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
} finally {
|
||||
setProvincesLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// دریافت شهرها
|
||||
const fetchCities = useCallback(async () => {
|
||||
try {
|
||||
setCitiesLoading(true);
|
||||
const list = await cityAPI.list();
|
||||
setCities(Array.isArray(list) ? list : []);
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
} finally {
|
||||
setCitiesLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'country') {
|
||||
fetchCountries();
|
||||
} else if (activeTab === 'province') {
|
||||
fetchProvinces();
|
||||
fetchCountries(); // برای dropdown
|
||||
} else if (activeTab === 'city') {
|
||||
fetchCities();
|
||||
fetchProvinces(); // برای dropdown
|
||||
}
|
||||
}, [activeTab, fetchCountries, fetchProvinces, fetchCities]);
|
||||
|
||||
// ========== 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();
|
||||
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 {
|
||||
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' },
|
||||
{ key: 'phoneCode', header: 'Phone Code' },
|
||||
{ key: 'currencyCode', header: 'Currency' },
|
||||
{ key: 'timeZoneName', header: 'Time Zone', render: (val, row) => val || row.timeZone || '—' },
|
||||
{
|
||||
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(() => [
|
||||
{ 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) ||
|
||||
c.phoneCode?.toLowerCase().includes(filter) ||
|
||||
c.currencyCode?.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>
|
||||
<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
|
||||
data={filteredCountries}
|
||||
columns={countryColumns}
|
||||
loading={countriesLoading}
|
||||
searchable={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
<button
|
||||
onClick={() => openProvinceModal()}
|
||||
className="btn-primary inline-flex items-center ml-4"
|
||||
>
|
||||
<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">
|
||||
<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>
|
||||
<button
|
||||
onClick={() => openCityModal()}
|
||||
className="btn-primary inline-flex items-center ml-4"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" /> Add City
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
data={filteredCities}
|
||||
columns={cityColumns}
|
||||
loading={citiesLoading}
|
||||
searchable={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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.currencyCode?.code} value={curr.currencyCode?.code}>
|
||||
{curr.currencyCode?.code} - {curr.currencyCode?.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Time Zone *</label>
|
||||
<input
|
||||
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
|
||||
/>
|
||||
</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 && (
|
||||
<>
|
||||
<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;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { CreditCard, Eye, EyeOff } from 'lucide-react';
|
||||
import { login as apiLogin } from '../services/api';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { getErrorMessage } from '../utils/errorHandler';
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
@@ -29,7 +30,7 @@ const Login = () => {
|
||||
setError('Invalid email or password');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(typeof err === 'string' ? err : 'Login failed. Please try again.');
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import DataTable from '../components/DataTable';
|
||||
import { rolesAPI, listPermissions } from '../services/api';
|
||||
import { Plus, Trash2, Search, Pencil } from 'lucide-react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||
|
||||
const Roles = () => {
|
||||
const [roles, setRoles] = useState([]);
|
||||
@@ -23,7 +26,9 @@ const Roles = () => {
|
||||
const list = await rolesAPI.list(q);
|
||||
setRoles(list);
|
||||
} catch (e) {
|
||||
setError('Failed to load roles');
|
||||
const errorMsg = getErrorMessage(e);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -54,14 +59,22 @@ const Roles = () => {
|
||||
setSelectedPermissions([]);
|
||||
await fetchRoles(nameFilter);
|
||||
setIsModalOpen(false);
|
||||
} catch (_) {
|
||||
setError('Failed to create role');
|
||||
toast.success('Role created successfully');
|
||||
} catch (err) {
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (id) => {
|
||||
await rolesAPI.remove(id);
|
||||
await fetchRoles(nameFilter);
|
||||
try {
|
||||
await rolesAPI.remove(id);
|
||||
await fetchRoles(nameFilter);
|
||||
toast.success('Role deleted successfully');
|
||||
} catch (err) {
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
const openEdit = (role) => {
|
||||
@@ -86,8 +99,11 @@ const Roles = () => {
|
||||
setName('');
|
||||
setPermissionsInput('');
|
||||
setSelectedPermissions([]);
|
||||
} catch (_) {
|
||||
setError('Failed to update role');
|
||||
toast.success('Role updated successfully');
|
||||
} catch (err) {
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -115,6 +131,7 @@ const Roles = () => {
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ToastContainer position="top-right" autoClose={3000} />
|
||||
<div className="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Roles</h1>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { usersAPI, rolesAPI } from '../services/api';
|
||||
import { Plus, Trash2, Search, Pencil, ShieldOff, RefreshCcw } from 'lucide-react';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { getErrorMessage, getSuccessMessage } from '../utils/errorHandler';
|
||||
|
||||
// debounce ساده
|
||||
function debounce(func, delay) {
|
||||
@@ -46,7 +47,9 @@ const Users = () => {
|
||||
setError('');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('Failed to load users');
|
||||
const errorMsg = getErrorMessage(err);
|
||||
setError(errorMsg);
|
||||
toast.error(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -72,10 +75,9 @@ const Users = () => {
|
||||
await fetchUsers(filter);
|
||||
setIsModalOpen(false);
|
||||
setAddForm({ firstName: '', lastName: '', email: '' });
|
||||
toast.success(newUser?.Message || 'User added successfully');
|
||||
toast.success(getSuccessMessage(newUser) || 'User added successfully');
|
||||
} catch (err) {
|
||||
const msg = err?.response?.data?.Message || err?.message || 'Failed to create user';
|
||||
toast.error(msg);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,10 +100,9 @@ const Users = () => {
|
||||
await fetchUsers(filter);
|
||||
setIsEditModalOpen(false);
|
||||
setEditingUser(null);
|
||||
toast.success(updatedUser?.Message || 'User updated successfully');
|
||||
toast.success(getSuccessMessage(updatedUser) || 'User updated successfully');
|
||||
} catch (err) {
|
||||
const msg = err?.response?.data?.Message || err?.message || 'Failed to update user';
|
||||
toast.error(msg);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,10 +112,9 @@ const Users = () => {
|
||||
try {
|
||||
const result = await usersAPI.remove(id);
|
||||
await fetchUsers(filter);
|
||||
toast.success(result?.Message || 'User deleted successfully');
|
||||
toast.success(getSuccessMessage(result) || 'User deleted successfully');
|
||||
} catch (err) {
|
||||
const msg = err?.response?.data?.Message || err?.message || 'Failed to delete user';
|
||||
toast.error(msg);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,10 +123,9 @@ const Users = () => {
|
||||
try {
|
||||
const updated = await usersAPI.toggleActivation(id);
|
||||
await fetchUsers(filter);
|
||||
toast.success(updated?.Message || 'User status updated');
|
||||
toast.success(getSuccessMessage(updated) || 'User status updated');
|
||||
} catch (err) {
|
||||
const msg = err?.response?.data?.Message || err?.message || 'Failed to update status';
|
||||
toast.error(msg);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,10 +133,9 @@ const Users = () => {
|
||||
const onResetPassword = async (id) => {
|
||||
try {
|
||||
const res = await usersAPI.resetPassword(id);
|
||||
toast.success(res?.Message || 'Password reset successfully');
|
||||
toast.success(getSuccessMessage(res) || 'Password reset successfully');
|
||||
} catch (err) {
|
||||
const msg = err?.response?.data?.Message || err?.message || 'Failed to reset password';
|
||||
toast.error(msg);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,8 +153,7 @@ const Users = () => {
|
||||
setRolesModalUser(null);
|
||||
fetchUsers(filter);
|
||||
} catch (err) {
|
||||
const msg = err?.response?.data?.Message || err?.message || 'Failed to update roles';
|
||||
toast.error(msg);
|
||||
toast.error(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,254 +1,15 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
// -----------------------------
|
||||
// تنظیم BASE_URL
|
||||
// Main API Export File
|
||||
// -----------------------------
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ||
|
||||
(import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir");
|
||||
// This file exports all API modules for backward compatibility
|
||||
// Individual APIs are now in separate files for better organization
|
||||
|
||||
// -----------------------------
|
||||
// ایجاد instance از axios
|
||||
// -----------------------------
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
withCredentials: true,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Global error handler برای suppress کردن 401/404
|
||||
// -----------------------------
|
||||
if (typeof window !== 'undefined') {
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
console.error = function(...args) {
|
||||
const errorStr = JSON.stringify(args);
|
||||
const isSilentError = errorStr.includes('401') || errorStr.includes('404') ||
|
||||
args.some(arg => typeof arg === 'object' && arg?.response?.status && [401,404].includes(arg.response.status));
|
||||
|
||||
if (isSilentError) return;
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
|
||||
console.warn = function(...args) {
|
||||
const warnStr = JSON.stringify(args);
|
||||
if (warnStr.includes('401') || warnStr.includes('404')) return;
|
||||
originalConsoleWarn.apply(console, args);
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Request interceptor
|
||||
// -----------------------------
|
||||
api.interceptors.request.use(config => {
|
||||
const skipAuthRedirect = config?.skipAuthRedirect === true;
|
||||
if (skipAuthRedirect) {
|
||||
const authState = useAuthStore.getState();
|
||||
if (!authState?.isLoggedIn) {
|
||||
const CancelToken = axios.CancelToken;
|
||||
const source = CancelToken.source();
|
||||
source.cancel('User not logged in');
|
||||
config.cancelToken = source.token;
|
||||
config._skipRequest = true;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}, error => Promise.reject(error));
|
||||
|
||||
// -----------------------------
|
||||
// Response interceptor
|
||||
// -----------------------------
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error?.isSilent) return Promise.reject(error);
|
||||
if (axios.isCancel(error)) return Promise.reject({ isSilent: true, isCancel: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config || {} });
|
||||
|
||||
const skipRedirect = error?.config?.skipAuthRedirect === true;
|
||||
const status = error?.response?.status;
|
||||
|
||||
if (status === 401) {
|
||||
if (!skipRedirect) {
|
||||
useAuthStore.getState().setLoggedIn(false);
|
||||
window.location.href = "/";
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
return Promise.reject({ isSilent: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config });
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 404 && skipRedirect) {
|
||||
return Promise.reject({ isSilent: true, response: { status: 404, data: { message: 'Not Found' } }, config: error.config });
|
||||
}
|
||||
|
||||
return Promise.reject(error.response?.data || error);
|
||||
}
|
||||
);
|
||||
|
||||
// -----------------------------
|
||||
// Auth API
|
||||
// -----------------------------
|
||||
export async function login(username, password) {
|
||||
const res = await api.post("/api/v1/Auth/SignIn", { userName: username, username, email: username, password });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
try {
|
||||
await api.post("/api/v1/Auth/SignOut", null, { skipAuthRedirect: true });
|
||||
} catch (error) {
|
||||
console.warn("SignOut API error:", error);
|
||||
}
|
||||
useAuthStore.getState().setLoggedIn(false);
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
export async function forgotPassword(email) {
|
||||
const res = await api.post("/api/v1/Auth/ForgotPassword", { email });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function fetchProtectedData(endpoint) {
|
||||
const res = await api.get(endpoint);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Payments API (mock data)
|
||||
// -----------------------------
|
||||
const mockData = {
|
||||
stats: { total: 1247, success: 1189, failed: 58 },
|
||||
payments: [
|
||||
{ id: 'TXN-001', user: 'John Doe', amount: 299.99, status: 'success', date: '2024-01-15T10:30:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-002', user: 'Jane Smith', amount: 150.00, status: 'pending', date: '2024-01-15T11:45:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-003', user: 'Bob Johnson', amount: 75.50, status: 'failed', date: '2024-01-15T12:15:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-004', user: 'Alice Brown', amount: 450.00, status: 'success', date: '2024-01-15T13:20:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-005', user: 'Charlie Wilson', amount: 89.99, status: 'success', date: '2024-01-15T14:30:00Z', currency: 'USD' }
|
||||
],
|
||||
chartData: [
|
||||
{ date: '2024-01-09', amount: 1200 },
|
||||
{ date: '2024-01-10', amount: 1900 },
|
||||
{ date: '2024-01-11', amount: 3000 },
|
||||
{ date: '2024-01-12', amount: 2800 },
|
||||
{ date: '2024-01-13', amount: 1890 },
|
||||
{ date: '2024-01-14', amount: 2390 },
|
||||
{ date: '2024-01-15', amount: 3490 }
|
||||
]
|
||||
};
|
||||
|
||||
export const paymentsAPI = {
|
||||
async getStats() { return mockData.stats; },
|
||||
async getChartData() { return mockData.chartData; },
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// Roles API
|
||||
// -----------------------------
|
||||
const ROLES_STORAGE_KEY = 'app_roles_v1';
|
||||
function readRolesFromStorage() { try { const raw = localStorage.getItem(ROLES_STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } }
|
||||
function writeRolesToStorage(roles) { localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles)); }
|
||||
function ensureSeedRoles() { if (readRolesFromStorage().length === 0) writeRolesToStorage([
|
||||
{ id: crypto.randomUUID(), name: 'Admin', permissions: ['read','write','delete'], userType: 'internal' },
|
||||
{ id: crypto.randomUUID(), name: 'Editor', permissions: ['read','write'], userType: 'internal' },
|
||||
{ id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' },
|
||||
]); }
|
||||
|
||||
export const rolesAPI = {
|
||||
async list(queryOrOptions='') {
|
||||
const opts = typeof queryOrOptions === 'string' ? { nameQuery: queryOrOptions, currentPage:1, pageSize:100 } : { nameQuery: queryOrOptions?.nameQuery||'', currentPage: queryOrOptions?.currentPage||1, pageSize: queryOrOptions?.pageSize||100 };
|
||||
if (!useAuthStore.getState()?.isLoggedIn) { ensureSeedRoles(); return readRolesFromStorage().filter(r=>r.name.toLowerCase().includes((opts.nameQuery||'').toLowerCase())); }
|
||||
try {
|
||||
const res = await api.get('/api/v1/Role', { params: opts, skipAuthRedirect: true });
|
||||
const items = Array.isArray(res?.data?.data?.data) ? res.data.data.data : [];
|
||||
writeRolesToStorage(items.map(r => ({ id: r.id||crypto.randomUUID(), ...r })));
|
||||
return items;
|
||||
} catch { ensureSeedRoles(); return readRolesFromStorage(); }
|
||||
},
|
||||
|
||||
async create(role) {
|
||||
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[] };
|
||||
let created = payload;
|
||||
if (useAuthStore.getState()?.isLoggedIn) {
|
||||
try { const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect:true }); created = res?.data||payload; } catch {}
|
||||
}
|
||||
const roles = readRolesFromStorage(); const newRole = { id: crypto.randomUUID(), ...created }; roles.push(newRole); writeRolesToStorage(roles); return newRole;
|
||||
},
|
||||
|
||||
async remove(id) {
|
||||
if (useAuthStore.getState()?.isLoggedIn) { try { await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect:true }); } catch {} }
|
||||
const roles = readRolesFromStorage(); writeRolesToStorage(roles.filter(r=>r.id!==id)); return { success:true };
|
||||
},
|
||||
|
||||
async update(id, role) {
|
||||
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[], userType: role?.userType||undefined };
|
||||
if (useAuthStore.getState()?.isLoggedIn) { try { await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect:true }); } catch {} }
|
||||
const roles = readRolesFromStorage(); const idx = roles.findIndex(r=>r.id===id); if(idx!==-1){ roles[idx]={...roles[idx], ...payload}; writeRolesToStorage(roles); return roles[idx]; }
|
||||
const updated={id,...payload}; roles.push(updated); writeRolesToStorage(roles); return updated;
|
||||
},
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// Users API با رولها
|
||||
// -----------------------------
|
||||
const USERS_STORAGE_KEY = 'app_users_v1';
|
||||
function readUsersFromStorage(){ try{ const raw = localStorage.getItem(USERS_STORAGE_KEY); return raw?JSON.parse(raw):[]; } catch{return [];} }
|
||||
function writeUsersToStorage(users){ localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users)); }
|
||||
|
||||
export const usersAPI = {
|
||||
async list({searchQuery='',currentPage=1,pageSize=100}={}) {
|
||||
const res = await api.get('/api/v1/User',{ params:{searchQuery,currentPage,pageSize}, skipAuthRedirect:true });
|
||||
return res?.data?.data?.data||[];
|
||||
},
|
||||
async create(user){
|
||||
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||
const res = await api.post('/api/v1/User',payload,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async update(id,user){
|
||||
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||
const res = await api.put(`/api/v1/User/${encodeURIComponent(id)}`,payload,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async remove(id){
|
||||
const res = await api.delete(`/api/v1/User/Delete/${encodeURIComponent(id)}/Role`,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async toggleActivation(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ToggleActivation`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||
async resetPassword(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ResetPassword`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||
async getRoles(userId){ try{ const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{skipAuthRedirect:true}); return res?.data?.data?.data||[]; }catch(err){ console.error(err); return []; } },
|
||||
|
||||
// مدیریت رولها
|
||||
async getRoles(userId){
|
||||
try {
|
||||
const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`, { skipAuthRedirect: true });
|
||||
return res?.data?.data || []; // ← اصلاح شد
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
async removeRole(userId,roleId){ try{ const res = await api.delete(`/api/v1/User/${encodeURIComponent(userId)}/Role/${encodeURIComponent(roleId)}`,{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
||||
async updateRoles(userId,roleIds=[]){ try{ const res = await api.post(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{roleIds},{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
||||
};
|
||||
|
||||
// -----------------------------
|
||||
// Permissions API
|
||||
// -----------------------------
|
||||
export async function listPermissions(){
|
||||
if(!useAuthStore.getState()?.isLoggedIn) return [
|
||||
{name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'},
|
||||
{name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'},
|
||||
{name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'}
|
||||
];
|
||||
try{
|
||||
const res = await api.get('/api/v1/General/Permissions',{skipAuthRedirect:true});
|
||||
const items = Array.isArray(res?.data?.data)?res.data.data:[];
|
||||
return items.map(p=>({name:p.name||p.displayName,description:p.description||p.displayName||p.name})).filter(Boolean);
|
||||
}catch{return [
|
||||
{name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'},
|
||||
{name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'},
|
||||
{name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'}
|
||||
];}
|
||||
}
|
||||
export * from './authAPI';
|
||||
export { paymentsAPI } from './paymentsAPI';
|
||||
export { rolesAPI } from './rolesAPI';
|
||||
export { usersAPI } from './usersAPI';
|
||||
export { currencyAPI } from './currencyAPI';
|
||||
export { countryAPI } from './countryAPI';
|
||||
export { provinceAPI } from './provinceAPI';
|
||||
export { cityAPI } from './cityAPI';
|
||||
export { listPermissions } from './permissionsAPI';
|
||||
|
||||
104
src/services/apiClient.js
Normal file
104
src/services/apiClient.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
// -----------------------------
|
||||
// تنظیم BASE_URL
|
||||
// -----------------------------
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ||
|
||||
(import.meta.env.DEV ? "/" : "https://khalijpay-core.qaserver.ir");
|
||||
|
||||
// -----------------------------
|
||||
// ایجاد instance از axios
|
||||
// -----------------------------
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
withCredentials: true,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
// -----------------------------
|
||||
// Global error handler برای suppress کردن 401/404
|
||||
// -----------------------------
|
||||
if (typeof window !== 'undefined') {
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
console.error = function(...args) {
|
||||
const errorStr = JSON.stringify(args);
|
||||
const isSilentError = errorStr.includes('401') || errorStr.includes('404') ||
|
||||
args.some(arg => typeof arg === 'object' && arg?.response?.status && [401,404].includes(arg.response.status));
|
||||
|
||||
if (isSilentError) return;
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
|
||||
console.warn = function(...args) {
|
||||
const warnStr = JSON.stringify(args);
|
||||
if (warnStr.includes('401') || warnStr.includes('404')) return;
|
||||
originalConsoleWarn.apply(console, args);
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Request interceptor
|
||||
// -----------------------------
|
||||
api.interceptors.request.use(config => {
|
||||
const skipAuthRedirect = config?.skipAuthRedirect === true;
|
||||
if (skipAuthRedirect) {
|
||||
const authState = useAuthStore.getState();
|
||||
if (!authState?.isLoggedIn) {
|
||||
const CancelToken = axios.CancelToken;
|
||||
const source = CancelToken.source();
|
||||
source.cancel('User not logged in');
|
||||
config.cancelToken = source.token;
|
||||
config._skipRequest = true;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}, error => Promise.reject(error));
|
||||
|
||||
// -----------------------------
|
||||
// Response interceptor
|
||||
// -----------------------------
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error?.isSilent) return Promise.reject(error);
|
||||
if (axios.isCancel(error)) return Promise.reject({ isSilent: true, isCancel: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config || {} });
|
||||
|
||||
const skipRedirect = error?.config?.skipAuthRedirect === true;
|
||||
const status = error?.response?.status;
|
||||
|
||||
if (status === 401) {
|
||||
if (!skipRedirect) {
|
||||
useAuthStore.getState().setLoggedIn(false);
|
||||
window.location.href = "/";
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
return Promise.reject({ isSilent: true, response: { status: 401, data: { message: 'Unauthorized' } }, config: error.config });
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 404 && skipRedirect) {
|
||||
return Promise.reject({ isSilent: true, response: { status: 404, data: { message: 'Not Found' } }, config: error.config });
|
||||
}
|
||||
|
||||
// حفظ status code در خطا برای مدیریت بهتر
|
||||
const errorData = error.response?.data || error;
|
||||
if (error.response) {
|
||||
return Promise.reject({
|
||||
...errorData,
|
||||
response: {
|
||||
...error.response,
|
||||
status: error.response.status,
|
||||
data: errorData
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(errorData);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
||||
|
||||
31
src/services/authAPI.js
Normal file
31
src/services/authAPI.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import api from './apiClient';
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
// -----------------------------
|
||||
// Auth API
|
||||
// -----------------------------
|
||||
export async function login(username, password) {
|
||||
const res = await api.post("/api/v1/Auth/SignIn", { userName: username, username, email: username, password });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function signOut() {
|
||||
try {
|
||||
await api.post("/api/v1/Auth/SignOut", null, { skipAuthRedirect: true });
|
||||
} catch (error) {
|
||||
console.warn("SignOut API error:", error);
|
||||
}
|
||||
useAuthStore.getState().setLoggedIn(false);
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
export async function forgotPassword(email) {
|
||||
const res = await api.post("/api/v1/Auth/ForgotPassword", { email });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function fetchProtectedData(endpoint) {
|
||||
const res = await api.get(endpoint);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
50
src/services/cityAPI.js
Normal file
50
src/services/cityAPI.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// City API
|
||||
// -----------------------------
|
||||
export const cityAPI = {
|
||||
// GET /api/v1/City (with pagination)
|
||||
async list(params = {}) {
|
||||
const { currentPage = 1, pageSize = 10, ...otherParams } = params;
|
||||
const res = await api.get('/api/v1/City', {
|
||||
params: { currentPage, pageSize, ...otherParams },
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data?.data || [];
|
||||
},
|
||||
|
||||
// POST /api/v1/City
|
||||
async create(city) {
|
||||
const payload = {
|
||||
ProvinceId: city?.provinceId || '',
|
||||
CityName: String(city?.cityName || ''),
|
||||
};
|
||||
const res = await api.post('/api/v1/City', payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/City/{cityId}
|
||||
async update(cityId, city) {
|
||||
if (!cityId) {
|
||||
throw new Error('City ID is required');
|
||||
}
|
||||
const payload = {
|
||||
ProvinceId: city?.provinceId || '',
|
||||
CityName: String(city?.cityName || '').trim(),
|
||||
};
|
||||
console.log('City API Update:', { cityId, payload });
|
||||
try {
|
||||
const res = await api.put(`/api/v1/City/${encodeURIComponent(cityId)}`, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('City API Update Error:', {
|
||||
cityId,
|
||||
payload,
|
||||
error: error?.response?.data || error?.message || error
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
81
src/services/countryAPI.js
Normal file
81
src/services/countryAPI.js
Normal file
@@ -0,0 +1,81 @@
|
||||
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() {
|
||||
const res = await api.get('/api/v1/Country/All', { skipAuthRedirect: true });
|
||||
return res?.data?.data || [];
|
||||
},
|
||||
|
||||
// POST /api/v1/Country
|
||||
async create(country) {
|
||||
const payload = {
|
||||
Name: String(country?.name || ''),
|
||||
PhoneCode: String(country?.phoneCode || ''),
|
||||
CurrencyCode: String(country?.currencyCode || ''),
|
||||
TimeZoneName: 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 - سرور انتظار فیلدها با حرف بزرگ دارد
|
||||
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)}`;
|
||||
console.log('Country API Update:', { url, countryId, payload });
|
||||
|
||||
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 || [];
|
||||
},
|
||||
};
|
||||
|
||||
57
src/services/currencyAPI.js
Normal file
57
src/services/currencyAPI.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// Currency API
|
||||
// -----------------------------
|
||||
export const currencyAPI = {
|
||||
// GET /api/v1/Currency
|
||||
async list() {
|
||||
const res = await api.get('/api/v1/Currency', { skipAuthRedirect: true });
|
||||
return res?.data?.data || [];
|
||||
},
|
||||
|
||||
// POST /api/v1/Currency
|
||||
async create(currencyCode) {
|
||||
const payload = { currencyCode: String(currencyCode || '') };
|
||||
const res = await api.post('/api/v1/Currency', payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PATCH /api/v1/Currency/{currencyCode}/ToggleActivation
|
||||
async toggleActivation(currencyCode) {
|
||||
const res = await api.patch(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/ToggleActivation`, null, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PATCH /api/v1/Currency/{currencyCode}/Maintenance/Enable
|
||||
async enableMaintenance(currencyCode, maintenanceMessage = null) {
|
||||
const payload = maintenanceMessage ? { maintenanceMessage } : {};
|
||||
const res = await api.patch(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Maintenance/Enable`, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PATCH /api/v1/Currency/{currencyCode}/Maintenance/Disable
|
||||
async disableMaintenance(currencyCode) {
|
||||
const res = await api.patch(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Maintenance/Disable`, null, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/Currency/{currencyCode}/Permissions
|
||||
async updatePermissions(currencyCode, permissions) {
|
||||
const res = await api.put(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Permissions`, permissions, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/Currency/{currencyCode}/Limits
|
||||
async updateLimits(currencyCode, limits) {
|
||||
const res = await api.put(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Limits`, limits, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/Currency/{currencyCode}/Fees
|
||||
async updateFees(currencyCode, fees) {
|
||||
const res = await api.put(`/api/v1/Currency/${encodeURIComponent(currencyCode)}/Fees`, fees, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
};
|
||||
|
||||
28
src/services/paymentsAPI.js
Normal file
28
src/services/paymentsAPI.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// -----------------------------
|
||||
// Payments API (mock data)
|
||||
// -----------------------------
|
||||
const mockData = {
|
||||
stats: { total: 1247, success: 1189, failed: 58 },
|
||||
payments: [
|
||||
{ id: 'TXN-001', user: 'John Doe', amount: 299.99, status: 'success', date: '2024-01-15T10:30:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-002', user: 'Jane Smith', amount: 150.00, status: 'pending', date: '2024-01-15T11:45:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-003', user: 'Bob Johnson', amount: 75.50, status: 'failed', date: '2024-01-15T12:15:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-004', user: 'Alice Brown', amount: 450.00, status: 'success', date: '2024-01-15T13:20:00Z', currency: 'USD' },
|
||||
{ id: 'TXN-005', user: 'Charlie Wilson', amount: 89.99, status: 'success', date: '2024-01-15T14:30:00Z', currency: 'USD' }
|
||||
],
|
||||
chartData: [
|
||||
{ date: '2024-01-09', amount: 1200 },
|
||||
{ date: '2024-01-10', amount: 1900 },
|
||||
{ date: '2024-01-11', amount: 3000 },
|
||||
{ date: '2024-01-12', amount: 2800 },
|
||||
{ date: '2024-01-13', amount: 1890 },
|
||||
{ date: '2024-01-14', amount: 2390 },
|
||||
{ date: '2024-01-15', amount: 3490 }
|
||||
]
|
||||
};
|
||||
|
||||
export const paymentsAPI = {
|
||||
async getStats() { return mockData.stats; },
|
||||
async getChartData() { return mockData.chartData; },
|
||||
};
|
||||
|
||||
23
src/services/permissionsAPI.js
Normal file
23
src/services/permissionsAPI.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import api from './apiClient';
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
// -----------------------------
|
||||
// Permissions API
|
||||
// -----------------------------
|
||||
export async function listPermissions(){
|
||||
if(!useAuthStore.getState()?.isLoggedIn) return [
|
||||
{name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'},
|
||||
{name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'},
|
||||
{name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'}
|
||||
];
|
||||
try{
|
||||
const res = await api.get('/api/v1/General/Permissions',{skipAuthRedirect:true});
|
||||
const items = Array.isArray(res?.data?.data)?res.data.data:[];
|
||||
return items.map(p=>({name:p.name||p.displayName,description:p.description||p.displayName||p.name})).filter(Boolean);
|
||||
}catch{return [
|
||||
{name:'Administrator',description:'Full access'}, {name:'UserManagement',description:'Manage users'}, {name:'AddUser',description:'Create user'}, {name:'EditUser',description:'Edit user'},
|
||||
{name:'UserPasswordManagement',description:'Reset/change password'}, {name:'UserRoleManagement',description:'Assign/modify roles'},
|
||||
{name:'RoleManagement',description:'Manage roles'}, {name:'AddRole',description:'Add role'}, {name:'EditRole',description:'Edit role'}, {name:'DeleteRole',description:'Delete role'}
|
||||
];}
|
||||
}
|
||||
|
||||
66
src/services/provinceAPI.js
Normal file
66
src/services/provinceAPI.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// Province API
|
||||
// -----------------------------
|
||||
export const provinceAPI = {
|
||||
// GET /api/v1/Province (with pagination)
|
||||
async list(params = {}) {
|
||||
const { currentPage = 1, pageSize = 10, ...otherParams } = params;
|
||||
const res = await api.get('/api/v1/Province', {
|
||||
params: { currentPage, pageSize, ...otherParams },
|
||||
skipAuthRedirect: true
|
||||
});
|
||||
return res?.data?.data?.data || [];
|
||||
},
|
||||
|
||||
// POST /api/v1/Province
|
||||
async create(province) {
|
||||
const payload = {
|
||||
CountryId: province?.countryId || '',
|
||||
ProvinceName: String(province?.provinceName || ''),
|
||||
};
|
||||
const res = await api.post('/api/v1/Province', payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// PUT /api/v1/Province/{provinceId} (اگر endpoint وجود دارد)
|
||||
async update(provinceId, province) {
|
||||
if (!provinceId) {
|
||||
throw new Error('Province ID is required');
|
||||
}
|
||||
const payload = {
|
||||
CountryId: province?.countryId || '',
|
||||
ProvinceName: String(province?.provinceName || '').trim(),
|
||||
};
|
||||
console.log('Province API Update:', { provinceId, payload });
|
||||
try {
|
||||
const res = await api.put(`/api/v1/Province/${encodeURIComponent(provinceId)}`, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
} catch (error) {
|
||||
console.error('Province API Update Error:', {
|
||||
provinceId,
|
||||
payload,
|
||||
error: error?.response?.data || error?.message || error
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/v1/Province/{provinceId}/City
|
||||
async createCity(provinceId, city) {
|
||||
const payload = {
|
||||
ProvinceId: provinceId,
|
||||
CityName: String(city?.cityName || ''),
|
||||
};
|
||||
const res = await api.post(`/api/v1/Province/${encodeURIComponent(provinceId)}/City`, payload, { skipAuthRedirect: true });
|
||||
return res?.data;
|
||||
},
|
||||
|
||||
// GET /api/v1/Province/{provinceId}/City
|
||||
async getCities(provinceId) {
|
||||
const res = await api.get(`/api/v1/Province/${encodeURIComponent(provinceId)}/City`, { skipAuthRedirect: true });
|
||||
return res?.data?.data?.data || [];
|
||||
},
|
||||
};
|
||||
|
||||
49
src/services/rolesAPI.js
Normal file
49
src/services/rolesAPI.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import api from './apiClient';
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
|
||||
// -----------------------------
|
||||
// Roles API
|
||||
// -----------------------------
|
||||
const ROLES_STORAGE_KEY = 'app_roles_v1';
|
||||
function readRolesFromStorage() { try { const raw = localStorage.getItem(ROLES_STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } }
|
||||
function writeRolesToStorage(roles) { localStorage.setItem(ROLES_STORAGE_KEY, JSON.stringify(roles)); }
|
||||
function ensureSeedRoles() { if (readRolesFromStorage().length === 0) writeRolesToStorage([
|
||||
{ id: crypto.randomUUID(), name: 'Admin', permissions: ['read','write','delete'], userType: 'internal' },
|
||||
{ id: crypto.randomUUID(), name: 'Editor', permissions: ['read','write'], userType: 'internal' },
|
||||
{ id: crypto.randomUUID(), name: 'Viewer', permissions: ['read'], userType: 'external' },
|
||||
]); }
|
||||
|
||||
export const rolesAPI = {
|
||||
async list(queryOrOptions='') {
|
||||
const opts = typeof queryOrOptions === 'string' ? { nameQuery: queryOrOptions, currentPage:1, pageSize:100 } : { nameQuery: queryOrOptions?.nameQuery||'', currentPage: queryOrOptions?.currentPage||1, pageSize: queryOrOptions?.pageSize||100 };
|
||||
if (!useAuthStore.getState()?.isLoggedIn) { ensureSeedRoles(); return readRolesFromStorage().filter(r=>r.name.toLowerCase().includes((opts.nameQuery||'').toLowerCase())); }
|
||||
try {
|
||||
const res = await api.get('/api/v1/Role', { params: opts, skipAuthRedirect: true });
|
||||
const items = Array.isArray(res?.data?.data?.data) ? res.data.data.data : [];
|
||||
writeRolesToStorage(items.map(r => ({ id: r.id||crypto.randomUUID(), ...r })));
|
||||
return items;
|
||||
} catch { ensureSeedRoles(); return readRolesFromStorage(); }
|
||||
},
|
||||
|
||||
async create(role) {
|
||||
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[] };
|
||||
let created = payload;
|
||||
if (useAuthStore.getState()?.isLoggedIn) {
|
||||
try { const res = await api.post('/api/v1/Role', payload, { skipAuthRedirect:true }); created = res?.data||payload; } catch {}
|
||||
}
|
||||
const roles = readRolesFromStorage(); const newRole = { id: crypto.randomUUID(), ...created }; roles.push(newRole); writeRolesToStorage(roles); return newRole;
|
||||
},
|
||||
|
||||
async remove(id) {
|
||||
if (useAuthStore.getState()?.isLoggedIn) { try { await api.delete(`/api/v1/Role/${encodeURIComponent(id)}`, { skipAuthRedirect:true }); } catch {} }
|
||||
const roles = readRolesFromStorage(); writeRolesToStorage(roles.filter(r=>r.id!==id)); return { success:true };
|
||||
},
|
||||
|
||||
async update(id, role) {
|
||||
const payload = { name: String(role?.name||''), permissions: Array.isArray(role?.permissions)?role.permissions:[], userType: role?.userType||undefined };
|
||||
if (useAuthStore.getState()?.isLoggedIn) { try { await api.put(`/api/v1/Role/${encodeURIComponent(id)}`, payload, { skipAuthRedirect:true }); } catch {} }
|
||||
const roles = readRolesFromStorage(); const idx = roles.findIndex(r=>r.id===id); if(idx!==-1){ roles[idx]={...roles[idx], ...payload}; writeRolesToStorage(roles); return roles[idx]; }
|
||||
const updated={id,...payload}; roles.push(updated); writeRolesToStorage(roles); return updated;
|
||||
},
|
||||
};
|
||||
|
||||
43
src/services/usersAPI.js
Normal file
43
src/services/usersAPI.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import api from './apiClient';
|
||||
|
||||
// -----------------------------
|
||||
// Users API با رولها
|
||||
// -----------------------------
|
||||
const USERS_STORAGE_KEY = 'app_users_v1';
|
||||
function readUsersFromStorage(){ try{ const raw = localStorage.getItem(USERS_STORAGE_KEY); return raw?JSON.parse(raw):[]; } catch{return [];} }
|
||||
function writeUsersToStorage(users){ localStorage.setItem(USERS_STORAGE_KEY, JSON.stringify(users)); }
|
||||
|
||||
export const usersAPI = {
|
||||
async list({searchQuery='',currentPage=1,pageSize=100}={}) {
|
||||
const res = await api.get('/api/v1/User',{ params:{searchQuery,currentPage,pageSize}, skipAuthRedirect:true });
|
||||
return res?.data?.data?.data||[];
|
||||
},
|
||||
async create(user){
|
||||
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||
const res = await api.post('/api/v1/User',payload,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async update(id,user){
|
||||
const payload = { firstName:String(user?.firstName||''), lastName:String(user?.lastName||''), email:String(user?.email||''), mobile:String(user?.mobile||''), isActive:!!user?.isActive };
|
||||
const res = await api.put(`/api/v1/User/${encodeURIComponent(id)}`,payload,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async remove(id){
|
||||
const res = await api.delete(`/api/v1/User/Delete/${encodeURIComponent(id)}/Role`,{skipAuthRedirect:true});
|
||||
return res?.data;
|
||||
},
|
||||
async toggleActivation(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ToggleActivation`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||
async resetPassword(id){ const res = await api.patch(`/api/v1/User/${encodeURIComponent(id)}/ResetPassword`,null,{skipAuthRedirect:true}); return res?.data; },
|
||||
async getRoles(userId){
|
||||
try {
|
||||
const res = await api.get(`/api/v1/User/${encodeURIComponent(userId)}/Role`, { skipAuthRedirect: true });
|
||||
return res?.data?.data || [];
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
async removeRole(userId,roleId){ try{ const res = await api.delete(`/api/v1/User/${encodeURIComponent(userId)}/Role/${encodeURIComponent(roleId)}`,{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
||||
async updateRoles(userId,roleIds=[]){ try{ const res = await api.post(`/api/v1/User/${encodeURIComponent(userId)}/Role`,{roleIds},{skipAuthRedirect:true}); return res?.data; } catch(err){ console.error(err); return null; } },
|
||||
};
|
||||
|
||||
86
src/utils/errorHandler.js
Normal file
86
src/utils/errorHandler.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* استخراج پیام خطا از response سرور
|
||||
* این تابع سعی میکند پیام خطا را از مکانهای مختلف در response پیدا کند
|
||||
*
|
||||
* @param {Object} error - شیء خطا از axios یا catch
|
||||
* @returns {string} - پیام خطا
|
||||
*/
|
||||
export function getErrorMessage(error) {
|
||||
// اگر error خودش یک string باشد
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
// بررسی response.data برای پیامهای مختلف
|
||||
const responseData = error?.response?.data || error?.data;
|
||||
|
||||
if (responseData) {
|
||||
// بررسی فیلدهای مختلف که ممکن است پیام خطا در آنها باشد
|
||||
if (responseData.message) return responseData.message;
|
||||
if (responseData.Message) return responseData.Message;
|
||||
if (responseData.error) return responseData.error;
|
||||
if (responseData.Error) return responseData.Error;
|
||||
if (responseData.errorMessage) return responseData.errorMessage;
|
||||
if (responseData.error_message) return responseData.error_message;
|
||||
|
||||
// اگر errors یک آرایه باشد
|
||||
if (Array.isArray(responseData.errors) && responseData.errors.length > 0) {
|
||||
return responseData.errors[0]?.message || responseData.errors[0] || String(responseData.errors[0]);
|
||||
}
|
||||
|
||||
// اگر errors یک object باشد
|
||||
if (responseData.errors && typeof responseData.errors === 'object') {
|
||||
const firstError = Object.values(responseData.errors)[0];
|
||||
if (Array.isArray(firstError) && firstError.length > 0) {
|
||||
return firstError[0];
|
||||
}
|
||||
if (typeof firstError === 'string') {
|
||||
return firstError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// بررسی error.message
|
||||
if (error?.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
// پیام پیشفرض بر اساس status code
|
||||
const status = error?.response?.status || error?.status;
|
||||
if (status) {
|
||||
const statusMessages = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
409: 'Conflict',
|
||||
422: 'Validation Error',
|
||||
500: 'Internal Server Error',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
};
|
||||
if (statusMessages[status]) {
|
||||
return statusMessages[status];
|
||||
}
|
||||
}
|
||||
|
||||
// پیام پیشفرض
|
||||
return 'An error occurred. Please try again.';
|
||||
}
|
||||
|
||||
/**
|
||||
* استخراج پیام موفقیت از response سرور
|
||||
*
|
||||
* @param {Object} response - response از axios
|
||||
* @returns {string} - پیام موفقیت
|
||||
*/
|
||||
export function getSuccessMessage(response) {
|
||||
const data = response?.data || response;
|
||||
|
||||
if (data?.message) return data.message;
|
||||
if (data?.Message) return data.Message;
|
||||
if (data?.successMessage) return data.successMessage;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user