chore: improve tariff admin layout

This commit is contained in:
3252a8
2026-06-01 11:55:41 +03:00
parent 63ec3a6152
commit 5bb1400917
4 changed files with 507 additions and 243 deletions
+270 -174
View File
@@ -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 @@
</div>
</article>
<article class="admin-card">
<header class="admin-card-head">
<div>
<h3>{at("tariffs_title", {}, "Каталог тарифов")}</h3>
<small>{tariffsPath || "data/tariffs.json"}</small>
</div>
<div class="admin-editor-section-actions">
<AdminButton
size="sm"
onclick={tariffsStore.loadTariffs}
disabled={tariffsLoading || tariffsSaving}
>
<RefreshCw size={13} />
{at("btn_refresh", {}, "Обновить")}
</AdminButton>
<AdminButton
size="sm"
variant="primary"
onclick={tariffsStore.openCreateTariff}
disabled={tariffsLoading || tariffsSaving}
>
<Plus size={13} />
{at("btn_create_tariff", {}, "Создать тариф")}
</AdminButton>
</div>
</header>
<div class="admin-card-body">
<div class="admin-tariff-catalog-bar">
<label class="admin-field-label-compact">
<span>{at("tariff_default_currency", {}, "Валюта оплаты")}</span>
<Input
class="input admin-currency-input"
type="text"
maxlength="12"
value={defaultCurrencyDraft}
oninput={(event) => (defaultCurrencyDraft = event.currentTarget.value.toUpperCase())}
onkeydown={(event) => {
if (event.key === "Enter" && defaultCurrencyDirty) saveDefaultCurrency();
}}
/>
</label>
<AdminBadge variant="muted">{catalogCurrencyKey.toUpperCase()}</AdminBadge>
{#if defaultCurrencyDirty}
<div class="admin-tariff-management">
<div class="admin-tariff-overview-grid">
<article class="admin-card admin-tariff-currency-card">
<header class="admin-card-head admin-tariff-panel-head">
<div>
<h3>{at("tariffs_currency_title", {}, "Валюта каталога")}</h3>
<small>
{at(
"tariffs_currency_subtitle",
{},
"Цены тарифов и платёжные провайдеры проверяются по этой валюте."
)}
</small>
</div>
<AdminBadge variant="muted">{catalogCurrencyCode}</AdminBadge>
</header>
<div class="admin-card-body admin-tariff-currency-body">
<div class="admin-tariff-currency-current">
<span>{at("tariffs_currency_current", {}, "Текущая валюта")}</span>
<strong>{catalogCurrencyCode}</strong>
</div>
<div class="admin-tariff-catalog-bar">
<label class="admin-field-label-compact admin-tariff-currency-field">
<span>{at("tariff_default_currency", {}, "Валюта оплаты")}</span>
<Input
class="input admin-currency-input"
type="text"
maxlength="12"
value={defaultCurrencyDraft}
oninput={(event) =>
(defaultCurrencyDraft = event.currentTarget.value.toUpperCase())}
onkeydown={(event) => {
if (event.key === "Enter" && defaultCurrencyDirty) saveDefaultCurrency();
}}
/>
</label>
{#if defaultCurrencyDirty}
<AdminButton
size="sm"
variant="primary"
onclick={saveDefaultCurrency}
disabled={tariffsSaving}
>
<Save size={13} />
{tariffsSaving
? at("btn_saving", {}, "Сохранение...")
: at("btn_save", {}, "Сохранить")}
</AdminButton>
{/if}
</div>
</div>
</article>
<article class="admin-card admin-tariff-providers-card">
<header class="admin-card-head admin-tariff-panel-head">
<div>
<h3>{at("tariffs_provider_title", {}, "Платёжные провайдеры")}</h3>
<small>
{at(
"tariffs_provider_subtitle",
{},
"Здесь видно, какие провайдеры смогут принять текущую валюту каталога."
)}
</small>
</div>
<div class="admin-provider-summary">
<AdminBadge variant="success">
{at(
"tariffs_provider_available_count",
{ count: providerSupportSummary.available },
"Доступно: {count}"
)}
</AdminBadge>
<AdminBadge variant="muted">
{at(
"tariffs_provider_enabled_count",
{ count: providerSupportSummary.enabled },
"Включено: {count}"
)}
</AdminBadge>
{#if providerSupportSummary.blocked}
<AdminBadge variant="warning">
{at(
"tariffs_provider_blocked_count",
{ count: providerSupportSummary.blocked },
"Не подходят: {count}"
)}
</AdminBadge>
{/if}
</div>
</header>
<div class="admin-card-body">
{#if providerCurrencySupport?.length}
<div class="admin-provider-currency-grid">
{#each providerCurrencySupport as provider}
<div
class="admin-provider-currency"
class:is-supported={provider.supports_default_currency &&
provider.enabled &&
provider.configured}
class:is-unavailable={!provider.supports_default_currency ||
!provider.enabled ||
!provider.configured}
>
<div class="admin-provider-currency-main">
<strong>{provider.label}</strong>
<small>{providerCurrencyLabel(provider)}</small>
</div>
<AdminBadge variant={providerCurrencyVariant(provider)}>
{providerCurrencyStatus(provider)}
</AdminBadge>
</div>
{/each}
</div>
{:else}
<AdminEmptyState>
{at("tariffs_provider_empty", {}, "Данные по провайдерам пока не загружены.")}
</AdminEmptyState>
{/if}
</div>
</article>
</div>
<article class="admin-card admin-tariff-list-card">
<header class="admin-card-head admin-tariff-list-head">
<div>
<h3>{at("tariffs_title", {}, "Каталог тарифов")}</h3>
<small>
{at("tariffs_catalog_subtitle", {}, "Периоды, цены, трафик и доступы пользователей.")}
</small>
<code class="admin-tariff-path">{tariffsPath || "data/tariffs.json"}</code>
</div>
<div class="admin-editor-section-actions">
<AdminButton
size="sm"
onclick={tariffsStore.loadTariffs}
disabled={tariffsLoading || tariffsSaving}
>
<RefreshCw size={13} />
{at("btn_refresh", {}, "Обновить")}
</AdminButton>
<AdminButton
size="sm"
variant="primary"
onclick={saveDefaultCurrency}
disabled={tariffsSaving}
onclick={tariffsStore.openCreateTariff}
disabled={tariffsLoading || tariffsSaving}
>
<Save size={13} />
{tariffsSaving
? at("btn_saving", {}, "Сохранение...")
: at("btn_save", {}, "Сохранить")}
<Plus size={13} />
{at("btn_create_tariff", {}, "Создать тариф")}
</AdminButton>
</div>
</header>
<div class="admin-card-body">
{#if !tariffsCatalog.tariffs.length}
<AdminEmptyState>
{at(
"tariffs_catalog_empty",
{},
"Каталог пуст. Добавьте первый тариф, после сохранения будет создан JSON-файл каталога."
)}
</AdminEmptyState>
{:else}
<div class="admin-tariff-grid">
{#each tariffsCatalog.tariffs as tariff}
<article class="admin-tariff-card" class:is-disabled={tariff.enabled === false}>
<div class="admin-tariff-top">
<div>
<div class="admin-tariff-title">
<strong>{tariffName(tariff)}</strong>
{#if tariff.key === tariffsCatalog.default_tariff}
<AdminBadge variant="success"
>{at("status_default", {}, "Default")}</AdminBadge
>
{/if}
</div>
<code>{tariff.key}</code>
</div>
{#if tariff.enabled === false}
<AdminBadge variant="muted">{at("status_disabled", {}, "Выключен")}</AdminBadge>
{:else}
<AdminBadge variant="success">{at("status_active", {}, "Активен")}</AdminBadge>
{/if}
</div>
<p>
{tariff.descriptions?.ru ||
tariff.descriptions?.en ||
at("no_description", {}, "Без описания")}
</p>
<div class="admin-tariff-facts">
<span
>{tariff.billing_model === "traffic"
? at("tariff_model_traffic", {}, "Трафик")
: at("tariff_model_periods", {}, "Периоды")}</span
>
<span>{tariffPriceSummary(tariff)}</span>
<span
>{at("tariff_squads", {}, "Squads")}: {(tariff.squad_uuids || []).length}</span
>
<span
>{at("tariff_premium", {}, "Premium")}: {(tariff.premium_squad_uuids || [])
.length
? `${tariff.premium_monthly_gb || 0} GB`
: "—"}</span
>
<span
>{at("tariff_devices", {}, "Устройства")}: {tariff.hwid_device_limit ??
"env"}</span
>
</div>
<div class="admin-tariff-actions">
<AdminButton size="sm" onclick={() => tariffsStore.openEditTariff(tariff)}>
{at("btn_configure", {}, "Настроить")}
</AdminButton>
<AdminButton
size="sm"
onclick={() => tariffsStore.toggleTariffEnabled(tariff)}
disabled={tariffsSaving}
>
{tariff.enabled === false
? at("btn_enable", {}, "Включить")
: at("btn_disable", {}, "Выключить")}
</AdminButton>
<AdminButton
size="sm"
onclick={() => tariffsStore.setDefaultTariff(tariff.key)}
disabled={tariffsSaving ||
tariff.enabled === false ||
tariff.key === tariffsCatalog.default_tariff}
>
{at("btn_set_default", {}, "По умолчанию")}
</AdminButton>
<AdminButton
size="sm"
variant="danger"
onclick={() =>
tariffsStore.updateState({
tariffDeleteTarget: tariff,
tariffDeleteOpen: true,
})}
disabled={tariffsSaving}
aria-label={at("btn_delete_tariff", {}, "Удалить тариф")}
>
<Trash2 size={13} />
</AdminButton>
</div>
</article>
{/each}
</div>
{/if}
</div>
{#if providerCurrencySupport?.length}
<div class="admin-provider-currency-grid">
{#each providerCurrencySupport as provider}
<div
class="admin-provider-currency"
class:is-supported={provider.supports_default_currency && provider.enabled}
class:is-unavailable={!provider.supports_default_currency || !provider.enabled}
>
<div>
<strong>{provider.label}</strong>
<small>{providerCurrencyLabel(provider)}</small>
</div>
<AdminBadge variant={providerCurrencyVariant(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}
</AdminBadge>
</div>
{/each}
</div>
{/if}
{#if !tariffsCatalog.tariffs.length}
<AdminEmptyState>
{at(
"tariffs_catalog_empty",
{},
"Каталог пуст. Добавьте первый тариф, после сохранения будет создан JSON-файл каталога."
)}
</AdminEmptyState>
{:else}
<div class="admin-tariff-grid">
{#each tariffsCatalog.tariffs as tariff}
<article class="admin-tariff-card" class:is-disabled={tariff.enabled === false}>
<div class="admin-tariff-top">
<div>
<div class="admin-tariff-title">
<strong>{tariffName(tariff)}</strong>
{#if tariff.key === tariffsCatalog.default_tariff}
<AdminBadge variant="success"
>{at("status_default", {}, "Default")}</AdminBadge
>
{/if}
</div>
<code>{tariff.key}</code>
</div>
{#if tariff.enabled === false}
<AdminBadge variant="muted">{at("status_disabled", {}, "Выключен")}</AdminBadge>
{:else}
<AdminBadge variant="success">{at("status_active", {}, "Активен")}</AdminBadge>
{/if}
</div>
<p>
{tariff.descriptions?.ru ||
tariff.descriptions?.en ||
at("no_description", {}, "Без описания")}
</p>
<div class="admin-tariff-facts">
<span
>{tariff.billing_model === "traffic"
? at("tariff_model_traffic", {}, "Трафик")
: at("tariff_model_periods", {}, "Периоды")}</span
>
<span>{tariffPriceSummary(tariff)}</span>
<span>{at("tariff_squads", {}, "Squads")}: {(tariff.squad_uuids || []).length}</span
>
<span
>{at("tariff_premium", {}, "Premium")}: {(tariff.premium_squad_uuids || []).length
? `${tariff.premium_monthly_gb || 0} GB`
: "—"}</span
>
<span
>{at("tariff_devices", {}, "Устройства")}: {tariff.hwid_device_limit ??
"env"}</span
>
</div>
<div class="admin-tariff-actions">
<AdminButton size="sm" onclick={() => tariffsStore.openEditTariff(tariff)}>
{at("btn_configure", {}, "Настроить")}
</AdminButton>
<AdminButton
size="sm"
onclick={() => tariffsStore.toggleTariffEnabled(tariff)}
disabled={tariffsSaving}
>
{tariff.enabled === false
? at("btn_enable", {}, "Включить")
: at("btn_disable", {}, "Выключить")}
</AdminButton>
<AdminButton
size="sm"
onclick={() => tariffsStore.setDefaultTariff(tariff.key)}
disabled={tariffsSaving ||
tariff.enabled === false ||
tariff.key === tariffsCatalog.default_tariff}
>
{at("btn_set_default", {}, "По умолчанию")}
</AdminButton>
<AdminButton
size="sm"
variant="danger"
onclick={() =>
tariffsStore.updateState({
tariffDeleteTarget: tariff,
tariffDeleteOpen: true,
})}
disabled={tariffsSaving}
aria-label={at("btn_delete_tariff", {}, "Удалить тариф")}
>
<Trash2 size={13} />
</AdminButton>
</div>
</article>
{/each}
</div>
{/if}
</div>
</article>
</article>
</div>
<Accordion.Root
type="multiple"
+205 -69
View File
@@ -3125,6 +3125,169 @@
}
}
.admin-tariff-management {
display: grid;
gap: 14px;
}
.admin-tariff-overview-grid {
display: grid;
gap: 14px;
}
.admin-tariff-overview-grid > .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