diff --git a/frontend/src/admin/sections/SettingsSection.svelte b/frontend/src/admin/sections/SettingsSection.svelte index 050e061..361c4a9 100644 --- a/frontend/src/admin/sections/SettingsSection.svelte +++ b/frontend/src/admin/sections/SettingsSection.svelte @@ -38,6 +38,19 @@ let copiedWebhookKey = ""; let copiedWebhookTimer = null; + const PLATEGA_SBP_KEYS = new Set(["PLATEGA_SBP_ENABLED", "PLATEGA_SBP_METHOD"]); + const PLATEGA_CRYPTO_KEYS = new Set(["PLATEGA_CRYPTO_ENABLED", "PLATEGA_CRYPTO_METHOD"]); + const PLATEGA_LEGACY_KEYS = new Set(["PLATEGA_PAYMENT_METHOD"]); + const LEGACY_TARIFF_TRAFFIC_KEYS = new Set(["TRAFFIC_PACKAGES", "STARS_TRAFFIC_PACKAGES"]); + const SEMANTIC_FIELD_GROUP_ORDER = { + platega_common: 1, + platega_sbp: 2, + platega_crypto: 3, + platega_legacy: 4, + legacy_tariff_periods: 1, + legacy_tariff_traffic: 2, + }; + $: settingsAllOpen = visibleSettingsSections.length > 0 && settingsOpenSections.length === visibleSettingsSections.length; @@ -220,6 +233,108 @@ })); } + function fieldGroupMeta( + id, + titleKey, + titleFallback, + descriptionKey = "", + descriptionFallback = "" + ) { + return { id, titleKey, titleFallback, descriptionKey, descriptionFallback }; + } + + function plategaSemanticGroup(field) { + const key = String(field?.key || ""); + if (PLATEGA_SBP_KEYS.has(key) || key.startsWith("PAYMENT_PLATEGA_SBP_")) { + return fieldGroupMeta( + "platega_sbp", + "settings_group_platega_sbp", + "SBP/card button", + "settings_group_platega_sbp_hint", + "Visibility, method ID, and labels for the SBP/card payment button." + ); + } + if (PLATEGA_CRYPTO_KEYS.has(key) || key.startsWith("PAYMENT_PLATEGA_CRYPTO_")) { + return fieldGroupMeta( + "platega_crypto", + "settings_group_platega_crypto", + "Crypto button", + "settings_group_platega_crypto_hint", + "Visibility, method ID, and labels for the crypto payment button." + ); + } + if (PLATEGA_LEGACY_KEYS.has(key)) { + return fieldGroupMeta( + "platega_legacy", + "settings_group_platega_legacy", + "Legacy compatibility", + "settings_group_platega_legacy_hint", + "Fallback method for old Platega callbacks and deployments." + ); + } + return fieldGroupMeta( + "platega_common", + "settings_group_platega_common", + "Common settings", + "settings_group_platega_common_hint", + "Shared merchant credentials, redirects, and API endpoint." + ); + } + + function legacyTariffSemanticGroup(field) { + const key = String(field?.key || ""); + if (LEGACY_TARIFF_TRAFFIC_KEYS.has(key)) { + return fieldGroupMeta( + "legacy_tariff_traffic", + "settings_group_legacy_tariff_traffic", + "Legacy traffic packages", + "settings_group_legacy_tariff_traffic_hint", + "Packages used only when the JSON tariff catalog is not configured." + ); + } + return fieldGroupMeta( + "legacy_tariff_periods", + "settings_group_legacy_tariff_periods", + "Legacy subscription periods", + "settings_group_legacy_tariff_periods_hint", + "Month toggles and prices used only by the old remnawave-tg-shop mode." + ); + } + + function semanticFieldGroup(section, group, field) { + if (section?.id === "payments" && group?.id === "Platega") { + return plategaSemanticGroup(field); + } + if (section?.id === "pricing") { + return legacyTariffSemanticGroup(field); + } + return null; + } + + function semanticFieldGroups(section, group) { + const fields = group?.fields || []; + const result = new Map(); + for (const field of fields) { + const meta = semanticFieldGroup(section, group, field) || fieldGroupMeta("_default", "", ""); + if (!result.has(meta.id)) { + result.set(meta.id, { ...meta, fields: [] }); + } + result.get(meta.id).fields.push(field); + } + return Array.from(result.values()).sort( + (a, b) => + (SEMANTIC_FIELD_GROUP_ORDER[a.id] || 999) - (SEMANTIC_FIELD_GROUP_ORDER[b.id] || 999) + ); + } + + function fieldGroupTitle(group) { + return group.titleKey ? at(group.titleKey, {}, group.titleFallback) : ""; + } + + function fieldGroupDescription(group) { + return group.descriptionKey ? at(group.descriptionKey, {}, group.descriptionFallback) : ""; + } + function adminLocaleKey(key) { const raw = String(key || ""); return raw.startsWith("admin_") ? raw.slice("admin_".length) : raw; @@ -358,6 +473,35 @@ {/snippet} +{#snippet renderGroupedFields(section, group)} + {@const fieldGroups = semanticFieldGroups(section, group)} + {#if fieldGroups.length === 1 && !fieldGroups[0].titleKey} + {#each fieldGroups[0].fields as field} + {@render renderField(field)} + {/each} + {:else} +
+ {#each fieldGroups as fieldGroup} +
+ {#if fieldGroup.titleKey} +
+ {fieldGroupTitle(fieldGroup)} + {#if fieldGroupDescription(fieldGroup)} + {fieldGroupDescription(fieldGroup)} + {/if} +
+ {/if} +
+ {#each fieldGroup.fields as field} + {@render renderField(field)} + {/each} +
+
+ {/each} +
+ {/if} +{/snippet} + {#snippet renderField(field)} {@const revealed = isSecretRevealed(field.key)}
@@ -600,9 +744,7 @@ {#if rootGroup.webhook} {@render renderWebhookHint(rootGroup.webhook)} {/if} - {#each rootGroup.fields as field} - {@render renderField(field)} - {/each} + {@render renderGroupedFields(section, rootGroup)} {/if} {#if labelGroups.length} diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index e0c08f1..ca84000 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -1823,7 +1823,6 @@ font-size: 13px; font-weight: 600; color: var(--admin-text); - text-transform: capitalize; word-break: break-word; min-width: 0; } @@ -1851,8 +1850,43 @@ overflow: hidden; } +.admin-accordion-content[data-state="open"] { + animation: admin-accordion-open 140ms cubic-bezier(0.16, 1, 0.3, 1); +} + .admin-accordion-content[data-state="closed"] { - display: none; + animation: admin-accordion-close 110ms cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes admin-accordion-open { + from { + height: 0; + opacity: 0.72; + } + + to { + height: var(--bits-accordion-content-height); + opacity: 1; + } +} + +@keyframes admin-accordion-close { + from { + height: var(--bits-accordion-content-height); + opacity: 1; + } + + to { + height: 0; + opacity: 0.72; + } +} + +@media (prefers-reduced-motion: reduce) { + .admin-accordion-content[data-state="open"], + .admin-accordion-content[data-state="closed"] { + animation: none; + } } @media (max-width: 720px) { @@ -1996,6 +2030,50 @@ text-decoration: underline; } +.admin-settings-field-groups { + display: grid; + gap: 10px; + padding: 12px 18px 18px; +} + +.admin-settings-field-group { + display: grid; + min-width: 0; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--admin-border) 88%, transparent); + border-radius: 10px; + background: color-mix(in srgb, var(--admin-bg) 52%, transparent); +} + +.admin-settings-field-group-head { + display: grid; + gap: 3px; + padding: 10px 14px; + border-bottom: 1px solid var(--admin-border); + background: color-mix(in srgb, var(--admin-surface-2) 56%, transparent); +} + +.admin-settings-field-group-head strong { + color: var(--admin-text); + font-size: 12px; + font-weight: 750; + line-height: 1.25; +} + +.admin-settings-field-group-head small { + color: var(--admin-muted); + font-size: 11px; + line-height: 1.35; +} + +.admin-settings-field-group-body .admin-setting { + padding-inline: 14px; +} + +.admin-settings-field-group-body .admin-setting:last-child { + border-bottom: 0; +} + .admin-setting:last-child { border-bottom: 0; } @@ -2762,6 +2840,14 @@ justify-self: start; } + .admin-settings-field-groups { + padding: 10px 14px 14px; + } + + .admin-settings-field-group-body .admin-setting { + padding-inline: 12px; + } + .admin-card-head { padding: 12px 14px; } @@ -3489,8 +3575,7 @@ font-size: 12px; font-weight: 700; color: var(--admin-text); - text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0; min-width: 0; } diff --git a/locales/en.json b/locales/en.json index bba929e..db21e6c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -943,7 +943,7 @@ "admin_logs_empty": "No entries", "admin_settings_section_general": "General", "admin_settings_section_appearance": "Appearance", - "admin_settings_section_pricing": "Tariffs and prices", + "admin_settings_section_pricing": "Legacy tariff compatibility", "admin_settings_section_payments": "Payment systems", "admin_settings_section_trial": "Trial", "admin_settings_section_referral": "Referral program", @@ -951,6 +951,7 @@ "admin_settings_section_devices": "Devices", "admin_settings_section_support": "Support", "admin_settings_subsection_common": "Common", + "admin_settings_subsection_checkout": "Checkout", "admin_settings_subsection_remnawave": "Remnawave", "admin_settings_subsection_telegram_stars": "Telegram Stars", "admin_settings_subsection_yookassa": "YooKassa", @@ -1241,9 +1242,21 @@ "admin_ads_col_status": "Status", "admin_no_data": "No data", "admin_settings_hint": "Changes in the admin panel take precedence over .env. The 'Reset' button returns the value from environment variables.", - "admin_settings_legacy_tariffs_warning_title": "Legacy tariffs", - "admin_settings_legacy_tariffs_warning_body": "These settings do not apply when tariffs are configured in the dedicated Tariffs section.", + "admin_settings_legacy_tariffs_warning_title": "remnawave-tg-shop legacy compatibility", + "admin_settings_legacy_tariffs_warning_body": "These fields are only for the old mode without a JSON catalog. They do not apply when tariffs are configured in the dedicated Tariffs section.", "admin_settings_legacy_tariffs_warning_link": "Open Tariffs", + "admin_settings_group_platega_common": "Common settings", + "admin_settings_group_platega_common_hint": "Merchant details, API URL, and return pages shared by all Platega buttons.", + "admin_settings_group_platega_sbp": "SBP/card button", + "admin_settings_group_platega_sbp_hint": "Visibility, method ID, and labels for the card or SBP payment button.", + "admin_settings_group_platega_crypto": "Crypto button", + "admin_settings_group_platega_crypto_hint": "Visibility, method ID, and labels for the crypto payment button.", + "admin_settings_group_platega_legacy": "Legacy compatibility", + "admin_settings_group_platega_legacy_hint": "Fallback method for old Platega callbacks and deployments.", + "admin_settings_group_legacy_tariff_periods": "Legacy subscription periods", + "admin_settings_group_legacy_tariff_periods_hint": "Monthly periods and prices for compatibility with old remnawave-tg-shop mode.", + "admin_settings_group_legacy_tariff_traffic": "Legacy traffic packages", + "admin_settings_group_legacy_tariff_traffic_hint": "Traffic packages for the old mode without a JSON tariff catalog.", "admin_collapse_all": "Collapse all", "admin_expand_all": "Expand all", "admin_settings_params_count": "{count} parameters", diff --git a/locales/ru.json b/locales/ru.json index c7a4985..b4790d6 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -943,7 +943,7 @@ "admin_logs_empty": "Записей нет", "admin_settings_section_general": "Общие", "admin_settings_section_appearance": "Внешний вид", - "admin_settings_section_pricing": "Тарифы и цены", + "admin_settings_section_pricing": "Совместимость с legacy-тарифами", "admin_settings_section_payments": "Платёжные системы", "admin_settings_section_trial": "Триал", "admin_settings_section_referral": "Реферальная программа", @@ -951,6 +951,7 @@ "admin_settings_section_devices": "Устройства", "admin_settings_section_support": "Поддержка", "admin_settings_subsection_common": "Общие", + "admin_settings_subsection_checkout": "Оформление оплаты", "admin_settings_subsection_remnawave": "Remnawave", "admin_settings_subsection_telegram_stars": "Telegram Stars", "admin_settings_subsection_yookassa": "YooKassa", @@ -1241,9 +1242,21 @@ "admin_ads_col_status": "Статус", "admin_no_data": "Нет данных", "admin_settings_hint": "Изменения в админке имеют приоритет над .env. Кнопка «Сбросить» возвращает значение из переменных окружения.", - "admin_settings_legacy_tariffs_warning_title": "Legacy-тарифы", - "admin_settings_legacy_tariffs_warning_body": "Эти настройки не применяются, если тарифы настроены через отдельный раздел «Тарифы».", + "admin_settings_legacy_tariffs_warning_title": "Совместимость с remnawave-tg-shop legacy", + "admin_settings_legacy_tariffs_warning_body": "Эти поля нужны только для старого режима без JSON-каталога. Если тарифы настроены через отдельный раздел «Тарифы», они не применяются.", "admin_settings_legacy_tariffs_warning_link": "Открыть тарифы", + "admin_settings_group_platega_common": "Общие настройки", + "admin_settings_group_platega_common_hint": "Данные мерчанта, URL API и страницы возврата для всех кнопок Platega.", + "admin_settings_group_platega_sbp": "Кнопка SBP/карта", + "admin_settings_group_platega_sbp_hint": "Включение, method ID и тексты кнопки для оплаты картой или через SBP.", + "admin_settings_group_platega_crypto": "Кнопка Crypto", + "admin_settings_group_platega_crypto_hint": "Включение, method ID и тексты кнопки для криптооплаты.", + "admin_settings_group_platega_legacy": "Legacy-совместимость", + "admin_settings_group_platega_legacy_hint": "Fallback-метод для старых callback и старых установок Platega.", + "admin_settings_group_legacy_tariff_periods": "Legacy-подписки на срок", + "admin_settings_group_legacy_tariff_periods_hint": "Месячные периоды и цены для режима совместимости со старым remnawave-tg-shop.", + "admin_settings_group_legacy_tariff_traffic": "Legacy-пакеты трафика", + "admin_settings_group_legacy_tariff_traffic_hint": "Пакеты трафика для старого режима без JSON-каталога тарифов.", "admin_collapse_all": "Свернуть всё", "admin_expand_all": "Развернуть всё", "admin_settings_params_count": "{count} параметров",