diff --git a/src/api/tariffs.ts b/src/api/tariffs.ts index a58224b..47bd121 100644 --- a/src/api/tariffs.ts +++ b/src/api/tariffs.ts @@ -236,4 +236,10 @@ export const tariffsApi = { const response = await apiClient.get('/cabinet/admin/tariffs/available-servers'); return response.data; }, + + // Get available promo groups for selection + getAvailablePromoGroups: async (): Promise<{ id: number; name: string }[]> => { + const response = await apiClient.get('/cabinet/admin/payment-methods/promo-groups'); + return response.data; + }, }; diff --git a/src/locales/en.json b/src/locales/en.json index c48d9ca..6fe3c86 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1251,6 +1251,7 @@ "nameLabel": "Tariff name", "nameExamplePeriod": "E.g.: Standard", "nameExampleDaily": "E.g.: Daily", + "nameHint": "2 to 50 characters", "descriptionLabel": "Description", "descriptionPlaceholder2": "Brief tariff description", "trafficLimitLabel": "Traffic limit", @@ -1267,8 +1268,12 @@ "addButton": "Add", "noPeriodsHint": "No added periods. Add at least one period.", "daysShort": "d.", + "serversTitle": "Servers", "serversTabHint": "Select servers available on this tariff.", "noServersAvailable": "No servers available", + "promoGroupsTitle": "Promo Groups", + "promoGroupsHint": "Select promo groups that have access to this tariff. If none selected — available to everyone.", + "noPromoGroups": "No promo groups", "extraDeviceTitle": "Additional devices", "devicePriceLabel": "Device price (30 days):", "devicePriceHint": "0 = device purchase unavailable", @@ -1278,6 +1283,8 @@ "trafficMaxLimitLabel": "Max. limit:", "trafficLimitHint2": "(0 = no limits)", "trafficPackagesLabel": "Traffic packages:", + "addPackageTitle": "Add package", + "noPackagesHint": "No packages added. Add at least one package.", "gbPackageUnit": "GB:", "customDaysTitle": "Custom number of days", "customDaysDesc": "User selects subscription duration", @@ -1309,7 +1316,18 @@ "dailyPriceLabel": "Price per day", "currencyPerDay": "\u20bd/day", "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:", + "validation": { + "nameRequired": "Enter tariff name", + "nameLength": "Name must be 2 to 50 characters", + "deviceLimitRequired": "Specify number of devices (minimum 1)", + "tierLevelInvalid": "Tier level must be between 1 and 10", + "fieldsRequired": "Fill in required fields (devices, tier level)", + "periodsRequired": "Add at least one period", + "dailyPriceRequired": "Enter a price per day greater than 0", + "trafficPackagesRequired": "Add at least one traffic package (or disable traffic topup)" + } }, "servers": { "title": "Server Management", diff --git a/src/locales/fa.json b/src/locales/fa.json index 0f3cd42..2467a55 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1021,6 +1021,7 @@ "nameLabel": "نام تعرفه", "nameExamplePeriod": "مثال: استاندارد", "nameExampleDaily": "مثال: روزانه", + "nameHint": "۲ تا ۵۰ کاراکتر", "descriptionLabel": "توضیحات", "descriptionPlaceholder2": "توضیح مختصر تعرفه", "trafficLimitLabel": "محدودیت ترافیک", @@ -1037,8 +1038,12 @@ "addButton": "افزودن", "noPeriodsHint": "هیچ دوره‌ای اضافه نشده. حداقل یک دوره اضافه کنید.", "daysShort": "روز", + "serversTitle": "سرورها", "serversTabHint": "سرورهای موجود در این تعرفه را انتخاب کنید.", "noServersAvailable": "هیچ سروری در دسترس نیست", + "promoGroupsTitle": "گروه‌های تبلیغاتی", + "promoGroupsHint": "گروه‌های تبلیغاتی که به این تعرفه دسترسی دارند را انتخاب کنید. اگر هیچ‌کدام انتخاب نشود، همه دسترسی دارند.", + "noPromoGroups": "هیچ گروه تبلیغاتی وجود ندارد", "extraDeviceTitle": "دستگاه‌های اضافی", "devicePriceLabel": "قیمت دستگاه (30 روز):", "devicePriceHint": "0 = خرید دستگاه در دسترس نیست", @@ -1048,6 +1053,8 @@ "trafficMaxLimitLabel": "حداکثر محدودیت:", "trafficLimitHint2": "(0 = بدون محدودیت)", "trafficPackagesLabel": "بسته‌های ترافیک:", + "addPackageTitle": "افزودن بسته", + "noPackagesHint": "هیچ بسته‌ای اضافه نشده است. حداقل یک بسته اضافه کنید.", "gbPackageUnit": "GB:", "customDaysTitle": "تعداد روزهای سفارشی", "customDaysDesc": "کاربر مدت اشتراک را انتخاب می‌کند", @@ -1079,7 +1086,18 @@ "dailyPriceLabel": "قیمت روزانه", "currencyPerDay": "₽/روز", "dailyDeductionDesc": "هر روز از موجودی کاربر کسر می‌شود", - "dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید" + "dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید", + "cannotSave": "امکان ذخیره تعرفه وجود ندارد:", + "validation": { + "nameRequired": "نام تعرفه را وارد کنید", + "nameLength": "نام باید ۲ تا ۵۰ کاراکتر باشد", + "deviceLimitRequired": "تعداد دستگاه‌ها را مشخص کنید (حداقل ۱)", + "tierLevelInvalid": "سطح تعرفه باید بین ۱ تا ۱۰ باشد", + "fieldsRequired": "فیلدهای اجباری را پر کنید (دستگاه‌ها، سطح)", + "periodsRequired": "حداقل یک دوره اضافه کنید", + "dailyPriceRequired": "قیمت روزانه بزرگتر از 0 را وارد کنید", + "trafficPackagesRequired": "حداقل یک بسته ترافیک اضافه کنید (یا خرید ترافیک را غیرفعال کنید)" + } }, "servers": { "title": "مدیریت سرورها", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4a0fc5d..884ba1d 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1762,6 +1762,7 @@ "nameLabel": "Название тарифа", "nameExamplePeriod": "Например: Стандарт", "nameExampleDaily": "Например: Суточный", + "nameHint": "От 2 до 50 символов", "descriptionLabel": "Описание", "descriptionPlaceholder": "Краткое описание тарифа", "trafficLimitLabel": "Лимит трафика", @@ -1778,8 +1779,12 @@ "addButton": "Добавить", "noPeriodsHint": "Нет добавленных периодов. Добавьте хотя бы один период.", "daysShort": "дн.", + "serversTitle": "Серверы", "serversTabHint": "Выберите серверы, доступные на этом тарифе.", "noServersAvailable": "Нет доступных серверов", + "promoGroupsTitle": "Промо-группы", + "promoGroupsHint": "Выберите промо-группы, которым доступен этот тариф. Если не выбрано ничего — доступен всем.", + "noPromoGroups": "Нет промо-групп", "extraDeviceTitle": "Докупка устройств", "devicePriceLabel": "Цена за устройство (30 дней):", "devicePriceHint": "0 = докупка недоступна", @@ -1789,6 +1794,8 @@ "trafficMaxLimitLabel": "Макс. лимит:", "trafficLimitHint2": "(0 = без ограничений)", "trafficPackagesLabel": "Пакеты трафика:", + "addPackageTitle": "Добавить пакет", + "noPackagesHint": "Нет добавленных пакетов. Добавьте хотя бы один пакет.", "gbPackageUnit": "ГБ:", "customDaysTitle": "Произвольное количество дней", "customDaysDesc": "Пользователь сам выбирает срок подписки", @@ -1820,7 +1827,18 @@ "dailyPriceLabel": "Цена за день", "currencyPerDay": "₽/день", "dailyDeductionDesc": "Списывается ежедневно с баланса пользователя", - "dailyPriceRequired": "Укажите цену за день больше 0" + "dailyPriceRequired": "Укажите цену за день больше 0", + "cannotSave": "Невозможно сохранить тариф:", + "validation": { + "nameRequired": "Введите название тарифа", + "nameLength": "Название должно быть от 2 до 50 символов", + "deviceLimitRequired": "Укажите количество устройств (минимум 1)", + "tierLevelInvalid": "Уровень тарифа должен быть от 1 до 10", + "fieldsRequired": "Заполните обязательные поля (устройства, уровень)", + "periodsRequired": "Добавьте хотя бы один период", + "dailyPriceRequired": "Укажите цену за день больше 0", + "trafficPackagesRequired": "Добавьте хотя бы один пакет трафика (или отключите докупку)" + } }, "servers": { "title": "Управление серверами", diff --git a/src/locales/zh.json b/src/locales/zh.json index 13f1fd1..345b1c0 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1059,6 +1059,7 @@ "nameLabel": "套餐名称", "nameExamplePeriod": "例如:标准", "nameExampleDaily": "例如:日套餐", + "nameHint": "2到50个字符", "descriptionLabel": "描述", "trafficLimitLabel": "流量限制", "gbUnit": "GB", @@ -1074,8 +1075,12 @@ "addButton": "添加", "noPeriodsHint": "没有添加的周期。请至少添加一个周期。", "daysShort": "天", + "serversTitle": "服务器", "serversTabHint": "选择此套餐上可用的服务器。", "noServersAvailable": "没有可用的服务器", + "promoGroupsTitle": "促销组", + "promoGroupsHint": "选择可以访问此套餐的促销组。如果未选择任何组,则所有人都可以访问。", + "noPromoGroups": "没有促销组", "extraDeviceTitle": "额外设备", "devicePriceLabel": "设备价格(30天):", "devicePriceHint": "0 = 无法购买设备", @@ -1085,6 +1090,8 @@ "trafficMaxLimitLabel": "最大限制:", "trafficLimitHint2": "(0 = 无限制)", "trafficPackagesLabel": "流量包:", + "addPackageTitle": "添加流量包", + "noPackagesHint": "没有添加流量包。请至少添加一个流量包。", "gbPackageUnit": "GB:", "customDaysTitle": "自定义天数", "customDaysDesc": "用户自行选择订阅期限", @@ -1116,7 +1123,18 @@ "dailyPriceLabel": "每天价格", "currencyPerDay": "₽/天", "dailyDeductionDesc": "每天从用户余额中扣除", - "dailyPriceRequired": "请输入大于0的每日价格" + "dailyPriceRequired": "请输入大于0的每日价格", + "cannotSave": "无法保存套餐:", + "validation": { + "nameRequired": "请输入套餐名称", + "nameLength": "名称必须为2到50个字符", + "deviceLimitRequired": "请指定设备数量(至少1个)", + "tierLevelInvalid": "等级必须在1到10之间", + "fieldsRequired": "请填写必填字段(设备数、等级)", + "periodsRequired": "请添加至少一个期限", + "dailyPriceRequired": "请输入大于0的每日价格", + "trafficPackagesRequired": "请添加至少一个流量包(或关闭流量购买)" + } }, "servers": { "title": "服务器管理", diff --git a/src/pages/AdminTariffCreate.tsx b/src/pages/AdminTariffCreate.tsx index a129b3a..156d302 100644 --- a/src/pages/AdminTariffCreate.tsx +++ b/src/pages/AdminTariffCreate.tsx @@ -94,16 +94,17 @@ export default function AdminTariffCreate() { // Step: null = type selection, 'period' or 'daily' = form const [tariffType, setTariffType] = useState(null); - // Form state + // Form state - matches bot fields const [name, setName] = useState(''); const [description, setDescription] = useState(''); - const [trafficLimitGb, setTrafficLimitGb] = useState(''); + const [trafficLimitGb, setTrafficLimitGb] = useState(100); const [deviceLimit, setDeviceLimit] = useState(1); const [devicePriceKopeks, setDevicePriceKopeks] = useState(0); const [maxDeviceLimit, setMaxDeviceLimit] = useState(0); const [tierLevel, setTierLevel] = useState(1); const [periodPrices, setPeriodPrices] = useState([]); const [selectedSquads, setSelectedSquads] = useState([]); + const [selectedPromoGroups, setSelectedPromoGroups] = useState([]); const [dailyPriceKopeks, setDailyPriceKopeks] = useState(0); // Traffic topup @@ -111,25 +112,23 @@ export default function AdminTariffCreate() { const [maxTopupTrafficGb, setMaxTopupTrafficGb] = useState(0); const [trafficTopupPackages, setTrafficTopupPackages] = useState>({}); + // New traffic package for adding + const [newPackageGb, setNewPackageGb] = useState(10); + const [newPackagePrice, setNewPackagePrice] = useState(100); + + // Track editing state for traffic package prices + const [editingPackagePrices, setEditingPackagePrices] = useState>({}); + // Traffic reset mode const [trafficResetMode, setTrafficResetMode] = useState(null); - // Custom days (period tariff) - const [customDaysEnabled, setCustomDaysEnabled] = useState(false); - const [pricePerDayKopeks, setPricePerDayKopeks] = useState(0); - const [minDays, setMinDays] = useState(1); - const [maxDays, setMaxDays] = useState(365); - - // Custom traffic (period tariff) - const [customTrafficEnabled, setCustomTrafficEnabled] = useState(false); - const [trafficPricePerGbKopeks, setTrafficPricePerGbKopeks] = useState(0); - const [minTrafficGb, setMinTrafficGb] = useState(1); - const [maxTrafficGb, setMaxTrafficGb] = useState(1000); - // New period for adding const [newPeriodDays, setNewPeriodDays] = useState(30); const [newPeriodPrice, setNewPeriodPrice] = useState(300); + // Track editing state for period prices + const [editingPeriodPrices, setEditingPeriodPrices] = useState>({}); + const [activeTab, setActiveTab] = useState<'basic' | 'periods' | 'servers' | 'extra'>('basic'); // Fetch servers @@ -138,6 +137,12 @@ export default function AdminTariffCreate() { queryFn: () => tariffsApi.getAvailableServers(), }); + // Fetch promo groups + const { data: promoGroups = [] } = useQuery({ + queryKey: ['admin-tariffs-promo-groups'], + queryFn: () => tariffsApi.getAvailablePromoGroups(), + }); + // Fetch tariff for editing const { isLoading: isLoadingTariff } = useQuery({ queryKey: ['admin-tariff', id], @@ -158,19 +163,14 @@ export default function AdminTariffCreate() { setTierLevel(data.tier_level || 1); setPeriodPrices(data.period_prices?.length ? data.period_prices : []); setSelectedSquads(data.allowed_squads || []); + setSelectedPromoGroups( + data.promo_groups?.filter((pg) => pg.is_selected).map((pg) => pg.id) || [], + ); setDailyPriceKopeks(data.daily_price_kopeks || 0); setTrafficTopupEnabled(data.traffic_topup_enabled || false); setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0); setTrafficTopupPackages(data.traffic_topup_packages || {}); setTrafficResetMode(data.traffic_reset_mode || null); - setCustomDaysEnabled(data.custom_days_enabled || false); - setPricePerDayKopeks(data.price_per_day_kopeks || 0); - setMinDays(data.min_days || 1); - setMaxDays(data.max_days || 365); - setCustomTrafficEnabled(data.custom_traffic_enabled || false); - setTrafficPricePerGbKopeks(data.traffic_price_per_gb_kopeks || 0); - setMinTrafficGb(data.min_traffic_gb || 1); - setMaxTrafficGb(data.max_traffic_gb || 1000); return data; }, []), }); @@ -200,7 +200,7 @@ export default function AdminTariffCreate() { const data: TariffCreateRequest | TariffUpdateRequest = { name, description: description || undefined, - traffic_limit_gb: toNumber(trafficLimitGb, 0), + traffic_limit_gb: toNumber(trafficLimitGb, 100), device_limit: toNumber(deviceLimit, 1), device_price_kopeks: toNumber(devicePriceKopeks) > 0 ? toNumber(devicePriceKopeks) : undefined, @@ -208,6 +208,7 @@ export default function AdminTariffCreate() { tier_level: toNumber(tierLevel, 1), period_prices: isDaily ? [] : periodPrices.filter((p) => p.price_kopeks >= 0), allowed_squads: selectedSquads, + promo_group_ids: selectedPromoGroups.length > 0 ? selectedPromoGroups : undefined, traffic_topup_enabled: trafficTopupEnabled, traffic_topup_packages: trafficTopupPackages, max_topup_traffic_gb: toNumber(maxTopupTrafficGb), @@ -216,17 +217,6 @@ export default function AdminTariffCreate() { traffic_reset_mode: trafficResetMode, }; - if (!isDaily) { - data.custom_days_enabled = customDaysEnabled; - data.price_per_day_kopeks = toNumber(pricePerDayKopeks); - data.min_days = toNumber(minDays, 1); - data.max_days = toNumber(maxDays, 365); - data.custom_traffic_enabled = customTrafficEnabled; - data.traffic_price_per_gb_kopeks = toNumber(trafficPricePerGbKopeks); - data.min_traffic_gb = toNumber(minTrafficGb, 1); - data.max_traffic_gb = toNumber(maxTrafficGb, 1000); - } - if (isEdit) { updateMutation.mutate({ id: Number(id), data }); } else { @@ -240,6 +230,12 @@ export default function AdminTariffCreate() { ); }; + const togglePromoGroup = (groupId: number) => { + setSelectedPromoGroups((prev) => + prev.includes(groupId) ? prev.filter((id) => id !== groupId) : [...prev, groupId], + ); + }; + const addPeriod = () => { const days = toNumber(newPeriodDays, 0); const price = toNumber(newPeriodPrice, 0); @@ -267,13 +263,48 @@ export default function AdminTariffCreate() { const isLoading = createMutation.isPending || updateMutation.isPending; - // Validate required numeric fields are not empty - const hasRequiredFields = deviceLimit !== '' && tierLevel !== ''; - const isValidPeriod = name && hasRequiredFields && (periodPrices.length > 0 || customDaysEnabled); - const isValidDaily = name && hasRequiredFields && toNumber(dailyPriceKopeks) > 0; + // Validation like bot: name 2-50 chars, device_limit >= 1, tier_level 1-10 + const isNameValid = name.length >= 2 && name.length <= 50; + const isDeviceLimitValid = deviceLimit !== '' && toNumber(deviceLimit) >= 1; + const isTierLevelValid = + tierLevel !== '' && toNumber(tierLevel) >= 1 && toNumber(tierLevel) <= 10; + const hasTrafficPackages = !trafficTopupEnabled || Object.keys(trafficTopupPackages).length > 0; + const isValidPeriod = + isNameValid && + isDeviceLimitValid && + isTierLevelValid && + periodPrices.length > 0 && + hasTrafficPackages; + const isValidDaily = + isNameValid && + isDeviceLimitValid && + isTierLevelValid && + toNumber(dailyPriceKopeks) > 0 && + hasTrafficPackages; const isValid = tariffType === 'period' ? isValidPeriod : tariffType === 'daily' ? isValidDaily : false; + // Collect validation errors for display + const validationErrors: string[] = []; + if (!isNameValid) { + if (name.length === 0) { + validationErrors.push('nameRequired'); + } else if (name.length < 2 || name.length > 50) { + validationErrors.push('nameLength'); + } + } + if (!isDeviceLimitValid) validationErrors.push('deviceLimitRequired'); + if (!isTierLevelValid) validationErrors.push('tierLevelInvalid'); + if (tariffType === 'period' && periodPrices.length === 0) { + validationErrors.push('periodsRequired'); + } + if (tariffType === 'daily' && toNumber(dailyPriceKopeks) === 0) { + validationErrors.push('dailyPriceRequired'); + } + if (trafficTopupEnabled && Object.keys(trafficTopupPackages).length === 0) { + validationErrors.push('trafficPackagesRequired'); + } + // Loading state if (isEdit && isLoadingTariff) { return ( @@ -394,16 +425,24 @@ export default function AdminTariffCreate() {
setName(e.target.value)} - className="input" + className={`input ${!isNameValid && name.length > 0 ? 'border-error-500/50' : ''}`} placeholder={ isDaily ? t('admin.tariffs.nameExampleDaily') : t('admin.tariffs.nameExamplePeriod') } + maxLength={50} /> +

