fix(vps): валюта тарифа — effectiveVpsTariffCurrency и поле в форме
- Для хостеров без API USD в VPS трактуется как дефолт формы, берётся baseCurrency хостера - В форме VPS добавлен выбор валюты тарифа; при смене хостера подставляется его валюта - Прогноз по проектам и ConvertedAmount для тарифа используют effectiveVpsTariffCurrency Made-with: Cursor
This commit is contained in:
@@ -171,6 +171,24 @@ function toIsoCurrency(currency) {
|
|||||||
return 'USD'
|
return 'USD'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валюта суммы тарифа VPS: поле vps.currency, иначе валюта приёма платежей хостера (справочник).
|
||||||
|
* Для хостеров без API «USD» в записи VPS часто остаётся дефолтом формы — тогда берём валюту хостера.
|
||||||
|
*/
|
||||||
|
export function effectiveVpsTariffCurrency(vps, provider) {
|
||||||
|
const ownRaw = (vps?.currency || '').trim()
|
||||||
|
const ownIso = ownRaw ? toIsoCurrency(ownRaw) : null
|
||||||
|
const provRaw = (provider?.baseCurrency || '').trim()
|
||||||
|
const provIso = provRaw ? toIsoCurrency(provRaw) : null
|
||||||
|
const noApi = !(provider?.apiType || '').trim()
|
||||||
|
if (noApi && ownIso === 'USD' && provIso && provIso !== 'USD') {
|
||||||
|
return provIso
|
||||||
|
}
|
||||||
|
if (ownIso) return ownIso
|
||||||
|
if (provIso) return provIso
|
||||||
|
return 'USD'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Форматирует сумму в валюте (ru-RU)
|
* Форматирует сумму в валюте (ru-RU)
|
||||||
* @param {number} amount
|
* @param {number} amount
|
||||||
|
|||||||
+45
-16
@@ -2,6 +2,7 @@ import { Fragment, useMemo, useState } from 'react'
|
|||||||
import { Link, useSearchParams } from 'react-router-dom'
|
import { Link, useSearchParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
convertCurrency,
|
convertCurrency,
|
||||||
|
effectiveVpsTariffCurrency,
|
||||||
faviconUrlFromWebsite,
|
faviconUrlFromWebsite,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
getCountryFlagEmoji,
|
getCountryFlagEmoji,
|
||||||
@@ -55,7 +56,7 @@ const emptyForm = {
|
|||||||
backupEnabled: false,
|
backupEnabled: false,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
tariffType: 'monthly',
|
tariffType: 'monthly',
|
||||||
currency: 'USD',
|
currency: '',
|
||||||
dailyRate: '',
|
dailyRate: '',
|
||||||
monthlyRate: '',
|
monthlyRate: '',
|
||||||
createdAt: new Date().toISOString().slice(0, 10),
|
createdAt: new Date().toISOString().slice(0, 10),
|
||||||
@@ -85,14 +86,16 @@ function accountSelectLabel(account, providerById, scopedProviderId) {
|
|||||||
return `${providerName} / ${name}`
|
return `${providerName} / ${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function vpsMonthlyEstimateInBase(item, baseCurrency, ratesData) {
|
function vpsMonthlyEstimateInBase(item, baseCurrency, ratesData, providerById) {
|
||||||
if (item.status !== 'active') return 0
|
if (item.status !== 'active') return 0
|
||||||
const tariffType = item.tariffType || (Number(item.dailyRate || 0) > 0 ? 'daily' : 'monthly')
|
const tariffType = item.tariffType || (Number(item.dailyRate || 0) > 0 ? 'daily' : 'monthly')
|
||||||
const amount =
|
const amount =
|
||||||
tariffType === 'daily'
|
tariffType === 'daily'
|
||||||
? Number(item.dailyRate || 0) * 30
|
? Number(item.dailyRate || 0) * 30
|
||||||
: Number(item.monthlyRate || 0)
|
: Number(item.monthlyRate || 0)
|
||||||
return convertCurrency(amount, item.currency || 'USD', baseCurrency, ratesData)
|
const provider = providerById.get(item.providerId)
|
||||||
|
const cur = effectiveVpsTariffCurrency(item, provider)
|
||||||
|
return convertCurrency(amount, cur, baseCurrency, ratesData)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDefaultVpsFilters(customFields) {
|
function buildDefaultVpsFilters(customFields) {
|
||||||
@@ -189,7 +192,9 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
const mr = Number(item.monthlyRate || 0)
|
const mr = Number(item.monthlyRate || 0)
|
||||||
const noMoney =
|
const noMoney =
|
||||||
(!Number.isFinite(dr) || dr <= 0) && (!Number.isFinite(mr) || mr <= 0)
|
(!Number.isFinite(dr) || dr <= 0) && (!Number.isFinite(mr) || mr <= 0)
|
||||||
const noCur = !(item.currency || '').trim()
|
const prov = providerById.get(item.providerId)
|
||||||
|
const noCur =
|
||||||
|
!(item.currency || '').trim() && !(prov?.baseCurrency || '').trim()
|
||||||
return noMoney || noCur
|
return noMoney || noCur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +206,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}, [healthKey, db.vps, db.providerAccounts, db.payments, db.balanceLedger])
|
}, [healthKey, db.vps, db.providerAccounts, db.payments, db.balanceLedger, providerById])
|
||||||
|
|
||||||
const filteredVps = useMemo(() => {
|
const filteredVps = useMemo(() => {
|
||||||
return db.vps.filter((item) => {
|
return db.vps.filter((item) => {
|
||||||
@@ -294,7 +299,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
items: filteredVps,
|
items: filteredVps,
|
||||||
count: filteredVps.length,
|
count: filteredVps.length,
|
||||||
forecast: filteredVps.reduce(
|
forecast: filteredVps.reduce(
|
||||||
(acc, item) => acc + vpsMonthlyEstimateInBase(item, baseCurrency, ratesData),
|
(acc, item) => acc + vpsMonthlyEstimateInBase(item, baseCurrency, ratesData, providerById),
|
||||||
0,
|
0,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -314,7 +319,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
return keys.map((key) => {
|
return keys.map((key) => {
|
||||||
const items = map.get(key)
|
const items = map.get(key)
|
||||||
const forecast = items.reduce(
|
const forecast = items.reduce(
|
||||||
(acc, item) => acc + vpsMonthlyEstimateInBase(item, baseCurrency, ratesData),
|
(acc, item) => acc + vpsMonthlyEstimateInBase(item, baseCurrency, ratesData, providerById),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@@ -325,7 +330,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
forecast,
|
forecast,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [filteredVps, filters.groupByProject, baseCurrency, ratesData])
|
}, [filteredVps, filters.groupByProject, baseCurrency, ratesData, providerById])
|
||||||
|
|
||||||
const tableColCount = 8 + (viewMode === 'extended' ? 16 + customFields.length : 0)
|
const tableColCount = 8 + (viewMode === 'extended' ? 16 + customFields.length : 0)
|
||||||
|
|
||||||
@@ -376,10 +381,14 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
customFields.forEach((f) => {
|
customFields.forEach((f) => {
|
||||||
customFieldValues[f.key] = form[f.key] ?? ''
|
customFieldValues[f.key] = form[f.key] ?? ''
|
||||||
})
|
})
|
||||||
|
const prov = providerById.get(form.providerId)
|
||||||
|
const resolvedCurrency =
|
||||||
|
(form.currency || '').trim() || (prov?.baseCurrency || '').trim() || 'USD'
|
||||||
const payload = {
|
const payload = {
|
||||||
...restForm,
|
...restForm,
|
||||||
...customFieldValues,
|
...customFieldValues,
|
||||||
additionalIps,
|
additionalIps,
|
||||||
|
currency: resolvedCurrency,
|
||||||
dailyRate: form.tariffType === 'daily' ? form.dailyRate : '',
|
dailyRate: form.tariffType === 'daily' ? form.dailyRate : '',
|
||||||
monthlyRate: form.tariffType === 'monthly' ? form.monthlyRate : '',
|
monthlyRate: form.tariffType === 'monthly' ? form.monthlyRate : '',
|
||||||
}
|
}
|
||||||
@@ -424,7 +433,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
status: vps.status || 'active',
|
status: vps.status || 'active',
|
||||||
tariffType:
|
tariffType:
|
||||||
vps.tariffType || (Number(vps.dailyRate || 0) > 0 ? 'daily' : 'monthly'),
|
vps.tariffType || (Number(vps.dailyRate || 0) > 0 ? 'daily' : 'monthly'),
|
||||||
currency: vps.currency || 'USD',
|
currency: effectiveVpsTariffCurrency(vps, providerById.get(vps.providerId)),
|
||||||
dailyRate: vps.dailyRate || '',
|
dailyRate: vps.dailyRate || '',
|
||||||
monthlyRate: vps.monthlyRate || '',
|
monthlyRate: vps.monthlyRate || '',
|
||||||
createdAt: vps.createdAt || '',
|
createdAt: vps.createdAt || '',
|
||||||
@@ -1319,7 +1328,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
</div>
|
</div>
|
||||||
<ConvertedAmount
|
<ConvertedAmount
|
||||||
amount={tariffAmount}
|
amount={tariffAmount}
|
||||||
currency={item.currency}
|
currency={effectiveVpsTariffCurrency(item, provider)}
|
||||||
provider={provider}
|
provider={provider}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
ratesData={ratesData}
|
ratesData={ratesData}
|
||||||
@@ -1438,13 +1447,18 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
<select autoComplete="off"
|
<select autoComplete="off"
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={form.providerId}
|
value={form.providerId}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
const newProviderId = e.target.value
|
||||||
|
const prov = providerById.get(newProviderId)
|
||||||
|
const nextCur =
|
||||||
|
(prov?.baseCurrency || '').trim().toUpperCase() || 'USD'
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
providerId: e.target.value,
|
providerId: newProviderId,
|
||||||
providerAccountId: '',
|
providerAccountId: '',
|
||||||
|
currency: nextCur,
|
||||||
}))
|
}))
|
||||||
}
|
}}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">— Выберите —</option>
|
<option value="">— Выберите —</option>
|
||||||
@@ -1674,7 +1688,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
<section className="vps-form-section">
|
<section className="vps-form-section">
|
||||||
<h6 className="vps-form-section-title">Тариф и оплата</h6>
|
<h6 className="vps-form-section-title">Тариф и оплата</h6>
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
<div className="col-12 col-sm-6 col-md-4">
|
<div className="col-12 col-sm-6 col-md-3">
|
||||||
<label className="form-label">Статус</label>
|
<label className="form-label">Статус</label>
|
||||||
<select autoComplete="off"
|
<select autoComplete="off"
|
||||||
className="form-select"
|
className="form-select"
|
||||||
@@ -1686,7 +1700,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
<option value="archived">{vpsStatusLabel('archived')}</option>
|
<option value="archived">{vpsStatusLabel('archived')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-sm-6 col-md-4">
|
<div className="col-12 col-sm-6 col-md-3">
|
||||||
<label className="form-label">Тип тарифа</label>
|
<label className="form-label">Тип тарифа</label>
|
||||||
<select autoComplete="off"
|
<select autoComplete="off"
|
||||||
className="form-select"
|
className="form-select"
|
||||||
@@ -1704,7 +1718,7 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
<option value="monthly">{tariffTypeLabel('monthly')}</option>
|
<option value="monthly">{tariffTypeLabel('monthly')}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-sm-6 col-md-4">
|
<div className="col-12 col-sm-6 col-md-3">
|
||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
{form.tariffType === 'daily' ? 'Суточный тариф' : 'Месячный тариф'}
|
{form.tariffType === 'daily' ? 'Суточный тариф' : 'Месячный тариф'}
|
||||||
</label>
|
</label>
|
||||||
@@ -1724,6 +1738,21 @@ export function VpsPage({ db, actions, settings, ratesData }) {
|
|||||||
placeholder="0.00"
|
placeholder="0.00"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 col-sm-6 col-md-3">
|
||||||
|
<label className="form-label">Валюта тарифа</label>
|
||||||
|
<select autoComplete="off"
|
||||||
|
className="form-select"
|
||||||
|
value={form.currency || (providerById.get(form.providerId)?.baseCurrency || '').trim() || 'USD'}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, currency: e.target.value }))}
|
||||||
|
>
|
||||||
|
<option value="RUB">RUB</option>
|
||||||
|
<option value="USD">USD</option>
|
||||||
|
<option value="EUR">EUR</option>
|
||||||
|
</select>
|
||||||
|
<div className="text-secondary small mt-1">
|
||||||
|
В списке VPS и конвертации используется эта валюта, а не только валюта хостера в справочнике.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="col-12 col-sm-6">
|
<div className="col-12 col-sm-6">
|
||||||
<label className="form-label">Дата создания</label>
|
<label className="form-label">Дата создания</label>
|
||||||
<input {...noBrowserSuggestProps}
|
<input {...noBrowserSuggestProps}
|
||||||
|
|||||||
Reference in New Issue
Block a user