Files
vps-tracker/src/pages/PaymentsPage.jsx
T
Denozordec 0dd2f0d7fa Add scheduled tariff synchronization and bulk update functionality
- Introduced `runScheduledSyncTariffs` function to handle tariff synchronization independently from VPS and payment sync.
- Updated `syncFromBillmanager` to accept options for skipping tariff and VPS payment syncs.
- Added new database columns for `syncTariffsIntervalMinutes` and `customFields` in settings.
- Enhanced settings page to configure tariff sync interval.
- Implemented bulk update functionality for VPS status and deletion in the VPS management page.
- Updated various components to support new features, including sync log display and improved payment handling.
2026-03-17 23:45:19 +07:00

307 lines
10 KiB
React

import { useMemo, useState } from 'react'
import { IconEdit } from '@tabler/icons-react'
import { paymentTypeLabel } from '../lib/utils'
import { UiModal } from '../components/UiModal'
import { EmptyState } from '../components/EmptyState'
import { PageHeader } from '../components/PageHeader'
import { ConvertedAmount } from '../components/ConvertedAmount'
const emptyForm = {
type: 'direct_vps_payment',
date: new Date().toISOString().slice(0, 10),
amount: '',
currency: 'USD',
providerAccountId: '',
vpsId: '',
note: '',
}
export function PaymentsPage({ db, actions, settings, ratesData }) {
const [form, setForm] = useState(emptyForm)
const [editingId, setEditingId] = useState(null)
const [error, setError] = useState('')
const [isModalOpen, setIsModalOpen] = useState(false)
const vpsOptions = useMemo(
() => db.vps.filter((item) => !form.providerAccountId || item.providerAccountId === form.providerAccountId),
[db.vps, form.providerAccountId],
)
const onSubmit = (event) => {
event.preventDefault()
const amount = Number(form.amount)
if (!Number.isFinite(amount) || amount <= 0) {
setError('Сумма должна быть больше 0')
return
}
if (!form.providerAccountId) {
setError('Выберите аккаунт хостера')
return
}
if (form.type === 'direct_vps_payment' && !form.vpsId) {
setError('Для прямого платежа выберите VPS')
return
}
setError('')
const payload = {
type: form.type,
date: form.date,
amount,
currency: form.currency,
providerAccountId: form.providerAccountId,
vpsId: form.vpsId || '',
note: form.note,
}
if (editingId) {
actions.update('payments', editingId, payload)
} else {
actions.create('payments', payload)
if (form.type === 'provider_balance_topup') {
actions.create('balanceLedger', {
type: 'provider_balance_topup',
date: form.date,
amount,
currency: form.currency,
direction: 'credit',
providerAccountId: form.providerAccountId,
vpsId: '',
note: form.note || 'Пополнение баланса',
})
}
}
setForm(emptyForm)
setEditingId(null)
setIsModalOpen(false)
}
const onEdit = (payment) => {
setForm({
type: payment.type || 'direct_vps_payment',
date: payment.date || new Date().toISOString().slice(0, 10),
amount: payment.amount ?? '',
currency: payment.currency || 'USD',
providerAccountId: payment.providerAccountId || '',
vpsId: payment.vpsId || '',
note: payment.note || '',
})
setEditingId(payment.id)
setIsModalOpen(true)
setError('')
}
return (
<>
<PageHeader pretitle="Финансы" title="Платежи" />
<div className="row row-cards">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">История платежей</h3>
<div className="card-actions">
<button
type="button"
className="btn btn-primary"
onClick={() => {
setForm(emptyForm)
setEditingId(null)
setIsModalOpen(true)
}}
>
Добавить платеж
</button>
</div>
</div>
<div className="table-responsive">
<table className="table card-table table-vcenter">
<thead>
<tr>
<th>Дата</th>
<th>Тип</th>
<th>Аккаунт / VPS</th>
<th>Сумма</th>
<th />
</tr>
</thead>
<tbody>
{db.payments.map((payment) => {
const account = db.providerAccounts.find((item) => item.id === payment.providerAccountId)
const provider = db.providers.find((item) => item.id === account?.providerId)
const vps = db.vps.find((item) => item.id === payment.vpsId)
return (
<tr key={payment.id}>
<td>{payment.date}</td>
<td>{paymentTypeLabel(payment.type)}</td>
<td>
<div>{account?.name || '-'}</div>
<div className="text-secondary">{vps?.dns || vps?.ip || '-'}</div>
</td>
<td>
<ConvertedAmount
amount={payment.amount}
currency={payment.currency}
provider={provider}
settings={settings}
ratesData={ratesData}
/>
</td>
<td className="text-end">
<div className="table-actions">
<button
type="button"
className="btn btn-sm btn-outline-primary me-1"
onClick={() => onEdit(payment)}
>
<IconEdit size={14} className="me-1" />
Изменить
</button>
<button
type="button"
className="btn btn-sm btn-outline-danger"
onClick={() => actions.remove('payments', payment.id)}
>
Удалить
</button>
</div>
</td>
</tr>
)
})}
{db.payments.length === 0 ? (
<EmptyState message="Нет платежей" colSpan={5} />
) : null}
</tbody>
</table>
</div>
</div>
</div>
</div>
<UiModal
open={isModalOpen}
title={editingId ? 'Редактировать платеж' : 'Новая операция платежа'}
onClose={() => {
setIsModalOpen(false)
setError('')
setForm(emptyForm)
setEditingId(null)
}}
size="modal-md"
>
<form className="row g-3" onSubmit={onSubmit}>
<div className="col-12">
<label className="form-label">Тип</label>
<select
className="form-select"
value={form.type}
onChange={(e) =>
setForm((prev) => ({
...prev,
type: e.target.value,
vpsId: '',
}))
}
>
<option value="direct_vps_payment">Прямой платеж за VPS</option>
<option value="provider_balance_topup">Пополнение баланса хостера</option>
</select>
</div>
<div className="col-12">
<label className="form-label">Аккаунт хостера</label>
<select
className="form-select"
value={form.providerAccountId}
onChange={(e) =>
setForm((prev) => ({
...prev,
providerAccountId: e.target.value,
vpsId: '',
}))
}
required
>
<option value="">Выберите аккаунт</option>
{db.providerAccounts.map((account) => (
<option key={account.id} value={account.id}>
{account.name}
</option>
))}
</select>
</div>
{form.type === 'direct_vps_payment' ? (
<div className="col-12">
<label className="form-label">VPS</label>
<select
className="form-select"
value={form.vpsId}
onChange={(e) => setForm((prev) => ({ ...prev, vpsId: e.target.value }))}
required
>
<option value="">Выберите VPS</option>
{vpsOptions.map((vps) => (
<option key={vps.id} value={vps.id}>
{vps.dns || vps.ip}
</option>
))}
</select>
</div>
) : null}
<div className="col-12 col-sm-6">
<label className="form-label">Дата</label>
<input
type="date"
className="form-control"
value={form.date}
onChange={(e) => setForm((prev) => ({ ...prev, date: e.target.value }))}
required
/>
</div>
<div className="col-12 col-sm-6">
<label className="form-label">Валюта</label>
<select
className="form-select"
value={form.currency}
onChange={(e) => setForm((prev) => ({ ...prev, currency: e.target.value }))}
>
<option>USD</option>
<option>EUR</option>
<option>RUB</option>
</select>
</div>
<div className="col-12">
<label className="form-label">Сумма</label>
<input
type="number"
step="0.01"
min="0.01"
className="form-control"
value={form.amount}
onChange={(e) => setForm((prev) => ({ ...prev, amount: e.target.value }))}
required
/>
</div>
<div className="col-12">
<label className="form-label">Комментарий</label>
<textarea
className="form-control"
value={form.note}
onChange={(e) => setForm((prev) => ({ ...prev, note: e.target.value }))}
/>
</div>
{error ? <div className="col-12 text-danger small">{error}</div> : null}
<div className="col-12 d-flex gap-2 justify-content-end">
<button type="button" className="btn btn-outline-secondary" onClick={() => setIsModalOpen(false)}>
Отмена
</button>
<button type="submit" className="btn btn-primary">
{editingId ? 'Сохранить' : 'Добавить'}
</button>
</div>
</form>
</UiModal>
</>
)
}