Files
vps-tracker/src/pages/SettingsPage.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

286 lines
12 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react'
import { IconPlus, IconTrash } from '@tabler/icons-react'
import { PageHeader } from '../components/PageHeader'
const defaultSettings = {
baseCurrency: 'RUB',
ratesUrl: 'https://www.cbr-xml-daily.ru/latest.js',
autoConvert: true,
syncEnabled: false,
syncIntervalMinutes: 60,
syncTariffsIntervalMinutes: 1440,
}
export function SettingsPage({ db, actions, ratesData, ratesError }) {
const current = db.settings?.[0] || defaultSettings
const [form, setForm] = useState({
baseCurrency: current.baseCurrency || 'RUB',
ratesUrl: current.ratesUrl || 'https://www.cbr-xml-daily.ru/latest.js',
autoConvert: current.autoConvert !== false,
syncEnabled: current.syncEnabled !== false && Boolean(current.syncEnabled),
syncIntervalMinutes: current.syncIntervalMinutes ?? 60,
syncTariffsIntervalMinutes: current.syncTariffsIntervalMinutes ?? 1440,
})
const [newFieldLabel, setNewFieldLabel] = useState('')
const customFields = Array.isArray(current.customFields) ? current.customFields : []
useEffect(() => {
/* eslint-disable-next-line react-hooks/set-state-in-effect -- sync form when settings change from parent */
setForm({
baseCurrency: current.baseCurrency || 'RUB',
ratesUrl: current.ratesUrl || 'https://www.cbr-xml-daily.ru/latest.js',
autoConvert: current.autoConvert !== false,
syncEnabled: Boolean(current.syncEnabled),
syncIntervalMinutes: current.syncIntervalMinutes ?? 60,
syncTariffsIntervalMinutes: current.syncTariffsIntervalMinutes ?? 1440,
})
}, [current.baseCurrency, current.ratesUrl, current.autoConvert, current.syncEnabled, current.syncIntervalMinutes, current.syncTariffsIntervalMinutes])
const availableCurrencies = useMemo(() => {
const list = new Set(['RUB', 'USD', 'EUR'])
if (ratesData?.rates) {
Object.keys(ratesData.rates).forEach((code) => list.add(code))
}
return [...list].sort()
}, [ratesData])
const onSubmit = (event) => {
event.preventDefault()
actions.upsertSettings({
baseCurrency: form.baseCurrency,
ratesUrl: form.ratesUrl,
autoConvert: form.autoConvert,
ratesUpdatedAt: ratesData?.date || '',
})
}
const onSyncSettingsSubmit = (event) => {
event.preventDefault()
actions.upsertSettings({
syncEnabled: form.syncEnabled,
syncIntervalMinutes: Math.max(15, Number(form.syncIntervalMinutes) || 60),
syncTariffsIntervalMinutes: Math.max(60, Number(form.syncTariffsIntervalMinutes) || 1440),
})
}
const addCustomField = () => {
const label = newFieldLabel.trim()
if (!label) return
const nextIndex = customFields.reduce((max, f) => {
const n = parseInt(f.key?.replace('cf_', '') || '0', 10)
return Math.max(max, n)
}, -1) + 1
const key = `cf_${nextIndex}`
actions.upsertSettings({ customFields: [...customFields, { key, label }] })
setNewFieldLabel('')
}
const removeCustomField = (key) => {
actions.upsertSettings({
customFields: customFields.filter((f) => f.key !== key),
})
}
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>
<div className="card-body">
<form className="row g-3" onSubmit={onSubmit}>
<div className="col-12 col-md-6">
<label className="form-label">Валюта отображения</label>
<select
className="form-select"
value={form.baseCurrency}
onChange={(e) => setForm((prev) => ({ ...prev, baseCurrency: e.target.value }))}
>
{availableCurrencies.map((code) => (
<option key={code} value={code}>
{code}
</option>
))}
</select>
</div>
<div className="col-12 col-md-6">
<label className="form-label">Автоконвертация</label>
<label className="form-check">
<input
className="form-check-input"
type="checkbox"
checked={form.autoConvert}
onChange={(e) => setForm((prev) => ({ ...prev, autoConvert: e.target.checked }))}
/>
<span className="form-check-label">Показывать суммы в валюте отображения</span>
</label>
</div>
<div className="col-12">
<label className="form-label">Ссылка на курсы валют</label>
<input
className="form-control"
value={form.ratesUrl}
onChange={(e) => setForm((prev) => ({ ...prev, ratesUrl: e.target.value }))}
placeholder="https://www.cbr-xml-daily.ru/latest.js"
/>
</div>
<div className="col-12 d-flex justify-content-end">
<button type="submit" className="btn btn-primary">
Сохранить настройки
</button>
</div>
<div className="col-12">
<div className="text-secondary small">
Валюта отображения в какой валюте показывать суммы на дашбордах. Курсы хостера (если указаны)
имеют приоритет над глобальными курсами по ссылке выше.
</div>
</div>
</form>
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="card">
<div className="card-header">
<h3 className="card-title">Дополнительные поля VPS</h3>
</div>
<div className="card-body">
<p className="text-secondary small mb-3">
Текстовые поля для расширенного режима просмотра списка VPS. Отображаются как колонки в таблице и в форме редактирования.
</p>
<div className="d-flex gap-2 mb-3">
<input
type="text"
className="form-control"
placeholder="Название поля (например: Контакт, ID заказа)"
value={newFieldLabel}
onChange={(e) => setNewFieldLabel(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCustomField())}
/>
<button
type="button"
className="btn btn-primary"
onClick={addCustomField}
>
<IconPlus size={16} />
</button>
</div>
{customFields.length > 0 ? (
<ul className="list-group list-group-flush">
{customFields.map((f) => (
<li key={f.key} className="list-group-item d-flex justify-content-between align-items-center">
<span>{f.label}</span>
<button
type="button"
className="btn btn-sm btn-outline-danger"
onClick={() => removeCustomField(f.key)}
>
<IconTrash size={14} />
</button>
</li>
))}
</ul>
) : (
<div className="text-secondary small">Нет дополнительных полей</div>
)}
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="card">
<div className="card-header">
<h3 className="card-title">Синхронизация с API хостеров</h3>
</div>
<div className="card-body">
<p className="text-secondary small mb-3">
Периодическая синхронизация данных из BILLmanager для аккаунтов с настроенным API.
Два независимых интервала: VPS и платежи обновляются чаще, тарифы реже.
</p>
<form className="row g-3" onSubmit={onSyncSettingsSubmit}>
<div className="col-12">
<label className="form-check">
<input
className="form-check-input"
type="checkbox"
checked={form.syncEnabled}
onChange={(e) => setForm((prev) => ({ ...prev, syncEnabled: e.target.checked }))}
/>
<span className="form-check-label">Включить периодическую синхронизацию</span>
</label>
</div>
<div className="col-12 col-md-6">
<label className="form-label">Интервал VPS и платежей (минуты)</label>
<input
type="number"
min="15"
className="form-control"
value={form.syncIntervalMinutes}
onChange={(e) => setForm((prev) => ({ ...prev, syncIntervalMinutes: e.target.value }))}
placeholder="60"
/>
</div>
<div className="col-12 col-md-6">
<label className="form-label">Интервал тарифов (минуты)</label>
<input
type="number"
min="60"
className="form-control"
value={form.syncTariffsIntervalMinutes}
onChange={(e) => setForm((prev) => ({ ...prev, syncTariffsIntervalMinutes: e.target.value }))}
placeholder="1440"
/>
<div className="text-secondary small mt-1">
Тарифы меняются редко, можно ставить 24 ч (1440) и больше
</div>
</div>
<div className="col-12 d-flex justify-content-end">
<button type="submit" className="btn btn-primary">
Сохранить
</button>
</div>
</form>
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="card">
<div className="card-header">
<h3 className="card-title">Статус источника курсов</h3>
</div>
<div className="card-body">
<div className="mb-2">
<span className="text-secondary">Источник:</span> {current.ratesUrl}
</div>
<div className="mb-2">
<span className="text-secondary">Дата курсов:</span> {ratesData?.date || '-'}
</div>
<div className="mb-2">
<span className="text-secondary">База API:</span> {ratesData?.base || '-'}
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm mb-3"
onClick={() => actions.upsertSettings({ ratesUpdatedAt: new Date().toISOString() })}
>
Обновить курсы сейчас
</button>
{ratesError ? <div className="alert alert-danger py-2">{ratesError}</div> : null}
{!ratesError && ratesData ? (
<div className="alert alert-success py-2 mb-0">
Курсы загружены. Текущая конвертация работает автоматически.
</div>
) : null}
</div>
</div>
</div>
</div>
</>
)
}