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() {
|
function BillingManager() {
|
||||||
const [billingData, setBillingData] = useState([]);
|
const [billingData, setBillingData] = useState([]);
|
||||||
|
const [servers, setServers] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
@@ -106,12 +107,14 @@ function BillingManager() {
|
|||||||
lastPaymentAmount: 0,
|
lastPaymentAmount: 0,
|
||||||
lastPaymentCurrency: 'USD',
|
lastPaymentCurrency: 'USD',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
notes: ''
|
notes: '',
|
||||||
|
serverId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBillingData();
|
fetchBillingData();
|
||||||
fetchExchangeRates();
|
fetchExchangeRates();
|
||||||
|
fetchServers();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchExchangeRates = async () => {
|
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 () => {
|
const fetchBillingData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -191,7 +204,8 @@ function BillingManager() {
|
|||||||
lastPaymentAmount: 0,
|
lastPaymentAmount: 0,
|
||||||
lastPaymentCurrency: 'USD',
|
lastPaymentCurrency: 'USD',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
notes: ''
|
notes: '',
|
||||||
|
serverId: '',
|
||||||
});
|
});
|
||||||
setShowAddModal(true);
|
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) => {
|
const totalLastPayments = billingData.reduce((sum, item) => {
|
||||||
return sum + (item.lastPaymentAmount || 0);
|
return sum + (item.lastPaymentAmount || 0);
|
||||||
@@ -713,6 +734,7 @@ function BillingManager() {
|
|||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
|
<th>Связанный сервер</th>
|
||||||
<th className="cursor-pointer" onClick={() => handleSort('provider')}>
|
<th className="cursor-pointer" onClick={() => handleSort('provider')}>
|
||||||
Провайдер
|
Провайдер
|
||||||
{sortField === 'provider' && (
|
{sortField === 'provider' && (
|
||||||
@@ -773,6 +795,29 @@ function BillingManager() {
|
|||||||
<span>{item.country}</span>
|
<span>{item.country}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
{item.loginUrl && (
|
{item.loginUrl && (
|
||||||
@@ -1372,6 +1417,7 @@ function BillingManager() {
|
|||||||
<AddBillingForm
|
<AddBillingForm
|
||||||
item={newItem}
|
item={newItem}
|
||||||
onItemChange={setNewItem}
|
onItemChange={setNewItem}
|
||||||
|
servers={servers}
|
||||||
/>
|
/>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|
||||||
@@ -1390,6 +1436,7 @@ function BillingManager() {
|
|||||||
<EditBillingForm
|
<EditBillingForm
|
||||||
item={editingItem}
|
item={editingItem}
|
||||||
onItemChange={setEditingItem}
|
onItemChange={setEditingItem}
|
||||||
|
servers={servers}
|
||||||
/>
|
/>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|
||||||
@@ -1528,7 +1575,7 @@ function BillingManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Компоненты форм для модальных окон
|
// Компоненты форм для модальных окон
|
||||||
function AddBillingForm({ item, onItemChange }) {
|
function AddBillingForm({ item, onItemChange, servers }) {
|
||||||
const purposeOptions = [
|
const purposeOptions = [
|
||||||
{ value: '', label: 'Выберите назначение' },
|
{ value: '', label: 'Выберите назначение' },
|
||||||
{ value: 'relay', label: 'Relay VPS' },
|
{ value: 'relay', label: 'Relay VPS' },
|
||||||
@@ -1551,6 +1598,14 @@ function AddBillingForm({ item, onItemChange }) {
|
|||||||
{ value: 'RUB', label: 'RUB' }
|
{ 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 (
|
return (
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
{/* Основная информация */}
|
{/* Основная информация */}
|
||||||
@@ -1566,6 +1621,17 @@ function AddBillingForm({ item, onItemChange }) {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="col-md-6">
|
||||||
<FormField
|
<FormField
|
||||||
label="Назначение"
|
label="Назначение"
|
||||||
@@ -1662,7 +1728,7 @@ function AddBillingForm({ item, onItemChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditBillingForm({ item, onItemChange }) {
|
function EditBillingForm({ item, onItemChange, servers }) {
|
||||||
const purposeOptions = [
|
const purposeOptions = [
|
||||||
{ value: '', label: 'Выберите назначение' },
|
{ value: '', label: 'Выберите назначение' },
|
||||||
{ value: 'relay', label: 'Relay VPS' },
|
{ value: 'relay', label: 'Relay VPS' },
|
||||||
@@ -1685,6 +1751,14 @@ function EditBillingForm({ item, onItemChange }) {
|
|||||||
{ value: 'RUB', label: 'RUB' }
|
{ 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 (
|
return (
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
{/* Основная информация */}
|
{/* Основная информация */}
|
||||||
@@ -1700,6 +1774,17 @@ function EditBillingForm({ item, onItemChange }) {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="col-md-6">
|
||||||
<FormField
|
<FormField
|
||||||
label="Назначение"
|
label="Назначение"
|
||||||
|
|||||||
Reference in New Issue
Block a user