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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m47s
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user