init commit
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Утилиты vps-tracker: ID, URL, форматирование, валюта, лейблы, CSV
|
||||
*/
|
||||
|
||||
/**
|
||||
* Генерирует уникальный ID (UUID или fallback)
|
||||
* @returns {string}
|
||||
*/
|
||||
export function uid() {
|
||||
if (crypto && crypto.randomUUID) {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return `id-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет https:// к URL при отсутствии протокола
|
||||
* @param {string} website
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeWebsiteUrl(website) {
|
||||
if (!website) {
|
||||
return ''
|
||||
}
|
||||
if (website.startsWith('http://') || website.startsWith('https://')) {
|
||||
return website
|
||||
}
|
||||
return `https://${website}`
|
||||
}
|
||||
|
||||
/**
|
||||
* URL иконки сайта через Google Favicon API
|
||||
* @param {string} website
|
||||
* @returns {string}
|
||||
*/
|
||||
export function faviconUrlFromWebsite(website) {
|
||||
const normalized = normalizeWebsiteUrl(website)
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
const { hostname } = new URL(normalized)
|
||||
return `https://www.google.com/s2/favicons?domain=${hostname}&sz=32`
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const countryCodeByName = {
|
||||
germany: 'DE',
|
||||
netherlands: 'NL',
|
||||
russia: 'RU',
|
||||
usa: 'US',
|
||||
'united states': 'US',
|
||||
ukraine: 'UA',
|
||||
poland: 'PL',
|
||||
france: 'FR',
|
||||
spain: 'ES',
|
||||
italy: 'IT',
|
||||
estonia: 'EE',
|
||||
finland: 'FI',
|
||||
sweden: 'SE',
|
||||
norway: 'NO',
|
||||
latvia: 'LV',
|
||||
lithuania: 'LT',
|
||||
czechia: 'CZ',
|
||||
czech: 'CZ',
|
||||
singapore: 'SG',
|
||||
japan: 'JP',
|
||||
canada: 'CA',
|
||||
brazil: 'BR',
|
||||
turkey: 'TR',
|
||||
georgia: 'GE',
|
||||
kazakhstan: 'KZ',
|
||||
}
|
||||
|
||||
/**
|
||||
* Эмодзи флага страны по названию (ru-RU)
|
||||
* @param {string} country - название страны
|
||||
* @returns {string} эмодзи флага или 🌐
|
||||
*/
|
||||
export function getCountryFlagEmoji(country) {
|
||||
if (!country) {
|
||||
return '🌐'
|
||||
}
|
||||
const code = countryCodeByName[country.trim().toLowerCase()]
|
||||
if (!code) {
|
||||
return '🌐'
|
||||
}
|
||||
return code
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
|
||||
.join('')
|
||||
}
|
||||
|
||||
const paymentTypeLabels = {
|
||||
direct_vps_payment: 'Прямой платеж за VPS',
|
||||
provider_balance_topup: 'Пополнение баланса хостера',
|
||||
daily_debit: 'Ежедневное списание',
|
||||
monthly_debit: 'Ежемесячное списание',
|
||||
}
|
||||
|
||||
/**
|
||||
* Человекочитаемая метка типа платежа
|
||||
* @param {string} type - direct_vps_payment | provider_balance_topup | daily_debit | monthly_debit
|
||||
* @returns {string}
|
||||
*/
|
||||
export function paymentTypeLabel(type) {
|
||||
return paymentTypeLabels[type] || type
|
||||
}
|
||||
|
||||
const vpsStatusLabels = {
|
||||
active: 'Активен',
|
||||
paused: 'Приостановлен',
|
||||
archived: 'Архив',
|
||||
}
|
||||
|
||||
/**
|
||||
* Человекочитаемая метка статуса VPS
|
||||
* @param {string} status - active | paused | archived
|
||||
* @returns {string}
|
||||
*/
|
||||
export function vpsStatusLabel(status) {
|
||||
return vpsStatusLabels[status] || status
|
||||
}
|
||||
|
||||
const billingModeLabels = {
|
||||
daily: 'Ежедневно',
|
||||
monthly: 'Ежемесячно',
|
||||
}
|
||||
|
||||
/**
|
||||
* Человекочитаемая метка режима биллинга
|
||||
* @param {string} mode - daily | monthly
|
||||
* @returns {string}
|
||||
*/
|
||||
export function billingModeLabel(mode) {
|
||||
return billingModeLabels[mode] || mode
|
||||
}
|
||||
|
||||
const tariffTypeLabels = {
|
||||
daily: 'Суточный',
|
||||
monthly: 'Месячный',
|
||||
}
|
||||
|
||||
/**
|
||||
* Человекочитаемая метка типа тарифа
|
||||
* @param {string} type - daily | monthly
|
||||
* @returns {string}
|
||||
*/
|
||||
export function tariffTypeLabel(type) {
|
||||
return tariffTypeLabels[type] || type
|
||||
}
|
||||
|
||||
const CURRENCY_SYMBOL_MAP = {
|
||||
'€': 'EUR',
|
||||
'$': 'USD',
|
||||
'₽': 'RUB',
|
||||
'£': 'GBP',
|
||||
'¥': 'JPY',
|
||||
'₴': 'UAH',
|
||||
'₸': 'KZT',
|
||||
}
|
||||
|
||||
function toIsoCurrency(currency) {
|
||||
if (!currency || typeof currency !== 'string') return 'USD'
|
||||
const trimmed = currency.trim()
|
||||
if (CURRENCY_SYMBOL_MAP[trimmed]) return CURRENCY_SYMBOL_MAP[trimmed]
|
||||
if (trimmed.length === 3 && /^[A-Z]{3}$/i.test(trimmed)) return trimmed.toUpperCase()
|
||||
return 'USD'
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует сумму в валюте (ru-RU)
|
||||
* @param {number} amount
|
||||
* @param {string} [currency='USD']
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatCurrency(amount, currency = 'USD') {
|
||||
const safeAmount = Number.isFinite(Number(amount)) ? Number(amount) : 0
|
||||
const isoCurrency = toIsoCurrency(currency)
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: isoCurrency,
|
||||
minimumFractionDigits: 2,
|
||||
}).format(safeAmount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует сумму из одной валюты в другую по ratesData (CBR и т.п.)
|
||||
* @param {number} amount
|
||||
* @param {string} fromCurrency
|
||||
* @param {string} toCurrency
|
||||
* @param {object} ratesData - { base, rates: { USD: 1.2, EUR: 1.1, ... } }
|
||||
* @returns {number}
|
||||
*/
|
||||
export function convertCurrency(amount, fromCurrency, toCurrency, ratesData) {
|
||||
const safeAmount = Number(amount)
|
||||
if (!Number.isFinite(safeAmount)) {
|
||||
return 0
|
||||
}
|
||||
const from = toIsoCurrency(fromCurrency)
|
||||
const to = toIsoCurrency(toCurrency)
|
||||
if (!from || !to || from === to) {
|
||||
return safeAmount
|
||||
}
|
||||
if (!ratesData || !ratesData.rates || !ratesData.base) {
|
||||
return safeAmount
|
||||
}
|
||||
const apiBase = ratesData.base.toUpperCase()
|
||||
const rates = { ...ratesData.rates, [apiBase]: 1 }
|
||||
|
||||
if (!rates[from] || !rates[to]) {
|
||||
return safeAmount
|
||||
}
|
||||
|
||||
const amountInApiBase = safeAmount / rates[from]
|
||||
return amountInApiBase * rates[to]
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует сумму в базовой валюте приложения (settings.baseCurrency)
|
||||
* @param {number} amount
|
||||
* @param {string} currency
|
||||
* @param {object[]} appSettings - settings из API
|
||||
* @param {object} ratesData
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatInBaseCurrency(amount, currency, appSettings, ratesData) {
|
||||
const settings = appSettings?.[0] || {}
|
||||
const baseCurrency = settings.baseCurrency || 'RUB'
|
||||
const autoConvert = settings.autoConvert !== false
|
||||
|
||||
if (!autoConvert) {
|
||||
return formatCurrency(amount, currency)
|
||||
}
|
||||
|
||||
const converted = convertCurrency(amount, currency, baseCurrency, ratesData)
|
||||
return formatCurrency(converted, baseCurrency)
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует сумму в валюту отображения (из настроек).
|
||||
* provider.baseCurrency — валюта, в которой хостер принимает платежи.
|
||||
* settings.baseCurrency — валюта отображения на дашбордах.
|
||||
* Курсы хостера (usdRate, eurRate) — курс 1 USD/EUR в валюту отображения.
|
||||
*/
|
||||
export function convertWithProviderRate(amount, currency, provider, appSettings, ratesData) {
|
||||
const safeAmount = Number(amount)
|
||||
const appBase = (appSettings?.[0]?.baseCurrency || 'RUB').toUpperCase()
|
||||
if (!Number.isFinite(safeAmount)) {
|
||||
return { value: 0, currency: appBase, source: 'global' }
|
||||
}
|
||||
|
||||
const fromCurrency = toIsoCurrency(currency || appBase)
|
||||
|
||||
if (fromCurrency === appBase) {
|
||||
return { value: safeAmount, currency: appBase, source: 'native' }
|
||||
}
|
||||
|
||||
const usdRate = Number(provider?.usdRate)
|
||||
const eurRate = Number(provider?.eurRate)
|
||||
if (fromCurrency === 'USD' && Number.isFinite(usdRate) && usdRate > 0) {
|
||||
return { value: safeAmount * usdRate, currency: appBase, source: 'provider' }
|
||||
}
|
||||
if (fromCurrency === 'EUR' && Number.isFinite(eurRate) && eurRate > 0) {
|
||||
return { value: safeAmount * eurRate, currency: appBase, source: 'provider' }
|
||||
}
|
||||
|
||||
return {
|
||||
value: convertCurrency(safeAmount, fromCurrency, appBase, ratesData),
|
||||
currency: appBase,
|
||||
source: 'global',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует сумму с учётом курсов провайдера (usdRate, eurRate)
|
||||
* @param {number} amount
|
||||
* @param {string} currency
|
||||
* @param {object} provider
|
||||
* @param {object[]} appSettings
|
||||
* @param {object} ratesData
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatInProviderCurrency(amount, currency, provider, appSettings, ratesData) {
|
||||
const converted = convertWithProviderRate(amount, currency, provider, appSettings, ratesData)
|
||||
return formatCurrency(converted.value, converted.currency)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ключ месяца для группировки: "2025-03"
|
||||
* @param {string} dateString
|
||||
* @returns {string}
|
||||
*/
|
||||
export function monthKey(dateString) {
|
||||
const date = new Date(dateString)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return ''
|
||||
}
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Преобразует массив объектов в CSV-строку
|
||||
* @param {object[]} rows
|
||||
* @returns {string}
|
||||
*/
|
||||
export function toCsv(rows) {
|
||||
if (!rows.length) {
|
||||
return ''
|
||||
}
|
||||
const headers = Object.keys(rows[0])
|
||||
const escapeValue = (value) => {
|
||||
const str = `${value ?? ''}`
|
||||
if (str.includes('"') || str.includes(',') || str.includes('\n')) {
|
||||
return `"${str.replaceAll('"', '""')}"`
|
||||
}
|
||||
return str
|
||||
}
|
||||
const lines = [headers.join(',')]
|
||||
for (const row of rows) {
|
||||
lines.push(headers.map((h) => escapeValue(row[h])).join(','))
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициирует скачивание текстового файла (CSV)
|
||||
* @param {string} fileName
|
||||
* @param {string} content
|
||||
*/
|
||||
export function downloadTextFile(fileName, content) {
|
||||
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = url
|
||||
anchor.download = fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
Reference in New Issue
Block a user