diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py index 6f2a175..82abd97 100644 --- a/bot/handlers/admin/promo_codes.py +++ b/bot/handlers/admin/promo_codes.py @@ -653,63 +653,258 @@ async def promo_edit_select_handler(callback: types.CallbackQuery, state: FSMCon return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - await state.update_data(edit_promo_id=promo_id) - await state.set_state(AdminStates.waiting_for_promo_edit_details) + await state.update_data( + edit_promo_id=promo_id, + original_code=promo.code, + original_bonus_days=promo.bonus_days, + original_max_activations=promo.max_activations, + original_valid_until=promo.valid_until + ) + + # Step 1: Edit code + prompt_text = _( + "admin_promo_edit_step1_code", + default="✏️ Редактирование промокода\n\nШаг 1 из 4: Код промокода\n\nТекущий код: {current_code}\n\nВведите новый код промокода (3-30 символов, только буквы и цифры) или отправьте текущий для сохранения:", + current_code=promo.code + ) + + await state.set_state(AdminStates.waiting_for_promo_edit_code) await callback.message.edit_text( - _("admin_promo_edit_prompt", code=promo.code), + prompt_text, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), parse_mode="HTML") await callback.answer() -@router.message(AdminStates.waiting_for_promo_edit_details, F.text) -async def process_promo_edit_details(message: types.Message, state: FSMContext, - i18n_data: dict, settings: Settings, - session: AsyncSession): - data = await state.get_data() - promo_id = data.get("edit_promo_id") +# Step 1: Process edited code +@router.message(AdminStates.waiting_for_promo_edit_code, F.text) +async def process_promo_edit_code_handler(message: types.Message, state: FSMContext, + i18n_data: dict, settings: Settings, + session: AsyncSession): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") - if not i18n or not promo_id: - await message.answer("Error") - await state.clear() + if not i18n: + await message.reply("Language service error.") return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - - parts = message.text.strip().split() - if not (3 <= len(parts) <= 4): - await message.answer(_("admin_promo_invalid_format")) - return + + data = await state.get_data() + original_code = data.get("original_code") + try: - code_str = parts[0].upper() - bonus = int(parts[1]) - max_act = int(parts[2]) - valid_until = None - if len(parts) == 4: - days = int(parts[3]) - valid_until = datetime.now(timezone.utc) + timedelta(days=days) - except Exception: - await message.answer(_("admin_promo_invalid_format_general")) - return - - update_data = { - "code": code_str, - "bonus_days": bonus, - "max_activations": max_act, - "valid_until": valid_until, - } - updated = await promo_code_dal.update_promo_code(session, promo_id, update_data) - if updated: - await session.commit() - await message.answer( - _("admin_promo_updated_success", code=code_str), - reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), - parse_mode="HTML", + code_str = message.text.strip().upper() + if not (3 <= len(code_str) <= 30 and code_str.isalnum()): + await message.answer(_( + "admin_promo_invalid_code_format", + default="❌ Код промокода должен содержать 3-30 символов (только буквы и цифры)" + )) + return + + # Check if code already exists (except current promo) + if code_str != original_code: + existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str) + if existing_promo: + await message.answer(_( + "admin_promo_code_already_exists", + default="❌ Промокод с таким кодом уже существует" + )) + return + + await state.update_data(new_code=code_str) + + # Step 2: Ask for bonus days + prompt_text = _( + "admin_promo_edit_step2_bonus_days", + default="✏️ Редактирование промокода\n\nШаг 2 из 4: Бонусные дни\n\nКод: {code}\nТекущие дни: {current_days}\n\nВведите количество бонусных дней (1-365):", + code=code_str, + current_days=data.get("original_bonus_days") ) - else: - await session.rollback() - await message.answer(_("admin_promo_not_found")) - await state.clear() + + await message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_promo_edit_bonus_days) + + except ValueError: + await message.answer(_( + "admin_promo_invalid_code_format", + default="❌ Код промокода должен содержать 3-30 символов (только буквы и цифры)" + )) + + +# Step 2: Process edited bonus days +@router.message(AdminStates.waiting_for_promo_edit_bonus_days, F.text) +async def process_promo_edit_bonus_days_handler(message: types.Message, state: FSMContext, + i18n_data: dict, settings: Settings): + 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.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + data = await state.get_data() + + try: + bonus_days = int(message.text.strip()) + if not (1 <= bonus_days <= 365): + await message.answer(_( + "admin_promo_invalid_bonus_days", + default="❌ Количество дней должно быть от 1 до 365" + )) + return + + await state.update_data(new_bonus_days=bonus_days) + + # Step 3: Ask for max activations + prompt_text = _( + "admin_promo_edit_step3_max_activations", + default="✏️ Редактирование промокода\n\nШаг 3 из 4: Максимальные активации\n\nКод: {code}\nДни: {days}\nТекущие активации: {current_max}\n\nВведите максимальное количество активаций (1-10000):", + code=data.get("new_code"), + days=bonus_days, + current_max=data.get("original_max_activations") + ) + + await message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_promo_edit_max_activations) + + except ValueError: + await message.answer(_( + "admin_promo_invalid_bonus_days", + default="❌ Количество дней должно быть от 1 до 365" + )) + + +# Step 3: Process edited max activations +@router.message(AdminStates.waiting_for_promo_edit_max_activations, F.text) +async def process_promo_edit_max_activations_handler(message: types.Message, state: FSMContext, + i18n_data: dict, settings: Settings): + 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.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + data = await state.get_data() + + try: + max_activations = int(message.text.strip()) + if not (1 <= max_activations <= 10000): + await message.answer(_( + "admin_promo_invalid_max_activations", + default="❌ Количество активаций должно быть от 1 до 10000" + )) + return + + await state.update_data(new_max_activations=max_activations) + + # Step 4: Ask for validity days + current_valid_until = data.get("original_valid_until") + current_validity_str = _("admin_promo_valid_indefinitely", default="Неограниченно") + if current_valid_until: + current_validity_str = current_valid_until.strftime('%Y-%m-%d') + + prompt_text = _( + "admin_promo_edit_step4_validity", + default="✏️ Редактирование промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nДни: {days}\nАктивации: {max_act}\nТекущий срок: {current_validity}\n\nВведите количество дней действия промокода от сегодня (1-365) или 0 для неограниченного срока:", + code=data.get("new_code"), + days=data.get("new_bonus_days"), + max_act=max_activations, + current_validity=current_validity_str + ) + + await message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_promo_edit_validity_days) + + except ValueError: + await message.answer(_( + "admin_promo_invalid_max_activations", + default="❌ Количество активаций должно быть от 1 до 10000" + )) + + +# Step 4: Process edited validity and finalize +@router.message(AdminStates.waiting_for_promo_edit_validity_days, F.text) +async def process_promo_edit_validity_handler(message: types.Message, state: FSMContext, + i18n_data: dict, settings: Settings, + session: AsyncSession): + 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.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + data = await state.get_data() + promo_id = data.get("edit_promo_id") + + try: + validity_days = int(message.text.strip()) + if not (0 <= validity_days <= 365): + await message.answer(_( + "admin_promo_invalid_validity_days", + default="❌ Срок действия должен быть от 0 до 365 дней (0 = неограниченно)" + )) + return + + # Calculate validity date + valid_until_date = None + valid_until_str_display = _("admin_promo_valid_indefinitely", default="Неограниченно") + if validity_days > 0: + valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days) + valid_until_str_display = valid_until_date.strftime('%Y-%m-%d') + + # Update promo code + update_data = { + "code": data.get("new_code"), + "bonus_days": data.get("new_bonus_days"), + "max_activations": data.get("new_max_activations"), + "valid_until": valid_until_date, + } + + updated = await promo_code_dal.update_promo_code(session, promo_id, update_data) + if updated: + await session.commit() + + success_text = _( + "admin_promo_updated_success", + default="✅ Промокод обновлен!\n\n🎟 Код: {code}\n🎁 Бонусные дни: {days}\n🔢 Максимальные активации: {max_act}\n⏰ Действует до: {validity}", + code=data.get("new_code"), + days=data.get("new_bonus_days"), + max_act=data.get("new_max_activations"), + validity=valid_until_str_display + ) + + await message.answer( + success_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + else: + await session.rollback() + await message.answer(_( + "admin_promo_update_failed", + default="❌ Ошибка обновления промокода" + )) + + await state.clear() + + except ValueError: + await message.answer(_( + "admin_promo_invalid_validity_days", + default="❌ Срок действия должен быть от 0 до 365 дней (0 = неограниченно)" + )) @router.callback_query(F.data.startswith("promo_delete:")) diff --git a/bot/states/admin_states.py b/bot/states/admin_states.py index 3b8af20..330e53e 100644 --- a/bot/states/admin_states.py +++ b/bot/states/admin_states.py @@ -11,6 +11,10 @@ class AdminStates(StatesGroup): waiting_for_promo_max_activations = State() waiting_for_promo_validity_days = State() waiting_for_promo_edit_details = State() + waiting_for_promo_edit_code = State() + waiting_for_promo_edit_bonus_days = State() + waiting_for_promo_edit_max_activations = State() + waiting_for_promo_edit_validity_days = State() waiting_for_bulk_promo_quantity = State() waiting_for_bulk_promo_bonus_days = State() waiting_for_bulk_promo_max_activations = State() diff --git a/locales/en.json b/locales/en.json index a7d5717..6c68dcc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -104,11 +104,11 @@ "admin_sync_panel_button": "🔄 Sync", "admin_unknown_action": "Unknown admin action.", - "admin_stats_and_monitoring_section": "📊 Statistics & Monitoring", - "admin_user_management_section": "👥 User Management", - "admin_promo_marketing_section": "🎁 Promos & Marketing", - "admin_system_functions_section": "⚙️ System Functions", - "admin_ban_management_section": "🚫 Ban Management", + "admin_stats_and_monitoring_section": "📊 Statistics", + "admin_user_management_section": "👥 Users", + "admin_promo_marketing_section": "🎁 Promos", + "admin_system_functions_section": "⚙️ System", + "admin_ban_management_section": "🚫 Bans", "admin_users_management_button": "👤 User Management", "back_to_user_management_button": "⬅️ Back to User Management", "admin_action_cancelled_default": "Action cancelled. Returning to menu.", @@ -308,6 +308,14 @@ "admin_bulk_promo_created_success": "✅ Created {count} promo codes for {days} days!", "admin_bulk_promo_codes_file": "📄 All created promo codes in file ({count} pcs.)", + "admin_promo_edit_step1_code": "✏️ Editing Promo Code\n\nStep 1 of 4: Promo Code\n\nCurrent code: {current_code}\n\nEnter new promo code (3-30 characters, letters and numbers only) or send current one to keep:", + "admin_promo_edit_step2_bonus_days": "✏️ Editing Promo Code\n\nStep 2 of 4: Bonus Days\n\nCode: {code}\nCurrent days: {current_days}\n\nEnter number of bonus days (1-365):", + "admin_promo_edit_step3_max_activations": "✏️ Editing Promo Code\n\nStep 3 of 4: Max Activations\n\nCode: {code}\nDays: {days}\nCurrent activations: {current_max}\n\nEnter maximum number of activations (1-10000):", + "admin_promo_edit_step4_validity": "✏️ Editing Promo Code\n\nStep 4 of 4: Validity Period\n\nCode: {code}\nDays: {days}\nActivations: {max_act}\nCurrent validity: {current_validity}\n\nEnter number of days the promo code is valid from today (1-365) or 0 for unlimited:", + "admin_promo_updated_success": "✅ Promo code updated!\n\n🎟 Code: {code}\n🎁 Bonus days: {days}\n🔢 Max activations: {max_act}\n⏰ Valid until: {validity}", + "admin_promo_update_failed": "❌ Error updating promo code", + "admin_promo_invalid_validity_days": "❌ Validity days must be from 0 to 365 days (0 = unlimited)", + "admin_logs_export_csv": "📄 Export to CSV", "admin_logs_csv_export_started": "📄 Starting log export to CSV...", "admin_logs_csv_export_success": "✅ Logs exported! File attached above.", diff --git a/locales/ru.json b/locales/ru.json index 438d89e..574bcc8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -104,11 +104,11 @@ "admin_sync_panel_button": "🔄 Синхронизация", "admin_unknown_action": "Неизвестное действие администратора.", - "admin_stats_and_monitoring_section": "📊 Статистика и мониторинг", - "admin_user_management_section": "👥 Управление пользователями", - "admin_promo_marketing_section": "🎁 Промокоды и маркетинг", - "admin_system_functions_section": "⚙️ Системные функции", - "admin_ban_management_section": "🚫 Управление блокировками", + "admin_stats_and_monitoring_section": "📊 Статистика", + "admin_user_management_section": "👥 Пользователи", + "admin_promo_marketing_section": "🎁 Промокоды", + "admin_system_functions_section": "⚙️ Система", + "admin_ban_management_section": "🚫 Блокировки", "admin_users_management_button": "👤 Управление пользователями", "back_to_user_management_button": "⬅️ К управлению пользователями", "admin_action_cancelled_default": "Действие отменено. Возврат в меню.", @@ -309,6 +309,14 @@ "admin_bulk_promo_created_success": "✅ Создано {count} промокодов на {days} дней!", "admin_bulk_promo_codes_file": "📄 Все созданные промокоды в файле ({count} шт.)", + "admin_promo_edit_step1_code": "✏️ Редактирование промокода\n\nШаг 1 из 4: Код промокода\n\nТекущий код: {current_code}\n\nВведите новый код промокода (3-30 символов, только буквы и цифры) или отправьте текущий для сохранения:", + "admin_promo_edit_step2_bonus_days": "✏️ Редактирование промокода\n\nШаг 2 из 4: Бонусные дни\n\nКод: {code}\nТекущие дни: {current_days}\n\nВведите количество бонусных дней (1-365):", + "admin_promo_edit_step3_max_activations": "✏️ Редактирование промокода\n\nШаг 3 из 4: Максимальные активации\n\nКод: {code}\nДни: {days}\nТекущие активации: {current_max}\n\nВведите максимальное количество активаций (1-10000):", + "admin_promo_edit_step4_validity": "✏️ Редактирование промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nДни: {days}\nАктивации: {max_act}\nТекущий срок: {current_validity}\n\nВведите количество дней действия промокода от сегодня (1-365) или 0 для неограниченного срока:", + "admin_promo_updated_success": "✅ Промокод обновлен!\n\n🎟 Код: {code}\n🎁 Бонусные дни: {days}\n🔢 Максимальные активации: {max_act}\n⏰ Действует до: {validity}", + "admin_promo_update_failed": "❌ Ошибка обновления промокода", + "admin_promo_invalid_validity_days": "❌ Срок действия должен быть от 0 до 365 дней (0 = неограниченно)", + "admin_logs_export_csv": "📄 Экспорт в CSV", "admin_logs_csv_export_started": "📄 Начинаю экспорт логов в CSV...", "admin_logs_csv_export_success": "✅ Логи экспортированы! Файл прикреплен выше.",