From 08a64b5be4ca9bf5e326fcef66a5c030e49872e2 Mon Sep 17 00:00:00 2001 From: shats Date: Mon, 16 Feb 2026 21:28:58 +0700 Subject: [PATCH] refactor(BillingManager): add tab navigation for subscriptions and payments, enhancing user experience and data visibility --- frontend/src/BillingManager.jsx | 580 +++++++++++++++++--------------- 1 file changed, 306 insertions(+), 274 deletions(-) diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index f386f82..2a9cbdf 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -54,6 +54,7 @@ function BillingManager() { const [filterStatus, setFilterStatus] = useState(''); const [filterUrgency, setFilterUrgency] = useState(''); const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table' + const [activeTab, setActiveTab] = useState('subscriptions'); // 'subscriptions' | 'payments' const pageSize = 15; // Модальные окна @@ -298,6 +299,10 @@ function BillingManager() { return sum + convertToRUB(item.monthlyCost || 0, item.monthlyCostCurrency || 'USD'); }, 0); + const totalPaymentsCount = billingData.reduce((sum, item) => { + return sum + ((item.payments && Array.isArray(item.payments) ? item.payments.length : 0)); + }, 0); + const urgentPayments = billingData.filter(item => { const nextPayment = new Date(item.nextPaymentDate); const now = new Date(); @@ -578,15 +583,39 @@ function BillingManager() { title="Биллинг" icon={} actions={ - { fetchBillingData(); fetchServers(); }} - disableRefresh={loading} - onExport={exportData} - disableExport={loading} - onSave={handleSaveChanges} - disableSave={loading} - /> +
+
+ + +
+ { fetchBillingData(); fetchServers(); }} + disableRefresh={loading} + onExport={exportData} + disableExport={loading} + onSave={handleSaveChanges} + disableSave={loading} + /> +
} /> @@ -656,279 +685,282 @@ function BillingManager() { - {/* Основная секция */} -
-
-
-

- Подписки - {filteredData.length} -

- - {/* Поиск */} -
- - - - { setSearchTerm(e.target.value); setCurrentPage(1); }} - /> -
- - {/* Быстрые фильтры */} -
- - - -
- - {/* Переключатель вида */} -
- - -
-
- -
- -
-
- - {/* Контент: карточки или таблица */} - {paginatedData.length === 0 ? ( -
- -

{searchTerm || filterUrgency ? 'Ничего не найдено' : 'Нет подписок'}

-

Добавьте первую подписку для отслеживания платежей

- -
- ) : viewMode === 'cards' ? ( - /* Карточный вид */ -
- {paginatedData.map(item => { - const days = getDaysUntilPayment(item.nextPaymentDate); - const linkedServer = item.serverId ? serversById.get(item.serverId) : null; + {/* Основная секция: Подписки */} + {activeTab === 'subscriptions' && ( +
+
+
+

+ Подписки + {filteredData.length} +

- return ( - { - setPaymentDraft({ - serverId: item.id, - date: new Date().toISOString().slice(0,10), - amount: item.monthlyCost || 0, - currency: item.monthlyCostCurrency || 'USD', - note: '' - }); - setShowPaymentModal(true); - }} - formatCurrencyInRUB={formatCurrencyInRUB} + {/* Поиск */} +
+ + + + { setSearchTerm(e.target.value); setCurrentPage(1); }} /> - ); - })} +
+ + {/* Быстрые фильтры */} +
+ + + +
+ + {/* Переключатель вида */} +
+ + +
+
+ +
+ +
- ) : ( - /* Табличный вид */ -
- - - - - - - - - - - - - {paginatedData.map(item => { - const days = getDaysUntilPayment(item.nextPaymentDate); - const linkedServer = item.serverId ? serversById.get(item.serverId) : null; - - return ( - - + + + + + + ); + })} + +
handleSort('hostName')} - style={{ minWidth: '200px' }} - > - Сервис - {sortField === 'hostName' && ( - {sortOrder === 'asc' ? '↑' : '↓'} - )} - handleSort('monthlyCost')} - style={{ width: '140px' }} - > - Стоимость - {sortField === 'monthlyCost' && ( - {sortOrder === 'asc' ? '↑' : '↓'} - )} - handleSort('nextPaymentDate')} - style={{ width: '120px' }} - > - Платёж - {sortField === 'nextPaymentDate' && ( - {sortOrder === 'asc' ? '↑' : '↓'} - )} - СрокСтатус
-
-
-
- {item.hostName} - {item._external && ( - - не заполнен - - )} -
-
- {item.provider} - {linkedServer && ( - <> - - {linkedServer.ip} - - )} - {item.loginUrl && ( - - - - )} + + {/* Контент: карточки или таблица */} + {paginatedData.length === 0 ? ( +
+ +

{searchTerm || filterUrgency ? 'Ничего не найдено' : 'Нет подписок'}

+

Добавьте первую подписку для отслеживания платежей

+ +
+ ) : viewMode === 'cards' ? ( + /* Карточный вид */ +
+ {paginatedData.map(item => { + const days = getDaysUntilPayment(item.nextPaymentDate); + const linkedServer = item.serverId ? serversById.get(item.serverId) : null; + + return ( + { + setPaymentDraft({ + serverId: item.id, + date: new Date().toISOString().slice(0,10), + amount: item.monthlyCost || 0, + currency: item.monthlyCostCurrency || 'USD', + note: '' + }); + setShowPaymentModal(true); + }} + formatCurrencyInRUB={formatCurrencyInRUB} + /> + ); + })} +
+ ) : ( + /* Табличный вид */ +
+ + + + + + + + + + + + + {paginatedData.map(item => { + const days = getDaysUntilPayment(item.nextPaymentDate); + const linkedServer = item.serverId ? serversById.get(item.serverId) : null; + + return ( + + - + - - - - - - ); - })} - -
handleSort('hostName')} + style={{ minWidth: '200px' }} + > + Сервис + {sortField === 'hostName' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + handleSort('monthlyCost')} + style={{ width: '140px' }} + > + Стоимость + {sortField === 'monthlyCost' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + handleSort('nextPaymentDate')} + style={{ width: '120px' }} + > + Платёж + {sortField === 'nextPaymentDate' && ( + {sortOrder === 'asc' ? '↑' : '↓'} + )} + СрокСтатус
+
+
+
+ {item.hostName} + {item._external && ( + + не заполнен + + )} +
+
+ {item.provider} + {linkedServer && ( + <> + + {linkedServer.ip} + + )} + {item.loginUrl && ( + + + + )} +
- -
-
- {formatCurrency(item.monthlyCost, item.monthlyCostCurrency)} -
- {item.monthlyCostCurrency !== 'RUB' && ( -
- ≈ {formatCurrencyInRUB(item.monthlyCost, item.monthlyCostCurrency)} +
+
+ {formatCurrency(item.monthlyCost, item.monthlyCostCurrency)}
- )} -
- {formatDate(item.nextPaymentDate)} - - - - {item.status === 'active' ? ( - Активен - ) : ( - Неактивен - )} - -
- - - -
-
-
- )} - - {totalPages > 1 && ( - - )} -
+ {item.monthlyCostCurrency !== 'RUB' && ( +
+ ≈ {formatCurrencyInRUB(item.monthlyCost, item.monthlyCostCurrency)} +
+ )} +
+ {formatDate(item.nextPaymentDate)} + + + + {item.status === 'active' ? ( + Активен + ) : ( + Неактивен + )} + +
+ + + +
+
+
+ )} + + {totalPages > 1 && ( + + )} +
+ )} {/* История платежей — компактная версия */} + {activeTab === 'payments' && (