feat: improve tariff builder UI

- Move promo groups from Servers tab to Extra tab
- Add isActive toggle in Extra tab (tariff status)
- Remove trial toggle from form (not needed for now)
- Replace "T" letter with gift icon for trial toggle in tariff list
- Add i18n translations for new status fields
This commit is contained in:
c0mrade
2026-02-02 04:46:00 +03:00
parent d6edb659a3
commit e19767af82
6 changed files with 105 additions and 45 deletions

View File

@@ -1318,6 +1318,11 @@
"dailyDeductionDesc": "Charged daily from user's balance",
"dailyPriceRequired": "Enter a price per day greater than 0",
"cannotSave": "Cannot save tariff:",
"statusTitle": "Tariff Status",
"isActiveLabel": "Active",
"isActiveHint": "Inactive tariffs are not shown to users",
"isTrialLabel": "Trial tariff",
"isTrialHint": "Available for free trial period",
"validation": {
"nameRequired": "Enter tariff name",
"nameLength": "Name must be 2 to 50 characters",

View File

@@ -1088,6 +1088,11 @@
"dailyDeductionDesc": "هر روز از موجودی کاربر کسر می‌شود",
"dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید",
"cannotSave": "امکان ذخیره تعرفه وجود ندارد:",
"statusTitle": "وضعیت تعرفه",
"isActiveLabel": "فعال",
"isActiveHint": "تعرفه‌های غیرفعال به کاربران نمایش داده نمی‌شوند",
"isTrialLabel": "تعرفه آزمایشی",
"isTrialHint": "برای دوره آزمایشی رایگان در دسترس است",
"validation": {
"nameRequired": "نام تعرفه را وارد کنید",
"nameLength": "نام باید ۲ تا ۵۰ کاراکتر باشد",

View File

@@ -1829,6 +1829,11 @@
"dailyDeductionDesc": "Списывается ежедневно с баланса пользователя",
"dailyPriceRequired": "Укажите цену за день больше 0",
"cannotSave": "Невозможно сохранить тариф:",
"statusTitle": "Статус тарифа",
"isActiveLabel": "Активен",
"isActiveHint": "Неактивные тарифы не отображаются пользователям",
"isTrialLabel": "Триальный тариф",
"isTrialHint": "Доступен для бесплатного пробного периода",
"validation": {
"nameRequired": "Введите название тарифа",
"nameLength": "Название должно быть от 2 до 50 символов",

View File

@@ -1125,6 +1125,11 @@
"dailyDeductionDesc": "每天从用户余额中扣除",
"dailyPriceRequired": "请输入大于0的每日价格",
"cannotSave": "无法保存套餐:",
"statusTitle": "套餐状态",
"isActiveLabel": "已激活",
"isActiveHint": "未激活的套餐不会显示给用户",
"isTrialLabel": "试用套餐",
"isTrialHint": "可用于免费试用期",
"validation": {
"nameRequired": "请输入套餐名称",
"nameLength": "名称必须为2到50个字符",

View File

@@ -97,6 +97,7 @@ export default function AdminTariffCreate() {
// Form state - matches bot fields
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [isActive, setIsActive] = useState(true);
const [trafficLimitGb, setTrafficLimitGb] = useState<number | ''>(100);
const [deviceLimit, setDeviceLimit] = useState<number | ''>(1);
const [devicePriceKopeks, setDevicePriceKopeks] = useState<number | ''>(0);
@@ -156,6 +157,7 @@ export default function AdminTariffCreate() {
setTariffType(data.is_daily ? 'daily' : 'period');
setName(data.name);
setDescription(data.description || '');
setIsActive(data.is_active ?? true);
setTrafficLimitGb(data.traffic_limit_gb ?? 100);
setDeviceLimit(data.device_limit || 1);
setDevicePriceKopeks(data.device_price_kopeks || 0);
@@ -200,6 +202,7 @@ export default function AdminTariffCreate() {
const data: TariffCreateRequest | TariffUpdateRequest = {
name,
description: description || undefined,
is_active: isActive,
traffic_limit_gb: toNumber(trafficLimitGb, 100),
device_limit: toNumber(deviceLimit, 1),
device_price_kopeks:
@@ -703,50 +706,6 @@ export default function AdminTariffCreate() {
</div>
)}
</div>
{/* Promo Groups */}
<div className="card space-y-4">
<h4 className="text-sm font-medium text-dark-200">
{t('admin.tariffs.promoGroupsTitle')}
</h4>
<p className="text-sm text-dark-400">{t('admin.tariffs.promoGroupsHint')}</p>
{promoGroups.length === 0 ? (
<p className="py-4 text-center text-dark-500">{t('admin.tariffs.noPromoGroups')}</p>
) : (
<div className="max-h-48 space-y-2 overflow-y-auto">
{promoGroups.map((group) => {
const isSelected = selectedPromoGroups.includes(group.id);
return (
<button
key={group.id}
type="button"
onClick={() => togglePromoGroup(group.id)}
className={`flex w-full items-center gap-3 rounded-lg p-3 text-left transition-colors ${
isSelected
? isDaily
? 'bg-warning-500/20 text-warning-300'
: 'bg-accent-500/20 text-accent-300'
: 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>
<div
className={`flex h-5 w-5 items-center justify-center rounded ${
isSelected
? isDaily
? 'bg-warning-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-dark-600'
}`}
>
{isSelected && <CheckIcon />}
</div>
<span className="flex-1 text-sm font-medium">{group.name}</span>
</button>
);
})}
</div>
)}
</div>
</div>
)}
@@ -1003,6 +962,77 @@ export default function AdminTariffCreate() {
))}
</div>
</div>
{/* Promo Groups */}
<div className="card space-y-4">
<h4 className="text-sm font-medium text-dark-200">
{t('admin.tariffs.promoGroupsTitle')}
</h4>
<p className="text-sm text-dark-400">{t('admin.tariffs.promoGroupsHint')}</p>
{promoGroups.length === 0 ? (
<p className="py-4 text-center text-dark-500">{t('admin.tariffs.noPromoGroups')}</p>
) : (
<div className="max-h-48 space-y-2 overflow-y-auto">
{promoGroups.map((group) => {
const isSelected = selectedPromoGroups.includes(group.id);
return (
<button
key={group.id}
type="button"
onClick={() => togglePromoGroup(group.id)}
className={`flex w-full items-center gap-3 rounded-lg p-3 text-left transition-colors ${
isSelected
? isDaily
? 'bg-warning-500/20 text-warning-300'
: 'bg-accent-500/20 text-accent-300'
: 'bg-dark-800 text-dark-300 hover:bg-dark-700'
}`}
>
<div
className={`flex h-5 w-5 items-center justify-center rounded ${
isSelected
? isDaily
? 'bg-warning-500 text-white'
: 'bg-accent-500 text-white'
: 'bg-dark-600'
}`}
>
{isSelected && <CheckIcon />}
</div>
<span className="flex-1 text-sm font-medium">{group.name}</span>
</button>
);
})}
</div>
)}
</div>
{/* Tariff status */}
<div className="card space-y-3">
<h4 className="text-sm font-medium text-dark-200">{t('admin.tariffs.statusTitle')}</h4>
{/* Active toggle */}
<div className="flex items-center justify-between rounded-lg bg-dark-800 p-3">
<div>
<span className="text-sm font-medium text-dark-200">
{t('admin.tariffs.isActiveLabel')}
</span>
<p className="text-xs text-dark-500">{t('admin.tariffs.isActiveHint')}</p>
</div>
<button
type="button"
onClick={() => setIsActive(!isActive)}
className={`relative h-6 w-11 rounded-full transition-colors ${
isActive ? 'bg-success-500' : 'bg-dark-600'
}`}
>
<span
className={`absolute top-1 h-4 w-4 rounded-full bg-white transition-transform ${
isActive ? 'left-6' : 'left-1'
}`}
/>
</button>
</div>
</div>
</div>
)}

View File

@@ -44,6 +44,16 @@ const XIcon = () => (
</svg>
);
const GiftIcon = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
/>
</svg>
);
export default function AdminTariffs() {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -210,7 +220,7 @@ export default function AdminTariffs() {
}`}
title={t('admin.tariffs.toggleTrial')}
>
T
<GiftIcon />
</button>
{/* Edit */}