mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-27 23:00:53 +00:00
Merge pull request #711 from Fr1ngg/revert-710-bedolaga/reorganize-admin-panel-menu
Revert "Reorganize admin panel pricing and server controls"
This commit is contained in:
@@ -40,7 +40,6 @@ from app.handlers.admin import (
|
||||
welcome_text as admin_welcome_text,
|
||||
tickets as admin_tickets,
|
||||
reports as admin_reports,
|
||||
pricing as admin_pricing,
|
||||
bot_configuration as admin_bot_configuration,
|
||||
)
|
||||
from app.handlers.stars_payments import register_stars_handlers
|
||||
@@ -145,7 +144,6 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
admin_welcome_text.register_welcome_text_handlers(dp)
|
||||
admin_tickets.register_handlers(dp)
|
||||
admin_reports.register_handlers(dp)
|
||||
admin_pricing.register_handlers(dp)
|
||||
admin_bot_configuration.register_handlers(dp)
|
||||
common.register_handlers(dp)
|
||||
register_stars_handlers(dp)
|
||||
|
||||
@@ -1,566 +0,0 @@
|
||||
import logging
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from aiogram import Dispatcher, F, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
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 AdminPricingStates
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PERIOD_SETTINGS: Tuple[Tuple[int, str], ...] = (
|
||||
(14, "PRICE_14_DAYS"),
|
||||
(30, "PRICE_30_DAYS"),
|
||||
(60, "PRICE_60_DAYS"),
|
||||
(90, "PRICE_90_DAYS"),
|
||||
(180, "PRICE_180_DAYS"),
|
||||
(360, "PRICE_360_DAYS"),
|
||||
)
|
||||
|
||||
TRAFFIC_SETTINGS: 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"),
|
||||
)
|
||||
|
||||
EXTRA_SETTINGS: Tuple[Tuple[str, str], ...] = (
|
||||
("device", "PRICE_PER_DEVICE"),
|
||||
)
|
||||
|
||||
MAX_PRICE_RUBLES = Decimal("100000")
|
||||
|
||||
|
||||
def _format_price(value: Optional[int]) -> str:
|
||||
return settings.format_price(int(value or 0))
|
||||
|
||||
|
||||
def _get_current_value(key: str) -> int:
|
||||
current = bot_configuration_service.get_current_value(key)
|
||||
if current is None:
|
||||
original = bot_configuration_service.get_original_value(key)
|
||||
return int(original or 0)
|
||||
return int(current)
|
||||
|
||||
|
||||
def _get_period_label(texts, days: int) -> str:
|
||||
return texts.t("ADMIN_PRICING_DAYS_LABEL", "{days} дн.").format(days=days)
|
||||
|
||||
|
||||
def _get_traffic_label(texts, amount: int) -> str:
|
||||
if amount == 0:
|
||||
return texts.t("ADMIN_PRICING_UNLIMITED_LABEL", "Безлимит")
|
||||
return texts.t("ADMIN_PRICING_TRAFFIC_GB", "{gb} ГБ").format(gb=amount)
|
||||
|
||||
|
||||
def _build_main_summary(texts) -> str:
|
||||
period_lines = [
|
||||
texts.t(
|
||||
f"PERIOD_{days}_DAYS",
|
||||
f"📅 {days} дней - {_format_price(_get_current_value(key))}",
|
||||
)
|
||||
for days, key in PERIOD_SETTINGS
|
||||
]
|
||||
|
||||
traffic_lines = []
|
||||
for amount, key in TRAFFIC_SETTINGS:
|
||||
traffic_key = "UNLIMITED" if amount == 0 else f"{amount}GB"
|
||||
default_label = (
|
||||
f"📊 {_get_traffic_label(texts, amount)} - {_format_price(_get_current_value(key))}"
|
||||
)
|
||||
traffic_lines.append(
|
||||
texts.t(f"TRAFFIC_{traffic_key}", default_label)
|
||||
)
|
||||
|
||||
device_line = texts.t(
|
||||
"ADMIN_PRICING_DEVICE_LABEL",
|
||||
"📱 Дополнительное устройство — {price}",
|
||||
).format(price=_format_price(_get_current_value("PRICE_PER_DEVICE")))
|
||||
|
||||
parts = [
|
||||
texts.t("ADMIN_PRICING_TITLE", "💰 <b>Управление ценами</b>"),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_DESCRIPTION",
|
||||
"Настройте стоимость подписок, пакетов трафика и дополнительных опций.",
|
||||
),
|
||||
"",
|
||||
texts.t("ADMIN_PRICING_SECTION_PERIODS", "<b>📅 Периоды подписки</b>"),
|
||||
"\n".join(period_lines),
|
||||
"",
|
||||
texts.t("ADMIN_PRICING_SECTION_TRAFFIC", "<b>📦 Пакеты трафика</b>"),
|
||||
"\n".join(traffic_lines),
|
||||
"",
|
||||
texts.t("ADMIN_PRICING_SECTION_EXTRAS", "<b>🔧 Дополнительные опции</b>"),
|
||||
device_line,
|
||||
]
|
||||
|
||||
return "\n".join(part for part in parts if part)
|
||||
|
||||
|
||||
def _period_keyboard(texts) -> InlineKeyboardMarkup:
|
||||
rows = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_EDIT_PERIOD", "✏️ {label}").format(
|
||||
label=_get_period_label(texts, days)
|
||||
),
|
||||
callback_data=f"admin_pricing_edit_period_{days}",
|
||||
)
|
||||
]
|
||||
for days, _ in PERIOD_SETTINGS
|
||||
]
|
||||
rows.append([InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
|
||||
def _traffic_keyboard(texts) -> InlineKeyboardMarkup:
|
||||
rows = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_EDIT_TRAFFIC", "✏️ {label}").format(
|
||||
label=_get_traffic_label(texts, amount)
|
||||
),
|
||||
callback_data=(
|
||||
f"admin_pricing_edit_traffic_{'unlimited' if amount == 0 else amount}"
|
||||
),
|
||||
)
|
||||
]
|
||||
for amount, _ in TRAFFIC_SETTINGS
|
||||
]
|
||||
rows.append([InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
|
||||
def _extras_keyboard(texts) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_EDIT_DEVICE", "✏️ Цена за устройство"),
|
||||
callback_data="admin_pricing_edit_extra_device",
|
||||
)
|
||||
],
|
||||
[InlineKeyboardButton(text=texts.BACK, callback_data="admin_pricing")],
|
||||
])
|
||||
|
||||
|
||||
def _pricing_main_keyboard(texts) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=texts.t("ADMIN_PRICING_PERIODS", "📅 Периоды подписки"), callback_data="admin_pricing_periods")],
|
||||
[InlineKeyboardButton(text=texts.t("ADMIN_PRICING_TRAFFIC", "📦 Трафик-пакеты"), callback_data="admin_pricing_traffic")],
|
||||
[InlineKeyboardButton(text=texts.t("ADMIN_PRICING_EXTRAS", "🔧 Доп. опции"), callback_data="admin_pricing_extras")],
|
||||
[InlineKeyboardButton(text=texts.BACK, callback_data="admin_panel")],
|
||||
])
|
||||
|
||||
|
||||
def _find_period_setting(days: int) -> Optional[str]:
|
||||
for item_days, key in PERIOD_SETTINGS:
|
||||
if item_days == days:
|
||||
return key
|
||||
return None
|
||||
|
||||
|
||||
def _find_traffic_setting(identifier: str) -> Optional[Tuple[int, str]]:
|
||||
if identifier == "unlimited":
|
||||
identifier_value = 0
|
||||
else:
|
||||
try:
|
||||
identifier_value = int(identifier)
|
||||
except ValueError:
|
||||
return None
|
||||
for amount, key in TRAFFIC_SETTINGS:
|
||||
if amount == identifier_value:
|
||||
return amount, key
|
||||
return None
|
||||
|
||||
|
||||
def _find_extra_setting(slug: str) -> Optional[str]:
|
||||
for extra_slug, key in EXTRA_SETTINGS:
|
||||
if extra_slug == slug:
|
||||
return key
|
||||
return None
|
||||
|
||||
|
||||
async def _prompt_price_input(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
texts,
|
||||
label: str,
|
||||
current_price: str,
|
||||
setting_key: str,
|
||||
return_callback: str,
|
||||
) -> None:
|
||||
await state.set_state(AdminPricingStates.waiting_for_price)
|
||||
await state.update_data(
|
||||
pricing_key=setting_key,
|
||||
pricing_label=label,
|
||||
return_callback=return_callback,
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_PROMPT",
|
||||
"Введите новую цену для {label}.\nТекущая цена: {current}.\n\nПришлите значение в рублях (пример: 990 или 349.99).",
|
||||
).format(label=label, current=current_price),
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=texts.BACK, callback_data=return_callback)]
|
||||
]),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_pricing_dashboard(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
await callback.message.edit_text(
|
||||
_build_main_summary(texts),
|
||||
reply_markup=_pricing_main_keyboard(texts),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_pricing_periods(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
period_lines = [
|
||||
texts.t(
|
||||
f"PERIOD_{days}_DAYS",
|
||||
f"📅 {days} дней - {_format_price(_get_current_value(key))}",
|
||||
)
|
||||
for days, key in PERIOD_SETTINGS
|
||||
]
|
||||
|
||||
text = "\n".join(
|
||||
part
|
||||
for part in (
|
||||
texts.t("ADMIN_PRICING_TITLE", "💰 <b>Управление ценами</b>"),
|
||||
"",
|
||||
texts.t("ADMIN_PRICING_SECTION_PERIODS", "<b>📅 Периоды подписки</b>"),
|
||||
"\n".join(period_lines),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_HINT_PERIODS",
|
||||
"Выберите период, чтобы изменить его стоимость.",
|
||||
),
|
||||
)
|
||||
if part
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=_period_keyboard(texts),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_pricing_traffic(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
traffic_lines = []
|
||||
for amount, key in TRAFFIC_SETTINGS:
|
||||
traffic_key = "UNLIMITED" if amount == 0 else f"{amount}GB"
|
||||
default_label = (
|
||||
f"📊 {_get_traffic_label(texts, amount)} - {_format_price(_get_current_value(key))}"
|
||||
)
|
||||
traffic_lines.append(texts.t(f"TRAFFIC_{traffic_key}", default_label))
|
||||
|
||||
text = "\n".join(
|
||||
part
|
||||
for part in (
|
||||
texts.t("ADMIN_PRICING_TITLE", "💰 <b>Управление ценами</b>"),
|
||||
"",
|
||||
texts.t("ADMIN_PRICING_SECTION_TRAFFIC", "<b>📦 Пакеты трафика</b>"),
|
||||
"\n".join(traffic_lines),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_HINT_TRAFFIC",
|
||||
"Выберите пакет, чтобы обновить его цену.",
|
||||
),
|
||||
)
|
||||
if part
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=_traffic_keyboard(texts),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_pricing_extras(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
device_price = _format_price(_get_current_value("PRICE_PER_DEVICE"))
|
||||
|
||||
text = "\n".join(
|
||||
part
|
||||
for part in (
|
||||
texts.t("ADMIN_PRICING_TITLE", "💰 <b>Управление ценами</b>"),
|
||||
"",
|
||||
texts.t("ADMIN_PRICING_SECTION_EXTRAS", "<b>🔧 Дополнительные опции</b>"),
|
||||
texts.t("ADMIN_PRICING_DEVICE_LABEL", "📱 Дополнительное устройство — {price}").format(
|
||||
price=device_price
|
||||
),
|
||||
"",
|
||||
texts.t(
|
||||
"ADMIN_PRICING_HINT_EXTRAS",
|
||||
"Обновите стоимость дополнительных опций.",
|
||||
),
|
||||
)
|
||||
if part
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=_extras_keyboard(texts),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_edit_period_price(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
try:
|
||||
days = int(callback.data.split("_")[-1])
|
||||
except ValueError:
|
||||
await callback.answer("❌", show_alert=True)
|
||||
return
|
||||
|
||||
setting_key = _find_period_setting(days)
|
||||
if not setting_key:
|
||||
await callback.answer("❌", show_alert=True)
|
||||
return
|
||||
|
||||
label = texts.t("ADMIN_PRICING_PERIOD_LABEL", "Тариф на {label}").format(
|
||||
label=_get_period_label(texts, days)
|
||||
)
|
||||
current_price = _format_price(_get_current_value(setting_key))
|
||||
|
||||
await _prompt_price_input(
|
||||
callback,
|
||||
state,
|
||||
texts,
|
||||
label,
|
||||
current_price,
|
||||
setting_key,
|
||||
"admin_pricing_periods",
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_edit_traffic_price(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
identifier = callback.data.split("_")[-1]
|
||||
setting = _find_traffic_setting(identifier)
|
||||
|
||||
if not setting:
|
||||
await callback.answer("❌", show_alert=True)
|
||||
return
|
||||
|
||||
amount, setting_key = setting
|
||||
label = texts.t("ADMIN_PRICING_TRAFFIC_LABEL", "Пакет {label}").format(
|
||||
label=_get_traffic_label(texts, amount)
|
||||
)
|
||||
current_price = _format_price(_get_current_value(setting_key))
|
||||
|
||||
await _prompt_price_input(
|
||||
callback,
|
||||
state,
|
||||
texts,
|
||||
label,
|
||||
current_price,
|
||||
setting_key,
|
||||
"admin_pricing_traffic",
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_edit_extra_price(
|
||||
callback: types.CallbackQuery,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
slug = callback.data.split("_")[-1]
|
||||
setting_key = _find_extra_setting(slug)
|
||||
|
||||
if not setting_key:
|
||||
await callback.answer("❌", show_alert=True)
|
||||
return
|
||||
|
||||
if slug == "device":
|
||||
label = texts.t("ADMIN_PRICING_DEVICE_EDIT_LABEL", "Цена за дополнительное устройство")
|
||||
else:
|
||||
label = slug
|
||||
|
||||
current_price = _format_price(_get_current_value(setting_key))
|
||||
|
||||
await _prompt_price_input(
|
||||
callback,
|
||||
state,
|
||||
texts,
|
||||
label,
|
||||
current_price,
|
||||
setting_key,
|
||||
"admin_pricing_extras",
|
||||
)
|
||||
|
||||
|
||||
def _parse_price(text: str) -> Decimal:
|
||||
cleaned = (text or "").strip().replace(" ", "")
|
||||
if not cleaned:
|
||||
raise InvalidOperation
|
||||
normalized = cleaned.replace(",", ".")
|
||||
value = Decimal(normalized)
|
||||
return value.quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_pricing_value(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
):
|
||||
data = await state.get_data()
|
||||
setting_key = data.get("pricing_key")
|
||||
label = data.get("pricing_label")
|
||||
return_callback = data.get("return_callback", "admin_pricing")
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if not setting_key or not label:
|
||||
await message.answer(texts.t("ADMIN_PRICING_STATE_EXPIRED", "⚠️ Истекло состояние ввода. Попробуйте снова."))
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
price_rubles = _parse_price(message.text)
|
||||
except (InvalidOperation, ValueError):
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_INVALID_FORMAT",
|
||||
"❌ Неверный формат. Используйте числа, например 990 или 349.99.",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if price_rubles < 0:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_NEGATIVE_VALUE",
|
||||
"❌ Цена не может быть отрицательной.",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if price_rubles > MAX_PRICE_RUBLES:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_TOO_HIGH",
|
||||
"❌ Слишком высокая цена. Укажите значение до {limit}.",
|
||||
).format(limit=_format_price(int(MAX_PRICE_RUBLES * 100))),
|
||||
)
|
||||
return
|
||||
|
||||
price_kopeks = int((price_rubles * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
||||
|
||||
await bot_configuration_service.set_value(db, setting_key, price_kopeks)
|
||||
await state.clear()
|
||||
|
||||
logger.info(
|
||||
"Админ %s обновил %s: %s коп.",
|
||||
db_user.telegram_id,
|
||||
setting_key,
|
||||
price_kopeks,
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_SUCCESS",
|
||||
"✅ Цена для {label} обновлена: {value}",
|
||||
).format(label=label, value=_format_price(price_kopeks)),
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text=texts.BACK, callback_data=return_callback)],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_RETURN", "🏠 К управлению ценами"),
|
||||
callback_data="admin_pricing",
|
||||
)
|
||||
],
|
||||
]),
|
||||
)
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher) -> None:
|
||||
dp.callback_query.register(show_pricing_dashboard, F.data == "admin_pricing")
|
||||
dp.callback_query.register(show_pricing_periods, F.data == "admin_pricing_periods")
|
||||
dp.callback_query.register(show_pricing_traffic, F.data == "admin_pricing_traffic")
|
||||
dp.callback_query.register(show_pricing_extras, F.data == "admin_pricing_extras")
|
||||
dp.callback_query.register(
|
||||
start_edit_period_price,
|
||||
F.data.startswith("admin_pricing_edit_period_"),
|
||||
)
|
||||
dp.callback_query.register(
|
||||
start_edit_traffic_price,
|
||||
F.data.startswith("admin_pricing_edit_traffic_"),
|
||||
)
|
||||
dp.callback_query.register(
|
||||
start_edit_extra_price,
|
||||
F.data.startswith("admin_pricing_edit_extra_"),
|
||||
)
|
||||
dp.message.register(
|
||||
process_pricing_value,
|
||||
AdminPricingStates.waiting_for_price,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -61,7 +62,6 @@ async def show_subscriptions_menu(
|
||||
db: AsyncSession
|
||||
):
|
||||
stats = await get_subscriptions_statistics(db)
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
text = f"""
|
||||
📱 <b>Управление подписками</b>
|
||||
@@ -77,20 +77,20 @@ async def show_subscriptions_menu(
|
||||
- За неделю: {stats['purchased_week']}
|
||||
- За месяц: {stats['purchased_month']}
|
||||
|
||||
ℹ️ {texts.t("ADMIN_SUBSCRIPTIONS_MAIN_HINT", "Управление серверами и ценами теперь доступно на главной странице админ-панели.")}
|
||||
|
||||
Выберите действие:
|
||||
"""
|
||||
|
||||
|
||||
keyboard = [
|
||||
[
|
||||
types.InlineKeyboardButton(text="📋 Список подписок", callback_data="admin_subs_list"),
|
||||
types.InlineKeyboardButton(text="⏰ Истекающие", callback_data="admin_subs_expiring")
|
||||
],
|
||||
[
|
||||
types.InlineKeyboardButton(text="📊 Статистика", callback_data="admin_subs_stats")
|
||||
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")
|
||||
],
|
||||
[
|
||||
@@ -282,22 +282,45 @@ async def show_pricing_settings(
|
||||
db_user: User,
|
||||
db: AsyncSession
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
text = f"""
|
||||
⚙️ <b>Настройки цен</b>
|
||||
|
||||
<b>Периоды подписки:</b>
|
||||
- 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)}
|
||||
|
||||
<b>Трафик-пакеты:</b>
|
||||
- 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)}
|
||||
|
||||
<b>Дополнительно:</b>
|
||||
- За устройство: {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(
|
||||
texts.t(
|
||||
"ADMIN_PRICING_RELOCATED",
|
||||
"ℹ️ Раздел управления ценами переехал. Откройте главный экран админ-панели и выберите пункт «Цены и тарифы».",
|
||||
),
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.t("ADMIN_PRICING_OPEN", "💰 Перейти к управлению ценами"),
|
||||
callback_data="admin_pricing"
|
||||
)
|
||||
],
|
||||
[types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_subscriptions")]
|
||||
])
|
||||
text,
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard)
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ def get_admin_main_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
|
||||
[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_SERVERS", "🌐 Серверы"), callback_data="admin_servers")],
|
||||
[InlineKeyboardButton(text=_t(texts, "ADMIN_MAIN_PRICING", "💰 Цены и тарифы"), callback_data="admin_pricing")],
|
||||
[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")]
|
||||
|
||||
@@ -42,8 +42,6 @@ def _build_dynamic_values(language: str) -> Dict[str, Any]:
|
||||
"TRAFFIC_50GB": f"📊 50 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_50GB)}",
|
||||
"TRAFFIC_100GB": f"📊 100 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}",
|
||||
"TRAFFIC_250GB": f"📊 250 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_250GB)}",
|
||||
"TRAFFIC_500GB": f"📊 500 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_500GB)}",
|
||||
"TRAFFIC_1000GB": f"📊 1000 ГБ - {settings.format_price(settings.PRICE_TRAFFIC_1000GB)}",
|
||||
"TRAFFIC_UNLIMITED": f"📊 Безлимит - {settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)}",
|
||||
"SUPPORT_INFO": (
|
||||
"\n🛟 <b>Поддержка</b>\n\n"
|
||||
@@ -69,8 +67,6 @@ def _build_dynamic_values(language: str) -> Dict[str, Any]:
|
||||
"TRAFFIC_50GB": f"📊 50 GB - {settings.format_price(settings.PRICE_TRAFFIC_50GB)}",
|
||||
"TRAFFIC_100GB": f"📊 100 GB - {settings.format_price(settings.PRICE_TRAFFIC_100GB)}",
|
||||
"TRAFFIC_250GB": f"📊 250 GB - {settings.format_price(settings.PRICE_TRAFFIC_250GB)}",
|
||||
"TRAFFIC_500GB": f"📊 500 GB - {settings.format_price(settings.PRICE_TRAFFIC_500GB)}",
|
||||
"TRAFFIC_1000GB": f"📊 1000 GB - {settings.format_price(settings.PRICE_TRAFFIC_1000GB)}",
|
||||
"TRAFFIC_UNLIMITED": f"📊 Unlimited - {settings.format_price(settings.PRICE_TRAFFIC_UNLIMITED)}",
|
||||
"SUPPORT_INFO": (
|
||||
"\n🛟 <b>RemnaWave Support</b>\n\n"
|
||||
|
||||
@@ -30,7 +30,7 @@ class PromoCodeStates(StatesGroup):
|
||||
waiting_for_referral_code = State()
|
||||
|
||||
class AdminStates(StatesGroup):
|
||||
|
||||
|
||||
waiting_for_user_search = State()
|
||||
editing_user_balance = State()
|
||||
extending_subscription = State()
|
||||
@@ -114,10 +114,6 @@ class AdminStates(StatesGroup):
|
||||
viewing_user_from_purchases_list = State()
|
||||
viewing_user_from_campaign_list = State()
|
||||
|
||||
|
||||
class AdminPricingStates(StatesGroup):
|
||||
waiting_for_price = State()
|
||||
|
||||
class SupportStates(StatesGroup):
|
||||
waiting_for_message = State()
|
||||
|
||||
|
||||
@@ -172,37 +172,6 @@
|
||||
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (default)",
|
||||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Members: {count}",
|
||||
"ADMIN_PROMO_GROUPS_EMPTY": "No promo groups found.",
|
||||
"ADMIN_SUBSCRIPTIONS_MAIN_HINT": "Server and pricing management is now available from the admin home screen.",
|
||||
"ADMIN_PRICING_TITLE": "💰 <b>Pricing management</b>",
|
||||
"ADMIN_PRICING_DESCRIPTION": "Adjust subscription plans, traffic bundles and extra options.",
|
||||
"ADMIN_PRICING_SECTION_PERIODS": "<b>📅 Subscription periods</b>",
|
||||
"ADMIN_PRICING_SECTION_TRAFFIC": "<b>📦 Traffic bundles</b>",
|
||||
"ADMIN_PRICING_SECTION_EXTRAS": "<b>🔧 Extra options</b>",
|
||||
"ADMIN_PRICING_PERIODS": "📅 Subscription periods",
|
||||
"ADMIN_PRICING_TRAFFIC": "📦 Traffic bundles",
|
||||
"ADMIN_PRICING_EXTRAS": "🔧 Extra options",
|
||||
"ADMIN_PRICING_EDIT_PERIOD": "✏️ {label}",
|
||||
"ADMIN_PRICING_EDIT_TRAFFIC": "✏️ {label}",
|
||||
"ADMIN_PRICING_EDIT_DEVICE": "✏️ Device price",
|
||||
"ADMIN_PRICING_DAYS_LABEL": "{days} d.",
|
||||
"ADMIN_PRICING_UNLIMITED_LABEL": "Unlimited",
|
||||
"ADMIN_PRICING_TRAFFIC_GB": "{gb} GB",
|
||||
"ADMIN_PRICING_DEVICE_LABEL": "📱 Extra device — {price}",
|
||||
"ADMIN_PRICING_HINT_PERIODS": "Choose a period to change its price.",
|
||||
"ADMIN_PRICING_HINT_TRAFFIC": "Choose a bundle to update its price.",
|
||||
"ADMIN_PRICING_HINT_EXTRAS": "Update the cost of extra options.",
|
||||
"ADMIN_PRICING_PERIOD_LABEL": "{label} plan",
|
||||
"ADMIN_PRICING_TRAFFIC_LABEL": "{label} bundle",
|
||||
"ADMIN_PRICING_DEVICE_EDIT_LABEL": "Extra device price",
|
||||
"ADMIN_PRICING_PROMPT": "Enter a new price for {label}.\nCurrent price: {current}.\n\nSend the value in RUB (example: 990 or 349.99).",
|
||||
"ADMIN_PRICING_STATE_EXPIRED": "⚠️ Input session expired. Please try again.",
|
||||
"ADMIN_PRICING_INVALID_FORMAT": "❌ Invalid format. Use numbers such as 990 or 349.99.",
|
||||
"ADMIN_PRICING_NEGATIVE_VALUE": "❌ Price cannot be negative.",
|
||||
"ADMIN_PRICING_TOO_HIGH": "❌ Price is too high. Use a value up to {limit}.",
|
||||
"ADMIN_PRICING_SUCCESS": "✅ Price for {label} updated: {value}",
|
||||
"ADMIN_PRICING_RETURN": "🏠 Back to pricing",
|
||||
"ADMIN_PRICING_RELOCATED": "ℹ️ Pricing management moved. Open the admin home screen and choose “Pricing”.",
|
||||
"ADMIN_PRICING_OPEN": "💰 Go to pricing management",
|
||||
"CREATE_TICKET_BUTTON": "🎫 Create ticket",
|
||||
"MY_TICKETS_BUTTON": "📋 My tickets",
|
||||
"CONTACT_SUPPORT_BUTTON": "💬 Contact support",
|
||||
@@ -577,8 +546,6 @@
|
||||
"ADMIN_MAIN_PROMO_STATS": "💰 Promo codes / Stats",
|
||||
"ADMIN_MAIN_SUPPORT": "🛟 Support",
|
||||
"ADMIN_MAIN_MESSAGES": "📨 Messages",
|
||||
"ADMIN_MAIN_SERVERS": "🌐 Servers",
|
||||
"ADMIN_MAIN_PRICING": "💰 Pricing",
|
||||
"ADMIN_MAIN_SETTINGS": "⚙️ Settings",
|
||||
"ADMIN_MAIN_SYSTEM": "🛠️ System",
|
||||
"ADMIN_USERS_SUBMENU_TITLE": "👥 **User and subscription management**\n\n",
|
||||
|
||||
@@ -30,37 +30,6 @@
|
||||
"ADMIN_PROMO_GROUPS_DEFAULT_LABEL": " (базовая)",
|
||||
"ADMIN_PROMO_GROUPS_MEMBERS_COUNT": "Участников: {count}",
|
||||
"ADMIN_PROMO_GROUPS_EMPTY": "Промогруппы не найдены.",
|
||||
"ADMIN_SUBSCRIPTIONS_MAIN_HINT": "Управление серверами и ценами теперь доступно на главной странице админ-панели.",
|
||||
"ADMIN_PRICING_TITLE": "💰 <b>Управление ценами</b>",
|
||||
"ADMIN_PRICING_DESCRIPTION": "Настройте стоимость подписок, пакетов трафика и дополнительных опций.",
|
||||
"ADMIN_PRICING_SECTION_PERIODS": "<b>📅 Периоды подписки</b>",
|
||||
"ADMIN_PRICING_SECTION_TRAFFIC": "<b>📦 Пакеты трафика</b>",
|
||||
"ADMIN_PRICING_SECTION_EXTRAS": "<b>🔧 Дополнительные опции</b>",
|
||||
"ADMIN_PRICING_PERIODS": "📅 Периоды подписки",
|
||||
"ADMIN_PRICING_TRAFFIC": "📦 Трафик-пакеты",
|
||||
"ADMIN_PRICING_EXTRAS": "🔧 Доп. опции",
|
||||
"ADMIN_PRICING_EDIT_PERIOD": "✏️ {label}",
|
||||
"ADMIN_PRICING_EDIT_TRAFFIC": "✏️ {label}",
|
||||
"ADMIN_PRICING_EDIT_DEVICE": "✏️ Цена за устройство",
|
||||
"ADMIN_PRICING_DAYS_LABEL": "{days} дн.",
|
||||
"ADMIN_PRICING_UNLIMITED_LABEL": "Безлимит",
|
||||
"ADMIN_PRICING_TRAFFIC_GB": "{gb} ГБ",
|
||||
"ADMIN_PRICING_DEVICE_LABEL": "📱 Дополнительное устройство — {price}",
|
||||
"ADMIN_PRICING_HINT_PERIODS": "Выберите период, чтобы изменить его стоимость.",
|
||||
"ADMIN_PRICING_HINT_TRAFFIC": "Выберите пакет, чтобы обновить его цену.",
|
||||
"ADMIN_PRICING_HINT_EXTRAS": "Обновите стоимость дополнительных опций.",
|
||||
"ADMIN_PRICING_PERIOD_LABEL": "Тариф на {label}",
|
||||
"ADMIN_PRICING_TRAFFIC_LABEL": "Пакет {label}",
|
||||
"ADMIN_PRICING_DEVICE_EDIT_LABEL": "Цена за дополнительное устройство",
|
||||
"ADMIN_PRICING_PROMPT": "Введите новую цену для {label}.\nТекущая цена: {current}.\n\nПришлите значение в рублях (пример: 990 или 349.99).",
|
||||
"ADMIN_PRICING_STATE_EXPIRED": "⚠️ Истекло состояние ввода. Попробуйте снова.",
|
||||
"ADMIN_PRICING_INVALID_FORMAT": "❌ Неверный формат. Используйте числа, например 990 или 349.99.",
|
||||
"ADMIN_PRICING_NEGATIVE_VALUE": "❌ Цена не может быть отрицательной.",
|
||||
"ADMIN_PRICING_TOO_HIGH": "❌ Слишком высокая цена. Укажите значение до {limit}.",
|
||||
"ADMIN_PRICING_SUCCESS": "✅ Цена для {label} обновлена: {value}",
|
||||
"ADMIN_PRICING_RETURN": "🏠 К управлению ценами",
|
||||
"ADMIN_PRICING_RELOCATED": "ℹ️ Раздел управления ценами переехал. Откройте главный экран админ-панели и выберите пункт «Цены и тарифы».",
|
||||
"ADMIN_PRICING_OPEN": "💰 Перейти к управлению ценами",
|
||||
"CREATE_TICKET_BUTTON": "🎫 Создать тикет",
|
||||
"MY_TICKETS_BUTTON": "📋 Мои тикеты",
|
||||
"CONTACT_SUPPORT_BUTTON": "💬 Связаться с поддержкой",
|
||||
@@ -577,8 +546,6 @@
|
||||
"ADMIN_MAIN_PROMO_STATS": "💰 Промокоды/Статистика",
|
||||
"ADMIN_MAIN_SUPPORT": "🛟 Поддержка",
|
||||
"ADMIN_MAIN_MESSAGES": "📨 Сообщения",
|
||||
"ADMIN_MAIN_SERVERS": "🌐 Серверы",
|
||||
"ADMIN_MAIN_PRICING": "💰 Цены и тарифы",
|
||||
"ADMIN_MAIN_SETTINGS": "⚙️ Настройки",
|
||||
"ADMIN_MAIN_SYSTEM": "🛠️ Система",
|
||||
"ADMIN_USERS_SUBMENU_TITLE": "👥 **Управление пользователями и подписками**\n\n",
|
||||
|
||||
Reference in New Issue
Block a user