From 5bb1400917f74339d9936edaa788c4acdec6db26 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Mon, 1 Jun 2026 11:55:41 +0300 Subject: [PATCH] chore: improve tariff admin layout --- .../src/admin/sections/TariffsSection.svelte | 444 +++++++++++------- frontend/src/styles/admin.css | 274 ++++++++--- locales/en.json | 16 + locales/ru.json | 16 + 4 files changed, 507 insertions(+), 243 deletions(-) diff --git a/frontend/src/admin/sections/TariffsSection.svelte b/frontend/src/admin/sections/TariffsSection.svelte index f5bda22..7d15750 100644 --- a/frontend/src/admin/sections/TariffsSection.svelte +++ b/frontend/src/admin/sections/TariffsSection.svelte @@ -211,9 +211,24 @@ } $: catalogCurrencyKey = normalizeCurrencyKey(tariffsCatalog.default_currency || "rub"); - $: defaultCurrencyDraft = catalogCurrencyKey.toUpperCase(); + $: catalogCurrencyCode = catalogCurrencyKey.toUpperCase(); + $: defaultCurrencyDraft = catalogCurrencyCode; $: defaultCurrencyDraftKey = normalizeCurrencyKey(defaultCurrencyDraft || "rub"); $: defaultCurrencyDirty = defaultCurrencyDraftKey !== catalogCurrencyKey; + $: providerSupportSummary = (providerCurrencySupport || []).reduce( + (summary, provider) => { + const enabled = Boolean(provider.enabled); + const configured = Boolean(provider.configured); + const supportsDefault = Boolean(provider.supports_default_currency); + summary.total += 1; + if (enabled) summary.enabled += 1; + if (enabled && configured) summary.configured += 1; + if (enabled && configured && supportsDefault) summary.available += 1; + if (enabled && configured && !supportsDefault) summary.blocked += 1; + return summary; + }, + { total: 0, enabled: 0, configured: 0, available: 0, blocked: 0 } + ); async function saveDefaultCurrency() { await tariffsStore.setDefaultCurrency(defaultCurrencyDraft); @@ -222,7 +237,8 @@ function providerCurrencyLabel(provider) { if (provider.accepts_any_currency) return at("tariff_provider_any_currency", {}, "Любая"); return ( - (provider.currencies || []).join(", ") || at("tariff_provider_not_declared", {}, "Не задано") + (provider.currencies || []).map((currency) => String(currency).toUpperCase()).join(", ") || + at("tariff_provider_not_declared", {}, "Не задано") ); } @@ -231,6 +247,13 @@ return provider.supports_default_currency ? "success" : "warning"; } + function providerCurrencyStatus(provider) { + if (!provider.enabled) return at("disabled", {}, "Отключен"); + if (!provider.configured) return at("status_not_configured", {}, "Не настроен"); + if (provider.supports_default_currency) return at("tariff_currency_supported", {}, "Доступен"); + return at("tariff_currency_unsupported", {}, "Заблокирован"); + } + function removeTrialSquad(uuid) { setCsvList( "TRIAL_SQUAD_UUIDS", @@ -654,185 +677,258 @@ -
-
-
-

{at("tariffs_title", {}, "Каталог тарифов")}

- {tariffsPath || "data/tariffs.json"} -
-
- - - {at("btn_refresh", {}, "Обновить")} - - - - {at("btn_create_tariff", {}, "Создать тариф")} - -
-
-
-
- - {catalogCurrencyKey.toUpperCase()} - {#if defaultCurrencyDirty} +
+
+
+
+
+

{at("tariffs_currency_title", {}, "Валюта каталога")}

+ + {at( + "tariffs_currency_subtitle", + {}, + "Цены тарифов и платёжные провайдеры проверяются по этой валюте." + )} + +
+ {catalogCurrencyCode} +
+
+
+ {at("tariffs_currency_current", {}, "Текущая валюта")} + {catalogCurrencyCode} +
+
+ + {#if defaultCurrencyDirty} + + + {tariffsSaving + ? at("btn_saving", {}, "Сохранение...") + : at("btn_save", {}, "Сохранить")} + + {/if} +
+
+
+ +
+
+
+

{at("tariffs_provider_title", {}, "Платёжные провайдеры")}

+ + {at( + "tariffs_provider_subtitle", + {}, + "Здесь видно, какие провайдеры смогут принять текущую валюту каталога." + )} + +
+
+ + {at( + "tariffs_provider_available_count", + { count: providerSupportSummary.available }, + "Доступно: {count}" + )} + + + {at( + "tariffs_provider_enabled_count", + { count: providerSupportSummary.enabled }, + "Включено: {count}" + )} + + {#if providerSupportSummary.blocked} + + {at( + "tariffs_provider_blocked_count", + { count: providerSupportSummary.blocked }, + "Не подходят: {count}" + )} + + {/if} +
+
+
+ {#if providerCurrencySupport?.length} +
+ {#each providerCurrencySupport as provider} +
+
+ {provider.label} + {providerCurrencyLabel(provider)} +
+ + {providerCurrencyStatus(provider)} + +
+ {/each} +
+ {:else} + + {at("tariffs_provider_empty", {}, "Данные по провайдерам пока не загружены.")} + + {/if} +
+
+
+ +
+
+
+

{at("tariffs_title", {}, "Каталог тарифов")}

+ + {at("tariffs_catalog_subtitle", {}, "Периоды, цены, трафик и доступы пользователей.")} + + {tariffsPath || "data/tariffs.json"} +
+
+ + + {at("btn_refresh", {}, "Обновить")} + - - {tariffsSaving - ? at("btn_saving", {}, "Сохранение...") - : at("btn_save", {}, "Сохранить")} + + {at("btn_create_tariff", {}, "Создать тариф")} +
+
+
+ {#if !tariffsCatalog.tariffs.length} + + {at( + "tariffs_catalog_empty", + {}, + "Каталог пуст. Добавьте первый тариф, после сохранения будет создан JSON-файл каталога." + )} + + {:else} +
+ {#each tariffsCatalog.tariffs as tariff} +
+
+
+
+ {tariffName(tariff)} + {#if tariff.key === tariffsCatalog.default_tariff} + {at("status_default", {}, "Default")} + {/if} +
+ {tariff.key} +
+ {#if tariff.enabled === false} + {at("status_disabled", {}, "Выключен")} + {:else} + {at("status_active", {}, "Активен")} + {/if} +
+

+ {tariff.descriptions?.ru || + tariff.descriptions?.en || + at("no_description", {}, "Без описания")} +

+
+ {tariff.billing_model === "traffic" + ? at("tariff_model_traffic", {}, "Трафик") + : at("tariff_model_periods", {}, "Периоды")} + {tariffPriceSummary(tariff)} + {at("tariff_squads", {}, "Squads")}: {(tariff.squad_uuids || []).length} + {at("tariff_premium", {}, "Premium")}: {(tariff.premium_squad_uuids || []) + .length + ? `${tariff.premium_monthly_gb || 0} GB` + : "—"} + {at("tariff_devices", {}, "Устройства")}: {tariff.hwid_device_limit ?? + "env"} +
+
+ tariffsStore.openEditTariff(tariff)}> + {at("btn_configure", {}, "Настроить")} + + tariffsStore.toggleTariffEnabled(tariff)} + disabled={tariffsSaving} + > + {tariff.enabled === false + ? at("btn_enable", {}, "Включить") + : at("btn_disable", {}, "Выключить")} + + tariffsStore.setDefaultTariff(tariff.key)} + disabled={tariffsSaving || + tariff.enabled === false || + tariff.key === tariffsCatalog.default_tariff} + > + {at("btn_set_default", {}, "По умолчанию")} + + + tariffsStore.updateState({ + tariffDeleteTarget: tariff, + tariffDeleteOpen: true, + })} + disabled={tariffsSaving} + aria-label={at("btn_delete_tariff", {}, "Удалить тариф")} + > + + +
+
+ {/each} +
{/if}
- {#if providerCurrencySupport?.length} -
- {#each providerCurrencySupport as provider} -
-
- {provider.label} - {providerCurrencyLabel(provider)} -
- - {#if !provider.enabled} - {at("disabled", {}, "Отключен")} - {:else if !provider.configured} - {at("status_not_configured", {}, "Не настроен")} - {:else if provider.supports_default_currency} - {at("tariff_currency_supported", {}, "Доступен")} - {:else} - {at("tariff_currency_unsupported", {}, "Заблокирован")} - {/if} - -
- {/each} -
- {/if} - {#if !tariffsCatalog.tariffs.length} - - {at( - "tariffs_catalog_empty", - {}, - "Каталог пуст. Добавьте первый тариф, после сохранения будет создан JSON-файл каталога." - )} - - {:else} -
- {#each tariffsCatalog.tariffs as tariff} -
-
-
-
- {tariffName(tariff)} - {#if tariff.key === tariffsCatalog.default_tariff} - {at("status_default", {}, "Default")} - {/if} -
- {tariff.key} -
- {#if tariff.enabled === false} - {at("status_disabled", {}, "Выключен")} - {:else} - {at("status_active", {}, "Активен")} - {/if} -
-

- {tariff.descriptions?.ru || - tariff.descriptions?.en || - at("no_description", {}, "Без описания")} -

-
- {tariff.billing_model === "traffic" - ? at("tariff_model_traffic", {}, "Трафик") - : at("tariff_model_periods", {}, "Периоды")} - {tariffPriceSummary(tariff)} - {at("tariff_squads", {}, "Squads")}: {(tariff.squad_uuids || []).length} - {at("tariff_premium", {}, "Premium")}: {(tariff.premium_squad_uuids || []).length - ? `${tariff.premium_monthly_gb || 0} GB` - : "—"} - {at("tariff_devices", {}, "Устройства")}: {tariff.hwid_device_limit ?? - "env"} -
-
- tariffsStore.openEditTariff(tariff)}> - {at("btn_configure", {}, "Настроить")} - - tariffsStore.toggleTariffEnabled(tariff)} - disabled={tariffsSaving} - > - {tariff.enabled === false - ? at("btn_enable", {}, "Включить") - : at("btn_disable", {}, "Выключить")} - - tariffsStore.setDefaultTariff(tariff.key)} - disabled={tariffsSaving || - tariff.enabled === false || - tariff.key === tariffsCatalog.default_tariff} - > - {at("btn_set_default", {}, "По умолчанию")} - - - tariffsStore.updateState({ - tariffDeleteTarget: tariff, - tariffDeleteOpen: true, - })} - disabled={tariffsSaving} - aria-label={at("btn_delete_tariff", {}, "Удалить тариф")} - > - - -
-
- {/each} -
- {/if} -
-
+ + .admin-card { + min-width: 0; +} + +@media (min-width: 980px) { + .admin-tariff-overview-grid { + grid-template-columns: minmax(260px, 0.82fr) minmax(0, 1.6fr); + align-items: stretch; + } +} + +.admin-tariff-panel-head > div, +.admin-tariff-list-head > div { + min-width: 0; +} + +.admin-tariff-panel-head small, +.admin-tariff-list-head small { + max-width: 72ch; +} + +.admin-tariff-path { + display: block; + width: fit-content; + max-width: 100%; + margin-top: 6px; + overflow: hidden; + color: var(--admin-dim); + font-family: var(--font-mono); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-tariff-currency-body { + display: grid; + align-content: start; + gap: 14px; +} + +.admin-tariff-currency-current { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-width: 0; + padding: 12px; + border: 1px solid var(--admin-border); + border-radius: 8px; + background: var(--admin-surface-2); +} + +.admin-tariff-currency-current span { + color: var(--admin-muted); + font-size: 12px; + font-weight: 600; +} + +.admin-tariff-currency-current strong { + color: var(--admin-text); + font-family: var(--font-mono); + font-size: 18px; + letter-spacing: 0; +} + +.admin-tariff-catalog-bar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: end; + gap: 10px; +} + +.admin-tariff-currency-field { + display: grid; + min-width: 0; + gap: 8px; +} + +.admin-tariff-currency-field > span { + color: var(--admin-text); + font-size: 13px; + font-weight: 500; +} + +.admin-currency-input { + width: 100%; + max-width: 160px; + text-transform: uppercase; +} + +.admin-provider-summary { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + gap: 6px; + min-width: 0; +} + +.admin-provider-currency-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 10px; +} + +.admin-provider-currency { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + min-width: 0; + padding: 10px 12px; + border: 1px solid var(--admin-border); + border-radius: 8px; + background: var(--admin-surface-2); +} + +.admin-provider-currency.is-supported { + border-color: color-mix(in srgb, var(--success-border) 70%, var(--admin-border)); +} + +.admin-provider-currency.is-unavailable { + opacity: 0.74; +} + +.admin-provider-currency-main { + display: grid; + gap: 3px; + min-width: 0; +} + +.admin-provider-currency-main strong, +.admin-provider-currency-main small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-provider-currency-main strong { + color: var(--admin-text); + font-size: 13px; + font-weight: 700; +} + +.admin-provider-currency-main small { + color: var(--admin-muted); + font-size: 11px; +} + +.admin-provider-currency > .admin-badge { + flex: 0 0 auto; +} + .admin-tariff-grid { display: grid; grid-template-columns: minmax(0, 1fr); @@ -3145,6 +3308,7 @@ .admin-tariff-card { display: grid; + align-content: start; gap: 12px; min-width: 0; border: 1px solid var(--admin-border); @@ -3168,6 +3332,11 @@ .admin-tariff-top { justify-content: space-between; + min-width: 0; +} + +.admin-tariff-top > div { + min-width: 0; } .admin-tariff-title { @@ -3204,79 +3373,13 @@ .admin-tariff-facts span { min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: anywhere; } .admin-tariff-actions { flex-wrap: wrap; } -.admin-tariff-catalog-bar { - display: flex; - align-items: end; - gap: 10px; - flex-wrap: wrap; - margin-bottom: 12px; -} - -.admin-currency-input { - width: 120px; - text-transform: uppercase; -} - -.admin-provider-currency-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 8px; - margin-bottom: 14px; -} - -.admin-provider-currency { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - min-width: 0; - padding: 9px 10px; - border: 1px solid var(--admin-border); - border-radius: 8px; - background: var(--admin-surface-2); -} - -.admin-provider-currency.is-supported { - border-color: color-mix(in srgb, var(--success-border) 70%, var(--admin-border)); -} - -.admin-provider-currency.is-unavailable { - opacity: 0.72; -} - -.admin-provider-currency div { - display: grid; - gap: 2px; - min-width: 0; -} - -.admin-provider-currency strong, -.admin-provider-currency small { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.admin-provider-currency strong { - color: var(--admin-text); - font-size: 12px; - font-weight: 700; -} - -.admin-provider-currency small { - color: var(--admin-muted); - font-size: 11px; -} - .admin-tariff-settings-card { margin-top: 14px; } @@ -3614,6 +3717,33 @@ padding: 14px; } + .admin-tariff-panel-head, + .admin-tariff-list-head { + gap: 10px; + } + + .admin-provider-summary { + justify-content: flex-start; + } + + .admin-tariff-catalog-bar { + grid-template-columns: minmax(0, 1fr); + } + + .admin-currency-input { + max-width: none; + } + + .admin-provider-currency { + align-items: flex-start; + flex-direction: column; + } + + .admin-provider-currency-main strong, + .admin-provider-currency-main small { + white-space: normal; + } + .admin-tariff-card { padding: 12px; } @@ -3626,12 +3756,18 @@ display: none; } + .admin-tariff-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .admin-tariff-actions .admin-btn { - flex: 1 1 calc(50% - 4px); + width: 100%; + min-width: 0; } .admin-tariff-actions .admin-btn[aria-label] { - flex: 0 0 auto; + width: 100%; } /* (legacy three-row user-row layout removed in favour of the compact diff --git a/locales/en.json b/locales/en.json index 2346c55..6acb519 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1233,6 +1233,7 @@ "admin_status_limited": "Limited", "admin_status_expired": "Expired", "admin_status_disabled": "Disabled", + "admin_status_not_configured": "Not configured", "admin_status_bot_only": "Bot Only", "admin_user_detail_title": "User #{id}", "admin_user_avatar_open": "Open avatar", @@ -1980,6 +1981,9 @@ "admin_tariff_btn_period": "Period", "admin_tariff_create_subtitle": "The catalog will be saved to JSON after confirmation", "admin_tariff_create_title": "New tariff", + "admin_tariff_currency_supported": "Available", + "admin_tariff_currency_unsupported": "Blocked", + "admin_tariff_default_currency": "Payment currency", "admin_tariff_default_updated": "Default tariff updated", "admin_tariff_delete_subtitle": "Tariff {key} will disappear from the sales catalog.", "admin_tariff_delete_title": "Delete tariff?", @@ -2026,6 +2030,8 @@ "admin_tariff_premium_subhead": "Premium squads give the user access to faster/premium nodes; their traffic is counted separately from main traffic so it can be limited or sold separately", "admin_tariff_premium_topup_subtitle": "Packages that extend the monthly premium limit when the user runs out", "admin_tariff_premium_topup_title": "Premium traffic top-up", + "admin_tariff_provider_any_currency": "Any", + "admin_tariff_provider_not_declared": "Not declared", "admin_tariff_pricing_empty": "Add at least one period so the tariff appears in the storefront.", "admin_tariff_pricing_period_subtitle": "Each row is a separate storefront option: how many months the user pays for and how much it costs", "admin_tariff_pricing_period_title": "Subscription periods and prices", @@ -2037,6 +2043,16 @@ "admin_tariff_topup_title": "Traffic top-up over the monthly limit", "admin_tariff_topup_traffic_hint": "For the traffic model, separate top-ups are not needed: packages configured on the Prices tab are the top-ups users can buy again as they run out.", "admin_tariff_visible": "Tariff is visible in the storefront", + "admin_tariffs_catalog_subtitle": "Periods, prices, traffic, and user access.", + "admin_tariffs_currency_current": "Current currency", + "admin_tariffs_currency_subtitle": "Tariff prices and payment providers are checked against this currency.", + "admin_tariffs_currency_title": "Catalog currency", + "admin_tariffs_provider_available_count": "Available: {count}", + "admin_tariffs_provider_blocked_count": "Unsupported: {count}", + "admin_tariffs_provider_empty": "Provider data has not been loaded yet.", + "admin_tariffs_provider_enabled_count": "Enabled: {count}", + "admin_tariffs_provider_subtitle": "Shows which providers can accept the current catalog currency.", + "admin_tariffs_provider_title": "Payment providers", "admin_tariffs_save_failed": "Failed to save tariffs", "admin_tariffs_saved": "Tariffs saved", "admin_tariffs_title": "Tariff catalog", diff --git a/locales/ru.json b/locales/ru.json index a8fc836..f35aa3a 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1233,6 +1233,7 @@ "admin_status_limited": "Ограничен", "admin_status_expired": "Истёк", "admin_status_disabled": "Выключен", + "admin_status_not_configured": "Не настроен", "admin_status_bot_only": "Только бот", "admin_user_detail_title": "Пользователь #{id}", "admin_user_avatar_open": "Открыть аватар", @@ -1980,6 +1981,9 @@ "admin_tariff_btn_period": "Период", "admin_tariff_create_subtitle": "Каталог будет сохранён в JSON после подтверждения", "admin_tariff_create_title": "Новый тариф", + "admin_tariff_currency_supported": "Доступен", + "admin_tariff_currency_unsupported": "Заблокирован", + "admin_tariff_default_currency": "Валюта оплаты", "admin_tariff_default_updated": "Тариф по умолчанию обновлён", "admin_tariff_delete_subtitle": "Тариф {key} исчезнет из каталога продаж.", "admin_tariff_delete_title": "Удалить тариф?", @@ -2026,6 +2030,8 @@ "admin_tariff_premium_subhead": "Premium-сквады дают пользователю доступ к более быстрым/премиальным нодам; их трафик считается отдельно от основного, чтобы можно было ограничить или продавать дополнительно", "admin_tariff_premium_topup_subtitle": "Пакеты для расширения месячного premium-лимита, когда пользователь его исчерпал", "admin_tariff_premium_topup_title": "Докупка premium-трафика", + "admin_tariff_provider_any_currency": "Любая", + "admin_tariff_provider_not_declared": "Не задано", "admin_tariff_pricing_empty": "Добавьте хотя бы один период — без него тариф не появится на витрине.", "admin_tariff_pricing_period_subtitle": "Каждая строка — отдельный вариант на витрине: за сколько месяцев пользователь платит и сколько это стоит", "admin_tariff_pricing_period_title": "Периоды подписки и цены", @@ -2037,6 +2043,16 @@ "admin_tariff_topup_title": "Докупка трафика поверх месячного лимита", "admin_tariff_topup_traffic_hint": "Для трафиковой модели отдельные «докупки» не нужны — пакеты, которые вы настроили на вкладке «Цены», и являются докупками: пользователь покупает их повторно по мере исчерпания.", "admin_tariff_visible": "Тариф виден на витрине", + "admin_tariffs_catalog_subtitle": "Периоды, цены, трафик и доступы пользователей.", + "admin_tariffs_currency_current": "Текущая валюта", + "admin_tariffs_currency_subtitle": "Цены тарифов и платёжные провайдеры проверяются по этой валюте.", + "admin_tariffs_currency_title": "Валюта каталога", + "admin_tariffs_provider_available_count": "Доступно: {count}", + "admin_tariffs_provider_blocked_count": "Не подходят: {count}", + "admin_tariffs_provider_empty": "Данные по провайдерам пока не загружены.", + "admin_tariffs_provider_enabled_count": "Включено: {count}", + "admin_tariffs_provider_subtitle": "Здесь видно, какие провайдеры смогут принять текущую валюту каталога.", + "admin_tariffs_provider_title": "Платёжные провайдеры", "admin_tariffs_save_failed": "Ошибка сохранения тарифов", "admin_tariffs_saved": "Тарифы сохранены", "admin_tariffs_title": "Каталог тарифов",