diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index f9fb7ae..b5510cd 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -1,5 +1,13 @@ import { useState, useEffect, useRef } from 'react'; import api from './lib/api.js'; +import axios from 'axios'; +import FormModal from './components/FormModal.jsx'; +import ConfirmModal from './components/ConfirmModal.jsx'; +import Pagination from './components/Pagination.jsx'; +import PageHeader from './components/PageHeader.jsx'; +import PageHeaderActions from './components/PageHeaderActions.jsx'; +import FormField from './components/FormField.jsx'; +import ErrorAlert from './components/ErrorAlert.jsx'; import { IconPlus, IconEdit, @@ -21,8 +29,6 @@ import { IconHistory } from '@tabler/icons-react'; -const API_URL = '/api'; - function BillingManager() { const [billingData, setBillingData] = useState([]); const [loading, setLoading] = useState(false); @@ -468,59 +474,51 @@ function BillingManager() { return (
{/* Заголовок страницы */} -
-
-
-

Инфра-биллинг

-
Панель управления / Ноды / Инфра-биллинг
-
-
-
- - - - -
-
+ + } + /> + + {/* Дополнительные действия */} +
+
+ +
{/* Уведомления */} {error && ( -
- - {error} - -
+ setError('')} + onRetry={fetchBillingData} + className="mb-4" + /> )} {success && ( -
+
{success} @@ -850,49 +848,14 @@ function BillingManager() {
- - {/* Пагинация */} - {totalPages > 1 && ( -
-
- Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredData.length)} из {filteredData.length} -
-
    -
  • - -
  • -
  • - -
  • - {(() => { - const pages = []; - let start = Math.max(1, currentPage - 2); - let end = Math.min(totalPages, currentPage + 2); - if (currentPage <= 3) end = Math.min(totalPages, 5); - if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4); - if (start > 1) pages.push('start-ellipsis'); - for (let p = start; p <= end; p++) pages.push(p); - if (end < totalPages) pages.push('end-ellipsis'); - return pages.map((p) => ( - p === 'start-ellipsis' || p === 'end-ellipsis' ? ( -
  • - ) : ( -
  • - -
  • - ) - )); - })()} -
  • - -
  • -
  • - -
  • -
-
- )}
+
{/* История платежей */} @@ -1034,103 +997,135 @@ function BillingManager() {
{/* Модальные окна */} - { + e.preventDefault(); + handleAddSubmit(); + }} onClose={() => setShowAddModal(false)} - /> + submitLabel="Добавить" + submitIcon={IconPlus} + size="lg" + > + + - { + e.preventDefault(); + handleEditSubmit(); + }} onClose={() => setShowEditModal(false)} - /> + submitLabel="Сохранить" + submitIcon={IconCheck} + size="lg" + > + + - setShowDeleteModal(false)} - /> - {/* Модалка добавления платежа */} - {showPaymentModal && ( - setShowPaymentModal(false)} - onSubmit={() => { - if (!paymentDraft.serverId || !paymentDraft.date || !paymentDraft.amount) { - setError('Заполните сервер, дату и сумму.'); - setTimeout(() => setError(''), 3000); - return; + title="Подтверждение удаления" + message={ + <> +

Вы уверены, что хотите удалить элемент {selectedItem?.hostName}?

+

Это действие нельзя отменить.

+ } - // Режим редактирования платежа - if (editingPayment && editingPayment.serverId) { - setBillingData(prev => prev.map(s => { - if (s.id !== editingPayment.serverId) return s; - const payments = Array.isArray(s.payments) ? [...s.payments] : []; - if (editingPayment.index != null && payments[editingPayment.index]) { - payments[editingPayment.index] = { - date: paymentDraft.date, - amount: paymentDraft.amount, - currency: paymentDraft.currency || 'USD', - note: paymentDraft.note || '' + onConfirm={confirmDelete} + onClose={() => setShowDeleteModal(false)} + confirmLabel="Удалить" + variant="danger" + /> + {/* Модалка добавления платежа */} + { + e.preventDefault(); + if (!paymentDraft.serverId || !paymentDraft.date || !paymentDraft.amount) { + setError('Заполните сервер, дату и сумму.'); + setTimeout(() => setError(''), 3000); + return; + } + // Режим редактирования платежа + if (editingPayment && editingPayment.serverId) { + setBillingData(prev => prev.map(s => { + if (s.id !== editingPayment.serverId) return s; + const payments = Array.isArray(s.payments) ? [...s.payments] : []; + if (editingPayment.index != null && payments[editingPayment.index]) { + payments[editingPayment.index] = { + date: paymentDraft.date, + amount: paymentDraft.amount, + currency: paymentDraft.currency || 'USD', + note: paymentDraft.note || '' + }; + } + // обновляем lastPayment* по последней дате + const last = payments + .slice() + .sort((a,b) => new Date(b.date) - new Date(a.date))[0]; + return { + ...s, + payments, + lastPaymentDate: last?.date || '', + lastPaymentAmount: last?.amount || 0, + lastPaymentCurrency: last?.currency || s.monthlyCostCurrency || 'USD' }; - } - // обновляем lastPayment* по последней дате + })); + setEditingPayment(null); + setShowPaymentModal(false); + setSuccess('Платеж обновлен'); + setTimeout(() => setSuccess(''), 3000); + return; + } + // Режим добавления платежа + setBillingData(prev => prev.map(s => { + if (s.id !== paymentDraft.serverId) return s; + const payments = Array.isArray(s.payments) ? [...s.payments] : []; + payments.push({ date: paymentDraft.date, amount: paymentDraft.amount, currency: paymentDraft.currency || 'USD', note: paymentDraft.note || '' }); const last = payments .slice() .sort((a,b) => new Date(b.date) - new Date(a.date))[0]; - return { - ...s, - payments, - lastPaymentDate: last?.date || '', - lastPaymentAmount: last?.amount || 0, - lastPaymentCurrency: last?.currency || s.monthlyCostCurrency || 'USD' - }; + return { ...s, payments, lastPaymentDate: last?.date || '', lastPaymentAmount: last?.amount || 0, lastPaymentCurrency: last?.currency || s.monthlyCostCurrency || 'USD' }; })); - setEditingPayment(null); setShowPaymentModal(false); - setSuccess('Платеж обновлен'); + setSuccess('Платеж добавлен'); setTimeout(() => setSuccess(''), 3000); - return; - } - // Режим добавления платежа - setBillingData(prev => prev.map(s => { - if (s.id !== paymentDraft.serverId) return s; - const payments = Array.isArray(s.payments) ? [...s.payments] : []; - payments.push({ date: paymentDraft.date, amount: paymentDraft.amount, currency: paymentDraft.currency || 'USD', note: paymentDraft.note || '' }); - const last = payments - .slice() - .sort((a,b) => new Date(b.date) - new Date(a.date))[0]; - return { ...s, payments, lastPaymentDate: last?.date || '', lastPaymentAmount: last?.amount || 0, lastPaymentCurrency: last?.currency || s.monthlyCostCurrency || 'USD' }; - })); - setShowPaymentModal(false); - setSuccess('Платеж добавлен'); - setTimeout(() => setSuccess(''), 3000); - }} - /> - )} + }} + onClose={() => { + setShowPaymentModal(false); + setEditingPayment(null); + }} + submitLabel={editingPayment ? "Сохранить" : "Добавить"} + submitIcon={editingPayment ? IconCheck : IconPlus} + > + + - {/* Модалка удаления платежа */} - {showDeletePaymentModal && ( -
-
-
-
-
Удалить платеж
- -
-
+ {/* Модалка удаления платежа */} +

Вы уверены, что хотите удалить выбранный платеж?

{paymentToDelete && ( -
+
Сервер: {paymentToDelete.data?.server?.hostName}
Дата: {formatDate(paymentToDelete.data?.date)}
Сумма: {formatCurrency(paymentToDelete.data?.amount, paymentToDelete.data?.currency)}
@@ -1139,406 +1134,350 @@ function BillingManager() { )}
)} -
-
- - -
-
-
-
- )} + + } + onConfirm={handleDeletePayment} + onClose={() => setShowDeletePaymentModal(false)} + confirmLabel="Удалить" + variant="danger" + />
); } -// Компоненты модальных окон -function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) { - if (!show) return null; +// Компоненты форм для модальных окон +function AddBillingForm({ item, onItemChange }) { + const purposeOptions = [ + { value: '', label: 'Выберите назначение' }, + { value: 'relay', label: 'Relay VPS' }, + { value: 'bgp', label: 'BGP сервер' }, + { value: 'monitoring', label: 'Мониторинг' }, + { value: 'dns', label: 'DNS сервер' }, + { value: 'proxy', label: 'Прокси сервер' }, + { value: 'backup', label: 'Backup сервер' }, + { value: 'other', label: 'Другое' } + ]; - const modalTitle = 'Добавить новый элемент'; + const statusOptions = [ + { value: 'active', label: 'Активен' }, + { value: 'inactive', label: 'Неактивен' } + ]; + + const currencyOptions = [ + { value: 'USD', label: 'USD' }, + { value: 'EUR', label: 'EUR' }, + { value: 'RUB', label: 'RUB' } + ]; return ( -
-
-
-
-
{modalTitle}
- -
-
-
- {/* Основная информация */} -
-
Основная информация
-
-
- - onItemChange({ ...item, hostName: e.target.value })} - required - /> -
-
- - -
-
- - onItemChange({ ...item, country: e.target.value })} - required - /> -
-
- - onItemChange({ ...item, provider: e.target.value })} - required - /> -
-
- - onItemChange({ ...item, loginUrl: e.target.value })} - placeholder="example.com" - /> -
-
- - -
+
+ {/* Основная информация */} +
+
Основная информация
+
+
+ onItemChange({ ...item, hostName: value })} + required + /> +
+
+ onItemChange({ ...item, purpose: value })} + options={purposeOptions} + /> +
+
+ onItemChange({ ...item, country: value })} + required + /> +
+
+ onItemChange({ ...item, provider: value })} + required + /> +
+
+ onItemChange({ ...item, loginUrl: value })} + placeholder="example.com" + /> +
+
+ onItemChange({ ...item, status: value })} + options={statusOptions} + /> +
- {/* Текущие платежи */} -
-
Текущие платежи
-
-
- - onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })} - /> -
-
- - -
-
- - onItemChange({ ...item, nextPaymentDate: e.target.value })} - /> -
+ {/* Текущие платежи */} +
+
Текущие платежи
+
+
+ onItemChange({ ...item, monthlyCost: parseFloat(value) || 0 })} + step="0.01" + /> +
+
+ onItemChange({ ...item, monthlyCostCurrency: value })} + options={currencyOptions} + /> +
+
+ onItemChange({ ...item, nextPaymentDate: value })} + /> +
- {/* Дополнительно */} -
- - onItemChange({ ...item, notes: e.target.value })} - placeholder="Дополнительная информация" - /> -
-
-
-
- - -
-
+ {/* Дополнительно */} +
+ onItemChange({ ...item, notes: value })} + placeholder="Дополнительная информация" + />
); } -function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) { - if (!show) return null; +function EditBillingForm({ item, onItemChange }) { + const purposeOptions = [ + { value: '', label: 'Выберите назначение' }, + { value: 'relay', label: 'Relay VPS' }, + { value: 'bgp', label: 'BGP сервер' }, + { value: 'monitoring', label: 'Мониторинг' }, + { value: 'dns', label: 'DNS сервер' }, + { value: 'proxy', label: 'Прокси сервер' }, + { value: 'backup', label: 'Backup сервер' }, + { value: 'other', label: 'Другое' } + ]; + + const statusOptions = [ + { value: 'active', label: 'Активен' }, + { value: 'inactive', label: 'Неактивен' } + ]; + + const currencyOptions = [ + { value: 'USD', label: 'USD' }, + { value: 'EUR', label: 'EUR' }, + { value: 'RUB', label: 'RUB' } + ]; return ( -
-
-
-
-
Редактировать элемент
- -
-
-
- {/* Основная информация */} -
-
Основная информация
-
-
- - onItemChange({ ...item, hostName: e.target.value })} - required - /> -
-
- - -
-
- - onItemChange({ ...item, country: e.target.value })} - required - /> -
-
- - onItemChange({ ...item, provider: e.target.value })} - required - /> -
-
- - onItemChange({ ...item, loginUrl: e.target.value })} - placeholder="example.com" - /> -
-
- - -
+
+ {/* Основная информация */} +
+
Основная информация
+
+
+ onItemChange({ ...item, hostName: value })} + required + /> +
+
+ onItemChange({ ...item, purpose: value })} + options={purposeOptions} + /> +
+
+ onItemChange({ ...item, country: value })} + required + /> +
+
+ onItemChange({ ...item, provider: value })} + required + /> +
+
+ onItemChange({ ...item, loginUrl: value })} + placeholder="example.com" + /> +
+
+ onItemChange({ ...item, status: value })} + options={statusOptions} + /> +
- {/* Текущие платежи */} -
-
Текущие платежи
-
-
- - onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })} - /> -
-
- - -
-
- - onItemChange({ ...item, nextPaymentDate: e.target.value })} - /> -
+ {/* Текущие платежи */} +
+
Текущие платежи
+
+
+ onItemChange({ ...item, monthlyCost: parseFloat(value) || 0 })} + step="0.01" + /> +
+
+ onItemChange({ ...item, monthlyCostCurrency: value })} + options={currencyOptions} + /> +
+
+ onItemChange({ ...item, nextPaymentDate: value })} + /> +
- {/* Дополнительно */} -
- - onItemChange({ ...item, notes: e.target.value })} - placeholder="Дополнительная информация" - /> -
-
-
-
- - -
-
+ {/* Дополнительно */} +
+ onItemChange({ ...item, notes: value })} + placeholder="Дополнительная информация" + />
); } -function DeleteBillingModal({ show, item, onDelete, onClose }) { - if (!show) return null; +// Форма добавления платежа +function AddPaymentForm({ servers, payment, onChange }) { + const serverOptions = [ + { value: '', label: 'Выберите сервер' }, + ...servers.map(s => ({ value: s.id, label: `${s.hostName} (${s.provider})` })) + ]; + + const currencyOptions = [ + { value: 'USD', label: 'USD' }, + { value: 'EUR', label: 'EUR' }, + { value: 'RUB', label: 'RUB' } + ]; return ( -
-
-
-
-
Подтверждение удаления
- -
-
-

Вы уверены, что хотите удалить элемент {item?.hostName}?

-

Это действие нельзя отменить.

-
-
- - -
+ <> + onChange({ ...payment, serverId: value })} + options={serverOptions} + required + /> + onChange({ ...payment, date: value })} + required + /> +
+
+ onChange({ ...payment, amount: parseFloat(value) || 0 })} + step="0.01" + required + /> +
+
+ onChange({ ...payment, currency: value })} + options={currencyOptions} + />
-
+ onChange({ ...payment, note: value })} + placeholder="Необязательно" + /> + ); } -export default BillingManager; - -// Модалка добавления платежа -function AddPaymentModal({ show, servers, payment, onChange, onSubmit, onClose }) { - if (!show) return null; - const serverOptions = servers.map(s => ({ id: s.id, label: `${s.hostName} (${s.provider})` })); - return ( -
-
-
-
-
Добавить платеж
- -
-
-
- - -
-
- - onChange({ ...payment, date: e.target.value })} /> -
-
-
- - onChange({ ...payment, amount: parseFloat(e.target.value) || 0 })} /> -
-
- - -
-
-
- - onChange({ ...payment, note: e.target.value })} placeholder="Необязательно" /> -
-
-
- - -
-
-
-
- ); -} \ No newline at end of file +export default BillingManager; \ No newline at end of file