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

This commit is contained in:
2025-12-01 16:07:28 +07:00
parent bf4b1b1554
commit 4655fc67cc
+89 -4
View File
@@ -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="Назначение"