mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +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",
|
"dailyDeductionDesc": "Charged daily from user's balance",
|
||||||
"dailyPriceRequired": "Enter a price per day greater than 0",
|
"dailyPriceRequired": "Enter a price per day greater than 0",
|
||||||
"cannotSave": "Cannot save tariff:",
|
"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": {
|
"validation": {
|
||||||
"nameRequired": "Enter tariff name",
|
"nameRequired": "Enter tariff name",
|
||||||
"nameLength": "Name must be 2 to 50 characters",
|
"nameLength": "Name must be 2 to 50 characters",
|
||||||
|
|||||||
@@ -1088,6 +1088,11 @@
|
|||||||
"dailyDeductionDesc": "هر روز از موجودی کاربر کسر میشود",
|
"dailyDeductionDesc": "هر روز از موجودی کاربر کسر میشود",
|
||||||
"dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید",
|
"dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید",
|
||||||
"cannotSave": "امکان ذخیره تعرفه وجود ندارد:",
|
"cannotSave": "امکان ذخیره تعرفه وجود ندارد:",
|
||||||
|
"statusTitle": "وضعیت تعرفه",
|
||||||
|
"isActiveLabel": "فعال",
|
||||||
|
"isActiveHint": "تعرفههای غیرفعال به کاربران نمایش داده نمیشوند",
|
||||||
|
"isTrialLabel": "تعرفه آزمایشی",
|
||||||
|
"isTrialHint": "برای دوره آزمایشی رایگان در دسترس است",
|
||||||
"validation": {
|
"validation": {
|
||||||
"nameRequired": "نام تعرفه را وارد کنید",
|
"nameRequired": "نام تعرفه را وارد کنید",
|
||||||
"nameLength": "نام باید ۲ تا ۵۰ کاراکتر باشد",
|
"nameLength": "نام باید ۲ تا ۵۰ کاراکتر باشد",
|
||||||
|
|||||||
@@ -1829,6 +1829,11 @@
|
|||||||
"dailyDeductionDesc": "Списывается ежедневно с баланса пользователя",
|
"dailyDeductionDesc": "Списывается ежедневно с баланса пользователя",
|
||||||
"dailyPriceRequired": "Укажите цену за день больше 0",
|
"dailyPriceRequired": "Укажите цену за день больше 0",
|
||||||
"cannotSave": "Невозможно сохранить тариф:",
|
"cannotSave": "Невозможно сохранить тариф:",
|
||||||
|
"statusTitle": "Статус тарифа",
|
||||||
|
"isActiveLabel": "Активен",
|
||||||
|
"isActiveHint": "Неактивные тарифы не отображаются пользователям",
|
||||||
|
"isTrialLabel": "Триальный тариф",
|
||||||
|
"isTrialHint": "Доступен для бесплатного пробного периода",
|
||||||
"validation": {
|
"validation": {
|
||||||
"nameRequired": "Введите название тарифа",
|
"nameRequired": "Введите название тарифа",
|
||||||
"nameLength": "Название должно быть от 2 до 50 символов",
|
"nameLength": "Название должно быть от 2 до 50 символов",
|
||||||
|
|||||||
@@ -1125,6 +1125,11 @@
|
|||||||
"dailyDeductionDesc": "每天从用户余额中扣除",
|
"dailyDeductionDesc": "每天从用户余额中扣除",
|
||||||
"dailyPriceRequired": "请输入大于0的每日价格",
|
"dailyPriceRequired": "请输入大于0的每日价格",
|
||||||
"cannotSave": "无法保存套餐:",
|
"cannotSave": "无法保存套餐:",
|
||||||
|
"statusTitle": "套餐状态",
|
||||||
|
"isActiveLabel": "已激活",
|
||||||
|
"isActiveHint": "未激活的套餐不会显示给用户",
|
||||||
|
"isTrialLabel": "试用套餐",
|
||||||
|
"isTrialHint": "可用于免费试用期",
|
||||||
"validation": {
|
"validation": {
|
||||||
"nameRequired": "请输入套餐名称",
|
"nameRequired": "请输入套餐名称",
|
||||||
"nameLength": "名称必须为2到50个字符",
|
"nameLength": "名称必须为2到50个字符",
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export default function AdminTariffCreate() {
|
|||||||
// Form state - matches bot fields
|
// Form state - matches bot fields
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
|
const [isActive, setIsActive] = useState(true);
|
||||||
const [trafficLimitGb, setTrafficLimitGb] = useState<number | ''>(100);
|
const [trafficLimitGb, setTrafficLimitGb] = useState<number | ''>(100);
|
||||||
const [deviceLimit, setDeviceLimit] = useState<number | ''>(1);
|
const [deviceLimit, setDeviceLimit] = useState<number | ''>(1);
|
||||||
const [devicePriceKopeks, setDevicePriceKopeks] = useState<number | ''>(0);
|
const [devicePriceKopeks, setDevicePriceKopeks] = useState<number | ''>(0);
|
||||||
@@ -156,6 +157,7 @@ export default function AdminTariffCreate() {
|
|||||||
setTariffType(data.is_daily ? 'daily' : 'period');
|
setTariffType(data.is_daily ? 'daily' : 'period');
|
||||||
setName(data.name);
|
setName(data.name);
|
||||||
setDescription(data.description || '');
|
setDescription(data.description || '');
|
||||||
|
setIsActive(data.is_active ?? true);
|
||||||
setTrafficLimitGb(data.traffic_limit_gb ?? 100);
|
setTrafficLimitGb(data.traffic_limit_gb ?? 100);
|
||||||
setDeviceLimit(data.device_limit || 1);
|
setDeviceLimit(data.device_limit || 1);
|
||||||
setDevicePriceKopeks(data.device_price_kopeks || 0);
|
setDevicePriceKopeks(data.device_price_kopeks || 0);
|
||||||
@@ -200,6 +202,7 @@ export default function AdminTariffCreate() {
|
|||||||
const data: TariffCreateRequest | TariffUpdateRequest = {
|
const data: TariffCreateRequest | TariffUpdateRequest = {
|
||||||
name,
|
name,
|
||||||
description: description || undefined,
|
description: description || undefined,
|
||||||
|
is_active: isActive,
|
||||||
traffic_limit_gb: toNumber(trafficLimitGb, 100),
|
traffic_limit_gb: toNumber(trafficLimitGb, 100),
|
||||||
device_limit: toNumber(deviceLimit, 1),
|
device_limit: toNumber(deviceLimit, 1),
|
||||||
device_price_kopeks:
|
device_price_kopeks:
|
||||||
@@ -703,50 +706,6 @@ export default function AdminTariffCreate() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1003,6 +962,77 @@ export default function AdminTariffCreate() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ const XIcon = () => (
|
|||||||
</svg>
|
</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() {
|
export default function AdminTariffs() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -210,7 +220,7 @@ export default function AdminTariffs() {
|
|||||||
}`}
|
}`}
|
||||||
title={t('admin.tariffs.toggleTrial')}
|
title={t('admin.tariffs.toggleTrial')}
|
||||||
>
|
>
|
||||||
T
|
<GiftIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Edit */}
|
{/* Edit */}
|
||||||
|
|||||||
Reference in New Issue
Block a user