mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -1088,6 +1088,11 @@
|
||||
"dailyDeductionDesc": "هر روز از موجودی کاربر کسر میشود",
|
||||
"dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید",
|
||||
"cannotSave": "امکان ذخیره تعرفه وجود ندارد:",
|
||||
"statusTitle": "وضعیت تعرفه",
|
||||
"isActiveLabel": "فعال",
|
||||
"isActiveHint": "تعرفههای غیرفعال به کاربران نمایش داده نمیشوند",
|
||||
"isTrialLabel": "تعرفه آزمایشی",
|
||||
"isTrialHint": "برای دوره آزمایشی رایگان در دسترس است",
|
||||
"validation": {
|
||||
"nameRequired": "نام تعرفه را وارد کنید",
|
||||
"nameLength": "نام باید ۲ تا ۵۰ کاراکتر باشد",
|
||||
|
||||
@@ -1829,6 +1829,11 @@
|
||||
"dailyDeductionDesc": "Списывается ежедневно с баланса пользователя",
|
||||
"dailyPriceRequired": "Укажите цену за день больше 0",
|
||||
"cannotSave": "Невозможно сохранить тариф:",
|
||||
"statusTitle": "Статус тарифа",
|
||||
"isActiveLabel": "Активен",
|
||||
"isActiveHint": "Неактивные тарифы не отображаются пользователям",
|
||||
"isTrialLabel": "Триальный тариф",
|
||||
"isTrialHint": "Доступен для бесплатного пробного периода",
|
||||
"validation": {
|
||||
"nameRequired": "Введите название тарифа",
|
||||
"nameLength": "Название должно быть от 2 до 50 символов",
|
||||
|
||||
@@ -1125,6 +1125,11 @@
|
||||
"dailyDeductionDesc": "每天从用户余额中扣除",
|
||||
"dailyPriceRequired": "请输入大于0的每日价格",
|
||||
"cannotSave": "无法保存套餐:",
|
||||
"statusTitle": "套餐状态",
|
||||
"isActiveLabel": "已激活",
|
||||
"isActiveHint": "未激活的套餐不会显示给用户",
|
||||
"isTrialLabel": "试用套餐",
|
||||
"isTrialHint": "可用于免费试用期",
|
||||
"validation": {
|
||||
"nameRequired": "请输入套餐名称",
|
||||
"nameLength": "名称必须为2到50个字符",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user