diff --git a/bot/handlers/admin/common.py b/bot/handlers/admin/common.py
index 996cd83..a3f5e62 100644
--- a/bot/handlers/admin/common.py
+++ b/bot/handlers/admin/common.py
@@ -98,8 +98,22 @@ async def admin_panel_actions_callback_handler(
await admin_user_mgmnt_handlers.unban_user_prompt_handler(
callback, state, i18n_data, settings, session)
elif action == "users_management":
+ # This is deprecated, kept for compatibility
from . import user_management as admin_user_management_handlers
- await admin_user_management_handlers.user_management_menu_handler(
+ await admin_user_management_handlers.user_search_prompt_handler(
+ callback, state, i18n_data, settings, session)
+ elif action == "users_list" and len(action_parts) > 2:
+ # Route to users list handler with page number
+ from . import user_management as admin_user_management_handlers
+ try:
+ page = int(action_parts[2])
+ await admin_user_management_handlers.users_list_handler(
+ callback, i18n_data, settings, session, page)
+ except (IndexError, ValueError):
+ await callback.answer("Invalid page number", show_alert=True)
+ elif action == "users_search_prompt":
+ from . import user_management as admin_user_management_handlers
+ await admin_user_management_handlers.user_search_prompt_handler(
callback, state, i18n_data, settings, session)
elif action == "view_banned":
diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py
index db0e602..e0ea2d1 100644
--- a/bot/handlers/admin/user_management.py
+++ b/bot/handlers/admin/user_management.py
@@ -29,14 +29,57 @@ router = Router(name="admin_user_management_router")
USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
-async def user_management_menu_handler(callback: types.CallbackQuery,
- state: FSMContext, i18n_data: dict,
- settings: Settings, session: AsyncSession):
- """Display user management menu"""
+async def users_list_handler(callback: types.CallbackQuery,
+ i18n_data: dict, settings: Settings,
+ session: AsyncSession, page: int = 0):
+ """Display paginated list of all users"""
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message:
- await callback.answer("Error preparing user management.", show_alert=True)
+ await callback.answer("Error preparing user list.", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ try:
+ # Get paginated users
+ from bot.keyboards.inline.admin_keyboards import get_users_list_keyboard
+ from db.dal import user_dal
+
+ users = await user_dal.get_all_users_paginated(session, page=page, page_size=15)
+ total_users = await user_dal.count_all_users(session)
+ total_pages = max(1, (total_users + 14) // 15)
+
+ # Format message
+ header_text = _(
+ "admin_users_list_header",
+ default="👥 Список пользователей\n\nСтраница {current}/{total} ({total_users} пользователей)",
+ current=page + 1,
+ total=total_pages,
+ total_users=total_users
+ )
+
+ keyboard = get_users_list_keyboard(users, page, total_users, i18n, current_lang, page_size=15)
+
+ await callback.message.edit_text(
+ header_text,
+ reply_markup=keyboard,
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error displaying user list: {e}")
+ await callback.answer("Ошибка отображения списка пользователей", show_alert=True)
+
+
+async def user_search_prompt_handler(callback: types.CallbackQuery,
+ state: FSMContext, i18n_data: dict,
+ settings: Settings, session: AsyncSession):
+ """Display search prompt for user management"""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n or not callback.message:
+ await callback.answer("Error preparing search.", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
@@ -95,8 +138,14 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb
text=_(key="admin_user_refresh_button", default="🔄 Обновить"),
callback_data=f"user_action:refresh:{user_id}"
)
+
+ # Row 4: Destructive action
+ builder.button(
+ text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"),
+ callback_data=f"user_action:delete_user:{user_id}"
+ )
- # Row 4: Back button
+ # Row 5: Navigation
builder.button(
text=_(key="admin_user_search_new_button", default="🔍 Найти другого"),
callback_data="admin_action:users_management"
@@ -106,7 +155,7 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb
callback_data="admin_action:main"
)
- builder.adjust(2, 2, 2, 2)
+ builder.adjust(2, 2, 2, 1, 2)
return builder
@@ -324,6 +373,10 @@ async def user_action_handler(callback: types.CallbackQuery, state: FSMContext,
await handle_view_user_logs(callback, user, session, settings, i18n, current_lang)
elif action == "refresh":
await handle_refresh_user_card(callback, user, subscription_service, session, i18n, current_lang)
+ elif action == "delete_user":
+ await handle_delete_user_prompt(
+ callback, state, user, settings, i18n, current_lang, session
+ )
else:
await callback.answer(_("admin_unknown_action"), show_alert=True)
@@ -549,8 +602,217 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User,
await callback.answer("Error refreshing user card", show_alert=True)
+# Destructive deletion flow
+async def handle_delete_user_prompt(callback: types.CallbackQuery, state: FSMContext,
+ user: User, settings: Settings, i18n_instance,
+ lang: str, session: AsyncSession):
+ """Trigger confirmation workflow for destructive deletion."""
+ _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
+
+ admin = callback.from_user
+ admin_id = admin.id if admin else None
+ if not admin_id or admin_id not in settings.ADMIN_IDS:
+ logging.warning(
+ f"Unauthorized delete attempt by user {admin_id} targeting {user.user_id}."
+ )
+ await callback.answer(
+ _(
+ "admin_user_delete_not_allowed",
+ default="❌ У вас нет прав для удаления пользователей.",
+ ),
+ show_alert=True,
+ )
+ return
+
+ await state.update_data(
+ target_user_id=user.user_id,
+ delete_initiator_id=admin_id,
+ )
+ await state.set_state(AdminStates.waiting_for_user_delete_confirmation)
+
+ prompt_text = _(
+ "admin_user_delete_confirmation_prompt",
+ default=(
+ "⚠️ Вы хотите полностью удалить пользователя {user_id}.\n\n"
+ "Отправьте точный Telegram ID этого пользователя, чтобы подтвердить удаление.\n"
+ "Любой другой ответ отменит операцию."
+ ),
+ user_id=hcode(str(user.user_id)),
+ )
+
+ try:
+ await callback.message.answer(prompt_text, parse_mode="HTML")
+ except Exception as e:
+ logging.error(
+ f"Failed to send delete confirmation prompt for user {user.user_id}: {e}"
+ )
+ await callback.message.reply(prompt_text, parse_mode="HTML")
+
+ await callback.answer()
+
+
+async def _log_admin_user_deletion(
+ session: AsyncSession,
+ admin_id: int,
+ admin_user: Optional[types.User],
+ target_user_id: int,
+) -> None:
+ """Store audit log for successful deletion."""
+ try:
+ await message_log_dal.create_message_log_no_commit(
+ session,
+ {
+ "user_id": admin_id,
+ "telegram_username": admin_user.username if admin_user else None,
+ "telegram_first_name": admin_user.first_name if admin_user else None,
+ "event_type": "admin:user_deleted",
+ "content": f"Admin {admin_id} deleted user {target_user_id}",
+ "raw_update_preview": None,
+ "is_admin_event": True,
+ "target_user_id": target_user_id,
+ "timestamp": datetime.now(timezone.utc),
+ },
+ )
+ except Exception as e:
+ logging.error(
+ f"Failed to log deletion audit for admin {admin_id} -> user {target_user_id}: {e}",
+ exc_info=True,
+ )
+
+
# Message handlers for state-based inputs
+@router.message(AdminStates.waiting_for_user_delete_confirmation, F.text)
+async def process_delete_user_confirmation_handler(message: types.Message,
+ state: FSMContext,
+ settings: Settings,
+ i18n_data: dict,
+ panel_service: PanelApiService,
+ session: AsyncSession):
+ """Confirm and execute destructive user deletion."""
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await message.reply("Language service error.")
+ await state.clear()
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ admin = message.from_user
+ admin_id = admin.id if admin else None
+ if not admin_id or admin_id not in settings.ADMIN_IDS:
+ logging.warning(
+ f"Unauthorized delete confirmation attempt by user {admin_id}."
+ )
+ await message.answer(
+ _(
+ "admin_user_delete_not_allowed",
+ default="❌ У вас нет прав для удаления пользователей.",
+ )
+ )
+ await state.clear()
+ return
+
+ data = await state.get_data()
+ target_user_id = data.get("target_user_id")
+ if not target_user_id:
+ await message.answer(
+ _(
+ "admin_user_delete_state_missing",
+ default="⚠️ Нет активной операции удаления. Начните заново.",
+ )
+ )
+ await state.clear()
+ return
+
+ confirmation_input = message.text.strip() if message.text else ""
+ if confirmation_input.lower() in {"/cancel", "cancel", "отмена"}:
+ await message.answer(
+ _(
+ "admin_user_delete_cancelled",
+ default="Операция удаления отменена по запросу.",
+ )
+ )
+ await state.clear()
+ return
+
+ if confirmation_input != str(target_user_id):
+ await message.answer(
+ _(
+ "admin_user_delete_mismatch",
+ default="⚠️ ID не совпадает. Удаление отменено.",
+ )
+ )
+ await state.clear()
+ return
+
+ user_model = await user_dal.get_user_by_id(session, target_user_id)
+ if not user_model:
+ await message.answer(
+ _(
+ "admin_user_delete_already_removed",
+ default="ℹ️ Пользователь уже удален.",
+ )
+ )
+ await state.clear()
+ return
+
+ try:
+ if user_model.panel_user_uuid:
+ panel_deleted = await panel_service.delete_user_from_panel(
+ user_model.panel_user_uuid
+ )
+ if not panel_deleted:
+ await message.answer(
+ _(
+ "admin_user_delete_panel_error",
+ default=(
+ "❌ Не удалось удалить пользователя на панели. "
+ "Операция прервана."
+ ),
+ )
+ )
+ await session.rollback()
+ await state.clear()
+ return
+
+ deleted = await user_dal.delete_user_and_relations(
+ session, target_user_id
+ )
+ if not deleted:
+ await message.answer(
+ _(
+ "admin_user_delete_already_removed",
+ default="ℹ️ Пользователь уже удален.",
+ )
+ )
+ await state.clear()
+ return
+
+ await _log_admin_user_deletion(session, admin_id, admin, target_user_id)
+ await session.commit()
+
+ await message.answer(
+ _(
+ "admin_user_delete_success",
+ default="✅ Пользователь {user_id} удален из бота и панели.",
+ user_id=hcode(str(target_user_id)),
+ ),
+ parse_mode="HTML",
+ )
+ except Exception as e:
+ logging.error(f"Error deleting user {target_user_id}: {e}", exc_info=True)
+ await session.rollback()
+ await message.answer(
+ _(
+ "admin_user_delete_error",
+ default="❌ Не удалось завершить удаление пользователя. Попробуйте позже.",
+ )
+ )
+ finally:
+ await state.clear()
+
+
@router.message(AdminStates.waiting_for_subscription_days_to_add, F.text)
async def process_subscription_days_handler(message: types.Message, state: FSMContext,
settings: Settings, i18n_data: dict,
@@ -911,9 +1173,9 @@ async def process_ban_user_handler(message: types.Message, state: FSMContext,
@router.message(AdminStates.waiting_for_user_id_to_unban, F.text)
async def process_unban_user_handler(message: types.Message, state: FSMContext,
- settings: Settings, i18n_data: dict,
- panel_service: PanelApiService,
- session: AsyncSession):
+ settings: Settings, i18n_data: dict,
+ panel_service: PanelApiService,
+ session: AsyncSession):
"""Process user unban input"""
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
@@ -978,3 +1240,58 @@ async def process_unban_user_handler(message: types.Message, state: FSMContext,
))
await state.clear()
+
+
+@router.callback_query(F.data.startswith("admin_user_card_from_list:"))
+async def user_card_from_list_handler(callback: types.CallbackQuery,
+ state: FSMContext, i18n_data: dict,
+ settings: Settings, bot: Bot,
+ subscription_service: SubscriptionService,
+ panel_service: PanelApiService,
+ session: AsyncSession):
+ """Display user card when clicked from user list"""
+ try:
+ parts = callback.data.split(":")
+ user_id = int(parts[1])
+ page = int(parts[2])
+ except (IndexError, ValueError):
+ await callback.answer("Invalid user data", show_alert=True)
+ return
+
+ current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
+ i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
+ if not i18n:
+ await callback.answer("Language service error", show_alert=True)
+ return
+ _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
+
+ # Get user from database
+ user = await user_dal.get_user_by_id(session, user_id)
+ if not user:
+ await callback.answer("User not found", show_alert=True)
+ return
+
+ # Create keyboard with back to list button
+ keyboard = get_user_card_keyboard(user_id, i18n, current_lang)
+ keyboard.button(
+ text=_("admin_user_back_to_list_button", default="⬅️ К списку"),
+ callback_data=f"admin_action:users_list:{page}"
+ )
+ keyboard.adjust(2, 2, 2, 2, 1)
+
+ # Format user card
+ try:
+ from bot.services.referral_service import ReferralService
+ referral_service = ReferralService(settings, subscription_service, bot, i18n)
+ user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang, referral_service)
+
+ await callback.message.edit_text(
+ user_card_text,
+ reply_markup=keyboard.as_markup(),
+ parse_mode="HTML"
+ )
+ await callback.answer()
+
+ except Exception as e:
+ logging.error(f"Error displaying user card: {e}")
+ await callback.answer("Error displaying user card", show_alert=True)
diff --git a/bot/keyboards/inline/admin_keyboards.py b/bot/keyboards/inline/admin_keyboards.py
index 37dfbf8..91d4379 100644
--- a/bot/keyboards/inline/admin_keyboards.py
+++ b/bot/keyboards/inline/admin_keyboards.py
@@ -59,13 +59,15 @@ def get_user_management_keyboard(i18n_instance, lang: str) -> InlineKeyboardMark
builder = InlineKeyboardBuilder()
builder.button(text=_(key="admin_users_management_button"),
- callback_data="admin_action:users_management")
+ callback_data="admin_action:users_list:0")
+ builder.button(text=_(key="admin_users_search_button"),
+ callback_data="admin_action:users_search_prompt")
builder.button(text=_(key="admin_ban_management_section"),
callback_data="admin_section:ban_management")
builder.button(text=_(key="back_to_admin_panel_button"),
callback_data="admin_action:main")
- builder.adjust(2, 1)
+ builder.adjust(2, 1, 1)
return builder.as_markup()
@@ -305,6 +307,68 @@ def get_banned_users_keyboard(banned_users: List[User], current_page: int,
return builder.as_markup()
+def get_users_list_keyboard(users: List[User], current_page: int,
+ total_users: int, i18n_instance, lang: str,
+ page_size: int = 15) -> InlineKeyboardMarkup:
+ """Generate keyboard for paginated user list"""
+ _ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
+ builder = InlineKeyboardBuilder()
+
+ # Add user buttons
+ for user in users:
+ user_display_parts = []
+ if user.username:
+ user_display_parts.append(f"@{user.username}")
+ user_display_parts.append(f"ID: {user.user_id}")
+ if user.first_name:
+ user_display_parts.append(f"- {user.first_name}")
+
+ button_text = " ".join(user_display_parts)
+ builder.row(
+ InlineKeyboardButton(
+ text=button_text,
+ callback_data=f"admin_user_card_from_list:{user.user_id}:{current_page}"
+ )
+ )
+
+ # Pagination buttons
+ if total_users > page_size:
+ total_pages = math.ceil(total_users / page_size)
+ pagination_buttons = []
+ if current_page > 0:
+ pagination_buttons.append(
+ InlineKeyboardButton(
+ text=_("prev_page_button"),
+ callback_data=f"admin_action:users_list:{current_page - 1}"
+ )
+ )
+ pagination_buttons.append(
+ InlineKeyboardButton(
+ text=f"{current_page + 1}/{total_pages}",
+ callback_data="stub_page_display"
+ )
+ )
+ if current_page < total_pages - 1:
+ pagination_buttons.append(
+ InlineKeyboardButton(
+ text=_("next_page_button"),
+ callback_data=f"admin_action:users_list:{current_page + 1}"
+ )
+ )
+ if pagination_buttons:
+ builder.row(*pagination_buttons)
+
+ # Back button
+ builder.row(
+ InlineKeyboardButton(
+ text=_("back_to_user_management_button"),
+ callback_data="admin_section:user_management"
+ )
+ )
+
+ return builder.as_markup()
+
+
def get_user_card_keyboard(user_id: int,
is_banned: bool,
i18n_instance,
diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py
index 60586d0..6acb08d 100644
--- a/bot/services/panel_api_service.py
+++ b/bot/services/panel_api_service.py
@@ -455,6 +455,37 @@ class PanelApiService:
)
return False
+ async def delete_user_from_panel(self,
+ user_uuid: str,
+ log_response: bool = True) -> bool:
+ """Delete a user from the panel. Treat not-found as already deleted."""
+ endpoint = f"/users/{user_uuid}"
+ response_data = await self._request(
+ "DELETE", endpoint, log_full_response=log_response
+ )
+
+ if not response_data:
+ logging.error(
+ f"Panel API delete_user_from_panel returned no data for user {user_uuid}."
+ )
+ return False
+
+ if response_data.get("error"):
+ details = response_data.get("details") or {}
+ error_code = details.get("errorCode") or response_data.get("errorCode")
+ if error_code in {"A062", "A040"}:
+ logging.info(
+ f"Panel user {user_uuid} already absent (errorCode {error_code}). Treating as deleted."
+ )
+ return True
+ logging.error(
+ f"Failed to delete user {user_uuid} on panel. Response: {response_data}"
+ )
+ return False
+
+ logging.info(f"Panel user {user_uuid} deleted successfully.")
+ return True
+
async def get_subscription_link(
self,
short_uuid_or_sub_uuid: str,
diff --git a/bot/states/admin_states.py b/bot/states/admin_states.py
index 6db5421..68b0c4a 100644
--- a/bot/states/admin_states.py
+++ b/bot/states/admin_states.py
@@ -28,6 +28,7 @@ class AdminStates(StatesGroup):
waiting_for_user_search = State()
waiting_for_subscription_days_to_add = State()
waiting_for_direct_message_to_user = State()
+ waiting_for_user_delete_confirmation = State()
# Ads campaigns
waiting_for_ad_source = State()
diff --git a/db/dal/user_dal.py b/db/dal/user_dal.py
index 423945f..4676a81 100644
--- a/db/dal/user_dal.py
+++ b/db/dal/user_dal.py
@@ -3,11 +3,20 @@ from typing import Optional, List, Dict, Any, Tuple
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
-from sqlalchemy import update, delete, func, and_
+from sqlalchemy import update, delete, func, and_, or_
from datetime import datetime, timezone
from sqlalchemy.dialects.postgresql import insert as pg_insert
-from ..models import User, Subscription
+from ..models import (
+ User,
+ Subscription,
+ Payment,
+ PromoCodeActivation,
+ MessageLog,
+ UserBilling,
+ UserPaymentMethod,
+ AdAttribution,
+)
async def get_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]:
@@ -102,6 +111,29 @@ async def get_banned_users(session: AsyncSession) -> List[User]:
return result.scalars().all()
+async def get_all_users_paginated(
+ session: AsyncSession, *, page: int = 0, page_size: int = 15
+) -> List[User]:
+ """Return a slice of users ordered by newest registration first."""
+ safe_page = max(page, 0)
+ safe_page_size = max(page_size, 1)
+
+ stmt = (
+ select(User)
+ .order_by(User.registration_date.desc())
+ .offset(safe_page * safe_page_size)
+ .limit(safe_page_size)
+ )
+ result = await session.execute(stmt)
+ return result.scalars().all()
+
+
+async def count_all_users(session: AsyncSession) -> int:
+ """Count total number of users."""
+ result = await session.execute(select(func.count(User.user_id)))
+ return result.scalar_one()
+
+
async def get_all_active_user_ids_for_broadcast(session: AsyncSession) -> List[int]:
stmt = select(User.user_id).where(User.is_banned == False)
result = await session.execute(stmt)
@@ -227,3 +259,41 @@ async def get_user_ids_without_active_subscription(session: AsyncSession) -> Lis
)
result = await session.execute(stmt)
return result.scalars().all()
+
+
+async def delete_user_and_relations(session: AsyncSession, user_id: int) -> bool:
+ """Completely remove a user and all dependent records from the database.
+
+ This helper ensures we do not leave dangling foreign keys or orphaned data.
+ """
+ user = await get_user_by_id(session, user_id)
+ if not user:
+ return False
+
+ # Ensure referral pointers do not block deletion
+ await session.execute(
+ update(User).where(User.referred_by_id == user_id).values(referred_by_id=None)
+ )
+
+ # Clean up dependent tables that do not cascade automatically
+ await session.execute(
+ delete(MessageLog).where(
+ or_(MessageLog.user_id == user_id, MessageLog.target_user_id == user_id)
+ )
+ )
+ await session.execute(delete(Payment).where(Payment.user_id == user_id))
+ await session.execute(
+ delete(Subscription).where(Subscription.user_id == user_id)
+ )
+ await session.execute(
+ delete(PromoCodeActivation).where(PromoCodeActivation.user_id == user_id)
+ )
+ await session.execute(
+ delete(UserPaymentMethod).where(UserPaymentMethod.user_id == user_id)
+ )
+ await session.execute(delete(UserBilling).where(UserBilling.user_id == user_id))
+ await session.execute(delete(AdAttribution).where(AdAttribution.user_id == user_id))
+
+ await session.delete(user)
+ await session.flush()
+ return True
diff --git a/locales/en.json b/locales/en.json
index cc2437e..b3ffec6 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -101,7 +101,10 @@
"admin_promo_marketing_section": "🎁 Promos",
"admin_system_functions_section": "⚙️ System",
"admin_ban_management_section": "🚫 Bans",
- "admin_users_management_button": "👤 Management",
+ "admin_users_search_button": "🔍 Search",
+ "admin_users_management_button": "👥 User List",
+ "admin_users_list_header": "👥 User List\n\nPage {current}/{total} ({total_users} users)",
+ "admin_user_back_to_list_button": "⬅️ Back to List",
"back_to_user_management_button": "⬅️ To Users",
"back_to_admin_panel_button": "⬅️ To Admin",
"admin_stats_header": "📊 Bot Statistics",
@@ -244,6 +247,16 @@
"admin_user_send_message_button": "✉️ Send Message",
"admin_user_view_logs_button": "📜 User Actions",
"admin_user_refresh_button": "🔄 Refresh",
+ "admin_user_delete_button": "❌ Delete User",
+ "admin_user_delete_not_allowed": "❌ You are not permitted to delete users.",
+ "admin_user_delete_confirmation_prompt": "⚠️ You are about to delete user {user_id} completely.\n\nSend the exact Telegram ID of this user to confirm.\nAny other reply will cancel the operation.",
+ "admin_user_delete_state_missing": "⚠️ No active delete operation. Start again.",
+ "admin_user_delete_cancelled": "Deletion cancelled.",
+ "admin_user_delete_mismatch": "⚠️ ID mismatch. Deletion aborted.",
+ "admin_user_delete_already_removed": "ℹ️ The user is already removed.",
+ "admin_user_delete_panel_error": "❌ Failed to remove the user from the panel. Operation aborted.",
+ "admin_user_delete_success": "✅ User {user_id} was removed from the bot and the panel.",
+ "admin_user_delete_error": "❌ Unable to delete the user. Please try again later.",
"admin_user_search_new_button": "🔍 Find Another",
"admin_user_view_all_logs_button": "📋 All Actions",
"admin_user_back_to_card_button": "🔙 Back to Card",
diff --git a/locales/ru.json b/locales/ru.json
index ad106ae..a9e97d9 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -101,7 +101,10 @@
"admin_promo_marketing_section": "🎁 Промокоды",
"admin_system_functions_section": "⚙️ Система",
"admin_ban_management_section": "🚫 Блокировки",
- "admin_users_management_button": "👤 Управление",
+ "admin_users_search_button": "🔍 Поиск",
+ "admin_users_management_button": "👥 Список пользователей",
+ "admin_users_list_header": "👥 Список пользователей\n\nСтраница {current}/{total} ({total_users} пользователей)",
+ "admin_user_back_to_list_button": "⬅️ К списку",
"back_to_user_management_button": "⬅️ К пользователям",
"back_to_admin_panel_button": "⬅️ В админку",
"admin_stats_header": "📊 Статистика Бота",
@@ -244,6 +247,16 @@
"admin_user_send_message_button": "✉️ Сообщение",
"admin_user_view_logs_button": "📜 Логи",
"admin_user_refresh_button": "🔄 Обновить",
+ "admin_user_delete_button": "❌ Удалить пользователя",
+ "admin_user_delete_not_allowed": "❌ У вас нет прав для удаления пользователей.",
+ "admin_user_delete_confirmation_prompt": "⚠️ Вы хотите полностью удалить пользователя {user_id}.\n\nОтправьте точный Telegram ID этого пользователя, чтобы подтвердить удаление.\nЛюбой другой ответ отменит операцию.",
+ "admin_user_delete_state_missing": "⚠️ Нет активной операции удаления. Начните заново.",
+ "admin_user_delete_cancelled": "Удаление отменено.",
+ "admin_user_delete_mismatch": "⚠️ ID не совпадает. Удаление отменено.",
+ "admin_user_delete_already_removed": "ℹ️ Пользователь уже удалён.",
+ "admin_user_delete_panel_error": "❌ Не удалось удалить пользователя на панели. Операция прервана.",
+ "admin_user_delete_success": "✅ Пользователь {user_id} удалён из бота и панели.",
+ "admin_user_delete_error": "❌ Не удалось удалить пользователя. Попробуйте позже.",
"admin_user_search_new_button": "🔍 Новый поиск",
"admin_user_view_all_logs_button": "📋 Все логи",
"admin_user_back_to_card_button": "🔙 К карточке",