feat: streamline admin tariff settings

This commit is contained in:
3252a8
2026-05-24 19:10:36 +03:00
parent 2b3078fcce
commit 3c4ff66150
6 changed files with 305 additions and 179 deletions
@@ -7,7 +7,6 @@
EyeOff,
FileText,
Search,
TriangleAlert,
X,
} from "$components/ui/icons.js";
import * as UiIcons from "$components/ui/icons.js";
@@ -28,7 +27,12 @@
const settingsStore = getContext("settingsStore");
$: ({ settingsSections, settingsLoading, settingsDirty, settingsSaving } = $settingsStore);
$: visibleSettingsSections = settingsSections.filter((section) => section.id !== "appearance");
const SETTINGS_SECTION_IDS_HIDDEN_IN_GENERAL_SETTINGS = new Set(["appearance", "pricing"]);
$: visibleSettingsSections = settingsSections.filter(
(section) => !SETTINGS_SECTION_IDS_HIDDEN_IN_GENERAL_SETTINGS.has(section.id)
);
let settingsOpenSections = [];
let settingsOpenSubsections = {};
@@ -41,22 +45,11 @@
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 TRIAL_TARIFF_KEYS = new Set([
"TRIAL_ENABLED",
"TRIAL_DURATION_DAYS",
"TRIAL_TRAFFIC_LIMIT_GB",
"TRIAL_TRAFFIC_STRATEGY",
"TRIAL_SQUAD_UUIDS",
]);
const SEMANTIC_FIELD_GROUP_ORDER = {
platega_common: 1,
platega_sbp: 2,
platega_crypto: 3,
platega_legacy: 4,
trial_tariff_settings: 0,
legacy_tariff_periods: 1,
legacy_tariff_traffic: 2,
};
$: settingsAllOpen =
@@ -289,42 +282,10 @@
);
}
function legacyTariffSemanticGroup(field) {
const key = String(field?.key || "");
if (TRIAL_TARIFF_KEYS.has(key)) {
return fieldGroupMeta(
"trial_tariff_settings",
"settings_group_trial_tariff_settings",
"Trial access",
"settings_group_trial_tariff_settings_hint",
"Trial duration, traffic limit, and Remnawave squads are also available on the Tariffs page."
);
}
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;
}
@@ -471,25 +432,6 @@
</div>
{/snippet}
{#snippet renderLegacyTariffsWarning()}
<div class="admin-settings-warning" role="status">
<TriangleAlert size={16} aria-hidden="true" />
<div class="admin-settings-warning-copy">
<strong>{at("settings_legacy_tariffs_warning_title", {}, "Legacy tariffs")}</strong>
<p>
{at(
"settings_legacy_tariffs_warning_body",
{},
"These settings are ignored when tariffs are configured in the dedicated Tariffs section."
)}
</p>
</div>
<a class="admin-settings-warning-link" href="/admin/tariffs">
{at("settings_legacy_tariffs_warning_link", {}, "Open Tariffs")}
</a>
</div>
{/snippet}
{#snippet renderGroupedFields(section, group)}
{@const fieldGroups = semanticFieldGroups(section, group)}
{#if fieldGroups.length === 1 && !fieldGroups[0].titleKey}
@@ -754,9 +696,6 @@
{@const rootGroup = groups.find((g) => !g.label)}
{@const labelGroups = groups.filter((g) => g.label)}
<div class="admin-settings-fields">
{#if section.id === "pricing"}
{@render renderLegacyTariffsWarning()}
{/if}
{#if rootGroup}
{#if rootGroup.webhook}
{@render renderWebhookHint(rootGroup.webhook)}
+139 -107
View File
@@ -1,5 +1,13 @@
<script>
import { RefreshCw, Trash2, Plus, Save, TriangleAlert, X } from "$components/ui/icons.js";
import {
ChevronRight,
RefreshCw,
Trash2,
Plus,
Save,
TriangleAlert,
X,
} from "$components/ui/icons.js";
import { getContext, onMount } from "svelte";
import {
AdminBadge,
@@ -7,7 +15,7 @@
AdminEmptyState,
AdminSelect,
} from "$components/patterns/admin/index.js";
import { Switch } from "$components/ui/primitives.js";
import { Accordion, Switch } from "$components/ui/primitives.js";
export let at;
export let fmtMoney;
@@ -68,6 +76,7 @@
}));
let selectedTrialSquad = "";
let tariffSettingsOpen = [];
function tariffName(tariff) {
return tariff?.names?.ru || tariff?.names?.en || tariff?.key || "—";
@@ -434,115 +443,138 @@
</div>
</article>
<article class="admin-card admin-tariff-settings-card">
<header class="admin-card-head">
<div>
<h3>{at("tariffs_legacy_title", {}, "Legacy tariff compatibility")}</h3>
<small>
{at(
"tariffs_legacy_subtitle",
{},
"Old remnawave-tg-shop periods and traffic packages used only when the JSON tariff catalog is not configured."
)}
</small>
</div>
<div class="admin-editor-section-actions">
{#if legacyDirtyCount}
<AdminBadge variant="warning">
<Accordion.Root
type="multiple"
bind:value={tariffSettingsOpen}
class="admin-accordion admin-tariff-settings-accordion"
>
<Accordion.Item
value="legacy-tariffs"
class="admin-accordion-item admin-card admin-tariff-settings-card"
>
<Accordion.Header class="admin-accordion-header">
<Accordion.Trigger class="admin-accordion-trigger">
<span class="admin-accordion-title">
{at("tariffs_legacy_title", {}, "Совместимость с legacy-тарифами")}
</span>
<span class="admin-accordion-meta">
{at(
"settings_dirty_count",
{ count: legacyDirtyCount },
`Changes: ${legacyDirtyCount}`
)}
</AdminBadge>
<AdminButton
size="sm"
variant="primary"
onclick={saveTariffSettings}
disabled={settingsSaving}
>
<Save size={13} />
{settingsSaving ? at("btn_saving", {}, "Saving...") : at("btn_save", {}, "Save")}
</AdminButton>
{/if}
</div>
</header>
<div class="admin-card-body">
<div class="admin-settings-warning" role="status">
<TriangleAlert size={16} aria-hidden="true" />
<div class="admin-settings-warning-copy">
<strong>{at("settings_legacy_tariffs_warning_title", {}, "Legacy tariffs")}</strong>
<p>
{at(
"settings_legacy_tariffs_warning_body",
"tariffs_legacy_subtitle",
{},
"These settings are ignored when tariffs are configured in the dedicated Tariffs section."
)}
</p>
</div>
</div>
<div class="admin-legacy-tariff-table">
<div class="admin-legacy-tariff-row admin-legacy-tariff-head">
<span>{at("tariffs_legacy_period", {}, "Period")}</span>
<span>{at("tariffs_legacy_enabled", {}, "Enabled")}</span>
<span>{at("payment_rub", {}, "RUB")}</span>
<span>{at("payment_stars", {}, "Stars")}</span>
</div>
{#each LEGACY_PERIODS as [months, enabledKey, rubKey, starsKey]}
<div class="admin-legacy-tariff-row">
<strong>{months} {at("months_short", {}, "mo")}</strong>
<div class="admin-setting-switch">
<Switch.Root
checked={boolValue(enabledKey)}
onCheckedChange={(checked) => setSetting(enabledKey, checked)}
class="admin-switch-root"
"Старые периоды и пакеты трафика remnawave-tg-shop, которые используются только без JSON-каталога."
)}{#if legacyDirtyCount}
· {at(
"settings_dirty_count",
{ count: legacyDirtyCount },
`Изменений: ${legacyDirtyCount}`
)}{/if}
</span>
<ChevronRight size={16} class="admin-accordion-chev" />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content class="admin-accordion-content">
<div class="admin-card-body">
{#if legacyDirtyCount}
<div class="admin-editor-section-actions admin-tariff-settings-save-row">
<AdminBadge variant="warning">
{at(
"settings_dirty_count",
{ count: legacyDirtyCount },
`Изменений: ${legacyDirtyCount}`
)}
</AdminBadge>
<AdminButton
size="sm"
variant="primary"
onclick={saveTariffSettings}
disabled={settingsSaving}
>
<Switch.Thumb class="admin-switch-thumb" />
</Switch.Root>
<Save size={13} />
{settingsSaving
? at("btn_saving", {}, "Сохранение...")
: at("btn_save", {}, "Сохранить")}
</AdminButton>
</div>
{/if}
<div class="admin-settings-warning" role="status">
<TriangleAlert size={16} aria-hidden="true" />
<div class="admin-settings-warning-copy">
<strong>{at("settings_legacy_tariffs_warning_title", {}, "Legacy tariffs")}</strong>
<p>
{at(
"settings_legacy_tariffs_warning_body",
{},
"These settings are ignored when tariffs are configured in the dedicated Tariffs section."
)}
</p>
</div>
<input
class="input"
type="number"
min="0"
step="1"
value={valueForKey(rubKey)}
oninput={(event) => setSetting(rubKey, event.currentTarget.value)}
/>
<input
class="input"
type="number"
min="0"
step="1"
value={valueForKey(starsKey)}
oninput={(event) => setSetting(starsKey, event.currentTarget.value)}
/>
</div>
{/each}
</div>
<div class="admin-form-row admin-form-row-2 admin-legacy-traffic-row">
<label class="admin-field-label admin-field-label-compact">
<span>{at("tariffs_legacy_traffic_packages", {}, "Traffic packages")}</span>
<small>{at("tariffs_legacy_traffic_hint", {}, "Format: 10:199,50:799")}</small>
<input
class="input"
type="text"
value={valueForKey("TRAFFIC_PACKAGES")}
oninput={(event) => setSetting("TRAFFIC_PACKAGES", event.currentTarget.value)}
/>
</label>
<label class="admin-field-label admin-field-label-compact">
<span>{at("tariffs_legacy_stars_traffic_packages", {}, "Traffic packages, Stars")}</span>
<small>{at("tariffs_legacy_traffic_hint", {}, "Format: 10:199,50:799")}</small>
<input
class="input"
type="text"
value={valueForKey("STARS_TRAFFIC_PACKAGES")}
oninput={(event) => setSetting("STARS_TRAFFIC_PACKAGES", event.currentTarget.value)}
/>
</label>
</div>
</div>
</article>
<div class="admin-legacy-tariff-table">
<div class="admin-legacy-tariff-row admin-legacy-tariff-head">
<span>{at("tariffs_legacy_period", {}, "Period")}</span>
<span>{at("tariffs_legacy_enabled", {}, "Enabled")}</span>
<span>{at("payment_rub", {}, "RUB")}</span>
<span>{at("payment_stars", {}, "Stars")}</span>
</div>
{#each LEGACY_PERIODS as [months, enabledKey, rubKey, starsKey]}
<div class="admin-legacy-tariff-row">
<strong>{months} {at("months_short", {}, "mo")}</strong>
<div class="admin-setting-switch">
<Switch.Root
checked={boolValue(enabledKey)}
onCheckedChange={(checked) => setSetting(enabledKey, checked)}
class="admin-switch-root"
>
<Switch.Thumb class="admin-switch-thumb" />
</Switch.Root>
</div>
<input
class="input"
type="number"
min="0"
step="1"
value={valueForKey(rubKey)}
oninput={(event) => setSetting(rubKey, event.currentTarget.value)}
/>
<input
class="input"
type="number"
min="0"
step="1"
value={valueForKey(starsKey)}
oninput={(event) => setSetting(starsKey, event.currentTarget.value)}
/>
</div>
{/each}
</div>
<div class="admin-form-row admin-form-row-2 admin-legacy-traffic-row">
<label class="admin-field-label admin-field-label-compact">
<span>{at("tariffs_legacy_traffic_packages", {}, "Traffic packages")}</span>
<small>{at("tariffs_legacy_traffic_hint", {}, "Format: 10:199,50:799")}</small>
<input
class="input"
type="text"
value={valueForKey("TRAFFIC_PACKAGES")}
oninput={(event) => setSetting("TRAFFIC_PACKAGES", event.currentTarget.value)}
/>
</label>
<label class="admin-field-label admin-field-label-compact">
<span
>{at("tariffs_legacy_stars_traffic_packages", {}, "Traffic packages, Stars")}</span
>
<small>{at("tariffs_legacy_traffic_hint", {}, "Format: 10:199,50:799")}</small>
<input
class="input"
type="text"
value={valueForKey("STARS_TRAFFIC_PACKAGES")}
oninput={(event) => setSetting("STARS_TRAFFIC_PACKAGES", event.currentTarget.value)}
/>
</label>
</div>
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
{/if}
+5
View File
@@ -2822,6 +2822,11 @@
gap: 14px;
}
.admin-tariff-settings-save-row {
justify-content: flex-end;
margin-bottom: 12px;
}
.admin-field-label-compact {
gap: 8px;
}