feat: Update BillingManager to replace 'nodeName' with 'purpose', add purpose formatting function, and enhance UI elements with consistent icon sizes
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m56s

This commit is contained in:
2025-07-30 04:47:17 +07:00
parent c2f0324817
commit 13767d7b3f
2 changed files with 164 additions and 132 deletions
+5 -5
View File
@@ -2,7 +2,7 @@
{ {
"id": "1", "id": "1",
"hostName": "VDSINA", "hostName": "VDSINA",
"nodeName": "VDSINA", "purpose": "relay",
"country": "RU", "country": "RU",
"provider": "VDSINA", "provider": "VDSINA",
"loginUrl": "cp.vdsina.com", "loginUrl": "cp.vdsina.com",
@@ -16,7 +16,7 @@
{ {
"id": "2", "id": "2",
"hostName": "Macloud", "hostName": "Macloud",
"nodeName": "Macloud", "purpose": "bgp",
"country": "RU", "country": "RU",
"provider": "Macloud", "provider": "Macloud",
"loginUrl": "cp.macloud.ru", "loginUrl": "cp.macloud.ru",
@@ -30,7 +30,7 @@
{ {
"id": "3", "id": "3",
"hostName": "Hosting VDS", "hostName": "Hosting VDS",
"nodeName": "Steal", "purpose": "monitoring",
"country": "RU", "country": "RU",
"provider": "Hosting VDS", "provider": "Hosting VDS",
"loginUrl": "my.hosting-vds.com/", "loginUrl": "my.hosting-vds.com/",
@@ -44,7 +44,7 @@
{ {
"id": "4", "id": "4",
"hostName": "Waicore", "hostName": "Waicore",
"nodeName": "Waicore", "purpose": "dns",
"country": "DE", "country": "DE",
"provider": "Waicore", "provider": "Waicore",
"loginUrl": "my.waicore.com/billmgr", "loginUrl": "my.waicore.com/billmgr",
@@ -58,7 +58,7 @@
{ {
"id": "5", "id": "5",
"hostName": "IHOR", "hostName": "IHOR",
"nodeName": "Ihore", "purpose": "proxy",
"country": "RU", "country": "RU",
"provider": "IHOR", "provider": "IHOR",
"loginUrl": "billing.ihor-hosting.ru/billmgr", "loginUrl": "billing.ihor-hosting.ru/billmgr",
+159 -127
View File
@@ -40,7 +40,7 @@ function BillingManager() {
// Новый элемент для добавления // Новый элемент для добавления
const [newItem, setNewItem] = useState({ const [newItem, setNewItem] = useState({
hostName: '', hostName: '',
nodeName: '', purpose: '',
country: '', country: '',
provider: '', provider: '',
loginUrl: '', loginUrl: '',
@@ -66,78 +66,78 @@ function BillingManager() {
console.error('Ошибка при загрузке данных биллинга:', err); console.error('Ошибка при загрузке данных биллинга:', err);
setError('Ошибка при загрузке данных'); setError('Ошибка при загрузке данных');
// Загружаем тестовые данные если API недоступен // Загружаем тестовые данные если API недоступен
setBillingData([ setBillingData([
{ {
"id": "1", "id": "1",
"hostName": "VDSINA", "hostName": "VDSINA",
"nodeName": "VDSINA", "purpose": "relay",
"country": "RU", "country": "RU",
"provider": "VDSINA", "provider": "VDSINA",
"loginUrl": "cp.vdsina.com", "loginUrl": "cp.vdsina.com",
"monthlyCost": 12.00, "monthlyCost": 12.00,
"nextPaymentDate": "2026-04-25", "nextPaymentDate": "2026-04-25",
"lastPaymentDate": "2025-05-05", "lastPaymentDate": "2025-05-05",
"lastPaymentAmount": 12.00, "lastPaymentAmount": 12.00,
"status": "active", "status": "active",
"notes": "" "notes": ""
}, },
{ {
"id": "2", "id": "2",
"hostName": "Macloud", "hostName": "Macloud",
"nodeName": "Macloud", "purpose": "bgp",
"country": "RU", "country": "RU",
"provider": "Macloud", "provider": "Macloud",
"loginUrl": "cp.macloud.ru", "loginUrl": "cp.macloud.ru",
"monthlyCost": 7.00, "monthlyCost": 7.00,
"nextPaymentDate": "2025-09-02", "nextPaymentDate": "2025-09-02",
"lastPaymentDate": "2025-02-03", "lastPaymentDate": "2025-02-03",
"lastPaymentAmount": 7.00, "lastPaymentAmount": 7.00,
"status": "active", "status": "active",
"notes": "" "notes": ""
}, },
{ {
"id": "3", "id": "3",
"hostName": "Hosting VDS", "hostName": "Hosting VDS",
"nodeName": "Steal", "purpose": "monitoring",
"country": "RU", "country": "RU",
"provider": "Hosting VDS", "provider": "Hosting VDS",
"loginUrl": "my.hosting-vds.com/", "loginUrl": "my.hosting-vds.com/",
"monthlyCost": 11.95, "monthlyCost": 11.95,
"nextPaymentDate": "2025-12-21", "nextPaymentDate": "2025-12-21",
"lastPaymentDate": "2025-06-13", "lastPaymentDate": "2025-06-13",
"lastPaymentAmount": 10.00, "lastPaymentAmount": 10.00,
"status": "active", "status": "active",
"notes": "" "notes": ""
}, },
{ {
"id": "4", "id": "4",
"hostName": "Waicore", "hostName": "Waicore",
"nodeName": "Waicore", "purpose": "dns",
"country": "DE", "country": "DE",
"provider": "Waicore", "provider": "Waicore",
"loginUrl": "my.waicore.com/billmgr", "loginUrl": "my.waicore.com/billmgr",
"monthlyCost": 9.60, "monthlyCost": 9.60,
"nextPaymentDate": "2026-04-25", "nextPaymentDate": "2026-04-25",
"lastPaymentDate": "2025-04-25", "lastPaymentDate": "2025-04-25",
"lastPaymentAmount": 9.60, "lastPaymentAmount": 9.60,
"status": "active", "status": "active",
"notes": "" "notes": ""
}, },
{ {
"id": "5", "id": "5",
"hostName": "IHOR", "hostName": "IHOR",
"nodeName": "Ihore", "purpose": "proxy",
"country": "RU", "country": "RU",
"provider": "IHOR", "provider": "IHOR",
"loginUrl": "billing.ihor-hosting.ru/billmgr", "loginUrl": "billing.ihor-hosting.ru/billmgr",
"monthlyCost": 10.00, "monthlyCost": 10.00,
"nextPaymentDate": "2025-09-10", "nextPaymentDate": "2025-09-10",
"lastPaymentDate": "2025-06-25", "lastPaymentDate": "2025-06-25",
"lastPaymentAmount": 10.00, "lastPaymentAmount": 10.00,
"status": "active", "status": "active",
"notes": "" "notes": ""
} }
]); ]);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -281,6 +281,19 @@ function BillingManager() {
}).format(amount); }).format(amount);
} }
function formatPurpose(purpose) {
const purposeMap = {
'relay': 'Relay VPS',
'bgp': 'BGP сервер',
'monitoring': 'Мониторинг',
'dns': 'DNS сервер',
'proxy': 'Прокси сервер',
'backup': 'Backup сервер',
'other': 'Другое'
};
return purposeMap[purpose] || purpose || 'Не указано';
}
return ( return (
<div> <div>
{/* Заголовок страницы */} {/* Заголовок страницы */}
@@ -291,23 +304,23 @@ function BillingManager() {
<div className="page-pretitle">Панель управления / Ноды / Инфра-биллинг</div> <div className="page-pretitle">Панель управления / Ноды / Инфра-биллинг</div>
</div> </div>
<div className="col-auto ms-auto d-print-none"> <div className="col-auto ms-auto d-print-none">
<div className="btn-list"> <div className="btn-list">
<button <button
className="btn btn-outline-primary" className="btn btn-outline-primary"
onClick={fetchBillingData} onClick={fetchBillingData}
disabled={loading} disabled={loading}
> >
<IconRefresh className="icon" /> <IconRefresh size={16} />
Обновить Обновить
</button> </button>
<button <button
className="btn btn-primary" className="btn btn-primary"
onClick={handleAddItem} onClick={handleAddItem}
> >
<IconPlus className="icon" /> <IconPlus size={16} />
Добавить Добавить
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -414,7 +427,7 @@ function BillingManager() {
onClick={handleSaveChanges} onClick={handleSaveChanges}
disabled={loading} disabled={loading}
> >
<IconDatabase className="icon" /> <IconDatabase size={16} />
Сохранить Сохранить
</button> </button>
</div> </div>
@@ -425,7 +438,7 @@ function BillingManager() {
<thead> <thead>
<tr> <tr>
<th>Имя хостера</th> <th>Имя хостера</th>
<th>Нода</th> <th>Назначение</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -441,10 +454,10 @@ function BillingManager() {
</td> </td>
<td> <td>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<span className="avatar avatar-sm me-2"> <span className="badge bg-blue-lt text-blue me-2">
{countryToFlag(item.country)} {formatPurpose(item.purpose)}
</span> </span>
{item.nodeName} {formatPurpose(item.purpose)}
</div> </div>
</td> </td>
</tr> </tr>
@@ -480,7 +493,7 @@ function BillingManager() {
{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) => sum + item.monthlyCost, 0);
const servers = providerItems.map(item => item.nodeName).join(', '); const servers = providerItems.map(item => formatPurpose(item.purpose)).join(', ');
return ( return (
<tr key={provider}> <tr key={provider}>
@@ -533,7 +546,7 @@ function BillingManager() {
className="btn btn-outline-primary btn-sm" className="btn btn-outline-primary btn-sm"
onClick={handleAddItem} onClick={handleAddItem}
> >
<IconPlus className="icon" /> <IconPlus size={16} />
</button> </button>
</div> </div>
</div> </div>
@@ -560,18 +573,18 @@ function BillingManager() {
<td> <td>
<div className="btn-list"> <div className="btn-list">
<button <button
className="btn btn-icon btn-sm" className="btn btn-icon btn-sm btn-outline-primary"
onClick={() => handleEditItem(item)} onClick={() => handleEditItem(item)}
title="Редактировать" title="Редактировать"
> >
<IconEdit className="icon" /> <IconEdit size={16} />
</button> </button>
<button <button
className="btn btn-icon btn-sm" className="btn btn-icon btn-sm btn-outline-danger"
onClick={() => handleDeleteItem(item)} onClick={() => handleDeleteItem(item)}
title="Удалить" title="Удалить"
> >
<IconTrash className="icon" /> <IconTrash size={16} />
</button> </button>
</div> </div>
</td> </td>
@@ -609,18 +622,18 @@ function BillingManager() {
<td> <td>
<div className="btn-list"> <div className="btn-list">
<button <button
className="btn btn-icon btn-sm" className="btn btn-icon btn-sm btn-outline-primary"
onClick={() => handleEditItem(item)} onClick={() => handleEditItem(item)}
title="Редактировать" title="Редактировать"
> >
<IconEdit className="icon" /> <IconEdit size={16} />
</button> </button>
<button <button
className="btn btn-icon btn-sm" className="btn btn-icon btn-sm btn-outline-danger"
onClick={() => handleDeleteItem(item)} onClick={() => handleDeleteItem(item)}
title="Удалить" title="Удалить"
> >
<IconTrash className="icon" /> <IconTrash size={16} />
</button> </button>
</div> </div>
</td> </td>
@@ -698,14 +711,22 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Нода</label> <label className="form-label">Назначение</label>
<input <select
type="text" className="form-select"
className="form-control" value={item.purpose || ''}
value={item.nodeName} onChange={(e) => handleChange('purpose', e.target.value)}
onChange={(e) => handleChange('nodeName', e.target.value)}
required required
/> >
<option value="">Выберите назначение</option>
<option value="relay">Relay VPS</option>
<option value="bgp">BGP сервер</option>
<option value="monitoring">Мониторинг</option>
<option value="dns">DNS сервер</option>
<option value="proxy">Прокси сервер</option>
<option value="backup">Backup сервер</option>
<option value="other">Другое</option>
</select>
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Страна</label> <label className="form-label">Страна</label>
@@ -812,6 +833,7 @@ function AddBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
Отмена Отмена
</button> </button>
<button type="submit" className="btn btn-primary"> <button type="submit" className="btn btn-primary">
<IconPlus size={16} className="me-1" />
Добавить Добавить
</button> </button>
</div> </div>
@@ -857,14 +879,22 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
/> />
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Нода</label> <label className="form-label">Назначение</label>
<input <select
type="text" className="form-select"
className="form-control" value={item.purpose || ''}
value={item.nodeName || ''} onChange={(e) => handleChange('purpose', e.target.value)}
onChange={(e) => handleChange('nodeName', e.target.value)}
required required
/> >
<option value="">Выберите назначение</option>
<option value="relay">Relay VPS</option>
<option value="bgp">BGP сервер</option>
<option value="monitoring">Мониторинг</option>
<option value="dns">DNS сервер</option>
<option value="proxy">Прокси сервер</option>
<option value="backup">Backup сервер</option>
<option value="other">Другое</option>
</select>
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label">Страна</label> <label className="form-label">Страна</label>
@@ -971,6 +1001,7 @@ function EditBillingModal({ show, item, onItemChange, onSubmit, onClose }) {
Отмена Отмена
</button> </button>
<button type="submit" className="btn btn-primary"> <button type="submit" className="btn btn-primary">
<IconCheck size={16} className="me-1" />
Сохранить Сохранить
</button> </button>
</div> </div>
@@ -997,14 +1028,15 @@ function DeleteBillingModal({ show, item, onDelete, onClose }) {
<p>Вы уверены, что хотите удалить ноду <strong>{item?.hostName}</strong>?</p> <p>Вы уверены, что хотите удалить ноду <strong>{item?.hostName}</strong>?</p>
<p className="text-muted">Это действие нельзя отменить.</p> <p className="text-muted">Это действие нельзя отменить.</p>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={onClose}> <button type="button" className="btn btn-secondary" onClick={onClose}>
Отмена Отмена
</button> </button>
<button type="button" className="btn btn-danger" onClick={onDelete}> <button type="button" className="btn btn-danger" onClick={onDelete}>
Удалить <IconTrash size={16} className="me-1" />
</button> Удалить
</div> </button>
</div>
</div> </div>
</div> </div>
</div> </div>