diff --git a/db.py b/db.py index 6fb74ff..fe12d87 100644 --- a/db.py +++ b/db.py @@ -472,3 +472,305 @@ class OracleDatabase: except Exception as e: print(f"Error saving payment log for user {user.id}: {e}") return False + + # ============================================== + # USER ANALYTICS METHODS + # ============================================== + + async def get_users_general_stats(self) -> dict: + """Общая статистика пользователей""" + def _get_stats(): + with self._pool.acquire() as conn: + with conn.cursor() as cur: + query = """ + SELECT + COUNT(*) as total_users, + COUNT(CASE WHEN is_premium = 1 THEN 1 END) as premium_users, + COUNT(CASE WHEN is_active = 1 THEN 1 END) as active_users, + COUNT(CASE WHEN is_blocked = 1 THEN 1 END) as blocked_users, + COUNT(CASE WHEN successful_payments_count > 0 THEN 1 END) as paying_users, + SUM(total_payments) as total_revenue, + SUM(successful_payments_count) as total_transactions, + SUM(interaction_count) as total_interactions, + AVG(interaction_count) as avg_interactions_per_user, + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 1 THEN 1 END) as active_24h, + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 7 THEN 1 END) as active_7d, + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 30 THEN 1 END) as active_30d + FROM bot_users + """ + cur.execute(query) + result = cur.fetchone() + + return { + "total_users": result[0] or 0, + "premium_users": result[1] or 0, + "active_users": result[2] or 0, + "blocked_users": result[3] or 0, + "paying_users": result[4] or 0, + "total_revenue": float(result[5]) if result[5] else 0.0, + "total_transactions": result[6] or 0, + "total_interactions": result[7] or 0, + "avg_interactions_per_user": round(float(result[8]), 2) if result[8] else 0.0, + "active_24h": result[9] or 0, + "active_7d": result[10] or 0, + "active_30d": result[11] or 0 + } + + try: + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_stats) + except Exception as e: + print(f"Error getting general user stats: {e}") + return {} + + async def get_users_growth_stats(self) -> dict: + """Статистика роста пользователей""" + def _get_stats(): + with self._pool.acquire() as conn: + with conn.cursor() as cur: + query = """ + SELECT + COUNT(CASE WHEN created_at >= SYSDATE - 1 THEN 1 END) as new_today, + COUNT(CASE WHEN created_at >= SYSDATE - 7 THEN 1 END) as new_week, + COUNT(CASE WHEN created_at >= SYSDATE - 30 THEN 1 END) as new_month, + COUNT(CASE WHEN created_at >= SYSDATE - 365 THEN 1 END) as new_year, + TO_CHAR(MIN(created_at), 'DD.MM.YYYY') as first_user_date, + ROUND((SYSDATE - MIN(created_at))) as days_since_start + FROM bot_users + """ + cur.execute(query) + result = cur.fetchone() + + # Получаем рост по дням за последние 30 дней + daily_query = """ + SELECT + TO_CHAR(created_at, 'DD.MM') as day, + COUNT(*) as count + FROM bot_users + WHERE created_at >= SYSDATE - 30 + GROUP BY TO_CHAR(created_at, 'DD.MM'), TRUNC(created_at) + ORDER BY TRUNC(created_at) DESC + """ + cur.execute(daily_query) + daily_growth = cur.fetchall() + + return { + "new_today": result[0] or 0, + "new_week": result[1] or 0, + "new_month": result[2] or 0, + "new_year": result[3] or 0, + "first_user_date": result[4] or "N/A", + "days_since_start": result[5] or 0, + "daily_growth": daily_growth[:10] if daily_growth else [] + } + + try: + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_stats) + except Exception as e: + print(f"Error getting growth stats: {e}") + return {} + + async def get_users_premium_stats(self) -> dict: + """Анализ Premium пользователей""" + def _get_stats(): + with self._pool.acquire() as conn: + with conn.cursor() as cur: + query = """ + SELECT + COUNT(CASE WHEN is_premium = 1 THEN 1 END) as premium_users, + COUNT(CASE WHEN is_premium = 0 THEN 1 END) as regular_users, + AVG(CASE WHEN is_premium = 1 THEN total_payments END) as premium_avg_payment, + AVG(CASE WHEN is_premium = 0 THEN total_payments END) as regular_avg_payment, + AVG(CASE WHEN is_premium = 1 THEN interaction_count END) as premium_avg_interactions, + AVG(CASE WHEN is_premium = 0 THEN interaction_count END) as regular_avg_interactions, + COUNT(CASE WHEN is_premium = 1 AND successful_payments_count > 0 THEN 1 END) as premium_paying, + COUNT(CASE WHEN is_premium = 0 AND successful_payments_count > 0 THEN 1 END) as regular_paying + FROM bot_users + WHERE is_active = 1 + """ + cur.execute(query) + result = cur.fetchone() + + premium_total = result[0] or 0 + regular_total = result[1] or 0 + + return { + "premium_users": premium_total, + "regular_users": regular_total, + "premium_percentage": round((premium_total / (premium_total + regular_total) * 100), 2) if (premium_total + regular_total) > 0 else 0, + "premium_avg_payment": round(float(result[2]), 2) if result[2] else 0.0, + "regular_avg_payment": round(float(result[3]), 2) if result[3] else 0.0, + "premium_avg_interactions": round(float(result[4]), 2) if result[4] else 0.0, + "regular_avg_interactions": round(float(result[5]), 2) if result[5] else 0.0, + "premium_paying": result[6] or 0, + "regular_paying": result[7] or 0, + "premium_conversion": round((result[6] / premium_total * 100), 2) if premium_total > 0 else 0, + "regular_conversion": round((result[7] / regular_total * 100), 2) if regular_total > 0 else 0 + } + + try: + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_stats) + except Exception as e: + print(f"Error getting premium stats: {e}") + return {} + + async def get_users_geography_stats(self) -> dict: + """География пользователей""" + def _get_stats(): + with self._pool.acquire() as conn: + with conn.cursor() as cur: + # Топ языков + lang_query = """ + SELECT + COALESCE(language_code, 'unknown') as language, + COUNT(*) as count, + ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM bot_users), 2) as percentage + FROM bot_users + GROUP BY language_code + ORDER BY COUNT(*) DESC + """ + cur.execute(lang_query) + languages = cur.fetchall() + + # Статистика источников регистрации + source_query = """ + SELECT + registration_source, + COUNT(*) as count + FROM bot_users + GROUP BY registration_source + ORDER BY COUNT(*) DESC + """ + cur.execute(source_query) + sources = cur.fetchall() + + return { + "top_languages": languages[:10] if languages else [], + "total_languages": len(languages) if languages else 0, + "registration_sources": sources if sources else [] + } + + try: + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_stats) + except Exception as e: + print(f"Error getting geography stats: {e}") + return {} + + async def get_users_activity_stats(self) -> dict: + """Анализ активности пользователей""" + def _get_stats(): + with self._pool.acquire() as conn: + with conn.cursor() as cur: + # Активность за разные периоды + activity_query = """ + SELECT + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 1 THEN 1 END) as active_1d, + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 3 THEN 1 END) as active_3d, + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 7 THEN 1 END) as active_7d, + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 14 THEN 1 END) as active_14d, + COUNT(CASE WHEN last_interaction_date >= SYSDATE - 30 THEN 1 END) as active_30d, + COUNT(CASE WHEN last_interaction_date < SYSDATE - 30 THEN 1 END) as inactive_30d + FROM bot_users + WHERE is_active = 1 + """ + cur.execute(activity_query) + activity = cur.fetchone() + + # Распределение по количеству взаимодействий + interaction_query = """ + SELECT + CASE + WHEN interaction_count = 1 THEN 'Новички (1)' + WHEN interaction_count BETWEEN 2 AND 5 THEN 'Начинающие (2-5)' + WHEN interaction_count BETWEEN 6 AND 20 THEN 'Активные (6-20)' + WHEN interaction_count BETWEEN 21 AND 100 THEN 'Постоянные (21-100)' + ELSE 'Суперактивные (100+)' + END as category, + COUNT(*) as count + FROM bot_users + WHERE is_active = 1 + GROUP BY + CASE + WHEN interaction_count = 1 THEN 'Новички (1)' + WHEN interaction_count BETWEEN 2 AND 5 THEN 'Начинающие (2-5)' + WHEN interaction_count BETWEEN 6 AND 20 THEN 'Активные (6-20)' + WHEN interaction_count BETWEEN 21 AND 100 THEN 'Постоянные (21-100)' + ELSE 'Суперактивные (100+)' + END + ORDER BY COUNT(*) DESC + """ + cur.execute(interaction_query) + interactions = cur.fetchall() + + return { + "active_1d": activity[0] or 0, + "active_3d": activity[1] or 0, + "active_7d": activity[2] or 0, + "active_14d": activity[3] or 0, + "active_30d": activity[4] or 0, + "inactive_30d": activity[5] or 0, + "interaction_distribution": interactions if interactions else [] + } + + try: + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_stats) + except Exception as e: + print(f"Error getting activity stats: {e}") + return {} + + async def get_users_sources_stats(self) -> dict: + """Анализ источников пользователей""" + def _get_stats(): + with self._pool.acquire() as conn: + with conn.cursor() as cur: + # Статистика по источникам + sources_query = """ + SELECT + registration_source, + COUNT(*) as total_users, + COUNT(CASE WHEN successful_payments_count > 0 THEN 1 END) as paying_users, + AVG(total_payments) as avg_revenue, + AVG(interaction_count) as avg_interactions, + COUNT(CASE WHEN created_at >= SYSDATE - 30 THEN 1 END) as new_last_30d + FROM bot_users + GROUP BY registration_source + ORDER BY COUNT(*) DESC + """ + cur.execute(sources_query) + sources = cur.fetchall() + + # Топ реферальные источники (если есть) + referral_query = """ + SELECT + registration_source, + COUNT(*) as count + FROM bot_users + WHERE registration_source NOT IN ('bot', 'direct', 'unknown') + GROUP BY registration_source + ORDER BY COUNT(*) DESC + FETCH FIRST 10 ROWS ONLY + """ + cur.execute(referral_query) + referrals = cur.fetchall() + + return { + "source_breakdown": sources if sources else [], + "top_referrals": referrals if referrals else [] + } + + try: + import asyncio + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _get_stats) + except Exception as e: + print(f"Error getting sources stats: {e}") + return {} diff --git a/main.py b/main.py index af0d2b5..5aac882 100644 --- a/main.py +++ b/main.py @@ -671,37 +671,494 @@ async def admin_stats_callback(callback: CallbackQuery, db: OracleDatabase = Non # Сохраняем данные пользователя при нажатии кнопки админки await database.save_user(callback.from_user, "admin_stats_button") + # Формируем меню админ панели с категориями отчетов + admin_menu_text = """🔧 **Admin Dashboard** + +Выберите категорию отчетов для анализа: + +📊 **Доступные отчеты:** +• Пользовательская аналитика +• Финансовая аналитика +• Техническая аналитика +• Операционные отчеты +• Бизнес-аналитика + +💡 Выберите интересующую категорию ниже:""" + + # Создаем кнопки для категорий отчетов + builder = InlineKeyboardBuilder() + builder.button(text="👥 Пользователи", callback_data="admin_users") + builder.button(text="💰 Финансы", callback_data="admin_finance") + builder.adjust(2) + builder.button(text="🔍 Техническая", callback_data="admin_technical") + builder.button(text="⚡ Операционная", callback_data="admin_operations") + builder.adjust(2) + builder.button(text="📈 Бизнес-аналитика", callback_data="admin_business") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(1, 1) + + await callback.message.answer(admin_menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + +# ============================================== +# ADMIN REPORTS CALLBACKS +# ============================================== + +@dp.callback_query(lambda c: c.data == "admin_users") +async def admin_users_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Пользовательская аналитика""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + menu_text = """👥 **Пользовательская аналитика** + +Выберите тип отчета:""" + + builder = InlineKeyboardBuilder() + builder.button(text="📊 Общая статистика", callback_data="admin_users_general") + builder.button(text="📈 Рост пользователей", callback_data="admin_users_growth") + builder.adjust(2) + builder.button(text="👑 Premium анализ", callback_data="admin_users_premium") + builder.button(text="🌍 География", callback_data="admin_users_geo") + builder.adjust(2) + builder.button(text="⚡ Активность", callback_data="admin_users_activity") + builder.button(text="📋 Источники", callback_data="admin_users_sources") + builder.adjust(2) + builder.button(text="🔙 Назад", callback_data="admin_stats") + builder.adjust(1) + + await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + +@dp.callback_query(lambda c: c.data == "admin_finance") +async def admin_finance_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Финансовая аналитика""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + menu_text = """💰 **Финансовая аналитика** + +Выберите тип отчета:""" + + builder = InlineKeyboardBuilder() + builder.button(text="💵 Доходы", callback_data="admin_finance_revenue") + builder.button(text="📊 По услугам", callback_data="admin_finance_services") + builder.adjust(2) + builder.button(text="🔄 Конверсия", callback_data="admin_finance_conversion") + builder.button(text="↩️ Возвраты", callback_data="admin_finance_refunds") + builder.adjust(2) + builder.button(text="💳 Транзакции", callback_data="admin_finance_transactions") + builder.button(text="🎯 Эффективность", callback_data="admin_finance_efficiency") + builder.adjust(2) + builder.button(text="🔙 Назад", callback_data="admin_stats") + builder.adjust(1) + + await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + +@dp.callback_query(lambda c: c.data == "admin_technical") +async def admin_technical_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Техническая аналитика""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + menu_text = """🔍 **Техническая аналитика** + +Выберите тип отчета:""" + + builder = InlineKeyboardBuilder() + builder.button(text="🚗 Популярные авто", callback_data="admin_tech_popular") + builder.button(text="📅 Тренды годов", callback_data="admin_tech_years") + builder.adjust(2) + builder.button(text="🗺️ География ДТП", callback_data="admin_tech_geography") + builder.button(text="💥 Типы повреждений", callback_data="admin_tech_damages") + builder.adjust(2) + builder.button(text="📊 Качество данных", callback_data="admin_tech_quality") + builder.button(text="📸 Статистика фото", callback_data="admin_tech_photos") + builder.adjust(2) + builder.button(text="🔙 Назад", callback_data="admin_stats") + builder.adjust(1) + + await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + +@dp.callback_query(lambda c: c.data == "admin_operations") +async def admin_operations_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Операционные отчеты""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + menu_text = """⚡ **Операционные отчеты** + +Выберите тип отчета:""" + + builder = InlineKeyboardBuilder() + builder.button(text="⏱️ Производительность", callback_data="admin_ops_performance") + builder.button(text="🚨 Ошибки системы", callback_data="admin_ops_errors") + builder.adjust(2) + builder.button(text="📈 Нагрузка", callback_data="admin_ops_load") + builder.button(text="🏪 Аукционы", callback_data="admin_ops_auctions") + builder.adjust(2) + builder.button(text="🔍 Проблемные VIN", callback_data="admin_ops_problem_vins") + builder.button(text="👀 Мониторинг", callback_data="admin_ops_monitoring") + builder.adjust(2) + builder.button(text="🔙 Назад", callback_data="admin_stats") + builder.adjust(1) + + await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + +@dp.callback_query(lambda c: c.data == "admin_business") +async def admin_business_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Бизнес-аналитика""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + menu_text = """📈 **Бизнес-аналитика** + +Выберите тип отчета:""" + + builder = InlineKeyboardBuilder() + builder.button(text="📊 Тренды", callback_data="admin_biz_trends") + builder.button(text="🔮 Прогнозы", callback_data="admin_biz_forecasts") + builder.adjust(2) + builder.button(text="🌍 Регионы роста", callback_data="admin_biz_regions") + builder.button(text="💎 Монетизация", callback_data="admin_biz_monetization") + builder.adjust(2) + builder.button(text="🎯 Оптимизация", callback_data="admin_biz_optimization") + builder.button(text="💡 Рекомендации", callback_data="admin_biz_recommendations") + builder.adjust(2) + builder.button(text="🔙 Назад", callback_data="admin_stats") + builder.adjust(1) + + await callback.message.answer(menu_text, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + +# ============================================== +# USER ANALYTICS REPORT HANDLERS +# ============================================== + +@dp.callback_query(lambda c: c.data == "admin_users_general") +async def admin_users_general_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Общая статистика пользователей""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + try: - # Получаем общую статистику - stats = await database.get_users_summary() + stats = await database.get_users_general_stats() - # Формируем отчет - report = f"""📊 **Bot Users Statistics** + report = f"""📊 **Общая статистика пользователей** -👥 **Users Overview:** -• Total users: {stats.get('total_users', 0)} -• Premium users: {stats.get('premium_users', 0)} +👥 **Пользователи:** +• Всего пользователей: **{stats['total_users']:,}** +• Активных: **{stats['active_users']:,}** ({round(stats['active_users']/stats['total_users']*100, 1) if stats['total_users'] > 0 else 0}%) +• Premium: **{stats['premium_users']:,}** ({round(stats['premium_users']/stats['total_users']*100, 1) if stats['total_users'] > 0 else 0}%) +• Заблокированных: **{stats['blocked_users']:,}** -💰 **Revenue:** -• Total revenue: {stats.get('total_revenue', 0)} ⭐️ -• Total transactions: {stats.get('total_transactions', 0)} +💰 **Монетизация:** +• Платящих пользователей: **{stats['paying_users']:,}** +• Конверсия в покупку: **{round(stats['paying_users']/stats['total_users']*100, 2) if stats['total_users'] > 0 else 0}%** +• Общая выручка: **{stats['total_revenue']:,.0f}** ⭐️ +• Всего транзакций: **{stats['total_transactions']:,}** -📈 **Activity:** -• Active last 24h: {stats.get('active_last_24h', 0)} -• Active last week: {stats.get('active_last_week', 0)} +⚡ **Активность:** +• За 24 часа: **{stats['active_24h']:,}** +• За 7 дней: **{stats['active_7d']:,}** +• За 30 дней: **{stats['active_30d']:,}** +• Всего взаимодействий: **{stats['total_interactions']:,}** +• Среднее на пользователя: **{stats['avg_interactions_per_user']:.1f}** -📅 **Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}""" +📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}""" - # Создаем кнопку возврата в главное меню builder = InlineKeyboardBuilder() - builder.button(text="🏠 Back to Main Menu", callback_data="main_menu") + builder.button(text="🔙 Назад", callback_data="admin_users") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2) await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown") await callback.answer() except Exception as e: - logging.error(f"Error generating admin stats via callback: {e}") - await callback.answer("❌ Error generating statistics. Please try again later.", show_alert=True) + logging.error(f"Error generating general user stats: {e}") + await callback.answer("❌ Ошибка генерации отчета", show_alert=True) + + +@dp.callback_query(lambda c: c.data == "admin_users_growth") +async def admin_users_growth_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Статистика роста пользователей""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + try: + stats = await database.get_users_growth_stats() + + # Формируем график роста за последние дни + daily_chart = "" + if stats['daily_growth']: + daily_chart = "\n📈 **Рост по дням (последние 10):**\n" + for day, count in stats['daily_growth']: + daily_chart += f"• {day}: **{count}** новых\n" + + report = f"""📈 **Рост пользователей** + +🆕 **Новые пользователи:** +• Сегодня: **{stats['new_today']:,}** +• За неделю: **{stats['new_week']:,}** +• За месяц: **{stats['new_month']:,}** +• За год: **{stats['new_year']:,}** + +📊 **Общая статистика:** +• Первый пользователь: **{stats['first_user_date']}** +• Дней с запуска: **{stats['days_since_start']:,}** +• Средний рост в день: **{round(stats['new_year']/365, 1) if stats['days_since_start'] > 0 else 0}** + +{daily_chart} + +📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}""" + + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад", callback_data="admin_users") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2) + + await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + except Exception as e: + logging.error(f"Error generating growth stats: {e}") + await callback.answer("❌ Ошибка генерации отчета", show_alert=True) + + +@dp.callback_query(lambda c: c.data == "admin_users_premium") +async def admin_users_premium_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Анализ Premium пользователей""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + try: + stats = await database.get_users_premium_stats() + + report = f"""👑 **Premium анализ** + +📊 **Распределение пользователей:** +• Premium: **{stats['premium_users']:,}** ({stats['premium_percentage']:.1f}%) +• Обычные: **{stats['regular_users']:,}** ({100-stats['premium_percentage']:.1f}%) + +💰 **Средние платежи:** +• Premium пользователи: **{stats['premium_avg_payment']:.1f}** ⭐️ +• Обычные пользователи: **{stats['regular_avg_payment']:.1f}** ⭐️ +• Разница: **{stats['premium_avg_payment'] - stats['regular_avg_payment']:.1f}** ⭐️ + +⚡ **Активность:** +• Premium - взаимодействий: **{stats['premium_avg_interactions']:.1f}** +• Обычные - взаимодействий: **{stats['regular_avg_interactions']:.1f}** + +🎯 **Конверсия в покупки:** +• Premium пользователи: **{stats['premium_conversion']:.1f}%** ({stats['premium_paying']}/{stats['premium_users']}) +• Обычные пользователи: **{stats['regular_conversion']:.1f}%** ({stats['regular_paying']}/{stats['regular_users']}) + +💡 **Вывод:** Premium пользователи платят в **{round(stats['premium_avg_payment']/stats['regular_avg_payment'], 1) if stats['regular_avg_payment'] > 0 else 0}х** раз больше и в **{round(stats['premium_conversion']/stats['regular_conversion'], 1) if stats['regular_conversion'] > 0 else 0}х** раз чаще покупают услуги. + +📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}""" + + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад", callback_data="admin_users") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2) + + await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + except Exception as e: + logging.error(f"Error generating premium stats: {e}") + await callback.answer("❌ Ошибка генерации отчета", show_alert=True) + + +@dp.callback_query(lambda c: c.data == "admin_users_geo") +async def admin_users_geo_callback(callback: CallbackQuery, db: OracleDatabase = None): + """География пользователей""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + try: + stats = await database.get_users_geography_stats() + + # Топ языков с экранированием специальных символов + languages_text = "" + if stats['top_languages']: + languages_text = "\n🌍 Топ языков:\n" + for lang, count, percentage in stats['top_languages']: + # Экранируем потенциально проблемные символы + safe_lang = str(lang).replace('&', '&').replace('<', '<').replace('>', '>') + flag = {"ru": "🇷🇺", "en": "🇺🇸", "uk": "🇺🇦", "de": "🇩🇪", "fr": "🇫🇷", "es": "🇪🇸", "it": "🇮🇹"}.get(lang, "🌐") + languages_text += f"• {flag} {safe_lang}: {count:,} ({percentage}%)\n" + + # Источники регистрации + sources_text = "" + if stats['registration_sources']: + sources_text = "\n📱 Источники регистрации:\n" + for source, count in stats['registration_sources']: + safe_source = str(source).replace('&', '&').replace('<', '<').replace('>', '>') + sources_text += f"• {safe_source}: {count:,}\n" + + report = f"""🌍 География пользователей + +📊 Языковая статистика: +• Всего языков: {stats['total_languages']} + +{languages_text} + +{sources_text} + +📅 Сгенерировано: {datetime.now().strftime('%d.%m.%Y %H:%M')}""" + + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад", callback_data="admin_users") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2) + + await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="HTML") + await callback.answer() + + except Exception as e: + logging.error(f"Error generating geography stats: {e}") + await callback.answer("❌ Ошибка генерации отчета", show_alert=True) + + +@dp.callback_query(lambda c: c.data == "admin_users_activity") +async def admin_users_activity_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Анализ активности пользователей""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + try: + stats = await database.get_users_activity_stats() + + # Распределение по активности + activity_text = "" + if stats['interaction_distribution']: + activity_text = "\n👥 **Распределение по активности:**\n" + for category, count in stats['interaction_distribution']: + activity_text += f"• {category}: **{count:,}**\n" + + report = f"""⚡ **Анализ активности** + +📊 **Активность по периодам:** +• За 1 день: **{stats['active_1d']:,}** +• За 3 дня: **{stats['active_3d']:,}** +• За 7 дней: **{stats['active_7d']:,}** +• За 14 дней: **{stats['active_14d']:,}** +• За 30 дней: **{stats['active_30d']:,}** +• Неактивны 30+ дней: **{stats['inactive_30d']:,}** + +📈 **Retention Rate:** +• 1 день: **{round(stats['active_1d']/stats['active_30d']*100, 1) if stats['active_30d'] > 0 else 0}%** +• 7 дней: **{round(stats['active_7d']/stats['active_30d']*100, 1) if stats['active_30d'] > 0 else 0}%** +• 14 дней: **{round(stats['active_14d']/stats['active_30d']*100, 1) if stats['active_30d'] > 0 else 0}%** + +{activity_text} + +📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}""" + + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад", callback_data="admin_users") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2) + + await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + except Exception as e: + logging.error(f"Error generating activity stats: {e}") + await callback.answer("❌ Ошибка генерации отчета", show_alert=True) + + +@dp.callback_query(lambda c: c.data == "admin_users_sources") +async def admin_users_sources_callback(callback: CallbackQuery, db: OracleDatabase = None): + """Анализ источников пользователей""" + database = db or oracle_db + + if callback.from_user.id != ADMIN_USER_ID: + await callback.answer("❌ Access denied.", show_alert=True) + return + + try: + stats = await database.get_users_sources_stats() + + # Основные источники + sources_text = "" + if stats['source_breakdown']: + sources_text = "\n📊 **Источники пользователей:**\n" + for source, total, paying, revenue, interactions, new_30d in stats['source_breakdown']: + conversion = round(paying/total*100, 1) if total > 0 else 0 + avg_revenue = round(revenue, 1) if revenue else 0 + avg_int = round(interactions, 1) if interactions else 0 + sources_text += f"• **{source}**: {total:,} чел. (конв: {conversion}%, ср.доход: {avg_revenue}⭐, активность: {avg_int}, новых за 30д: {new_30d})\n" + + # Реферальные источники + referrals_text = "" + if stats['top_referrals']: + referrals_text = "\n🔗 **Топ реферальные источники:**\n" + for source, count in stats['top_referrals']: + referrals_text += f"• {source}: **{count:,}**\n" + + report = f"""📋 **Источники пользователей** + +{sources_text} + +{referrals_text} + +💡 **Анализ:** Наиболее эффективными являются источники с высокой конверсией в покупки и средним доходом на пользователя. + +📅 **Сгенерировано:** {datetime.now().strftime('%d.%m.%Y %H:%M')}""" + + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад", callback_data="admin_users") + builder.button(text="🏠 Main Menu", callback_data="main_menu") + builder.adjust(2) + + await callback.message.answer(report, reply_markup=builder.as_markup(), parse_mode="Markdown") + await callback.answer() + + except Exception as e: + logging.error(f"Error generating sources stats: {e}") + await callback.answer("❌ Ошибка генерации отчета", show_alert=True) @dp.callback_query(lambda c: c.data and c.data.startswith("pay_detailed_info:"))