diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index e98ebee..fc44d9b 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -34,9 +34,12 @@ import { IconX, IconChecks, IconServer, - IconClock + IconClock, + IconLayoutGrid, + IconList } from '@tabler/icons-react'; import BulkActionsBar from './components/BulkActionsBar.jsx'; +import { BillingCard } from './components/billing/index.js'; function BillingManager() { const [billingData, setBillingData] = useState([]); @@ -50,6 +53,7 @@ function BillingManager() { const [sortOrder, setSortOrder] = useState('asc'); const [filterStatus, setFilterStatus] = useState(''); const [filterUrgency, setFilterUrgency] = useState(''); + const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table' const pageSize = 15; // Модальные окна @@ -566,7 +570,7 @@ function BillingManager() { - {/* Основная таблица */} + {/* Основная секция */}
@@ -610,6 +614,24 @@ function BillingManager() { Срочно
+ + {/* Переключатель вида */} +
+ + +
@@ -620,148 +642,187 @@ function BillingManager() {
-
- - - - - - - - - - - - - {paginatedData.length === 0 ? ( + {/* Контент: карточки или таблица */} + {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} + /> + ); + })} +
+ ) : ( + /* Табличный вид */ +
+
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' ? '↑' : '↓'} - )} - СрокСтатус
+ - + + + + + + - ) : paginatedData.map(item => { - const days = getDaysUntilPayment(item.nextPaymentDate); - const linkedServer = item.serverId ? serversById.get(item.serverId) : null; - - return ( - - + + {paginatedData.map(item => { + const days = getDaysUntilPayment(item.nextPaymentDate); + const linkedServer = item.serverId ? serversById.get(item.serverId) : null; + + return ( + + - + - - - - - - ); - })} - -
- {searchTerm || filterUrgency ? 'Ничего не найдено' : 'Нет данных'} - 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.provider} - {linkedServer && ( - <> - - {linkedServer.ip} - - )} - {item.loginUrl && ( - - - - )} +
+
+
+
{item.hostName}
+
+ {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' ? ( - Активен - ) : ( - Неактивен - )} - -
- - - -
-
-
+ {item.monthlyCostCurrency !== 'RUB' && ( +
+ ≈ {formatCurrencyInRUB(item.monthlyCost, item.monthlyCostCurrency)} +
+ )} + + + {formatDate(item.nextPaymentDate)} + + + + + + {item.status === 'active' ? ( + Активен + ) : ( + Неактивен + )} + + +
+ + + +
+ + + ); + })} + + + + )} {totalPages > 1 && ( = 0 && days <= 7; + const isActive = item.status === 'active'; + + // Определяем цвет левой панели + const getPanelColor = () => { + if (isOverdue) return 'bg-red-lt'; + if (isUrgent) return 'bg-yellow-lt'; + if (!isActive) return 'bg-secondary-lt'; + return 'bg-green-lt'; + }; + + const getPanelTextColor = () => { + if (isOverdue) return 'text-red'; + if (isUrgent) return 'text-yellow'; + if (!isActive) return 'text-secondary'; + return 'text-green'; + }; + + return ( +
+
+ {/* Основная строка */} +
+ {/* Левая часть: статус */} +
+
+
+ {isOverdue ? ( + + ) : isUrgent ? ( + + ) : isActive ? ( + + ) : ( + + )} +
+
+ {isOverdue ? 'ПРОСРОЧЕН' : isUrgent ? 'СРОЧНО' : isActive ? 'АКТИВЕН' : 'НЕАКТИВЕН'} +
+
+
+ + {/* Центральная часть: информация */} +
setExpanded(!expanded)}> + {/* Верхняя строка */} +
+
+
+ {item.hostName} + + {item.provider} +
+
+ + {/* Стоимость справа */} +
+ {formatCurrency(item.monthlyCost, item.monthlyCostCurrency)} + {item.monthlyCostCurrency !== 'RUB' && formatCurrencyInRUB && ( + + ≈ {formatCurrencyInRUB(item.monthlyCost, item.monthlyCostCurrency)} + + )} +
+
+ + {/* Нижняя строка */} +
+ {linkedServer && ( + + + {linkedServer.ip} + + )} + + + + {formatDate(item.nextPaymentDate)} + {days !== null && ( + + ({days < 0 ? `${Math.abs(days)} дн. назад` : days === 0 ? 'сегодня' : `через ${days} дн.`}) + + )} + + + {item.loginUrl && ( + e.stopPropagation()} + > + + Панель + + )} +
+
+ + {/* Правая часть: действия */} +
+ + + + +
+
+ + {/* Расширенная информация */} + {expanded && ( +
+
+ {/* Детали подписки */} +
+
+ + Детали подписки +
+
+
+
Провайдер
+
{item.provider || '—'}
+
+
+
Статус
+ + {isActive ? 'Активен' : 'Неактивен'} + +
+ {item.notes && ( +
+
Заметки
+
{item.notes}
+
+ )} +
+
+ + {/* История платежей */} +
+
+ + Последние платежи ({item.payments?.length || 0}) +
+ {item.payments?.length > 0 ? ( +
+ {item.payments.slice(-3).reverse().map((p, idx) => ( +
+ {formatDate(p.date)} + {formatCurrency(p.amount, p.currency)} +
+ ))} +
+ ) : ( + Нет платежей + )} +
+
+
+ )} +
+
+ ); +} diff --git a/frontend/src/components/billing/index.js b/frontend/src/components/billing/index.js new file mode 100644 index 0000000..6a0f813 --- /dev/null +++ b/frontend/src/components/billing/index.js @@ -0,0 +1 @@ +export { default as BillingCard } from './BillingCard.jsx';