feat: Refactor BillingManager component to enhance UI with reusable modals for adding, editing, and deleting billing items. Improved error handling and introduced pagination for better data management.
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 58s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 58s
This commit is contained in:
+308
-369
@@ -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 (
|
||||
<div>
|
||||
{/* Заголовок страницы */}
|
||||
<div className="page-header d-print-none mb-4">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title">Инфра-биллинг</h2>
|
||||
<div className="page-pretitle">Панель управления / Ноды / Инфра-биллинг</div>
|
||||
</div>
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<PageHeader
|
||||
title="Инфра-биллинг"
|
||||
pretitle="Панель управления / Ноды / Инфра-биллинг"
|
||||
actions={
|
||||
<PageHeaderActions
|
||||
loading={loading}
|
||||
onRefresh={fetchBillingData}
|
||||
disableRefresh={loading}
|
||||
onExport={exportData}
|
||||
disableExport={loading}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Дополнительные действия */}
|
||||
<div className="mb-4 d-print-none">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
>
|
||||
<IconFilter size={16} />
|
||||
<IconFilter size={16} className="me-1" />
|
||||
Фильтры
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={exportData}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconDownload size={16} />
|
||||
Экспорт
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={fetchBillingData}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconRefresh size={16} />
|
||||
Обновить
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleAddItem}
|
||||
>
|
||||
<IconPlus size={16} />
|
||||
<IconPlus size={16} className="me-1" />
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Уведомления */}
|
||||
{error && (
|
||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
||||
<IconAlertTriangle className="me-2" />
|
||||
{error}
|
||||
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
||||
</div>
|
||||
<ErrorAlert
|
||||
message={error}
|
||||
onClose={() => setError('')}
|
||||
onRetry={fetchBillingData}
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
{success && (
|
||||
<div className="alert alert-success alert-dismissible" role="alert">
|
||||
<div className="alert alert-success alert-dismissible mb-4" role="alert">
|
||||
<IconCheck className="me-2" />
|
||||
{success}
|
||||
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
||||
@@ -850,49 +848,14 @@ function BillingManager() {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Пагинация */}
|
||||
{totalPages > 1 && (
|
||||
<div className="d-flex align-items-center justify-content-between mt-3">
|
||||
<div className="text-muted">
|
||||
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredData.length)} из {filteredData.length}
|
||||
</div>
|
||||
<ul className="pagination m-0">
|
||||
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||
<button className="page-link" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>Первая</button>
|
||||
</li>
|
||||
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>Назад</button>
|
||||
</li>
|
||||
{(() => {
|
||||
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' ? (
|
||||
<li key={p} className="page-item disabled"><span className="page-link">…</span></li>
|
||||
) : (
|
||||
<li key={p} className={`page-item${currentPage === p ? ' active' : ''}`}>
|
||||
<button className="page-link" onClick={() => setCurrentPage(p)}>{p}</button>
|
||||
</li>
|
||||
)
|
||||
));
|
||||
})()}
|
||||
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>Вперед</button>
|
||||
</li>
|
||||
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||
<button className="page-link" onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages}>Последняя</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalItems={filteredData.length}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* История платежей */}
|
||||
@@ -1034,37 +997,62 @@ function BillingManager() {
|
||||
</div>
|
||||
|
||||
{/* Модальные окна */}
|
||||
<AddBillingModal
|
||||
<FormModal
|
||||
show={showAddModal}
|
||||
title="Добавить новый элемент"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleAddSubmit();
|
||||
}}
|
||||
onClose={() => setShowAddModal(false)}
|
||||
submitLabel="Добавить"
|
||||
submitIcon={IconPlus}
|
||||
size="lg"
|
||||
>
|
||||
<AddBillingForm
|
||||
item={newItem}
|
||||
onItemChange={setNewItem}
|
||||
onSubmit={handleAddSubmit}
|
||||
onClose={() => setShowAddModal(false)}
|
||||
/>
|
||||
</FormModal>
|
||||
|
||||
<EditBillingModal
|
||||
<FormModal
|
||||
show={showEditModal}
|
||||
title="Редактировать элемент"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleEditSubmit();
|
||||
}}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
submitLabel="Сохранить"
|
||||
submitIcon={IconCheck}
|
||||
size="lg"
|
||||
>
|
||||
<EditBillingForm
|
||||
item={editingItem}
|
||||
onItemChange={setEditingItem}
|
||||
onSubmit={handleEditSubmit}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
/>
|
||||
</FormModal>
|
||||
|
||||
<DeleteBillingModal
|
||||
<ConfirmModal
|
||||
show={showDeleteModal}
|
||||
item={selectedItem}
|
||||
onDelete={confirmDelete}
|
||||
title="Подтверждение удаления"
|
||||
message={
|
||||
<>
|
||||
<p>Вы уверены, что хотите удалить элемент <strong>{selectedItem?.hostName}</strong>?</p>
|
||||
<p className="text-muted">Это действие нельзя отменить.</p>
|
||||
</>
|
||||
}
|
||||
onConfirm={confirmDelete}
|
||||
onClose={() => setShowDeleteModal(false)}
|
||||
confirmLabel="Удалить"
|
||||
variant="danger"
|
||||
/>
|
||||
{/* Модалка добавления платежа */}
|
||||
{showPaymentModal && (
|
||||
<AddPaymentModal
|
||||
<FormModal
|
||||
show={showPaymentModal}
|
||||
servers={billingData}
|
||||
payment={paymentDraft}
|
||||
onChange={setPaymentDraft}
|
||||
onClose={() => setShowPaymentModal(false)}
|
||||
onSubmit={() => {
|
||||
title={editingPayment ? "Редактировать платеж" : "Добавить платеж"}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (!paymentDraft.serverId || !paymentDraft.date || !paymentDraft.amount) {
|
||||
setError('Заполните сервер, дату и сумму.');
|
||||
setTimeout(() => setError(''), 3000);
|
||||
@@ -1115,22 +1103,29 @@ function BillingManager() {
|
||||
setSuccess('Платеж добавлен');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowPaymentModal(false);
|
||||
setEditingPayment(null);
|
||||
}}
|
||||
submitLabel={editingPayment ? "Сохранить" : "Добавить"}
|
||||
submitIcon={editingPayment ? IconCheck : IconPlus}
|
||||
>
|
||||
<AddPaymentForm
|
||||
servers={billingData}
|
||||
payment={paymentDraft}
|
||||
onChange={setPaymentDraft}
|
||||
/>
|
||||
)}
|
||||
</FormModal>
|
||||
|
||||
{/* Модалка удаления платежа */}
|
||||
{showDeletePaymentModal && (
|
||||
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Удалить платеж</h5>
|
||||
<button type="button" className="btn-close" onClick={() => setShowDeletePaymentModal(false)}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<ConfirmModal
|
||||
show={showDeletePaymentModal}
|
||||
title="Удалить платеж"
|
||||
message={
|
||||
<>
|
||||
<p>Вы уверены, что хотите удалить выбранный платеж?</p>
|
||||
{paymentToDelete && (
|
||||
<div className="alert alert-warning">
|
||||
<div className="alert alert-warning mt-3">
|
||||
<div><strong>Сервер:</strong> {paymentToDelete.data?.server?.hostName}</div>
|
||||
<div><strong>Дата:</strong> {formatDate(paymentToDelete.data?.date)}</div>
|
||||
<div><strong>Сумма:</strong> {formatCurrency(paymentToDelete.data?.amount, paymentToDelete.data?.currency)}</div>
|
||||
@@ -1139,110 +1134,102 @@ function BillingManager() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setShowDeletePaymentModal(false)}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="button" className="btn btn-danger" onClick={handleDeletePayment}>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onConfirm={handleDeletePayment}
|
||||
onClose={() => setShowDeletePaymentModal(false)}
|
||||
confirmLabel="Удалить"
|
||||
variant="danger"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Компоненты модальных окон
|
||||
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 (
|
||||
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog modal-lg">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{modalTitle}</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="row g-3">
|
||||
{/* Основная информация */}
|
||||
<div className="col-12">
|
||||
<h6 className="text-muted mb-3">Основная информация</h6>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Имя хоста *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Имя хоста"
|
||||
name="hostName"
|
||||
value={item.hostName}
|
||||
onChange={(e) => onItemChange({ ...item, hostName: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, hostName: value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Назначение</label>
|
||||
<select
|
||||
className="form-select"
|
||||
<FormField
|
||||
label="Назначение"
|
||||
name="purpose"
|
||||
type="select"
|
||||
value={item.purpose}
|
||||
onChange={(e) => onItemChange({ ...item, purpose: e.target.value })}
|
||||
>
|
||||
<option value="">Выберите назначение</option>
|
||||
<option value="relay">Relay VPS</option>
|
||||
<option value="bgp">BGP сервер</option>
|
||||
<option value="monitoring">Мониторинг</option>
|
||||
<option value="dns">DNS сервер</option>
|
||||
<option value="proxy">Прокси сервер</option>
|
||||
<option value="backup">Backup сервер</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
onChange={(value) => onItemChange({ ...item, purpose: value })}
|
||||
options={purposeOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Страна *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Страна"
|
||||
name="country"
|
||||
value={item.country}
|
||||
onChange={(e) => onItemChange({ ...item, country: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, country: value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Провайдер *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Провайдер"
|
||||
name="provider"
|
||||
value={item.provider}
|
||||
onChange={(e) => onItemChange({ ...item, provider: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, provider: value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Ссылка для входа</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Ссылка для входа"
|
||||
name="loginUrl"
|
||||
value={item.loginUrl}
|
||||
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, loginUrl: value })}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Статус</label>
|
||||
<select
|
||||
className="form-select"
|
||||
<FormField
|
||||
label="Статус"
|
||||
name="status"
|
||||
type="select"
|
||||
value={item.status}
|
||||
onChange={(e) => onItemChange({ ...item, status: e.target.value })}
|
||||
>
|
||||
<option value="active">Активен</option>
|
||||
<option value="inactive">Неактивен</option>
|
||||
</select>
|
||||
onChange={(value) => onItemChange({ ...item, status: value })}
|
||||
options={statusOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Текущие платежи */}
|
||||
@@ -1250,148 +1237,133 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
<h6 className="text-muted mb-3 mt-4">Текущие платежи</h6>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Месячная стоимость</label>
|
||||
<input
|
||||
<FormField
|
||||
label="Месячная стоимость"
|
||||
name="monthlyCost"
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="form-control"
|
||||
value={item.monthlyCost}
|
||||
onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })}
|
||||
onChange={(value) => onItemChange({ ...item, monthlyCost: parseFloat(value) || 0 })}
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Валюта месячной стоимости</label>
|
||||
<select
|
||||
className="form-select"
|
||||
<FormField
|
||||
label="Валюта месячной стоимости"
|
||||
name="monthlyCostCurrency"
|
||||
type="select"
|
||||
value={item.monthlyCostCurrency}
|
||||
onChange={(e) => onItemChange({ ...item, monthlyCostCurrency: e.target.value })}
|
||||
>
|
||||
<option value="USD">USD</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="RUB">RUB</option>
|
||||
</select>
|
||||
onChange={(value) => onItemChange({ ...item, monthlyCostCurrency: value })}
|
||||
options={currencyOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Дата следующего платежа</label>
|
||||
<input
|
||||
<FormField
|
||||
label="Дата следующего платежа"
|
||||
name="nextPaymentDate"
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={item.nextPaymentDate}
|
||||
onChange={(e) => onItemChange({ ...item, nextPaymentDate: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, nextPaymentDate: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Дополнительно */}
|
||||
<div className="col-md-12">
|
||||
<label className="form-label">Заметки</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Заметки"
|
||||
name="notes"
|
||||
value={item.notes}
|
||||
onChange={(e) => onItemChange({ ...item, notes: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, notes: value })}
|
||||
placeholder="Дополнительная информация"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={onSubmit}>
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog modal-lg">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Редактировать элемент</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="row g-3">
|
||||
{/* Основная информация */}
|
||||
<div className="col-12">
|
||||
<h6 className="text-muted mb-3">Основная информация</h6>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Имя хоста *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Имя хоста"
|
||||
name="hostName"
|
||||
value={item.hostName || ''}
|
||||
onChange={(e) => onItemChange({ ...item, hostName: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, hostName: value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Назначение</label>
|
||||
<select
|
||||
className="form-select"
|
||||
<FormField
|
||||
label="Назначение"
|
||||
name="purpose"
|
||||
type="select"
|
||||
value={item.purpose || ''}
|
||||
onChange={(e) => onItemChange({ ...item, purpose: e.target.value })}
|
||||
>
|
||||
<option value="">Выберите назначение</option>
|
||||
<option value="relay">Relay VPS</option>
|
||||
<option value="bgp">BGP сервер</option>
|
||||
<option value="monitoring">Мониторинг</option>
|
||||
<option value="dns">DNS сервер</option>
|
||||
<option value="proxy">Прокси сервер</option>
|
||||
<option value="backup">Backup сервер</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
onChange={(value) => onItemChange({ ...item, purpose: value })}
|
||||
options={purposeOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Страна *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Страна"
|
||||
name="country"
|
||||
value={item.country || ''}
|
||||
onChange={(e) => onItemChange({ ...item, country: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, country: value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Провайдер *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Провайдер"
|
||||
name="provider"
|
||||
value={item.provider || ''}
|
||||
onChange={(e) => onItemChange({ ...item, provider: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, provider: value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Ссылка для входа</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Ссылка для входа"
|
||||
name="loginUrl"
|
||||
value={item.loginUrl || ''}
|
||||
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, loginUrl: value })}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Статус</label>
|
||||
<select
|
||||
className="form-select"
|
||||
<FormField
|
||||
label="Статус"
|
||||
name="status"
|
||||
type="select"
|
||||
value={item.status || 'active'}
|
||||
onChange={(e) => onItemChange({ ...item, status: e.target.value })}
|
||||
>
|
||||
<option value="active">Активен</option>
|
||||
<option value="inactive">Неактивен</option>
|
||||
</select>
|
||||
onChange={(value) => onItemChange({ ...item, status: value })}
|
||||
options={statusOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Текущие платежи */}
|
||||
@@ -1399,146 +1371,113 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
||||
<h6 className="text-muted mb-3 mt-4">Текущие платежи</h6>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Месячная стоимость</label>
|
||||
<input
|
||||
<FormField
|
||||
label="Месячная стоимость"
|
||||
name="monthlyCost"
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="form-control"
|
||||
value={item.monthlyCost || 0}
|
||||
onChange={(e) => onItemChange({ ...item, monthlyCost: parseFloat(e.target.value) || 0 })}
|
||||
onChange={(value) => onItemChange({ ...item, monthlyCost: parseFloat(value) || 0 })}
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Валюта месячной стоимости</label>
|
||||
<select
|
||||
className="form-select"
|
||||
<FormField
|
||||
label="Валюта месячной стоимости"
|
||||
name="monthlyCostCurrency"
|
||||
type="select"
|
||||
value={item.monthlyCostCurrency || 'USD'}
|
||||
onChange={(e) => onItemChange({ ...item, monthlyCostCurrency: e.target.value })}
|
||||
>
|
||||
<option value="USD">USD</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="RUB">RUB</option>
|
||||
</select>
|
||||
onChange={(value) => onItemChange({ ...item, monthlyCostCurrency: value })}
|
||||
options={currencyOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Дата следующего платежа</label>
|
||||
<input
|
||||
<FormField
|
||||
label="Дата следующего платежа"
|
||||
name="nextPaymentDate"
|
||||
type="date"
|
||||
className="form-control"
|
||||
value={item.nextPaymentDate || ''}
|
||||
onChange={(e) => onItemChange({ ...item, nextPaymentDate: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, nextPaymentDate: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Дополнительно */}
|
||||
<div className="col-md-12">
|
||||
<label className="form-label">Заметки</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
<FormField
|
||||
label="Заметки"
|
||||
name="notes"
|
||||
value={item.notes || ''}
|
||||
onChange={(e) => onItemChange({ ...item, notes: e.target.value })}
|
||||
onChange={(value) => onItemChange({ ...item, notes: value })}
|
||||
placeholder="Дополнительная информация"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" onClick={onSubmit}>
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Подтверждение удаления</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>Вы уверены, что хотите удалить элемент <strong>{item?.hostName}</strong>?</p>
|
||||
<p className="text-muted">Это действие нельзя отменить.</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
||||
Отмена
|
||||
</button>
|
||||
<button type="button" className="btn btn-danger" onClick={onDelete}>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
<FormField
|
||||
label="Сервер"
|
||||
name="serverId"
|
||||
type="select"
|
||||
value={payment.serverId}
|
||||
onChange={(value) => onChange({ ...payment, serverId: value })}
|
||||
options={serverOptions}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Дата"
|
||||
name="date"
|
||||
type="date"
|
||||
value={payment.date}
|
||||
onChange={(value) => onChange({ ...payment, date: value })}
|
||||
required
|
||||
/>
|
||||
<div className="row g-2">
|
||||
<div className="col-8">
|
||||
<FormField
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
type="number"
|
||||
value={payment.amount}
|
||||
onChange={(value) => onChange({ ...payment, amount: parseFloat(value) || 0 })}
|
||||
step="0.01"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<FormField
|
||||
label="Валюта"
|
||||
name="currency"
|
||||
type="select"
|
||||
value={payment.currency}
|
||||
onChange={(value) => onChange({ ...payment, currency: value })}
|
||||
options={currencyOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormField
|
||||
label="Комментарий"
|
||||
name="note"
|
||||
value={payment.note}
|
||||
onChange={(value) => 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 (
|
||||
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Добавить платеж</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Сервер *</label>
|
||||
<select className="form-select" value={payment.serverId} onChange={e => onChange({ ...payment, serverId: e.target.value })}>
|
||||
<option value="">Выберите сервер</option>
|
||||
{serverOptions.map(o => (
|
||||
<option key={o.id} value={o.id}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Дата *</label>
|
||||
<input type="date" className="form-control" value={payment.date} onChange={e => onChange({ ...payment, date: e.target.value })} />
|
||||
</div>
|
||||
<div className="row g-2">
|
||||
<div className="col-8">
|
||||
<label className="form-label">Сумма *</label>
|
||||
<input type="number" step="0.01" className="form-control" value={payment.amount} onChange={e => onChange({ ...payment, amount: parseFloat(e.target.value) || 0 })} />
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<label className="form-label">Валюта</label>
|
||||
<select className="form-select" value={payment.currency} onChange={e => onChange({ ...payment, currency: e.target.value })}>
|
||||
<option value="USD">USD</option>
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="RUB">RUB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<label className="form-label">Комментарий</label>
|
||||
<input type="text" className="form-control" value={payment.note} onChange={e => onChange({ ...payment, note: e.target.value })} placeholder="Необязательно" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-secondary" onClick={onClose}>Отмена</button>
|
||||
<button className="btn btn-primary" onClick={onSubmit}>Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user