feat(BillingManager, ServerManager): implement automatic synchronization of billing entries for new servers; enhance server management modals for improved error handling and user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m41s

This commit is contained in:
2026-01-20 17:31:41 +07:00
parent d173112bae
commit 86d4ecb7e2
5 changed files with 673 additions and 329 deletions
+58 -4
View File
@@ -117,6 +117,48 @@ function BillingManager() {
fetchServers();
}, []);
// Автоматическая синхронизация: добавляем серверы без записей в биллинге
useEffect(() => {
if (servers.length === 0 || loading) return;
setBillingData((prev) => {
const existingServerIds = new Set(
prev.map(b => b.serverId).filter(Boolean)
);
// Находим серверы без записей в биллинге
const serversToAdd = servers.filter(server =>
server.id && !existingServerIds.has(server.id)
);
if (serversToAdd.length === 0) return prev;
// Создаём записи с флагом _external
const newItems = serversToAdd.map(server => ({
id: `billing-${server.id}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
hostName: server.dns || server.ip,
purpose: '',
country: server.country || '',
provider: server.provider || '',
loginUrl: '',
monthlyCost: 0,
monthlyCostCurrency: 'USD',
nextPaymentDate: '',
lastPaymentDate: '',
lastPaymentAmount: 0,
lastPaymentCurrency: 'USD',
status: 'active',
notes: '',
serverId: server.id,
payments: [],
_external: true, // флаг "не заполнен"
}));
return [...prev, ...newItems];
});
}, [servers, loading]);
const fetchExchangeRates = async () => {
setRatesLoading(true);
try {
@@ -193,7 +235,11 @@ function BillingManager() {
const handleSaveChanges = async () => {
setLoading(true);
try {
await api.post(`/billing`, { domains: billingData });
// Убираем служебное поле _external перед сохранением
const payload = billingData.map(({ _external, ...rest }) => rest);
await api.post(`/billing`, { domains: payload });
// После сохранения снимаем флаг _external со всех записей
setBillingData(prev => prev.map(item => ({ ...item, _external: false })));
setSuccess('Данные успешно сохранены');
setTimeout(() => setSuccess(''), 3000);
} catch (err) {
@@ -393,8 +439,9 @@ function BillingManager() {
return;
}
// Снимаем флаг _external при редактировании (запись теперь заполнена)
setBillingData(prev => prev.map(item =>
item.id === editingItem.id ? editingItem : item
item.id === editingItem.id ? { ...editingItem, _external: false } : item
));
setShowEditModal(false);
setSuccess('Элемент обновлен');
@@ -494,7 +541,7 @@ function BillingManager() {
actions={
<PageHeaderActions
loading={loading}
onRefresh={fetchBillingData}
onRefresh={() => { fetchBillingData(); fetchServers(); }}
disableRefresh={loading}
onExport={exportData}
disableExport={loading}
@@ -734,7 +781,14 @@ function BillingManager() {
<td>
<div className="d-flex align-items-center">
<div>
<div className="fw-semibold">{item.hostName}</div>
<div className="fw-semibold">
{item.hostName}
{item._external && (
<span className="badge bg-yellow-lt text-yellow ms-2" title="Импортирован из серверов, требует заполнения">
не заполнен
</span>
)}
</div>
<div className="text-muted small d-flex align-items-center gap-2">
<span>{item.provider}</span>
{linkedServer && (