feat(country-province-city): add, edit functionality

This commit is contained in:
ghazall-ag
2025-11-17 00:40:53 +03:30
parent 77cc7534a0
commit 7cc442b600
20 changed files with 2008 additions and 280 deletions

View File

@@ -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>
}
/>

View File

@@ -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
View 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;

View File

@@ -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
View 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;

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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));
}
};

View File

@@ -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
View 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
View 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
View 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;
}
},
};

View 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 || [];
},
};

View 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;
},
};

View 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; },
};

View 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'}
];}
}

View 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
View 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
View 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
View 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;
}