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": "📊 Статистика Бота",