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 { useState, useEffect, useRef } from 'react';
|
||||||
import api from './lib/api.js';
|
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 {
|
import {
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
@@ -21,8 +29,6 @@ import {
|
|||||||
IconHistory
|
IconHistory
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const API_URL = '/api';
|
|
||||||
|
|
||||||
function BillingManager() {
|
function BillingManager() {
|
||||||
const [billingData, setBillingData] = useState([]);
|
const [billingData, setBillingData] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -468,59 +474,51 @@ function BillingManager() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Заголовок страницы */}
|
{/* Заголовок страницы */}
|
||||||
<div className="page-header d-print-none mb-4">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title="Инфра-биллинг"
|
||||||
<div className="col">
|
pretitle="Панель управления / Ноды / Инфра-биллинг"
|
||||||
<h2 className="page-title">Инфра-биллинг</h2>
|
actions={
|
||||||
<div className="page-pretitle">Панель управления / Ноды / Инфра-биллинг</div>
|
<PageHeaderActions
|
||||||
</div>
|
loading={loading}
|
||||||
<div className="col-auto ms-auto d-print-none">
|
onRefresh={fetchBillingData}
|
||||||
|
disableRefresh={loading}
|
||||||
|
onExport={exportData}
|
||||||
|
disableExport={loading}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Дополнительные действия */}
|
||||||
|
<div className="mb-4 d-print-none">
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-secondary"
|
className="btn btn-outline-secondary"
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
>
|
>
|
||||||
<IconFilter size={16} />
|
<IconFilter size={16} className="me-1" />
|
||||||
Фильтры
|
Фильтры
|
||||||
</button>
|
</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
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={handleAddItem}
|
onClick={handleAddItem}
|
||||||
>
|
>
|
||||||
<IconPlus size={16} />
|
<IconPlus size={16} className="me-1" />
|
||||||
Добавить
|
Добавить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Уведомления */}
|
{/* Уведомления */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
<ErrorAlert
|
||||||
<IconAlertTriangle className="me-2" />
|
message={error}
|
||||||
{error}
|
onClose={() => setError('')}
|
||||||
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
onRetry={fetchBillingData}
|
||||||
</div>
|
className="mb-4"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{success && (
|
{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" />
|
<IconCheck className="me-2" />
|
||||||
{success}
|
{success}
|
||||||
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
||||||
@@ -850,49 +848,14 @@ function BillingManager() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalItems={filteredData.length}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* История платежей */}
|
{/* История платежей */}
|
||||||
@@ -1034,37 +997,62 @@ function BillingManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Модальные окна */}
|
{/* Модальные окна */}
|
||||||
<AddBillingModal
|
<FormModal
|
||||||
show={showAddModal}
|
show={showAddModal}
|
||||||
|
title="Добавить новый элемент"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddSubmit();
|
||||||
|
}}
|
||||||
|
onClose={() => setShowAddModal(false)}
|
||||||
|
submitLabel="Добавить"
|
||||||
|
submitIcon={IconPlus}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<AddBillingForm
|
||||||
item={newItem}
|
item={newItem}
|
||||||
onItemChange={setNewItem}
|
onItemChange={setNewItem}
|
||||||
onSubmit={handleAddSubmit}
|
|
||||||
onClose={() => setShowAddModal(false)}
|
|
||||||
/>
|
/>
|
||||||
|
</FormModal>
|
||||||
|
|
||||||
<EditBillingModal
|
<FormModal
|
||||||
show={showEditModal}
|
show={showEditModal}
|
||||||
|
title="Редактировать элемент"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleEditSubmit();
|
||||||
|
}}
|
||||||
|
onClose={() => setShowEditModal(false)}
|
||||||
|
submitLabel="Сохранить"
|
||||||
|
submitIcon={IconCheck}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<EditBillingForm
|
||||||
item={editingItem}
|
item={editingItem}
|
||||||
onItemChange={setEditingItem}
|
onItemChange={setEditingItem}
|
||||||
onSubmit={handleEditSubmit}
|
|
||||||
onClose={() => setShowEditModal(false)}
|
|
||||||
/>
|
/>
|
||||||
|
</FormModal>
|
||||||
|
|
||||||
<DeleteBillingModal
|
<ConfirmModal
|
||||||
show={showDeleteModal}
|
show={showDeleteModal}
|
||||||
item={selectedItem}
|
title="Подтверждение удаления"
|
||||||
onDelete={confirmDelete}
|
message={
|
||||||
|
<>
|
||||||
|
<p>Вы уверены, что хотите удалить элемент <strong>{selectedItem?.hostName}</strong>?</p>
|
||||||
|
<p className="text-muted">Это действие нельзя отменить.</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onConfirm={confirmDelete}
|
||||||
onClose={() => setShowDeleteModal(false)}
|
onClose={() => setShowDeleteModal(false)}
|
||||||
|
confirmLabel="Удалить"
|
||||||
|
variant="danger"
|
||||||
/>
|
/>
|
||||||
{/* Модалка добавления платежа */}
|
{/* Модалка добавления платежа */}
|
||||||
{showPaymentModal && (
|
<FormModal
|
||||||
<AddPaymentModal
|
|
||||||
show={showPaymentModal}
|
show={showPaymentModal}
|
||||||
servers={billingData}
|
title={editingPayment ? "Редактировать платеж" : "Добавить платеж"}
|
||||||
payment={paymentDraft}
|
onSubmit={(e) => {
|
||||||
onChange={setPaymentDraft}
|
e.preventDefault();
|
||||||
onClose={() => setShowPaymentModal(false)}
|
|
||||||
onSubmit={() => {
|
|
||||||
if (!paymentDraft.serverId || !paymentDraft.date || !paymentDraft.amount) {
|
if (!paymentDraft.serverId || !paymentDraft.date || !paymentDraft.amount) {
|
||||||
setError('Заполните сервер, дату и сумму.');
|
setError('Заполните сервер, дату и сумму.');
|
||||||
setTimeout(() => setError(''), 3000);
|
setTimeout(() => setError(''), 3000);
|
||||||
@@ -1115,22 +1103,29 @@ function BillingManager() {
|
|||||||
setSuccess('Платеж добавлен');
|
setSuccess('Платеж добавлен');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
}}
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setShowPaymentModal(false);
|
||||||
|
setEditingPayment(null);
|
||||||
|
}}
|
||||||
|
submitLabel={editingPayment ? "Сохранить" : "Добавить"}
|
||||||
|
submitIcon={editingPayment ? IconCheck : IconPlus}
|
||||||
|
>
|
||||||
|
<AddPaymentForm
|
||||||
|
servers={billingData}
|
||||||
|
payment={paymentDraft}
|
||||||
|
onChange={setPaymentDraft}
|
||||||
/>
|
/>
|
||||||
)}
|
</FormModal>
|
||||||
|
|
||||||
{/* Модалка удаления платежа */}
|
{/* Модалка удаления платежа */}
|
||||||
{showDeletePaymentModal && (
|
<ConfirmModal
|
||||||
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
show={showDeletePaymentModal}
|
||||||
<div className="modal-dialog">
|
title="Удалить платеж"
|
||||||
<div className="modal-content">
|
message={
|
||||||
<div className="modal-header">
|
<>
|
||||||
<h5 className="modal-title">Удалить платеж</h5>
|
|
||||||
<button type="button" className="btn-close" onClick={() => setShowDeletePaymentModal(false)}></button>
|
|
||||||
</div>
|
|
||||||
<div className="modal-body">
|
|
||||||
<p>Вы уверены, что хотите удалить выбранный платеж?</p>
|
<p>Вы уверены, что хотите удалить выбранный платеж?</p>
|
||||||
{paymentToDelete && (
|
{paymentToDelete && (
|
||||||
<div className="alert alert-warning">
|
<div className="alert alert-warning mt-3">
|
||||||
<div><strong>Сервер:</strong> {paymentToDelete.data?.server?.hostName}</div>
|
<div><strong>Сервер:</strong> {paymentToDelete.data?.server?.hostName}</div>
|
||||||
<div><strong>Дата:</strong> {formatDate(paymentToDelete.data?.date)}</div>
|
<div><strong>Дата:</strong> {formatDate(paymentToDelete.data?.date)}</div>
|
||||||
<div><strong>Сумма:</strong> {formatCurrency(paymentToDelete.data?.amount, paymentToDelete.data?.currency)}</div>
|
<div><strong>Сумма:</strong> {formatCurrency(paymentToDelete.data?.amount, paymentToDelete.data?.currency)}</div>
|
||||||
@@ -1139,110 +1134,102 @@ function BillingManager() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
<div className="modal-footer">
|
}
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => setShowDeletePaymentModal(false)}>
|
onConfirm={handleDeletePayment}
|
||||||
Отмена
|
onClose={() => setShowDeletePaymentModal(false)}
|
||||||
</button>
|
confirmLabel="Удалить"
|
||||||
<button type="button" className="btn btn-danger" onClick={handleDeletePayment}>
|
variant="danger"
|
||||||
Удалить
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Компоненты модальных окон
|
// Компоненты форм для модальных окон
|
||||||
function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
function AddBillingForm({ item, onItemChange }) {
|
||||||
if (!show) return null;
|
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 (
|
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="row g-3">
|
||||||
{/* Основная информация */}
|
{/* Основная информация */}
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<h6 className="text-muted mb-3">Основная информация</h6>
|
<h6 className="text-muted mb-3">Основная информация</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Имя хоста *</label>
|
<FormField
|
||||||
<input
|
label="Имя хоста"
|
||||||
type="text"
|
name="hostName"
|
||||||
className="form-control"
|
|
||||||
value={item.hostName}
|
value={item.hostName}
|
||||||
onChange={(e) => onItemChange({ ...item, hostName: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, hostName: value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Назначение</label>
|
<FormField
|
||||||
<select
|
label="Назначение"
|
||||||
className="form-select"
|
name="purpose"
|
||||||
|
type="select"
|
||||||
value={item.purpose}
|
value={item.purpose}
|
||||||
onChange={(e) => onItemChange({ ...item, purpose: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, purpose: value })}
|
||||||
>
|
options={purposeOptions}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Страна *</label>
|
<FormField
|
||||||
<input
|
label="Страна"
|
||||||
type="text"
|
name="country"
|
||||||
className="form-control"
|
|
||||||
value={item.country}
|
value={item.country}
|
||||||
onChange={(e) => onItemChange({ ...item, country: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, country: value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Провайдер *</label>
|
<FormField
|
||||||
<input
|
label="Провайдер"
|
||||||
type="text"
|
name="provider"
|
||||||
className="form-control"
|
|
||||||
value={item.provider}
|
value={item.provider}
|
||||||
onChange={(e) => onItemChange({ ...item, provider: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, provider: value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Ссылка для входа</label>
|
<FormField
|
||||||
<input
|
label="Ссылка для входа"
|
||||||
type="text"
|
name="loginUrl"
|
||||||
className="form-control"
|
|
||||||
value={item.loginUrl}
|
value={item.loginUrl}
|
||||||
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, loginUrl: value })}
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Статус</label>
|
<FormField
|
||||||
<select
|
label="Статус"
|
||||||
className="form-select"
|
name="status"
|
||||||
|
type="select"
|
||||||
value={item.status}
|
value={item.status}
|
||||||
onChange={(e) => onItemChange({ ...item, status: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, status: value })}
|
||||||
>
|
options={statusOptions}
|
||||||
<option value="active">Активен</option>
|
/>
|
||||||
<option value="inactive">Неактивен</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Текущие платежи */}
|
{/* Текущие платежи */}
|
||||||
@@ -1250,148 +1237,133 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
|||||||
<h6 className="text-muted mb-3 mt-4">Текущие платежи</h6>
|
<h6 className="text-muted mb-3 mt-4">Текущие платежи</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Месячная стоимость</label>
|
<FormField
|
||||||
<input
|
label="Месячная стоимость"
|
||||||
|
name="monthlyCost"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
|
||||||
className="form-control"
|
|
||||||
value={item.monthlyCost}
|
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>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Валюта месячной стоимости</label>
|
<FormField
|
||||||
<select
|
label="Валюта месячной стоимости"
|
||||||
className="form-select"
|
name="monthlyCostCurrency"
|
||||||
|
type="select"
|
||||||
value={item.monthlyCostCurrency}
|
value={item.monthlyCostCurrency}
|
||||||
onChange={(e) => onItemChange({ ...item, monthlyCostCurrency: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, monthlyCostCurrency: value })}
|
||||||
>
|
options={currencyOptions}
|
||||||
<option value="USD">USD</option>
|
/>
|
||||||
<option value="EUR">EUR</option>
|
|
||||||
<option value="RUB">RUB</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Дата следующего платежа</label>
|
<FormField
|
||||||
<input
|
label="Дата следующего платежа"
|
||||||
|
name="nextPaymentDate"
|
||||||
type="date"
|
type="date"
|
||||||
className="form-control"
|
|
||||||
value={item.nextPaymentDate}
|
value={item.nextPaymentDate}
|
||||||
onChange={(e) => onItemChange({ ...item, nextPaymentDate: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, nextPaymentDate: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Дополнительно */}
|
{/* Дополнительно */}
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<label className="form-label">Заметки</label>
|
<FormField
|
||||||
<input
|
label="Заметки"
|
||||||
type="text"
|
name="notes"
|
||||||
className="form-control"
|
|
||||||
value={item.notes}
|
value={item.notes}
|
||||||
onChange={(e) => onItemChange({ ...item, notes: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, notes: value })}
|
||||||
placeholder="Дополнительная информация"
|
placeholder="Дополнительная информация"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 }) {
|
function EditBillingForm({ item, onItemChange }) {
|
||||||
if (!show) return null;
|
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 (
|
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="row g-3">
|
||||||
{/* Основная информация */}
|
{/* Основная информация */}
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<h6 className="text-muted mb-3">Основная информация</h6>
|
<h6 className="text-muted mb-3">Основная информация</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Имя хоста *</label>
|
<FormField
|
||||||
<input
|
label="Имя хоста"
|
||||||
type="text"
|
name="hostName"
|
||||||
className="form-control"
|
|
||||||
value={item.hostName || ''}
|
value={item.hostName || ''}
|
||||||
onChange={(e) => onItemChange({ ...item, hostName: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, hostName: value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Назначение</label>
|
<FormField
|
||||||
<select
|
label="Назначение"
|
||||||
className="form-select"
|
name="purpose"
|
||||||
|
type="select"
|
||||||
value={item.purpose || ''}
|
value={item.purpose || ''}
|
||||||
onChange={(e) => onItemChange({ ...item, purpose: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, purpose: value })}
|
||||||
>
|
options={purposeOptions}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Страна *</label>
|
<FormField
|
||||||
<input
|
label="Страна"
|
||||||
type="text"
|
name="country"
|
||||||
className="form-control"
|
|
||||||
value={item.country || ''}
|
value={item.country || ''}
|
||||||
onChange={(e) => onItemChange({ ...item, country: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, country: value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Провайдер *</label>
|
<FormField
|
||||||
<input
|
label="Провайдер"
|
||||||
type="text"
|
name="provider"
|
||||||
className="form-control"
|
|
||||||
value={item.provider || ''}
|
value={item.provider || ''}
|
||||||
onChange={(e) => onItemChange({ ...item, provider: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, provider: value })}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Ссылка для входа</label>
|
<FormField
|
||||||
<input
|
label="Ссылка для входа"
|
||||||
type="text"
|
name="loginUrl"
|
||||||
className="form-control"
|
|
||||||
value={item.loginUrl || ''}
|
value={item.loginUrl || ''}
|
||||||
onChange={(e) => onItemChange({ ...item, loginUrl: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, loginUrl: value })}
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Статус</label>
|
<FormField
|
||||||
<select
|
label="Статус"
|
||||||
className="form-select"
|
name="status"
|
||||||
|
type="select"
|
||||||
value={item.status || 'active'}
|
value={item.status || 'active'}
|
||||||
onChange={(e) => onItemChange({ ...item, status: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, status: value })}
|
||||||
>
|
options={statusOptions}
|
||||||
<option value="active">Активен</option>
|
/>
|
||||||
<option value="inactive">Неактивен</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Текущие платежи */}
|
{/* Текущие платежи */}
|
||||||
@@ -1399,146 +1371,113 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
|
|||||||
<h6 className="text-muted mb-3 mt-4">Текущие платежи</h6>
|
<h6 className="text-muted mb-3 mt-4">Текущие платежи</h6>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Месячная стоимость</label>
|
<FormField
|
||||||
<input
|
label="Месячная стоимость"
|
||||||
|
name="monthlyCost"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
|
||||||
className="form-control"
|
|
||||||
value={item.monthlyCost || 0}
|
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>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Валюта месячной стоимости</label>
|
<FormField
|
||||||
<select
|
label="Валюта месячной стоимости"
|
||||||
className="form-select"
|
name="monthlyCostCurrency"
|
||||||
|
type="select"
|
||||||
value={item.monthlyCostCurrency || 'USD'}
|
value={item.monthlyCostCurrency || 'USD'}
|
||||||
onChange={(e) => onItemChange({ ...item, monthlyCostCurrency: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, monthlyCostCurrency: value })}
|
||||||
>
|
options={currencyOptions}
|
||||||
<option value="USD">USD</option>
|
/>
|
||||||
<option value="EUR">EUR</option>
|
|
||||||
<option value="RUB">RUB</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<label className="form-label">Дата следующего платежа</label>
|
<FormField
|
||||||
<input
|
label="Дата следующего платежа"
|
||||||
|
name="nextPaymentDate"
|
||||||
type="date"
|
type="date"
|
||||||
className="form-control"
|
|
||||||
value={item.nextPaymentDate || ''}
|
value={item.nextPaymentDate || ''}
|
||||||
onChange={(e) => onItemChange({ ...item, nextPaymentDate: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, nextPaymentDate: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Дополнительно */}
|
{/* Дополнительно */}
|
||||||
<div className="col-md-12">
|
<div className="col-md-12">
|
||||||
<label className="form-label">Заметки</label>
|
<FormField
|
||||||
<input
|
label="Заметки"
|
||||||
type="text"
|
name="notes"
|
||||||
className="form-control"
|
|
||||||
value={item.notes || ''}
|
value={item.notes || ''}
|
||||||
onChange={(e) => onItemChange({ ...item, notes: e.target.value })}
|
onChange={(value) => onItemChange({ ...item, notes: value })}
|
||||||
placeholder="Дополнительная информация"
|
placeholder="Дополнительная информация"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 (
|
return (
|
||||||
<div className="modal show d-block" tabIndex="-1" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
<>
|
||||||
<div className="modal-dialog">
|
<FormField
|
||||||
<div className="modal-content">
|
label="Сервер"
|
||||||
<div className="modal-header">
|
name="serverId"
|
||||||
<h5 className="modal-title">Подтверждение удаления</h5>
|
type="select"
|
||||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
value={payment.serverId}
|
||||||
</div>
|
onChange={(value) => onChange({ ...payment, serverId: value })}
|
||||||
<div className="modal-body">
|
options={serverOptions}
|
||||||
<p>Вы уверены, что хотите удалить элемент <strong>{item?.hostName}</strong>?</p>
|
required
|
||||||
<p className="text-muted">Это действие нельзя отменить.</p>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div className="modal-footer">
|
label="Дата"
|
||||||
<button type="button" className="btn btn-secondary" onClick={onClose}>
|
name="date"
|
||||||
Отмена
|
type="date"
|
||||||
</button>
|
value={payment.date}
|
||||||
<button type="button" className="btn btn-danger" onClick={onDelete}>
|
onChange={(value) => onChange({ ...payment, date: value })}
|
||||||
Удалить
|
required
|
||||||
</button>
|
/>
|
||||||
</div>
|
<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>
|
||||||
|
<div className="col-4">
|
||||||
|
<FormField
|
||||||
|
label="Валюта"
|
||||||
|
name="currency"
|
||||||
|
type="select"
|
||||||
|
value={payment.currency}
|
||||||
|
onChange={(value) => onChange({ ...payment, currency: value })}
|
||||||
|
options={currencyOptions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<FormField
|
||||||
|
label="Комментарий"
|
||||||
|
name="note"
|
||||||
|
value={payment.note}
|
||||||
|
onChange={(value) => onChange({ ...payment, note: value })}
|
||||||
|
placeholder="Необязательно"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BillingManager;
|
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