Add page hints to admin and user plan interfaces
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 47s

- Introduced page hints in the PlanContent, AdminPlansPage, and AdminRolesPage components to provide users with contextual information.
- Updated i18n resource files to include new translations for page hints related to plans and roles, enhancing user experience and clarity.
- Improved existing text for downgrade notices and discount tier hints for better understanding of billing implications.
This commit is contained in:
Leonid Pershin
2026-07-23 23:03:45 +03:00
parent fad03c2834
commit 694683455b
4 changed files with 18 additions and 6 deletions
+1
View File
@@ -51,6 +51,7 @@ function AdminPlansPage() {
return (
<div className="flex flex-col gap-4">
<p className="text-xs text-muted-foreground">{t('admin.plans.pageHint')}</p>
<div className="flex justify-end">
<PlanFormDialog />
</div>
+1
View File
@@ -29,6 +29,7 @@ function AdminRolesPage() {
return (
<div className="flex flex-col gap-4">
<p className="text-xs text-muted-foreground">{t('admin.roles.pageHint')}</p>
<div className="flex justify-end">
<RoleFormDialog />
</div>
+2
View File
@@ -101,6 +101,7 @@ function PlanContent() {
: t('plan.current', { count: statusQuery.data.configQuota })}
</p>
)}
<p className="mt-1 text-xs text-muted-foreground">{t('plan.pageHint')}</p>
</div>
<div className="flex flex-col gap-3">
@@ -136,6 +137,7 @@ function PlanContent() {
}}
/>
</label>
{selection === 'custom' && <p className="text-xs text-muted-foreground">{t('plan.customHint')}</p>}
</div>
{needsRevokePicker && (
+14 -6
View File
@@ -105,14 +105,16 @@ const resources = {
plan: {
title: 'Тариф',
pageHint: 'Смена тарифа применяется сразу, без подтверждения администратора. Если у вашей роли включена оплата и уже есть оплаченный период — при увеличении тарифа автоматически будет выставлена доплата за разницу в цене на оставшийся срок.',
changePlan: 'Изменить тариф',
current: 'Текущая квота: {{count}} конфигов',
currentUnlimited: 'Текущая квота: без лимита',
custom: 'Своё количество',
customConfigCount: 'Количество конфигов',
customHint: 'Если ни один из вариантов выше не подходит — укажите нужное количество вручную (в разумных пределах).',
submit: 'Применить',
changed: 'Тариф изменён.',
downgradeNotice: 'Новая квота меньше текущего числа активных конфигов — выберите, какие отозвать:',
downgradeNotice: 'Новая квота меньше текущего числа конфигов, занимающих место в квоте (включая приостановленные за неуплату) — выберите, какие отозвать:',
selectExactly: 'Нужно выбрать ровно {{count}} конфиг(ов) для отзыва.',
topUpCreated: 'Тариф увеличен. Создана доплата за оставшийся оплаченный период.',
goToBilling: 'Перейти к оплате',
@@ -333,6 +335,7 @@ const resources = {
},
roles: {
create: 'Создать роль',
pageHint: 'Роль определяет лимит устройств на конфиг, доступ к инбаундам и участие в оплате. Количество конфигов (квота) больше не задаётся на роли — см. вкладку «Тарифы».',
name: 'Название',
maxIpLimit: 'Лимит IP на конфиг',
maxIpLimitHint: '−1 = без лимита. Применяется только к новым конфигам.',
@@ -349,6 +352,7 @@ const resources = {
},
plans: {
create: 'Создать тариф',
pageHint: 'Каталог тарифов, из которых пользователи выбирают квоту конфигов сами, без подтверждения (страница «Изменить тариф»). Каталожные тарифы не поддерживают безлимит — он доступен только роли admin.',
name: 'Название',
configCount: 'Количество конфигов',
sortOrder: 'Порядок сортировки',
@@ -370,7 +374,7 @@ const resources = {
pricing: {
title: 'Справочная цена конфига',
pricePerConfigPerQuarter: 'Цена за конфиг в месяц / оплата раз в 3 мес',
pricePerConfigPerQuarterHint: 'Минимальный период оплаты. Видно только админу. Итог за период = ставка × число месяцев × квота роли.',
pricePerConfigPerQuarterHint: 'Минимальный период оплаты. Видно только админу. Итог за период = ставка × число месяцев × количество конфигов в тарифе пользователя.',
pricePerConfigPerHalfYear: 'Цена за конфиг в месяц / оплата раз в полгода',
pricePerConfigPerHalfYearHint: 'Может быть ниже квартальной (скидка за оплату на полгода вперёд), но итог за полгода не должен быть дешевле итога за квартал.',
halfYearCheaperThanQuarter: 'Цена за полгода (в пересчёте на 6 месяцев) не может быть меньше цены за 3 месяца.',
@@ -380,7 +384,7 @@ const resources = {
yearCheaperThanQuarter: 'Цена за год (в пересчёте на 12 месяцев) не может быть меньше цены за 3 месяца.',
updated: 'Цена обновлена.',
discountTiersTitle: 'Скидка за объём',
discountTiersHint: 'Роль с квотой конфигов не меньше порога получает указанную скидку от итоговой цены за период. Действует наивысший подходящий порог, скидки не суммируются.',
discountTiersHint: 'Если количество конфигов в тарифе пользователя не меньше порога — применяется указанная скидка от итоговой цены за период. Действует наивысший подходящий порог, скидки не суммируются.',
discountTierMinConfigs: 'От скольки конфигов',
discountTierPercent: 'Скидка, %',
addDiscountTier: 'Добавить ступень',
@@ -692,14 +696,16 @@ const resources = {
plan: {
title: 'Plan',
pageHint: "Changing your plan applies immediately, with no admin approval needed. If your role has billing enabled and you already have a paid period, increasing your plan automatically creates a top-up charge for the price difference over the remaining time.",
changePlan: 'Change plan',
current: 'Current quota: {{count}} configs',
currentUnlimited: 'Current quota: unlimited',
custom: 'Custom amount',
customConfigCount: 'Number of configs',
customHint: "If none of the plans above fit, enter the amount you need (within reasonable limits).",
submit: 'Apply',
changed: 'Plan changed.',
downgradeNotice: 'The new quota is lower than your current number of active configs — choose which to revoke:',
downgradeNotice: 'The new quota is lower than your current number of configs that still count against it (including ones suspended for non-payment) — choose which to revoke:',
selectExactly: 'Select exactly {{count}} config(s) to revoke.',
topUpCreated: 'Plan increased. A top-up was created for the remaining paid period.',
goToBilling: 'Go to payment',
@@ -920,6 +926,7 @@ const resources = {
},
roles: {
create: 'Create role',
pageHint: "A role sets the device limit per config, inbound access, and billing eligibility. Config quota is no longer set on the role — see the Plans tab.",
name: 'Name',
maxIpLimit: 'IP limit per config',
maxIpLimitHint: '1 = unlimited. Applies to new configs only.',
@@ -936,6 +943,7 @@ const resources = {
},
plans: {
create: 'Create plan',
pageHint: "The catalog of plans users can pick a config quota from themselves, with no approval needed (the \"Change plan\" page). Catalog plans cannot be unlimited — that's reserved for the admin role.",
name: 'Name',
configCount: 'Number of configs',
sortOrder: 'Sort order',
@@ -957,7 +965,7 @@ const resources = {
pricing: {
title: 'Reference config price',
pricePerConfigPerQuarter: 'Price per config per month / billed every 3 months',
pricePerConfigPerQuarterHint: 'Minimum billing period. Visible to admin only. Total for a period = rate × months × role quota.',
pricePerConfigPerQuarterHint: "Minimum billing period. Visible to admin only. Total for a period = rate × months × the user's plan config count.",
pricePerConfigPerHalfYear: 'Price per config per month / billed every 6 months',
pricePerConfigPerHalfYearHint: 'Can be lower than the quarterly rate (discount for a 6-month prepayment), but the 6-month total must not be cheaper than the 3-month total.',
halfYearCheaperThanQuarter: 'The 6-month price (over 6 months) cannot be lower than the 3-month price.',
@@ -967,7 +975,7 @@ const resources = {
yearCheaperThanQuarter: 'The annual price (over 12 months) cannot be lower than the 3-month price.',
updated: 'Pricing updated.',
discountTiersTitle: 'Volume discount',
discountTiersHint: 'A role with a config quota at or above the threshold gets the listed discount off the period total. The highest applicable threshold wins — discounts do not stack.',
discountTiersHint: "If the user's plan config count is at or above the threshold, the listed discount applies to the period total. The highest applicable threshold wins — discounts do not stack.",
discountTierMinConfigs: 'From how many configs',
discountTierPercent: 'Discount, %',
addDiscountTier: 'Add tier',