feat: Add country flag display functionality in BillingManager for improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m47s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m47s
This commit is contained in:
@@ -314,6 +314,66 @@ function BillingManager() {
|
|||||||
return purposeMap[purpose] || purpose || 'Не указано';
|
return purposeMap[purpose] || purpose || 'Не указано';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Флаги стран (эмодзи) по коду/названию
|
||||||
|
function countryCodeToFlag(isoCode) {
|
||||||
|
if (!isoCode || isoCode.length !== 2) return '';
|
||||||
|
const codePoints = isoCode
|
||||||
|
.toUpperCase()
|
||||||
|
.split('')
|
||||||
|
.map(char => 127397 + char.charCodeAt(0));
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCountryFlag(countryRaw) {
|
||||||
|
if (!countryRaw) return '';
|
||||||
|
const value = String(countryRaw).trim();
|
||||||
|
if (!value) return '';
|
||||||
|
const upper = value.toUpperCase();
|
||||||
|
// Если уже ISO-2
|
||||||
|
if (/^[A-Z]{2}$/.test(upper)) {
|
||||||
|
return countryCodeToFlag(upper);
|
||||||
|
}
|
||||||
|
// Маппинг распространённых названий -> ISO-2
|
||||||
|
const mapNameToCode = {
|
||||||
|
// RU
|
||||||
|
'RUSSIA': 'RU', 'RUSSIAN FEDERATION': 'RU', 'РОССИЯ': 'RU',
|
||||||
|
// US
|
||||||
|
'USA': 'US', 'UNITED STATES': 'US', 'США': 'US', 'СОЕДИНЕННЫЕ ШТАТЫ': 'US',
|
||||||
|
// DE
|
||||||
|
'GERMANY': 'DE', 'DEUTSCHLAND': 'DE', 'ГЕРМАНИЯ': 'DE',
|
||||||
|
// NL
|
||||||
|
'NETHERLANDS': 'NL', 'NEDERLAND': 'NL', 'НИДЕРЛАНДЫ': 'NL',
|
||||||
|
// FR
|
||||||
|
'FRANCE': 'FR', 'ФРАНЦИЯ': 'FR',
|
||||||
|
// GB
|
||||||
|
'UNITED KINGDOM': 'GB', 'UK': 'GB', 'ВЕЛИКОБРИТАНИЯ': 'GB',
|
||||||
|
// SG
|
||||||
|
'SINGAPORE': 'SG', 'СИНГАПУР': 'SG',
|
||||||
|
// PL
|
||||||
|
'POLAND': 'PL', 'ПОЛЬША': 'PL',
|
||||||
|
// ES
|
||||||
|
'SPAIN': 'ES', 'ИСПАНИЯ': 'ES',
|
||||||
|
// IT
|
||||||
|
'ITALY': 'IT', 'ИТАЛИЯ': 'IT',
|
||||||
|
// TR
|
||||||
|
'TURKEY': 'TR', 'ТУРЦИЯ': 'TR',
|
||||||
|
// KZ
|
||||||
|
'KAZAKHSTAN': 'KZ', 'КАЗАХСТАН': 'KZ',
|
||||||
|
// LT, LV, EE, CZ, UA, BY, SE, FI, CA
|
||||||
|
'LITHUANIA': 'LT', 'ЛИТВА': 'LT',
|
||||||
|
'LATVIA': 'LV', 'ЛАТВИЯ': 'LV',
|
||||||
|
'ESTONIA': 'EE', 'ЭСТОНИЯ': 'EE',
|
||||||
|
'CZECHIA': 'CZ', 'CZECH REPUBLIC': 'CZ', 'ЧЕХИЯ': 'CZ',
|
||||||
|
'UKRAINE': 'UA', 'УКРАИНА': 'UA',
|
||||||
|
'BELARUS': 'BY', 'БЕЛАРУСЬ': 'BY',
|
||||||
|
'SWEDEN': 'SE', 'ШВЕЦИЯ': 'SE',
|
||||||
|
'FINLAND': 'FI', 'ФИНЛЯНДИЯ': 'FI',
|
||||||
|
'CANADA': 'CA', 'КАНАДА': 'CA',
|
||||||
|
};
|
||||||
|
const code = mapNameToCode[upper];
|
||||||
|
return code ? countryCodeToFlag(code) : '';
|
||||||
|
}
|
||||||
|
|
||||||
// Экспорт данных
|
// Экспорт данных
|
||||||
const exportData = () => {
|
const exportData = () => {
|
||||||
const csvContent = [
|
const csvContent = [
|
||||||
@@ -542,7 +602,7 @@ function BillingManager() {
|
|||||||
<h3 className="card-title">Оплачиваемые ноды</h3>
|
<h3 className="card-title">Оплачиваемые ноды</h3>
|
||||||
<div className="card-actions">
|
<div className="card-actions">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary"
|
||||||
onClick={handleSaveChanges}
|
onClick={handleSaveChanges}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
@@ -650,8 +710,9 @@ function BillingManager() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center gap-2">
|
||||||
{item.country}
|
<span style={{ fontSize: '18px' }}>{getCountryFlag(item.country)}</span>
|
||||||
|
<span>{item.country}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -697,14 +758,14 @@ function BillingManager() {
|
|||||||
<td>
|
<td>
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-primary"
|
className="btn btn-outline-primary btn-icon"
|
||||||
onClick={() => handleEditItem(item)}
|
onClick={() => handleEditItem(item)}
|
||||||
title="Редактировать"
|
title="Редактировать"
|
||||||
>
|
>
|
||||||
<IconEdit size={14} />
|
<IconEdit size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-success"
|
className="btn btn-outline-success btn-icon"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPaymentDraft({
|
setPaymentDraft({
|
||||||
serverId: item.id,
|
serverId: item.id,
|
||||||
@@ -717,14 +778,14 @@ function BillingManager() {
|
|||||||
}}
|
}}
|
||||||
title="Добавить платеж"
|
title="Добавить платеж"
|
||||||
>
|
>
|
||||||
<IconPlus size={14} />
|
<IconPlus size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-outline-danger"
|
className="btn btn-outline-danger btn-icon"
|
||||||
onClick={() => handleDeleteItem(item)}
|
onClick={() => handleDeleteItem(item)}
|
||||||
title="Удалить"
|
title="Удалить"
|
||||||
>
|
>
|
||||||
<IconTrash size={14} />
|
<IconTrash size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -786,8 +847,8 @@ function BillingManager() {
|
|||||||
История платежей
|
История платежей
|
||||||
</h3>
|
</h3>
|
||||||
<div className="card-actions">
|
<div className="card-actions">
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-primary btn-sm"
|
className="btn btn-outline-primary"
|
||||||
onClick={() => { setPaymentDraft({ serverId: '', date: '', amount: 0, currency: 'USD', note: '' }); setShowPaymentModal(true); }}
|
onClick={() => { setPaymentDraft({ serverId: '', date: '', amount: 0, currency: 'USD', note: '' }); setShowPaymentModal(true); }}
|
||||||
>
|
>
|
||||||
<IconPlus size={16} />
|
<IconPlus size={16} />
|
||||||
|
|||||||
Reference in New Issue
Block a user