refactor(BillingManager): add tab navigation for subscriptions and payments, enhancing user experience and data visibility
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:
+305
-273
@@ -54,6 +54,7 @@ function BillingManager() {
|
|||||||
const [filterStatus, setFilterStatus] = useState('');
|
const [filterStatus, setFilterStatus] = useState('');
|
||||||
const [filterUrgency, setFilterUrgency] = useState('');
|
const [filterUrgency, setFilterUrgency] = useState('');
|
||||||
const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table'
|
const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table'
|
||||||
|
const [activeTab, setActiveTab] = useState('subscriptions'); // 'subscriptions' | 'payments'
|
||||||
const pageSize = 15;
|
const pageSize = 15;
|
||||||
|
|
||||||
// Модальные окна
|
// Модальные окна
|
||||||
@@ -298,6 +299,10 @@ function BillingManager() {
|
|||||||
return sum + convertToRUB(item.monthlyCost || 0, item.monthlyCostCurrency || 'USD');
|
return sum + convertToRUB(item.monthlyCost || 0, item.monthlyCostCurrency || 'USD');
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
const totalPaymentsCount = billingData.reduce((sum, item) => {
|
||||||
|
return sum + ((item.payments && Array.isArray(item.payments) ? item.payments.length : 0));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
const urgentPayments = billingData.filter(item => {
|
const urgentPayments = billingData.filter(item => {
|
||||||
const nextPayment = new Date(item.nextPaymentDate);
|
const nextPayment = new Date(item.nextPaymentDate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -578,15 +583,39 @@ function BillingManager() {
|
|||||||
title="Биллинг"
|
title="Биллинг"
|
||||||
icon={<IconCreditCard size={24} />}
|
icon={<IconCreditCard size={24} />}
|
||||||
actions={
|
actions={
|
||||||
<PageHeaderActions
|
<div className="d-flex flex-column flex-sm-row align-items-stretch align-items-sm-center gap-2">
|
||||||
loading={loading}
|
<div className="btn-group me-sm-2" role="group">
|
||||||
onRefresh={() => { fetchBillingData(); fetchServers(); }}
|
<button
|
||||||
disableRefresh={loading}
|
className={`btn ${activeTab === 'subscriptions' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
onExport={exportData}
|
onClick={() => setActiveTab('subscriptions')}
|
||||||
disableExport={loading}
|
>
|
||||||
onSave={handleSaveChanges}
|
<IconServer size={16} className="me-1 d-none d-sm-inline" />
|
||||||
disableSave={loading}
|
Подписки
|
||||||
/>
|
<span className={`badge ms-1 ${activeTab === 'subscriptions' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
||||||
|
{billingData.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn ${activeTab === 'payments' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
|
onClick={() => setActiveTab('payments')}
|
||||||
|
>
|
||||||
|
<IconHistory size={16} className="me-1 d-none d-sm-inline" />
|
||||||
|
История платежей
|
||||||
|
<span className={`badge ms-1 ${activeTab === 'payments' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
||||||
|
{totalPaymentsCount}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<PageHeaderActions
|
||||||
|
loading={loading}
|
||||||
|
onRefresh={() => { fetchBillingData(); fetchServers(); }}
|
||||||
|
disableRefresh={loading}
|
||||||
|
onExport={exportData}
|
||||||
|
disableExport={loading}
|
||||||
|
onSave={handleSaveChanges}
|
||||||
|
disableSave={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -656,279 +685,282 @@ function BillingManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Основная секция */}
|
{/* Основная секция: Подписки */}
|
||||||
<div className="card">
|
{activeTab === 'subscriptions' && (
|
||||||
<div className="card-header">
|
<div className="card">
|
||||||
<div className="d-flex align-items-center gap-3 flex-grow-1">
|
<div className="card-header">
|
||||||
<h3 className="card-title mb-0">
|
<div className="d-flex align-items-center gap-3 flex-grow-1">
|
||||||
Подписки
|
<h3 className="card-title mb-0">
|
||||||
<span className="badge bg-blue-lt text-blue ms-2">{filteredData.length}</span>
|
Подписки
|
||||||
</h3>
|
<span className="badge bg-blue-lt text-blue ms-2">{filteredData.length}</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
{/* Поиск */}
|
{/* Поиск */}
|
||||||
<div className="input-icon flex-grow-1" style={{ maxWidth: '280px' }}>
|
<div className="input-icon flex-grow-1" style={{ maxWidth: '280px' }}>
|
||||||
<span className="input-icon-addon">
|
<span className="input-icon-addon">
|
||||||
<IconSearch size={16} />
|
<IconSearch size={16} />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
placeholder="Поиск..."
|
placeholder="Поиск..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Быстрые фильтры */}
|
|
||||||
<div className="d-flex gap-1">
|
|
||||||
<button
|
|
||||||
className={`btn btn-sm ${filterUrgency === '' ? 'btn-primary' : 'btn-outline-secondary'}`}
|
|
||||||
onClick={() => { setFilterUrgency(''); setCurrentPage(1); }}
|
|
||||||
>
|
|
||||||
Все
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn btn-sm ${filterUrgency === 'overdue' ? 'btn-red' : 'btn-outline-secondary'}`}
|
|
||||||
onClick={() => { setFilterUrgency('overdue'); setCurrentPage(1); }}
|
|
||||||
>
|
|
||||||
Просрочено
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn btn-sm ${filterUrgency === 'urgent' ? 'btn-yellow' : 'btn-outline-secondary'}`}
|
|
||||||
onClick={() => { setFilterUrgency('urgent'); setCurrentPage(1); }}
|
|
||||||
>
|
|
||||||
Срочно
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Переключатель вида */}
|
|
||||||
<div className="btn-group btn-group-sm" role="group">
|
|
||||||
<button
|
|
||||||
className={`btn ${viewMode === 'cards' ? 'btn-primary' : 'btn-outline-secondary'}`}
|
|
||||||
onClick={() => setViewMode('cards')}
|
|
||||||
title="Карточки"
|
|
||||||
>
|
|
||||||
<IconLayoutGrid size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn ${viewMode === 'table' ? 'btn-primary' : 'btn-outline-secondary'}`}
|
|
||||||
onClick={() => setViewMode('table')}
|
|
||||||
title="Таблица"
|
|
||||||
>
|
|
||||||
<IconList size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card-actions">
|
|
||||||
<button className="btn btn-primary" onClick={handleAddItem}>
|
|
||||||
<IconPlus size={18} className="me-1" />
|
|
||||||
Добавить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Контент: карточки или таблица */}
|
|
||||||
{paginatedData.length === 0 ? (
|
|
||||||
<div className="card-body text-center text-muted py-5">
|
|
||||||
<IconCreditCard size={48} className="mb-3 text-muted" />
|
|
||||||
<h3>{searchTerm || filterUrgency ? 'Ничего не найдено' : 'Нет подписок'}</h3>
|
|
||||||
<p className="text-muted">Добавьте первую подписку для отслеживания платежей</p>
|
|
||||||
<button className="btn btn-primary" onClick={handleAddItem}>
|
|
||||||
<IconPlus size={18} className="me-1" />
|
|
||||||
Добавить подписку
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : viewMode === 'cards' ? (
|
|
||||||
/* Карточный вид */
|
|
||||||
<div className="card-body">
|
|
||||||
{paginatedData.map(item => {
|
|
||||||
const days = getDaysUntilPayment(item.nextPaymentDate);
|
|
||||||
const linkedServer = item.serverId ? serversById.get(item.serverId) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BillingCard
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
linkedServer={linkedServer}
|
|
||||||
daysUntilPayment={days}
|
|
||||||
onEdit={handleEditItem}
|
|
||||||
onDelete={handleDeleteItem}
|
|
||||||
onAddPayment={(item) => {
|
|
||||||
setPaymentDraft({
|
|
||||||
serverId: item.id,
|
|
||||||
date: new Date().toISOString().slice(0,10),
|
|
||||||
amount: item.monthlyCost || 0,
|
|
||||||
currency: item.monthlyCostCurrency || 'USD',
|
|
||||||
note: ''
|
|
||||||
});
|
|
||||||
setShowPaymentModal(true);
|
|
||||||
}}
|
|
||||||
formatCurrencyInRUB={formatCurrencyInRUB}
|
|
||||||
/>
|
/>
|
||||||
);
|
</div>
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
/* Табличный вид */
|
|
||||||
<div className="table-responsive">
|
|
||||||
<table className="table card-table table-vcenter mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('hostName')}
|
|
||||||
style={{ minWidth: '200px' }}
|
|
||||||
>
|
|
||||||
Сервис
|
|
||||||
{sortField === 'hostName' && (
|
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
className="cursor-pointer text-end"
|
|
||||||
onClick={() => handleSort('monthlyCost')}
|
|
||||||
style={{ width: '140px' }}
|
|
||||||
>
|
|
||||||
Стоимость
|
|
||||||
{sortField === 'monthlyCost' && (
|
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
className="cursor-pointer text-center"
|
|
||||||
onClick={() => handleSort('nextPaymentDate')}
|
|
||||||
style={{ width: '120px' }}
|
|
||||||
>
|
|
||||||
Платёж
|
|
||||||
{sortField === 'nextPaymentDate' && (
|
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th className="text-center" style={{ width: '100px' }}>Срок</th>
|
|
||||||
<th className="text-center" style={{ width: '80px' }}>Статус</th>
|
|
||||||
<th style={{ width: '140px' }}></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{paginatedData.map(item => {
|
|
||||||
const days = getDaysUntilPayment(item.nextPaymentDate);
|
|
||||||
const linkedServer = item.serverId ? serversById.get(item.serverId) : null;
|
|
||||||
|
|
||||||
return (
|
{/* Быстрые фильтры */}
|
||||||
<tr key={item.id}>
|
<div className="d-flex gap-1">
|
||||||
<td>
|
<button
|
||||||
<div className="d-flex align-items-center">
|
className={`btn btn-sm ${filterUrgency === '' ? 'btn-primary' : 'btn-outline-secondary'}`}
|
||||||
<div>
|
onClick={() => { setFilterUrgency(''); setCurrentPage(1); }}
|
||||||
<div className="fw-semibold">
|
>
|
||||||
{item.hostName}
|
Все
|
||||||
{item._external && (
|
</button>
|
||||||
<span className="badge bg-yellow-lt text-yellow ms-2" title="Импортирован из серверов, требует заполнения">
|
<button
|
||||||
не заполнен
|
className={`btn btn-sm ${filterUrgency === 'overdue' ? 'btn-red' : 'btn-outline-secondary'}`}
|
||||||
</span>
|
onClick={() => { setFilterUrgency('overdue'); setCurrentPage(1); }}
|
||||||
)}
|
>
|
||||||
</div>
|
Просрочено
|
||||||
<div className="text-muted small d-flex align-items-center gap-2">
|
</button>
|
||||||
<span>{item.provider}</span>
|
<button
|
||||||
{linkedServer && (
|
className={`btn btn-sm ${filterUrgency === 'urgent' ? 'btn-yellow' : 'btn-outline-secondary'}`}
|
||||||
<>
|
onClick={() => { setFilterUrgency('urgent'); setCurrentPage(1); }}
|
||||||
<span>•</span>
|
>
|
||||||
<span className="text-blue">{linkedServer.ip}</span>
|
Срочно
|
||||||
</>
|
</button>
|
||||||
)}
|
</div>
|
||||||
{item.loginUrl && (
|
|
||||||
<a
|
{/* Переключатель вида */}
|
||||||
href={item.loginUrl.startsWith('http') ? item.loginUrl : `https://${item.loginUrl}`}
|
<div className="btn-group btn-group-sm" role="group">
|
||||||
target="_blank"
|
<button
|
||||||
rel="noopener noreferrer"
|
className={`btn ${viewMode === 'cards' ? 'btn-primary' : 'btn-outline-secondary'}`}
|
||||||
className="text-muted"
|
onClick={() => setViewMode('cards')}
|
||||||
title="Открыть панель"
|
title="Карточки"
|
||||||
>
|
>
|
||||||
<IconExternalLink size={12} />
|
<IconLayoutGrid size={16} />
|
||||||
</a>
|
</button>
|
||||||
)}
|
<button
|
||||||
|
className={`btn ${viewMode === 'table' ? 'btn-primary' : 'btn-outline-secondary'}`}
|
||||||
|
onClick={() => setViewMode('table')}
|
||||||
|
title="Таблица"
|
||||||
|
>
|
||||||
|
<IconList size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-actions">
|
||||||
|
<button className="btn btn-primary" onClick={handleAddItem}>
|
||||||
|
<IconPlus size={18} className="me-1" />
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Контент: карточки или таблица */}
|
||||||
|
{paginatedData.length === 0 ? (
|
||||||
|
<div className="card-body text-center text-muted py-5">
|
||||||
|
<IconCreditCard size={48} className="mb-3 text-muted" />
|
||||||
|
<h3>{searchTerm || filterUrgency ? 'Ничего не найдено' : 'Нет подписок'}</h3>
|
||||||
|
<p className="text-muted">Добавьте первую подписку для отслеживания платежей</p>
|
||||||
|
<button className="btn btn-primary" onClick={handleAddItem}>
|
||||||
|
<IconPlus size={18} className="me-1" />
|
||||||
|
Добавить подписку
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : viewMode === 'cards' ? (
|
||||||
|
/* Карточный вид */
|
||||||
|
<div className="card-body">
|
||||||
|
{paginatedData.map(item => {
|
||||||
|
const days = getDaysUntilPayment(item.nextPaymentDate);
|
||||||
|
const linkedServer = item.serverId ? serversById.get(item.serverId) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BillingCard
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
linkedServer={linkedServer}
|
||||||
|
daysUntilPayment={days}
|
||||||
|
onEdit={handleEditItem}
|
||||||
|
onDelete={handleDeleteItem}
|
||||||
|
onAddPayment={(item) => {
|
||||||
|
setPaymentDraft({
|
||||||
|
serverId: item.id,
|
||||||
|
date: new Date().toISOString().slice(0,10),
|
||||||
|
amount: item.monthlyCost || 0,
|
||||||
|
currency: item.monthlyCostCurrency || 'USD',
|
||||||
|
note: ''
|
||||||
|
});
|
||||||
|
setShowPaymentModal(true);
|
||||||
|
}}
|
||||||
|
formatCurrencyInRUB={formatCurrencyInRUB}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Табличный вид */
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => handleSort('hostName')}
|
||||||
|
style={{ minWidth: '200px' }}
|
||||||
|
>
|
||||||
|
Сервис
|
||||||
|
{sortField === 'hostName' && (
|
||||||
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="cursor-pointer text-end"
|
||||||
|
onClick={() => handleSort('monthlyCost')}
|
||||||
|
style={{ width: '140px' }}
|
||||||
|
>
|
||||||
|
Стоимость
|
||||||
|
{sortField === 'monthlyCost' && (
|
||||||
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="cursor-pointer text-center"
|
||||||
|
onClick={() => handleSort('nextPaymentDate')}
|
||||||
|
style={{ width: '120px' }}
|
||||||
|
>
|
||||||
|
Платёж
|
||||||
|
{sortField === 'nextPaymentDate' && (
|
||||||
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
<th className="text-center" style={{ width: '100px' }}>Срок</th>
|
||||||
|
<th className="text-center" style={{ width: '80px' }}>Статус</th>
|
||||||
|
<th style={{ width: '140px' }}></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{paginatedData.map(item => {
|
||||||
|
const days = getDaysUntilPayment(item.nextPaymentDate);
|
||||||
|
const linkedServer = item.serverId ? serversById.get(item.serverId) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<div>
|
||||||
|
<div className="fw-semibold">
|
||||||
|
{item.hostName}
|
||||||
|
{item._external && (
|
||||||
|
<span className="badge bg-yellow-lt text-yellow ms-2" title="Импортирован из серверов, требует заполнения">
|
||||||
|
не заполнен
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted small d-flex align-items-center gap-2">
|
||||||
|
<span>{item.provider}</span>
|
||||||
|
{linkedServer && (
|
||||||
|
<>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="text-blue">{linkedServer.ip}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{item.loginUrl && (
|
||||||
|
<a
|
||||||
|
href={item.loginUrl.startsWith('http') ? item.loginUrl : `https://${item.loginUrl}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-muted"
|
||||||
|
title="Открыть панель"
|
||||||
|
>
|
||||||
|
<IconExternalLink size={12} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
<td className="text-end">
|
||||||
<td className="text-end">
|
<div className="fw-semibold">
|
||||||
<div className="fw-semibold">
|
{formatCurrency(item.monthlyCost, item.monthlyCostCurrency)}
|
||||||
{formatCurrency(item.monthlyCost, item.monthlyCostCurrency)}
|
|
||||||
</div>
|
|
||||||
{item.monthlyCostCurrency !== 'RUB' && (
|
|
||||||
<div className="text-muted small">
|
|
||||||
≈ {formatCurrencyInRUB(item.monthlyCost, item.monthlyCostCurrency)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{item.monthlyCostCurrency !== 'RUB' && (
|
||||||
</td>
|
<div className="text-muted small">
|
||||||
<td className="text-center">
|
≈ {formatCurrencyInRUB(item.monthlyCost, item.monthlyCostCurrency)}
|
||||||
{formatDate(item.nextPaymentDate)}
|
</div>
|
||||||
</td>
|
)}
|
||||||
<td className="text-center">
|
</td>
|
||||||
<UrgencyBadge days={days} />
|
<td className="text-center">
|
||||||
</td>
|
{formatDate(item.nextPaymentDate)}
|
||||||
<td className="text-center">
|
</td>
|
||||||
{item.status === 'active' ? (
|
<td className="text-center">
|
||||||
<span className="badge bg-green-lt text-green">Активен</span>
|
<UrgencyBadge days={days} />
|
||||||
) : (
|
</td>
|
||||||
<span className="badge bg-secondary-lt text-secondary">Неактивен</span>
|
<td className="text-center">
|
||||||
)}
|
{item.status === 'active' ? (
|
||||||
</td>
|
<span className="badge bg-green-lt text-green">Активен</span>
|
||||||
<td>
|
) : (
|
||||||
<div className="btn-list gap-1 justify-content-end flex-nowrap">
|
<span className="badge bg-secondary-lt text-secondary">Неактивен</span>
|
||||||
<button
|
)}
|
||||||
className="btn btn-outline-success btn-sm"
|
</td>
|
||||||
onClick={() => {
|
<td>
|
||||||
setPaymentDraft({
|
<div className="btn-list gap-1 justify-content-end flex-nowrap">
|
||||||
serverId: item.id,
|
<button
|
||||||
date: new Date().toISOString().slice(0,10),
|
className="btn btn-outline-success btn-sm"
|
||||||
amount: item.monthlyCost || 0,
|
onClick={() => {
|
||||||
currency: item.monthlyCostCurrency || 'USD',
|
setPaymentDraft({
|
||||||
note: ''
|
serverId: item.id,
|
||||||
});
|
date: new Date().toISOString().slice(0,10),
|
||||||
setShowPaymentModal(true);
|
amount: item.monthlyCost || 0,
|
||||||
}}
|
currency: item.monthlyCostCurrency || 'USD',
|
||||||
title="Записать платёж"
|
note: ''
|
||||||
>
|
});
|
||||||
<IconCreditCard size={14} className="me-1" />
|
setShowPaymentModal(true);
|
||||||
Оплата
|
}}
|
||||||
</button>
|
title="Записать платёж"
|
||||||
<button
|
>
|
||||||
className="btn btn-outline-secondary btn-sm"
|
<IconCreditCard size={14} className="me-1" />
|
||||||
onClick={() => handleEditItem(item)}
|
Оплата
|
||||||
title="Редактировать"
|
</button>
|
||||||
>
|
<button
|
||||||
<IconEdit size={14} />
|
className="btn btn-outline-secondary btn-sm"
|
||||||
</button>
|
onClick={() => handleEditItem(item)}
|
||||||
<button
|
title="Редактировать"
|
||||||
className="btn btn-outline-danger btn-sm"
|
>
|
||||||
onClick={() => handleDeleteItem(item)}
|
<IconEdit size={14} />
|
||||||
title="Удалить"
|
</button>
|
||||||
>
|
<button
|
||||||
<IconTrash size={14} />
|
className="btn btn-outline-danger btn-sm"
|
||||||
</button>
|
onClick={() => handleDeleteItem(item)}
|
||||||
</div>
|
title="Удалить"
|
||||||
</td>
|
>
|
||||||
</tr>
|
<IconTrash size={14} />
|
||||||
);
|
</button>
|
||||||
})}
|
</div>
|
||||||
</tbody>
|
</td>
|
||||||
</table>
|
</tr>
|
||||||
</div>
|
);
|
||||||
)}
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
totalItems={filteredData.length}
|
totalItems={filteredData.length}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onPageChange={setCurrentPage}
|
onPageChange={setCurrentPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* История платежей — компактная версия */}
|
{/* История платежей — компактная версия */}
|
||||||
|
{activeTab === 'payments' && (
|
||||||
<div className="card mt-4">
|
<div className="card mt-4">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3 className="card-title d-flex align-items-center gap-2">
|
<h3 className="card-title d-flex align-items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user