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 { 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>
);
}