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

This commit is contained in:
2025-12-01 13:13:40 +07:00
parent aa882bad20
commit 11a45ef690
+308 -369
View File
@@ -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>
);
}