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..b970156 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)
@@ -911,9 +954,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 +1021,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/db/dal/user_dal.py b/db/dal/user_dal.py
index 423945f..b0b7b42 100644
--- a/db/dal/user_dal.py
+++ b/db/dal/user_dal.py
@@ -227,3 +227,23 @@ async def get_user_ids_without_active_subscription(session: AsyncSession) -> Lis
)
result = await session.execute(stmt)
return result.scalars().all()
+
+
+async def get_all_users_paginated(session: AsyncSession, page: int = 0, page_size: int = 15) -> List[User]:
+ """Get all users with pagination, ordered by registration date descending."""
+ offset = page * page_size
+ stmt = (
+ select(User)
+ .order_by(User.registration_date.desc())
+ .offset(offset)
+ .limit(page_size)
+ )
+ result = await session.execute(stmt)
+ return result.scalars().all()
+
+
+async def count_all_users(session: AsyncSession) -> int:
+ """Count total number of users in the database."""
+ stmt = select(func.count(User.user_id))
+ result = await session.execute(stmt)
+ return result.scalar() or 0
\ No newline at end of file
diff --git a/locales/en.json b/locales/en.json
index cc2437e..9fc48f6 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",
diff --git a/locales/ru.json b/locales/ru.json
index ad106ae..b2c3b2d 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": "📊 Статистика Бота",