{t('admin.tariffs.nameHint')}

+ {name.length > 0 && (name.length < 2 || name.length > 50) && ( +

+ {t('admin.tariffs.validation.nameLength')} +

+ )}
{/* Description */} @@ -442,16 +481,11 @@ export default function AdminTariffCreate() { className={`input w-32 ${dailyPriceKopeks === '' || dailyPriceKopeks === 0 ? 'border-error-500/50' : ''}`} min={0} step={0.1} - placeholder="0.00" + placeholder="50" /> {t('admin.tariffs.currencyPerDay')}

{t('admin.tariffs.dailyDeductionDesc')}

- {(dailyPriceKopeks === '' || dailyPriceKopeks === 0) && ( -

- {t('admin.tariffs.dailyPriceRequired')} -

- )} )} @@ -467,7 +501,7 @@ export default function AdminTariffCreate() { onChange={createNumberInputHandler(setTrafficLimitGb, 0)} className="input w-32" min={0} - placeholder="0" + placeholder="100" /> {t('admin.tariffs.gbUnit')} {(trafficLimitGb === 0 || trafficLimitGb === '') && ( @@ -490,13 +524,10 @@ export default function AdminTariffCreate() { type="number" value={deviceLimit} onChange={createNumberInputHandler(setDeviceLimit, 1)} - className={`input w-32 ${deviceLimit === '' ? 'border-error-500/50' : ''}`} + className={`input w-32 ${!isDeviceLimitValid ? 'border-error-500/50' : ''}`} min={1} placeholder="1" /> - {deviceLimit === '' && ( -

{t('admin.tariffs.fieldRequired')}

- )} {/* Tier Level */} @@ -509,15 +540,12 @@ export default function AdminTariffCreate() { type="number" value={tierLevel} onChange={createNumberInputHandler(setTierLevel, 1, 10)} - className={`input w-32 ${tierLevel === '' ? 'border-error-500/50' : ''}`} + className={`input w-32 ${!isTierLevelValid ? 'border-error-500/50' : ''}`} min={1} max={10} placeholder="1" />

{t('admin.tariffs.tierLevelHint')}

- {tierLevel === '' && ( -

{t('admin.tariffs.fieldRequired')}

- )} )} @@ -541,7 +569,6 @@ export default function AdminTariffCreate() { value={newPeriodDays} onChange={createNumberInputHandler(setNewPeriodDays, 1)} className="input w-24" - min={1} placeholder="30" /> @@ -554,7 +581,6 @@ export default function AdminTariffCreate() { value={newPeriodPrice} onChange={createNumberInputHandler(setNewPeriodPrice, 1)} className="input w-28" - min={1} placeholder="300" /> @@ -584,12 +610,33 @@ export default function AdminTariffCreate() { - updatePeriodPrice(period.days, Math.max(0, parseFloat(e.target.value) || 0)) + value={ + editingPeriodPrices[period.days] !== undefined + ? editingPeriodPrices[period.days] + : period.price_kopeks / 100 } + onChange={(e) => { + const val = e.target.value; + setEditingPeriodPrices((prev) => ({ ...prev, [period.days]: val })); + if (val !== '') { + const num = parseFloat(val); + if (!isNaN(num)) { + updatePeriodPrice(period.days, Math.max(0, num)); + } + } + }} + onBlur={(e) => { + const val = e.target.value; + if (val === '') { + updatePeriodPrice(period.days, 0); + } + setEditingPeriodPrices((prev) => { + const copy = { ...prev }; + delete copy[period.days]; + return copy; + }); + }} className="input w-28" - min={0} step={1} placeholder="0" /> @@ -609,49 +656,97 @@ export default function AdminTariffCreate() { )} {activeTab === 'servers' && ( -
-

{t('admin.tariffs.serversTabHint')}

- {servers.length === 0 ? ( -

- {t('admin.tariffs.noServersAvailable')} -

- ) : ( -
- {servers.map((server: ServerInfo) => { - const isSelected = selectedSquads.includes(server.squad_uuid); - return ( -
- {server.display_name} - {server.country_code && ( - {server.country_code} - )} - - ); - })} -
- )} +
+ {isSelected && } +
+ {server.display_name} + {server.country_code && ( + {server.country_code} + )} + + ); + })} + + )} + + + {/* Promo Groups */} +
+

+ {t('admin.tariffs.promoGroupsTitle')} +

+

{t('admin.tariffs.promoGroupsHint')}

+ {promoGroups.length === 0 ? ( +

{t('admin.tariffs.noPromoGroups')}

+ ) : ( +
+ {promoGroups.map((group) => { + const isSelected = selectedPromoGroups.includes(group.id); + return ( + + ); + })} +
+ )} +
)} @@ -737,200 +832,146 @@ export default function AdminTariffCreate() { /> {t('admin.tariffs.gbUnit')} + {/* Add new package */} +
+
+ {t('admin.tariffs.addPackageTitle')} +
+
+
+ + +
+
+ + +
+ +
+
+ + {/* Package list */}
{t('admin.tariffs.trafficPackagesLabel')} -
- {[5, 10, 20, 50].map((gb) => ( -
- - {gb} {t('admin.tariffs.gbPackageUnit')} - - { - const price = Math.max(0, parseFloat(e.target.value) || 0) * 100; - setTrafficTopupPackages((prev) => ({ - ...prev, - [String(gb)]: price, - })); - }} - className="input w-20" - min={0} - step={1} - placeholder="0" - /> - -
- ))} -
+ {Object.keys(trafficTopupPackages).length === 0 ? ( +
+ {t('admin.tariffs.noPackagesHint')} +
+ ) : ( +
+ {Object.entries(trafficTopupPackages) + .sort(([a], [b]) => Number(a) - Number(b)) + .map(([gb, priceKopeks]) => ( +
+ + {gb} {t('admin.tariffs.gbPackageUnit')} + + { + const val = e.target.value; + setEditingPackagePrices((prev) => ({ ...prev, [gb]: val })); + if (val !== '') { + const num = parseFloat(val); + if (!isNaN(num)) { + setTrafficTopupPackages((prev) => ({ + ...prev, + [gb]: Math.max(0, num) * 100, + })); + } + } + }} + onBlur={(e) => { + const val = e.target.value; + if (val === '') { + setTrafficTopupPackages((prev) => ({ + ...prev, + [gb]: 0, + })); + } + setEditingPackagePrices((prev) => { + const copy = { ...prev }; + delete copy[gb]; + return copy; + }); + }} + className="input w-24" + step={1} + placeholder="0" + /> + +
+ +
+ ))} +
+ )}
)}
- {/* Custom days (period only) */} - {!isDaily && ( -
-
-
-

- {t('admin.tariffs.customDaysTitle')} -

-

{t('admin.tariffs.customDaysDesc')}

-
- -
- {customDaysEnabled && ( -
-
- - {t('admin.tariffs.pricePerDayLabel')} - - { - const val = e.target.value; - if (val === '') { - setPricePerDayKopeks(''); - } else { - setPricePerDayKopeks(Math.max(0, parseFloat(val) || 0) * 100); - } - }} - className="input w-24" - min={0} - step={0.1} - placeholder="0" - /> - -
-
- - {t('admin.tariffs.minDaysLabel')} - - -
-
- - {t('admin.tariffs.maxDaysLabel')} - - -
-
- )} -
- )} - - {/* Custom traffic (period only) */} - {!isDaily && ( -
-
-
-

- {t('admin.tariffs.customTrafficTitle')} -

-

- {t('admin.tariffs.customTrafficDesc')} -

-
- -
- {customTrafficEnabled && ( -
-
- - {t('admin.tariffs.pricePerGbLabel')} - - { - const val = e.target.value; - if (val === '') { - setTrafficPricePerGbKopeks(''); - } else { - setTrafficPricePerGbKopeks(Math.max(0, parseFloat(val) || 0) * 100); - } - }} - className="input w-24" - min={0} - step={0.1} - placeholder="0" - /> - -
-
- - {t('admin.tariffs.minTrafficLabel')} - - -
-
- - {t('admin.tariffs.maxTrafficLabel')} - - -
-
- )} -
- )} - {/* Traffic reset mode */}

@@ -966,15 +1007,29 @@ export default function AdminTariffCreate() { )} {/* Footer */} -
- +
+ {validationErrors.length > 0 && ( +
+

+ {t('admin.tariffs.cannotSave')} +

+
    + {validationErrors.map((error) => ( +
  • {t(`admin.tariffs.validation.${error}`)}
  • + ))} +
+
+ )} +
+ +
);