diff --git a/app/bot.py b/app/bot.py index 36532569..ebae217e 100644 --- a/app/bot.py +++ b/app/bot.py @@ -41,6 +41,7 @@ from app.handlers.admin import ( tickets as admin_tickets, reports as admin_reports, bot_configuration as admin_bot_configuration, + pricing as admin_pricing, ) from app.handlers.stars_payments import register_stars_handlers @@ -145,6 +146,7 @@ async def setup_bot() -> tuple[Bot, Dispatcher]: admin_tickets.register_handlers(dp) admin_reports.register_handlers(dp) admin_bot_configuration.register_handlers(dp) + admin_pricing.register_handlers(dp) common.register_handlers(dp) register_stars_handlers(dp) logger.info("⭐ Зарегистрированы обработчики Telegram Stars платежей") diff --git a/app/handlers/admin/pricing.py b/app/handlers/admin/pricing.py new file mode 100644 index 00000000..dbccaa83 --- /dev/null +++ b/app/handlers/admin/pricing.py @@ -0,0 +1,455 @@ +import logging +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP +from typing import Iterable, List, Tuple + +from aiogram import Bot, Dispatcher, F, types +from aiogram.fsm.context import FSMContext +from aiogram.exceptions import TelegramBadRequest +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.database.models import User +from app.localization.texts import get_texts +from app.services.system_settings_service import bot_configuration_service +from app.states import PricingStates +from app.utils.decorators import admin_required, error_handler + +logger = logging.getLogger(__name__) + + +PriceItem = Tuple[str, str, int] + + +def _language_code(language: str | None) -> str: + return (language or "ru").split("-")[0].lower() + + +def _format_period_label(days: int, lang_code: str, short: bool = False) -> str: + if short: + suffix = "д" if lang_code == "ru" else "d" + return f"{days}{suffix}" + if lang_code == "ru": + return f"{days} дней" + if days == 1: + return "1 day" + return f"{days}-day plan" + + +def _format_traffic_label(gb: int, lang_code: str, short: bool = False) -> str: + if gb == 0: + return "∞" if short else ("Безлимит" if lang_code == "ru" else "Unlimited") + unit = "ГБ" if lang_code == "ru" else "GB" + if short: + return f"{gb}{unit}" if lang_code == "ru" else f"{gb}{unit}" + return f"{gb} {unit}" + + +def _get_period_items(lang_code: str) -> List[PriceItem]: + items: List[PriceItem] = [] + for days in settings.get_available_subscription_periods(): + key = f"PRICE_{days}_DAYS" + if hasattr(settings, key): + price = getattr(settings, key) + items.append((key, _format_period_label(days, lang_code), price)) + return items + + +def _get_traffic_items(lang_code: str) -> List[PriceItem]: + traffic_keys: Tuple[Tuple[int, str], ...] = ( + (5, "PRICE_TRAFFIC_5GB"), + (10, "PRICE_TRAFFIC_10GB"), + (25, "PRICE_TRAFFIC_25GB"), + (50, "PRICE_TRAFFIC_50GB"), + (100, "PRICE_TRAFFIC_100GB"), + (250, "PRICE_TRAFFIC_250GB"), + (500, "PRICE_TRAFFIC_500GB"), + (1000, "PRICE_TRAFFIC_1000GB"), + (0, "PRICE_TRAFFIC_UNLIMITED"), + ) + + items: List[PriceItem] = [] + for gb, key in traffic_keys: + if hasattr(settings, key): + price = getattr(settings, key) + items.append((key, _format_traffic_label(gb, lang_code), price)) + return items + + +def _get_extra_items(lang_code: str) -> List[PriceItem]: + items: List[PriceItem] = [] + + if hasattr(settings, "PRICE_PER_DEVICE"): + label = "Дополнительное устройство" if lang_code == "ru" else "Extra device" + items.append(("PRICE_PER_DEVICE", label, settings.PRICE_PER_DEVICE)) + + return items + + +def _build_period_summary(items: Iterable[PriceItem], lang_code: str, fallback: str) -> str: + parts: List[str] = [] + for key, label, price in items: + try: + days = int(key.replace("PRICE_", "").replace("_DAYS", "")) + except ValueError: + days = None + + if days is not None: + suffix = "д" if lang_code == "ru" else "d" + short_label = f"{days}{suffix}" + else: + short_label = label + + parts.append(f"{short_label}: {settings.format_price(price)}") + + return ", ".join(parts) if parts else fallback + + +def _build_traffic_summary(items: Iterable[PriceItem], lang_code: str, fallback: str) -> str: + parts: List[str] = [] + for key, label, price in items: + if key.endswith("UNLIMITED"): + short_label = "∞" + else: + digits = ''.join(ch for ch in key if ch.isdigit()) + unit = "ГБ" if lang_code == "ru" else "GB" + short_label = f"{digits}{unit}" if digits else label + + parts.append(f"{short_label}: {settings.format_price(price)}") + + return ", ".join(parts) if parts else fallback + + +def _build_extra_summary(items: Iterable[PriceItem], fallback: str) -> str: + parts = [f"{label}: {settings.format_price(price)}" for key, label, price in items] + return ", ".join(parts) if parts else fallback + + +def _build_overview(language: str) -> Tuple[str, types.InlineKeyboardMarkup]: + texts = get_texts(language) + lang_code = _language_code(language) + + period_items = _get_period_items(lang_code) + traffic_items = _get_traffic_items(lang_code) + extra_items = _get_extra_items(lang_code) + + fallback = texts.t("ADMIN_PRICING_SUMMARY_EMPTY", "—") + summary_periods = _build_period_summary(period_items, lang_code, fallback) + summary_traffic = _build_traffic_summary(traffic_items, lang_code, fallback) + summary_extra = _build_extra_summary(extra_items, fallback) + + text = ( + f"💰 {texts.t('ADMIN_PRICING_MENU_TITLE', 'Управление ценами')}\n\n" + f"{texts.t('ADMIN_PRICING_MENU_DESCRIPTION', 'Быстрый доступ к тарифам и пакетам.')}\n\n" + f"{texts.t('ADMIN_PRICING_MENU_SUMMARY', 'Краткая сводка:')}\n" + f"{texts.t('ADMIN_PRICING_MENU_SUMMARY_PERIODS', '• Периоды: {summary}').format(summary=summary_periods)}\n" + f"{texts.t('ADMIN_PRICING_MENU_SUMMARY_TRAFFIC', '• Трафик: {summary}').format(summary=summary_traffic)}\n" + f"{texts.t('ADMIN_PRICING_MENU_SUMMARY_EXTRA', '• Дополнительно: {summary}').format(summary=summary_extra)}\n\n" + f"{texts.t('ADMIN_PRICING_MENU_PROMPT', 'Выберите раздел для редактирования:')}" + ) + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text=texts.t("ADMIN_PRICING_BUTTON_PERIODS", "🗓 Периоды подписки"), + callback_data="admin_pricing_section:periods", + ) + ], + [ + types.InlineKeyboardButton( + text=texts.t("ADMIN_PRICING_BUTTON_TRAFFIC", "📦 Пакеты трафика"), + callback_data="admin_pricing_section:traffic", + ) + ], + [ + types.InlineKeyboardButton( + text=texts.t("ADMIN_PRICING_BUTTON_EXTRA", "➕ Дополнительно"), + callback_data="admin_pricing_section:extra", + ) + ], + [types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")], + ] + ) + + return text, keyboard + + +def _build_section( + section: str, + language: str, +) -> Tuple[str, types.InlineKeyboardMarkup]: + texts = get_texts(language) + lang_code = _language_code(language) + + if section == "periods": + items = _get_period_items(lang_code) + title = texts.t("ADMIN_PRICING_SECTION_PERIODS_TITLE", "🗓 Периоды подписки") + elif section == "traffic": + items = _get_traffic_items(lang_code) + title = texts.t("ADMIN_PRICING_SECTION_TRAFFIC_TITLE", "📦 Пакеты трафика") + else: + items = _get_extra_items(lang_code) + title = texts.t("ADMIN_PRICING_SECTION_EXTRA_TITLE", "➕ Дополнительные опции") + + lines = [title, ""] + + if items: + for key, label, price in items: + lines.append(f"• {label} — {settings.format_price(price)}") + lines.append("") + lines.append(texts.t("ADMIN_PRICING_SECTION_PROMPT", "Выберите что изменить:")) + else: + lines.append(texts.t("ADMIN_PRICING_SECTION_EMPTY", "Нет доступных значений.")) + + keyboard_rows: List[List[types.InlineKeyboardButton]] = [] + for key, label, price in items: + keyboard_rows.append( + [ + types.InlineKeyboardButton( + text=f"{label} • {settings.format_price(price)}", + callback_data=f"admin_pricing_edit:{section}:{key}", + ) + ] + ) + + keyboard_rows.append( + [types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")] + ) + + keyboard = types.InlineKeyboardMarkup(inline_keyboard=keyboard_rows) + return "\n".join(lines), keyboard + + +async def _render_message( + message: types.Message, + text: str, + keyboard: types.InlineKeyboardMarkup, +) -> None: + try: + await message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") + except TelegramBadRequest as error: # message changed elsewhere + logger.debug("Failed to edit pricing message: %s", error) + await message.answer(text, reply_markup=keyboard, parse_mode="HTML") + + +async def _render_message_by_id( + bot: Bot, + chat_id: int, + message_id: int, + text: str, + keyboard: types.InlineKeyboardMarkup, +) -> None: + try: + await bot.edit_message_text( + text=text, + chat_id=chat_id, + message_id=message_id, + reply_markup=keyboard, + parse_mode="HTML", + ) + except TelegramBadRequest as error: + logger.debug("Failed to edit pricing message by id: %s", error) + await bot.send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML") + + +def _parse_price_input(text: str) -> int: + normalized = text.replace("₽", "").replace("р", "").replace("RUB", "") + normalized = normalized.replace(" ", "").replace(",", ".").strip() + if not normalized: + raise ValueError("empty") + + try: + value = Decimal(normalized) + except InvalidOperation as error: + raise ValueError("invalid") from error + + if value < 0: + raise ValueError("negative") + + kopeks = int((value * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP)) + return kopeks + + +def _resolve_label(section: str, key: str, language: str) -> str: + lang_code = _language_code(language) + + if section == "periods" and key.startswith("PRICE_") and key.endswith("_DAYS"): + try: + days = int(key.replace("PRICE_", "").replace("_DAYS", "")) + except ValueError: + days = None + if days is not None: + return _format_period_label(days, lang_code) + + if section == "traffic" and key.startswith("PRICE_TRAFFIC_"): + if key.endswith("UNLIMITED"): + return _format_traffic_label(0, lang_code) + digits = ''.join(ch for ch in key if ch.isdigit()) + try: + gb = int(digits) + except ValueError: + gb = None + if gb is not None: + return _format_traffic_label(gb, lang_code) + + if key == "PRICE_PER_DEVICE": + return "Дополнительное устройство" if lang_code == "ru" else "Extra device" + + return key + + +@admin_required +@error_handler +async def show_pricing_menu( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +) -> None: + text, keyboard = _build_overview(db_user.language) + await _render_message(callback.message, text, keyboard) + await state.clear() + await callback.answer() + + +@admin_required +@error_handler +async def show_pricing_section( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +) -> None: + section = callback.data.split(":", 1)[1] + text, keyboard = _build_section(section, db_user.language) + await _render_message(callback.message, text, keyboard) + await state.clear() + await callback.answer() + + +@admin_required +@error_handler +async def start_price_edit( + callback: types.CallbackQuery, + db_user: User, + db: AsyncSession, + state: FSMContext, +) -> None: + _, section, key = callback.data.split(":", 2) + texts = get_texts(db_user.language) + label = _resolve_label(section, key, db_user.language) + + await state.update_data( + pricing_key=key, + pricing_section=section, + pricing_message_id=callback.message.message_id, + ) + await state.set_state(PricingStates.waiting_for_value) + + prompt = ( + f"💰 {texts.t('ADMIN_PRICING_EDIT_TITLE', 'Изменение цены')}\n\n" + f"{texts.t('ADMIN_PRICING_EDIT_TARGET', 'Текущий тариф')}: {label}\n" + f"{texts.t('ADMIN_PRICING_EDIT_CURRENT', 'Текущее значение')}: {settings.format_price(getattr(settings, key, 0))}\n\n" + f"{texts.t('ADMIN_PRICING_EDIT_PROMPT', 'Введите новую стоимость в рублях (например 990 или 990.50). Для бесплатного тарифа укажите 0.')}" + ) + + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text=texts.t("ADMIN_PRICING_EDIT_CANCEL", "❌ Отмена"), + callback_data=f"admin_pricing_section:{section}", + ) + ] + ] + ) + + await _render_message(callback.message, prompt, keyboard) + await callback.answer() + + +async def process_price_input( + message: types.Message, + state: FSMContext, + db_user: User, + db: AsyncSession, +) -> None: + data = await state.get_data() + key = data.get("pricing_key") + section = data.get("pricing_section", "periods") + message_id = data.get("pricing_message_id") + + texts = get_texts(db_user.language) + + if not key: + await message.answer(texts.t("ADMIN_PRICING_EDIT_EXPIRED", "Сессия редактирования истекла.")) + await state.clear() + return + + raw_value = message.text or "" + if raw_value.strip().lower() in {"cancel", "отмена"}: + await state.clear() + section_text, section_keyboard = _build_section(section, db_user.language) + if message_id: + await _render_message_by_id( + message.bot, + message.chat.id, + message_id, + section_text, + section_keyboard, + ) + await message.answer(texts.t("ADMIN_PRICING_EDIT_CANCELLED", "Изменения отменены.")) + return + + try: + price_kopeks = _parse_price_input(raw_value) + except ValueError: + await message.answer( + texts.t( + "ADMIN_PRICING_EDIT_INVALID", + "Не удалось распознать цену. Укажите число в рублях (например 990 или 990.50).", + ) + ) + return + + await bot_configuration_service.set_value(db, key, price_kopeks) + await db.commit() + + label = _resolve_label(section, key, db_user.language) + await message.answer( + texts.t("ADMIN_PRICING_EDIT_SUCCESS", "Цена для {item} обновлена: {price}").format( + item=label, + price=settings.format_price(price_kopeks), + ) + ) + + await state.clear() + + if message_id: + section_text, section_keyboard = _build_section(section, db_user.language) + await _render_message_by_id( + message.bot, + message.chat.id, + message_id, + section_text, + section_keyboard, + ) + + +def register_handlers(dp: Dispatcher) -> None: + dp.callback_query.register( + show_pricing_menu, + F.data.in_({"admin_pricing", "admin_subs_pricing"}), + ) + dp.callback_query.register( + show_pricing_section, + F.data.startswith("admin_pricing_section:"), + ) + dp.callback_query.register( + start_price_edit, + F.data.startswith("admin_pricing_edit:"), + ) + dp.message.register( + process_price_input, + PricingStates.waiting_for_value, + ) diff --git a/app/handlers/admin/servers.py b/app/handlers/admin/servers.py index 2a1ba638..ab918955 100644 --- a/app/handlers/admin/servers.py +++ b/app/handlers/admin/servers.py @@ -171,7 +171,7 @@ async def show_servers_menu( types.InlineKeyboardButton(text="📈 Подробная статистика", callback_data="admin_servers_stats") ], [ - types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions") + types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_panel") ] ] diff --git a/app/handlers/admin/subscriptions.py b/app/handlers/admin/subscriptions.py index 0c341a04..ae2d8f43 100644 --- a/app/handlers/admin/subscriptions.py +++ b/app/handlers/admin/subscriptions.py @@ -4,7 +4,6 @@ from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func -from app.config import settings from app.states import AdminStates from app.database.models import User from app.keyboards.admin import get_admin_subscriptions_keyboard @@ -87,10 +86,6 @@ async def show_subscriptions_menu( ], [ types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_subs_stats"), - types.InlineKeyboardButton(text="💰 Настройки цен", callback_data="admin_subs_pricing") - ], - [ - types.InlineKeyboardButton(text="🌐 Управление серверами", callback_data="admin_servers"), types.InlineKeyboardButton(text="🌍 География", callback_data="admin_subs_countries") ], [ @@ -277,56 +272,6 @@ async def show_subscriptions_stats( @admin_required @error_handler -async def show_pricing_settings( - callback: types.CallbackQuery, - db_user: User, - db: AsyncSession -): - text = f""" -⚙️ Настройки цен - -Периоды подписки: -- 14 дней: {settings.format_price(settings.PRICE_14_DAYS)} -- 30 дней: {settings.format_price(settings.PRICE_30_DAYS)} -- 60 дней: {settings.format_price(settings.PRICE_60_DAYS)} -- 90 дней: {settings.format_price(settings.PRICE_90_DAYS)} -- 180 дней: {settings.format_price(settings.PRICE_180_DAYS)} -- 360 дней: {settings.format_price(settings.PRICE_360_DAYS)} - -Трафик-пакеты: -- 5 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_5GB)} -- 10 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_10GB)} -- 25 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_25GB)} -- 50 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_50GB)} -- 100 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_100GB)} -- 250 ГБ: {settings.format_price(settings.PRICE_TRAFFIC_250GB)} - -Дополнительно: -- За устройство: {settings.format_price(settings.PRICE_PER_DEVICE)} -""" - - keyboard = [ - # [ - # types.InlineKeyboardButton(text="📅 Периоды", callback_data="admin_edit_period_prices"), - # types.InlineKeyboardButton(text="📈 Трафик", callback_data="admin_edit_traffic_prices") - # ], - # [ - # types.InlineKeyboardButton(text="📱 Устройства", callback_data="admin_edit_device_price") - # ], - [ - types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions") - ] - ] - - await callback.message.edit_text( - text, - reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard) - ) - await callback.answer() - - -@admin_required -@error_handler async def show_countries_management( callback: types.CallbackQuery, db_user: User, @@ -487,7 +432,6 @@ def register_handlers(dp: Dispatcher): dp.callback_query.register(show_subscriptions_list, F.data == "admin_subs_list") dp.callback_query.register(show_expiring_subscriptions, F.data == "admin_subs_expiring") dp.callback_query.register(show_subscriptions_stats, F.data == "admin_subs_stats") - dp.callback_query.register(show_pricing_settings, F.data == "admin_subs_pricing") dp.callback_query.register(show_countries_management, F.data == "admin_subs_countries") dp.callback_query.register(send_expiry_reminders, F.data == "admin_send_expiry_reminders") diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py index ee7729af..60be21e8 100644 --- a/app/keyboards/admin.py +++ b/app/keyboards/admin.py @@ -13,12 +13,46 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup: texts = get_texts(language) return InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_USERS_SUBSCRIPTIONS", "👥 Юзеры/Подписки"), callback_data="admin_submenu_users")], - [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_PROMO_STATS", "💰 Промокоды/Статистика"), callback_data="admin_submenu_promo")], - [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_SUPPORT", "🛟 Поддержка"), callback_data="admin_submenu_support")], - [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_MESSAGES", "📨 Сообщения"), callback_data="admin_submenu_communications")], - [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_SETTINGS", "⚙️ Настройки"), callback_data="admin_submenu_settings")], - [InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_SYSTEM", "🛠️ Система"), callback_data="admin_submenu_system")], + [ + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_USERS_SUBSCRIPTIONS", "👥 Юзеры/Подписки"), + callback_data="admin_submenu_users", + ), + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_SERVERS", "🌐 Серверы"), + callback_data="admin_servers", + ), + ], + [ + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_PRICING", "💰 Цены"), + callback_data="admin_pricing", + ), + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_PROMO_STATS", "💰 Промокоды/Статистика"), + callback_data="admin_submenu_promo", + ), + ], + [ + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_SUPPORT", "🛟 Поддержка"), + callback_data="admin_submenu_support", + ), + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_MESSAGES", "📨 Сообщения"), + callback_data="admin_submenu_communications", + ), + ], + [ + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_SETTINGS", "⚙️ Настройки"), + callback_data="admin_submenu_settings", + ), + InlineKeyboardButton( + text=_t(texts, "ADMIN_MAIN_SYSTEM", "🛠️ Система"), + callback_data="admin_submenu_system", + ), + ], [InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu")] ]) @@ -299,10 +333,6 @@ def get_admin_subscriptions_keyboard(language: str = "ru") -> InlineKeyboardMark ) ], [ - InlineKeyboardButton( - text=_t(texts, "ADMIN_SUBSCRIPTIONS_PRICING", "⚙️ Настройки цен"), - callback_data="admin_subs_pricing" - ), InlineKeyboardButton( text=_t(texts, "ADMIN_SUBSCRIPTIONS_COUNTRIES", "🌍 Управление странами"), callback_data="admin_subs_countries" diff --git a/app/states.py b/app/states.py index 61015d48..25ceccd1 100644 --- a/app/states.py +++ b/app/states.py @@ -135,6 +135,10 @@ class BotConfigStates(StatesGroup): waiting_for_search_query = State() waiting_for_import_file = State() + +class PricingStates(StatesGroup): + waiting_for_value = State() + class AutoPayStates(StatesGroup): setting_autopay_days = State() confirming_autopay_toggle = State() diff --git a/locales/en.json b/locales/en.json index d970b566..63c0b1cd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -543,6 +543,8 @@ "NOTIFY_PROMPT_THIRD_HOURS": "Enter the number of hours the late discount is active (1-168):", "NOTIFY_PROMPT_THIRD_DAYS": "After how many days without a subscription should we send the offer? (minimum 2):", "ADMIN_MAIN_USERS_SUBSCRIPTIONS": "👥 Users / Subscriptions", + "ADMIN_MAIN_SERVERS": "🌐 Servers", + "ADMIN_MAIN_PRICING": "💰 Pricing", "ADMIN_MAIN_PROMO_STATS": "💰 Promo codes / Stats", "ADMIN_MAIN_SUPPORT": "🛟 Support", "ADMIN_MAIN_MESSAGES": "📨 Messages", @@ -815,5 +817,30 @@ "ADMIN_BROADCAST_BUTTON_CONNECT": "🔗 Connect", "ADMIN_BROADCAST_BUTTON_SUBSCRIPTION": "📱 Subscription", "ADMIN_BROADCAST_BUTTON_SUPPORT": "🛠️ Support", - "ADMIN_BROADCAST_BUTTON_HOME": "🏠 Main menu" + "ADMIN_BROADCAST_BUTTON_HOME": "🏠 Main menu", + "ADMIN_PRICING_SUMMARY_EMPTY": "—", + "ADMIN_PRICING_MENU_TITLE": "Pricing management", + "ADMIN_PRICING_MENU_DESCRIPTION": "Quick access to subscription plans, traffic bundles and extra services.", + "ADMIN_PRICING_MENU_SUMMARY": "Quick summary:", + "ADMIN_PRICING_MENU_SUMMARY_PERIODS": "• Periods: {summary}", + "ADMIN_PRICING_MENU_SUMMARY_TRAFFIC": "• Traffic: {summary}", + "ADMIN_PRICING_MENU_SUMMARY_EXTRA": "• Extras: {summary}", + "ADMIN_PRICING_MENU_PROMPT": "Choose a section to edit:", + "ADMIN_PRICING_BUTTON_PERIODS": "🗓 Subscription periods", + "ADMIN_PRICING_BUTTON_TRAFFIC": "📦 Traffic packages", + "ADMIN_PRICING_BUTTON_EXTRA": "➕ Extras", + "ADMIN_PRICING_SECTION_PERIODS_TITLE": "🗓 Subscription periods", + "ADMIN_PRICING_SECTION_TRAFFIC_TITLE": "📦 Traffic packages", + "ADMIN_PRICING_SECTION_EXTRA_TITLE": "➕ Extra options", + "ADMIN_PRICING_SECTION_PROMPT": "Select what to update:", + "ADMIN_PRICING_SECTION_EMPTY": "No values available.", + "ADMIN_PRICING_EDIT_TITLE": "Update price", + "ADMIN_PRICING_EDIT_TARGET": "Current item", + "ADMIN_PRICING_EDIT_CURRENT": "Current value", + "ADMIN_PRICING_EDIT_PROMPT": "Enter a new price in RUB (e.g. 990 or 990.50). Use 0 for a free plan.", + "ADMIN_PRICING_EDIT_CANCEL": "❌ Cancel", + "ADMIN_PRICING_EDIT_EXPIRED": "Editing session expired.", + "ADMIN_PRICING_EDIT_CANCELLED": "Changes cancelled.", + "ADMIN_PRICING_EDIT_INVALID": "Could not parse the price. Please enter a number in RUB (e.g. 990 or 990.50).", + "ADMIN_PRICING_EDIT_SUCCESS": "Price for {item} updated: {price}" } diff --git a/locales/ru.json b/locales/ru.json index 13c5c434..6c7c9422 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -543,6 +543,8 @@ "NOTIFY_PROMPT_THIRD_HOURS": "Введите количество часов действия скидки (1-168):", "NOTIFY_PROMPT_THIRD_DAYS": "Через сколько дней после истечения отправлять предложение? (минимум 2):", "ADMIN_MAIN_USERS_SUBSCRIPTIONS": "👥 Юзеры/Подписки", + "ADMIN_MAIN_SERVERS": "🌐 Серверы", + "ADMIN_MAIN_PRICING": "💰 Цены", "ADMIN_MAIN_PROMO_STATS": "💰 Промокоды/Статистика", "ADMIN_MAIN_SUPPORT": "🛟 Поддержка", "ADMIN_MAIN_MESSAGES": "📨 Сообщения", @@ -815,5 +817,30 @@ "ADMIN_BROADCAST_BUTTON_CONNECT": "🔗 Подключиться", "ADMIN_BROADCAST_BUTTON_SUBSCRIPTION": "📱 Подписка", "ADMIN_BROADCAST_BUTTON_SUPPORT": "🛠️ Техподдержка", - "ADMIN_BROADCAST_BUTTON_HOME": "🏠 На главную" + "ADMIN_BROADCAST_BUTTON_HOME": "🏠 На главную", + "ADMIN_PRICING_SUMMARY_EMPTY": "—", + "ADMIN_PRICING_MENU_TITLE": "Управление ценами", + "ADMIN_PRICING_MENU_DESCRIPTION": "Быстрый доступ к тарифам подписок, пакетам трафика и дополнительным услугам.", + "ADMIN_PRICING_MENU_SUMMARY": "Краткая сводка:", + "ADMIN_PRICING_MENU_SUMMARY_PERIODS": "• Периоды: {summary}", + "ADMIN_PRICING_MENU_SUMMARY_TRAFFIC": "• Трафик: {summary}", + "ADMIN_PRICING_MENU_SUMMARY_EXTRA": "• Дополнительно: {summary}", + "ADMIN_PRICING_MENU_PROMPT": "Выберите раздел для редактирования:", + "ADMIN_PRICING_BUTTON_PERIODS": "🗓 Периоды подписки", + "ADMIN_PRICING_BUTTON_TRAFFIC": "📦 Пакеты трафика", + "ADMIN_PRICING_BUTTON_EXTRA": "➕ Дополнительно", + "ADMIN_PRICING_SECTION_PERIODS_TITLE": "🗓 Периоды подписки", + "ADMIN_PRICING_SECTION_TRAFFIC_TITLE": "📦 Пакеты трафика", + "ADMIN_PRICING_SECTION_EXTRA_TITLE": "➕ Дополнительные опции", + "ADMIN_PRICING_SECTION_PROMPT": "Выберите что изменить:", + "ADMIN_PRICING_SECTION_EMPTY": "Нет доступных значений.", + "ADMIN_PRICING_EDIT_TITLE": "Изменение цены", + "ADMIN_PRICING_EDIT_TARGET": "Текущий тариф", + "ADMIN_PRICING_EDIT_CURRENT": "Текущее значение", + "ADMIN_PRICING_EDIT_PROMPT": "Введите новую стоимость в рублях (например 990 или 990.50). Для бесплатного тарифа укажите 0.", + "ADMIN_PRICING_EDIT_CANCEL": "❌ Отмена", + "ADMIN_PRICING_EDIT_EXPIRED": "Сессия редактирования истекла.", + "ADMIN_PRICING_EDIT_CANCELLED": "Изменения отменены.", + "ADMIN_PRICING_EDIT_INVALID": "Не удалось распознать цену. Укажите число в рублях (например 990 или 990.50).", + "ADMIN_PRICING_EDIT_SUCCESS": "Цена для {item} обновлена: {price}" }