feat: Add currency support to billing data and enhance BillingManager with exchange rate fetching and formatting functions
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m47s

This commit is contained in:
2025-07-30 04:56:49 +07:00
parent 13767d7b3f
commit 4fbffad878
2 changed files with 152 additions and 17 deletions
+10
View File
@@ -7,9 +7,11 @@
"provider": "VDSINA",
"loginUrl": "cp.vdsina.com",
"monthlyCost": 12.00,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2026-04-25",
"lastPaymentDate": "2025-05-05",
"lastPaymentAmount": 12.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -21,9 +23,11 @@
"provider": "Macloud",
"loginUrl": "cp.macloud.ru",
"monthlyCost": 7.00,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2025-09-02",
"lastPaymentDate": "2025-02-03",
"lastPaymentAmount": 7.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -35,9 +39,11 @@
"provider": "Hosting VDS",
"loginUrl": "my.hosting-vds.com/",
"monthlyCost": 11.95,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2025-12-21",
"lastPaymentDate": "2025-06-13",
"lastPaymentAmount": 10.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -49,9 +55,11 @@
"provider": "Waicore",
"loginUrl": "my.waicore.com/billmgr",
"monthlyCost": 9.60,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2026-04-25",
"lastPaymentDate": "2025-04-25",
"lastPaymentAmount": 9.60,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -63,9 +71,11 @@
"provider": "IHOR",
"loginUrl": "billing.ihor-hosting.ru/billmgr",
"monthlyCost": 10.00,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2025-09-10",
"lastPaymentDate": "2025-06-25",
"lastPaymentAmount": 10.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
}
+142 -17
View File
@@ -37,6 +37,14 @@ function BillingManager() {
const [selectedItem, setSelectedItem] = useState(null);
const [editingItem, setEditingItem] = useState({});
// Состояние для курсов валют
const [exchangeRates, setExchangeRates] = useState({
USD: 1,
EUR: 1,
RUB: 1
});
const [ratesLoading, setRatesLoading] = useState(false);
// Новый элемент для добавления
const [newItem, setNewItem] = useState({
hostName: '',
@@ -45,17 +53,45 @@ function BillingManager() {
provider: '',
loginUrl: '',
monthlyCost: 0,
monthlyCostCurrency: 'USD',
nextPaymentDate: '',
lastPaymentDate: '',
lastPaymentAmount: 0,
lastPaymentCurrency: 'USD',
status: 'active',
notes: ''
});
useEffect(() => {
fetchBillingData();
fetchExchangeRates();
}, []);
const fetchExchangeRates = async () => {
setRatesLoading(true);
try {
// Используем бесплатный API для получения курсов валют
const response = await axios.get('https://api.exchangerate-api.com/v4/latest/USD');
const rates = response.data.rates;
setExchangeRates({
USD: 1,
EUR: rates.EUR || 1,
RUB: rates.RUB || 1
});
} catch (error) {
console.error('Ошибка при загрузке курсов валют:', error);
// Используем примерные курсы если API недоступен
setExchangeRates({
USD: 1,
EUR: 0.85,
RUB: 95
});
} finally {
setRatesLoading(false);
}
};
const fetchBillingData = async () => {
setLoading(true);
try {
@@ -75,9 +111,11 @@ function BillingManager() {
"provider": "VDSINA",
"loginUrl": "cp.vdsina.com",
"monthlyCost": 12.00,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2026-04-25",
"lastPaymentDate": "2025-05-05",
"lastPaymentAmount": 12.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -89,9 +127,11 @@ function BillingManager() {
"provider": "Macloud",
"loginUrl": "cp.macloud.ru",
"monthlyCost": 7.00,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2025-09-02",
"lastPaymentDate": "2025-02-03",
"lastPaymentAmount": 7.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -103,9 +143,11 @@ function BillingManager() {
"provider": "Hosting VDS",
"loginUrl": "my.hosting-vds.com/",
"monthlyCost": 11.95,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2025-12-21",
"lastPaymentDate": "2025-06-13",
"lastPaymentAmount": 10.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -117,9 +159,11 @@ function BillingManager() {
"provider": "Waicore",
"loginUrl": "my.waicore.com/billmgr",
"monthlyCost": 9.60,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2026-04-25",
"lastPaymentDate": "2025-04-25",
"lastPaymentAmount": 9.60,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
},
@@ -131,9 +175,11 @@ function BillingManager() {
"provider": "IHOR",
"loginUrl": "billing.ihor-hosting.ru/billmgr",
"monthlyCost": 10.00,
"monthlyCostCurrency": "USD",
"nextPaymentDate": "2025-09-10",
"lastPaymentDate": "2025-06-25",
"lastPaymentAmount": 10.00,
"lastPaymentCurrency": "USD",
"status": "active",
"notes": ""
}
@@ -247,8 +293,14 @@ function BillingManager() {
const totalPages = Math.ceil(filteredData.length / pageSize);
// Статистика
const totalMonthlyCost = billingData.reduce((sum, item) => sum + item.monthlyCost, 0);
const totalLastPayments = billingData.reduce((sum, item) => sum + item.lastPaymentAmount, 0);
const totalMonthlyCost = billingData.reduce((sum, item) => {
const currency = item.monthlyCostCurrency || 'USD';
return sum + convertToRUB(item.monthlyCost, currency);
}, 0);
const totalLastPayments = billingData.reduce((sum, item) => {
const currency = item.lastPaymentCurrency || 'USD';
return sum + convertToRUB(item.lastPaymentAmount, currency);
}, 0);
const activeProviders = [...new Set(billingData.map(item => item.provider))].length;
const activeNodes = billingData.filter(item => item.status === 'active').length;
@@ -274,13 +326,26 @@ function BillingManager() {
});
}
function formatCurrency(amount) {
function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
currency: currency
}).format(amount);
}
function convertToRUB(amount, currency) {
const rate = exchangeRates[currency] || 1;
return amount * rate;
}
function formatCurrencyInRUB(amount, currency) {
const rubAmount = convertToRUB(amount, currency);
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB'
}).format(rubAmount);
}
function formatPurpose(purpose) {
const purposeMap = {
'relay': 'Relay VPS',
@@ -375,7 +440,7 @@ function BillingManager() {
</span>
<div>
<div className="h3 mb-0 fw-bold">
{formatCurrency(totalLastPayments)}
{formatCurrencyInRUB(totalLastPayments, 'USD')}
</div>
<div className="text-muted lh-1">всего платежей</div>
</div>
@@ -390,7 +455,7 @@ function BillingManager() {
</span>
<div>
<div className="h3 mb-0 fw-bold">
{formatCurrency(totalMonthlyCost)}
{formatCurrencyInRUB(totalMonthlyCost, 'USD')}
</div>
<div className="text-muted lh-1">суммарных трат</div>
</div>
@@ -492,7 +557,10 @@ function BillingManager() {
<tbody>
{providers.map(provider => {
const providerItems = billingData.filter(item => item.provider === provider);
const totalCost = providerItems.reduce((sum, item) => sum + item.monthlyCost, 0);
const totalCost = providerItems.reduce((sum, item) => {
const currency = item.monthlyCostCurrency || 'USD';
return sum + convertToRUB(item.monthlyCost, currency);
}, 0);
const servers = providerItems.map(item => formatPurpose(item.purpose)).join(', ');
return (
@@ -509,7 +577,7 @@ function BillingManager() {
<IconExternalLink className="icon ms-1" size={14} />
</a>
</td>
<td>{formatCurrency(totalCost)}</td>
<td>{formatCurrencyInRUB(totalCost, 'USD')}</td>
<td>
<div className="d-flex align-items-center">
<span className="avatar avatar-sm me-1">
@@ -569,7 +637,14 @@ function BillingManager() {
<tr key={item.id}>
<td>{item.hostName}</td>
<td>{formatDate(item.lastPaymentDate)}</td>
<td>{formatCurrency(item.lastPaymentAmount)}</td>
<td>
<div>
<div>{formatCurrency(item.lastPaymentAmount, item.lastPaymentCurrency || 'USD')}</div>
<small className="text-muted">
{formatCurrencyInRUB(item.lastPaymentAmount, item.lastPaymentCurrency || 'USD')}
</small>
</div>
</td>
<td>
<div className="btn-list">
<button
@@ -764,8 +839,8 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
placeholder="example.com"
/>
</div>
<div className="col-md-6">
<label className="form-label">Ежемесячная стоимость ($)</label>
<div className="col-md-4">
<label className="form-label">Ежемесячная стоимость</label>
<input
type="number"
step="0.01"
@@ -775,6 +850,19 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
required
/>
</div>
<div className="col-md-2">
<label className="form-label">Валюта</label>
<select
className="form-select"
value={item.monthlyCostCurrency || 'USD'}
onChange={(e) => handleChange('monthlyCostCurrency', e.target.value)}
required
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="RUB">RUB</option>
</select>
</div>
<div className="col-md-6">
<label className="form-label">Следующий платеж</label>
<input
@@ -794,8 +882,8 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
onChange={(e) => handleChange('lastPaymentDate', e.target.value)}
/>
</div>
<div className="col-md-6">
<label className="form-label">Сумма последнего платежа ($)</label>
<div className="col-md-4">
<label className="form-label">Сумма последнего платежа</label>
<input
type="number"
step="0.01"
@@ -804,6 +892,18 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
onChange={(e) => handleChange('lastPaymentAmount', parseFloat(e.target.value) || 0)}
/>
</div>
<div className="col-md-2">
<label className="form-label">Валюта</label>
<select
className="form-select"
value={item.lastPaymentCurrency || 'USD'}
onChange={(e) => handleChange('lastPaymentCurrency', e.target.value)}
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="RUB">RUB</option>
</select>
</div>
<div className="col-md-6">
<label className="form-label">Статус</label>
<select
@@ -932,8 +1032,8 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
placeholder="example.com"
/>
</div>
<div className="col-md-6">
<label className="form-label">Ежемесячная стоимость ($)</label>
<div className="col-md-4">
<label className="form-label">Ежемесячная стоимость</label>
<input
type="number"
step="0.01"
@@ -943,6 +1043,19 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
required
/>
</div>
<div className="col-md-2">
<label className="form-label">Валюта</label>
<select
className="form-select"
value={item.monthlyCostCurrency || 'USD'}
onChange={(e) => handleChange('monthlyCostCurrency', e.target.value)}
required
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="RUB">RUB</option>
</select>
</div>
<div className="col-md-6">
<label className="form-label">Следующий платеж</label>
<input
@@ -962,8 +1075,8 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
onChange={(e) => handleChange('lastPaymentDate', e.target.value)}
/>
</div>
<div className="col-md-6">
<label className="form-label">Сумма последнего платежа ($)</label>
<div className="col-md-4">
<label className="form-label">Сумма последнего платежа</label>
<input
type="number"
step="0.01"
@@ -972,6 +1085,18 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
onChange={(e) => handleChange('lastPaymentAmount', parseFloat(e.target.value) || 0)}
/>
</div>
<div className="col-md-2">
<label className="form-label">Валюта</label>
<select
className="form-select"
value={item.lastPaymentCurrency || 'USD'}
onChange={(e) => handleChange('lastPaymentCurrency', e.target.value)}
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="RUB">RUB</option>
</select>
</div>
<div className="col-md-6">
<label className="form-label">Статус</label>
<select