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