import { useMemo, useState } from 'react' import { convertCurrency, formatCurrency, faviconUrlFromWebsite, normalizeWebsiteUrl } from '../lib/utils' import { IconArrowDown, IconArrowUp, IconMapPin, IconRefresh, IconSearch, IconServer, } from '@tabler/icons-react' import { syncAccount } from '../lib/api' import { EmptyState } from '../components/EmptyState' import { PageHeader } from '../components/PageHeader' import { noBrowserSuggestProps } from '../lib/noBrowserSuggestProps' import { billmanagerSyncableAccounts } from '../lib/billmanager-ui' const SORT_COLUMNS = ['name', 'vcpu', 'ramGb', 'diskGb', 'diskType', 'virtualization', 'channel', 'country', 'location', 'price'] function SortHeader({ column, children, onSort, sortBy, sortDir }) { return ( onSort(column)} onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && onSort(column)} style={SORT_COLUMNS.includes(column) ? { cursor: 'pointer', userSelect: 'none' } : undefined} > {children} {sortBy === column && (sortDir === 'asc' ? : )} ) } function parsePrice(priceStr) { if (!priceStr || typeof priceStr !== 'string') return { amount: 0, currency: 'RUB' } const match = priceStr.match(/([\d\s.,]+)\s*(RUB|USD|EUR|€|₽|\$)/i) || priceStr.match(/([\d\s.,]+)\s+([A-Z]{3})\b/i) if (!match) return { amount: 0, currency: 'RUB' } const amount = parseFloat(String(match[1]).replace(/\s/g, '').replace(',', '.')) || 0 let currency = 'RUB' if (match[2]) { if (match[2] === '€') currency = 'EUR' else if (match[2] === '₽' || match[2].toUpperCase() === 'RUB') currency = 'RUB' else if (match[2] === '$' || match[2].toUpperCase() === 'USD') currency = 'USD' else currency = match[2].toUpperCase() } return { amount, currency } } export function TariffsPage({ db, actions, settings, ratesData }) { const [filters, setFilters] = useState({ search: '', providerId: '', providerAccountId: '', country: '', orderAvailable: 'all', }) const [syncLoading, setSyncLoading] = useState(false) const [syncMessage, setSyncMessage] = useState(null) const [sortBy, setSortBy] = useState('name') const [sortDir, setSortDir] = useState('asc') const baseCurrency = (settings?.[0]?.baseCurrency || 'RUB').toUpperCase() const billmanagerAccounts = useMemo( () => billmanagerSyncableAccounts(db.providerAccounts, db.providers), [db.providerAccounts, db.providers], ) const filteredAndSortedTariffs = useMemo(() => { const filtered = db.activeTariffs.filter((item) => { const search = filters.search.toLowerCase() const bySearch = !search || item.name?.toLowerCase().includes(search) || item.desc?.toLowerCase().includes(search) || item.location?.toLowerCase().includes(search) || item.country?.toLowerCase().includes(search) || item.datacenterName?.toLowerCase().includes(search) || item.cpuModel?.toLowerCase().includes(search) || String(item.vcpu || '').includes(search) || String(item.ramGb || '').includes(search) || String(item.diskGb || '').includes(search) || item.diskType?.toLowerCase().includes(search) || item.virtualization?.toLowerCase().includes(search) const byProvider = !filters.providerId || item.providerId === filters.providerId const byAccount = !filters.providerAccountId || item.providerAccountId === filters.providerAccountId const byCountry = !filters.country || item.country === filters.country const byOrderAvailable = filters.orderAvailable === 'all' || (filters.orderAvailable === 'yes' && item.orderAvailable) || (filters.orderAvailable === 'no' && !item.orderAvailable) return bySearch && byProvider && byAccount && byCountry && byOrderAvailable }) const sorted = [...filtered].sort((a, b) => { let cmp = 0 if (sortBy === 'price') { const pa = parsePrice(a.price) const pb = parsePrice(b.price) const va = convertCurrency(pa.amount, pa.currency, baseCurrency, ratesData) const vb = convertCurrency(pb.amount, pb.currency, baseCurrency, ratesData) cmp = va - vb } else if (['vcpu', 'ramGb', 'diskGb'].includes(sortBy)) { const va = Number(a[sortBy]) || 0 const vb = Number(b[sortBy]) || 0 cmp = va - vb } else { const va = String(a[sortBy] ?? '').toLowerCase() const vb = String(b[sortBy] ?? '').toLowerCase() cmp = va.localeCompare(vb) } return sortDir === 'asc' ? cmp : -cmp }) return sorted }, [db.activeTariffs, filters, sortBy, sortDir, baseCurrency, ratesData]) const handleSort = (col) => { if (!SORT_COLUMNS.includes(col)) return setSortBy(col) setSortDir((prev) => (sortBy === col && prev === 'asc' ? 'desc' : 'asc')) } const accountFilterOptions = useMemo( () => db.providerAccounts.filter( (account) => !filters.providerId || account.providerId === filters.providerId, ), [db.providerAccounts, filters.providerId], ) const availableCountries = useMemo(() => { const filtered = db.activeTariffs.filter((item) => { const byProvider = !filters.providerId || item.providerId === filters.providerId const byAccount = !filters.providerAccountId || item.providerAccountId === filters.providerAccountId return byProvider && byAccount && item.country }) const countries = [...new Set(filtered.map((t) => t.country).filter(Boolean))].sort() return countries }, [db.activeTariffs, filters.providerId, filters.providerAccountId]) const onSync = async () => { if (billmanagerAccounts.length === 0) return setSyncLoading(true) setSyncMessage(null) let totalTariffs = 0 let lastError = null for (const account of billmanagerAccounts) { try { const result = await syncAccount(account.id, { onlyTariffs: true }) if (result.ok) { totalTariffs += result.synced?.tariffsCount ?? 0 } else { lastError = result.error } } catch (err) { lastError = err.message } } if (lastError && totalTariffs === 0) { setSyncMessage(lastError) } else { setSyncMessage( totalTariffs > 0 ? `Синхронизировано: ${totalTariffs} тарифов${lastError ? `. Ошибки: ${lastError}` : ''}` : lastError ? `Ошибка: ${lastError}` : 'Нет новых тарифов для синхронизации', ) } if (totalTariffs > 0) await actions.refreshData() setSyncLoading(false) } const resetFilters = () => { setFilters({ search: '', providerId: '', providerAccountId: '', country: '', orderAvailable: 'all', }) } const syncOptionsByAccount = useMemo(() => { const map = {} for (const opt of db.tariffSyncOptions || []) { map[opt.providerAccountId] = opt } return map }, [db.tariffSyncOptions]) return ( <>
{Object.keys(syncOptionsByAccount).length > 0 ? (

Доступные датацентры и страны

{Object.entries(syncOptionsByAccount).map(([accountId, opt]) => { const account = db.providerAccounts.find((a) => a.id === accountId) const provider = db.providers.find((p) => p.id === account?.providerId) const dcs = opt.datacenters || [] const periods = opt.periods || [] if (dcs.length === 0) return null return (
{provider?.name} / {account?.name}
{dcs.map((dc) => ( {dc.v} ))}
{periods.length > 0 ? (
Периоды: {periods.map((p) => p.v).join(', ')}
) : null}
) })}
) : null}
setFilters((prev) => ({ ...prev, search: e.target.value })) } />

Список тарифов

{syncMessage ? (
{syncMessage}
) : null}
{billmanagerAccounts.length > 0 ? ( ) : ( Добавьте аккаунт BILLmanager для синхронизации тарифов )}
ТарифvCPURAMДискТип дискаВиртуализацияКаналСтранаЛокацияЦена {filteredAndSortedTariffs.map((item) => { const provider = db.providers.find((p) => p.id === item.providerId) const account = db.providerAccounts.find( (a) => a.id === item.providerAccountId, ) const { amount: priceAmount, currency: priceCurrency } = parsePrice(item.price) const priceInBase = convertCurrency(priceAmount, priceCurrency, baseCurrency, ratesData) const vcpu = Number(item.vcpu) || 0 const ramGb = Number(item.ramGb) || 0 const diskGb = Number(item.diskGb) || 0 const pricePerVcpu = vcpu > 0 ? priceInBase / vcpu : null const pricePerRam = ramGb > 0 ? priceInBase / ramGb : null const pricePerDisk = diskGb > 0 ? priceInBase / diskGb : null return ( ) })} {filteredAndSortedTariffs.length === 0 ? ( ) : null}
Хостер / Аккаунт CPU ₽/vCPU ₽/GB RAM ₽/GB диск Заказ Панель
{item.name || '—'}
{item.desc ? (
{item.desc}
) : null}
{faviconUrlFromWebsite(provider?.website) ? ( ) : null} {provider?.name || '—'}
{account?.name || '—'}
{item.vcpu || '—'} {item.ramGb ? `${item.ramGb} GB` : '—'} {item.diskGb ? `${item.diskGb} GB` : '—'} {item.diskType || '—'} {item.virtualization || '—'} {item.channel || '—'} {item.country ? ( {item.country} ) : ( '—' )} {item.location || item.datacenterName || '—'} {item.cpuModel ? ( {item.cpuModel.length > 20 ? `${item.cpuModel.slice(0, 20)}…` : item.cpuModel} ) : ( '—' )} {item.price || '—'} {pricePerVcpu != null ? formatCurrency(pricePerVcpu, baseCurrency) : '—'} {pricePerRam != null ? formatCurrency(pricePerRam, baseCurrency) : '—'} {pricePerDisk != null ? formatCurrency(pricePerDisk, baseCurrency) : '—'} {item.orderAvailable ? 'Да' : 'Нет'} {normalizeWebsiteUrl(account?.panelUrl || provider?.website) ? ( Открыть ) : ( )}
) }