/** * Утилиты 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) }