feat: Add server selection functionality to BillingManager, including fetching server data and integrating server options in add/edit forms for improved billing management.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
@@ -36,6 +36,7 @@ import BulkActionsBar from './components/BulkActionsBar.jsx';
|
||||
|
||||
function BillingManager() {
|
||||
const [billingData, setBillingData] = useState([]);
|
||||
const [servers, setServers] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
@@ -106,12 +107,14 @@ function BillingManager() {
|
||||
lastPaymentAmount: 0,
|
||||
lastPaymentCurrency: 'USD',
|
||||
status: 'active',
|
||||
notes: ''
|
||||
notes: '',
|
||||
serverId: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchBillingData();
|
||||
fetchExchangeRates();
|
||||
fetchServers();
|
||||
}, []);
|
||||
|
||||
const fetchExchangeRates = async () => {
|
||||
@@ -137,6 +140,16 @@ function BillingManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchServers = async () => {
|
||||
try {
|
||||
const res = await api.get(`/servers`);
|
||||
setServers(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при загрузке серверов для привязки биллинга:', error);
|
||||
// связь необязательная, поэтому не показываем отдельную ошибку пользователю
|
||||
}
|
||||
};
|
||||
|
||||
const fetchBillingData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -191,7 +204,8 @@ function BillingManager() {
|
||||
lastPaymentAmount: 0,
|
||||
lastPaymentCurrency: 'USD',
|
||||
status: 'active',
|
||||
notes: ''
|
||||
notes: '',
|
||||
serverId: '',
|
||||
});
|
||||
setShowAddModal(true);
|
||||
};
|
||||
@@ -251,6 +265,13 @@ function BillingManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// Карта серверов по id для быстрой привязки
|
||||
const serversById = new Map(
|
||||
(servers || [])
|
||||
.filter(s => s && s.id)
|
||||
.map(s => [s.id, s])
|
||||
);
|
||||
|
||||
// Вычисляем общую сумму последних платежей
|
||||
const totalLastPayments = billingData.reduce((sum, item) => {
|
||||
return sum + (item.lastPaymentAmount || 0);
|
||||
@@ -713,6 +734,7 @@ function BillingManager() {
|
||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||
)}
|
||||
</th>
|
||||
<th>Связанный сервер</th>
|
||||
<th className="cursor-pointer" onClick={() => handleSort('provider')}>
|
||||
Провайдер
|
||||
{sortField === 'provider' && (
|
||||
@@ -773,6 +795,29 @@ function BillingManager() {
|
||||
<span>{item.country}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{item.serverId ? (
|
||||
(() => {
|
||||
const srv = serversById.get(item.serverId);
|
||||
if (!srv) {
|
||||
return <span className="text-warning small">Сервер не найден</span>;
|
||||
}
|
||||
return (
|
||||
<div className="small">
|
||||
<div className="fw-bold">{srv.ip}</div>
|
||||
<div className="text-muted">
|
||||
{srv.dns || srv.provider || 'Без DNS'}
|
||||
</div>
|
||||
<a href="/servers" className="small text-blue text-decoration-underline">
|
||||
Открыть в разделе «Серверы»
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<span className="text-muted small">Не привязан</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{item.loginUrl && (
|
||||
@@ -1372,6 +1417,7 @@ function BillingManager() {
|
||||
<AddBillingForm
|
||||
item={newItem}
|
||||
onItemChange={setNewItem}
|
||||
servers={servers}
|
||||
/>
|
||||
</FormModal>
|
||||
|
||||
@@ -1390,6 +1436,7 @@ function BillingManager() {
|
||||
<EditBillingForm
|
||||
item={editingItem}
|
||||
onItemChange={setEditingItem}
|
||||
servers={servers}
|
||||
/>
|
||||
</FormModal>
|
||||
|
||||
@@ -1528,7 +1575,7 @@ function BillingManager() {
|
||||
}
|
||||
|
||||
// Компоненты форм для модальных окон
|
||||
function AddBillingForm({ item, onItemChange }) {
|
||||
function AddBillingForm({ item, onItemChange, servers }) {
|
||||
const purposeOptions = [
|
||||
{ value: '', label: 'Выберите назначение' },
|
||||
{ value: 'relay', label: 'Relay VPS' },
|
||||
@@ -1551,6 +1598,14 @@ function AddBillingForm({ item, onItemChange }) {
|
||||
{ value: 'RUB', label: 'RUB' }
|
||||
];
|
||||
|
||||
const serverOptions = [
|
||||
{ value: '', label: 'Без привязки' },
|
||||
...(Array.isArray(servers) ? servers : []).map((s) => ({
|
||||
value: s.id || s.ip,
|
||||
label: `${s.ip}${s.dns ? ` (${s.dns})` : ''}${s.provider ? ` – ${s.provider}` : ''}`,
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="row g-3">
|
||||
{/* Основная информация */}
|
||||
@@ -1566,6 +1621,17 @@ function AddBillingForm({ item, onItemChange }) {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<FormField
|
||||
label="Связанный сервер (опционально)"
|
||||
name="serverId"
|
||||
type="select"
|
||||
value={item.serverId || ''}
|
||||
onChange={(value) => onItemChange({ ...item, serverId: value })}
|
||||
options={serverOptions}
|
||||
helpText="Используется для связи с разделом «Серверы»"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<FormField
|
||||
label="Назначение"
|
||||
@@ -1662,7 +1728,7 @@ function AddBillingForm({ item, onItemChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
function EditBillingForm({ item, onItemChange }) {
|
||||
function EditBillingForm({ item, onItemChange, servers }) {
|
||||
const purposeOptions = [
|
||||
{ value: '', label: 'Выберите назначение' },
|
||||
{ value: 'relay', label: 'Relay VPS' },
|
||||
@@ -1685,6 +1751,14 @@ function EditBillingForm({ item, onItemChange }) {
|
||||
{ value: 'RUB', label: 'RUB' }
|
||||
];
|
||||
|
||||
const serverOptions = [
|
||||
{ value: '', label: 'Без привязки' },
|
||||
...(Array.isArray(servers) ? servers : []).map((s) => ({
|
||||
value: s.id || s.ip,
|
||||
label: `${s.ip}${s.dns ? ` (${s.dns})` : ''}${s.provider ? ` – ${s.provider}` : ''}`,
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="row g-3">
|
||||
{/* Основная информация */}
|
||||
@@ -1700,6 +1774,17 @@ function EditBillingForm({ item, onItemChange }) {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<FormField
|
||||
label="Связанный сервер (опционально)"
|
||||
name="serverId"
|
||||
type="select"
|
||||
value={item.serverId || ''}
|
||||
onChange={(value) => onItemChange({ ...item, serverId: value })}
|
||||
options={serverOptions}
|
||||
helpText="Используется для связи с разделом «Серверы»"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<FormField
|
||||
label="Назначение"
|
||||
|
||||
Reference in New Issue
Block a user