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 ( <>

Настройки валют

setForm((prev) => ({ ...prev, ratesUrl: e.target.value }))} placeholder="https://www.cbr-xml-daily.ru/latest.js" />
Валюта отображения — в какой валюте показывать суммы на дашбордах. Курсы хостера (если указаны) имеют приоритет над глобальными курсами по ссылке выше.

Дополнительные поля VPS

Текстовые поля для расширенного режима просмотра списка VPS. Отображаются как колонки в таблице и в форме редактирования.

setNewFieldLabel(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCustomField())} />
{customFields.length > 0 ? (
    {customFields.map((f) => (
  • {f.label}
  • ))}
) : (
Нет дополнительных полей
)}

Синхронизация с API хостеров

Периодическая синхронизация данных из BILLmanager для аккаунтов с настроенным API. Два независимых интервала: VPS и платежи обновляются чаще, тарифы — реже.

setForm((prev) => ({ ...prev, syncIntervalMinutes: e.target.value }))} placeholder="60" />
setForm((prev) => ({ ...prev, syncTariffsIntervalMinutes: e.target.value }))} placeholder="1440" />
Тарифы меняются редко, можно ставить 24 ч (1440) и больше

Статус источника курсов

Источник: {current.ratesUrl}
Дата курсов: {ratesData?.date || '-'}
База API: {ratesData?.base || '-'}
{ratesError ?
{ratesError}
: null} {!ratesError && ratesData ? (
Курсы загружены. Текущая конвертация работает автоматически.
) : null}
) }