import re from datetime import datetime from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import ( CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder from sqlalchemy import delete, distinct, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from database import create_tariff from database.models import Gift, Key, Server, Tariff from database.tariffs import create_subgroup_hash, find_subgroup_by_hash from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback from .keyboard import ( AdminTariffCallback, build_cancel_kb, build_edit_tariff_fields_kb, build_single_tariff_kb, build_tariff_groups_kb, build_tariff_list_kb, build_tariff_menu_kb, ) router = Router() class TariffCreateState(StatesGroup): group = State() name = State() duration = State() price = State() traffic = State() confirm_more = State() device_limit = State() class TariffEditState(StatesGroup): choosing_field = State() editing_value = State() class TariffSubgroupState(StatesGroup): selecting_tariffs = State() entering_subgroup_title = State() class SubgroupEditState(StatesGroup): entering_new_title = State() confirming_deletion = State() editing_tariffs = State() MAX_TARIFF_NAME_LENGTH = 40 MAX_SUBGROUP_TITLE_LENGTH = 40 def validate_tariff_name(name: str) -> tuple[bool, str]: if len(name) > MAX_TARIFF_NAME_LENGTH: return False, f"Название тарифа слишком длинное. Максимум {MAX_TARIFF_NAME_LENGTH} символов." return True, "" def validate_subgroup_title(title: str) -> tuple[bool, str]: if len(title) > MAX_SUBGROUP_TITLE_LENGTH: return False, f"Название подгруппы слишком длинное. Максимум {MAX_SUBGROUP_TITLE_LENGTH} символов." return True, "" @router.callback_query(AdminPanelCallback.filter(F.action == "tariffs"), IsAdminFilter()) async def handle_tariff_menu(callback_query: CallbackQuery): text = ( "💸 Управление тарифами\n\n" "Вы можете выполнить следующие действия:\n\n" "🆕 Создать тариф\n" "
• Установите длительность (в днях)\n" "• Задайте цену (в рублях)\n" "• Задайте лимит устройств (hwid/ip_limit)\n" "• Укажите лимит трафика (в ГБ)
\n\n" "📋 Редактировать тарифы\n" "
• Просматривайте список текущих тарифов\n" "• Изменяйте параметры или удаляйте при необходимости
" ) await callback_query.message.edit_text(text=text, reply_markup=build_tariff_menu_kb()) @router.callback_query(AdminTariffCallback.filter(F.action == "create"), IsAdminFilter()) async def start_tariff_creation(callback: CallbackQuery, state: FSMContext): await state.set_state(TariffCreateState.group) await callback.message.edit_text( "📁 Введите код группы, в которую вы хотите добавить тариф.\n\n" "Например: basic, vip, business\n\n" "Специальные группы:\n" "• discounts — тарифы со скидкой\n" "• discounts_max — тарифы с максимальной скидкой\n" "• gifts — тарифы для подарков\n" "• trial — тариф для пробного периода", reply_markup=build_cancel_kb(), ) @router.message(TariffCreateState.group, IsAdminFilter()) async def process_tariff_group(message: Message, state: FSMContext): group_code = message.text.strip().lower() if not re.fullmatch(r"[a-z0-9_-]+", group_code): await message.answer( "❌ Код группы должен содержать только латинские буквы, цифры, дефисы и подчёркивания.\n\nПовторите ввод:", reply_markup=build_cancel_kb(), ) return await state.update_data(group_code=group_code) await state.set_state(TariffCreateState.name) await message.answer( "📝 Введите название тарифа\n\n" "Например: 30 дней или 1 месяц\n\n" "Это название будет отображаться пользователю при выборе тарифа", reply_markup=build_cancel_kb(), ) @router.message(TariffCreateState.name, IsAdminFilter()) async def process_tariff_name(message: Message, state: FSMContext): name = message.text.strip() is_valid, error_msg = validate_tariff_name(name) if not is_valid: await message.answer( f"❌ {error_msg}\n\nПовторите ввод:", reply_markup=build_cancel_kb(), ) return await state.update_data(name=name) await state.set_state(TariffCreateState.duration) await message.answer( "📅 Введите длительность тарифа в днях (например: 30):", reply_markup=build_cancel_kb(), ) @router.message(TariffCreateState.duration, IsAdminFilter()) async def process_tariff_duration(message: Message, state: FSMContext): try: days = int(message.text.strip()) if days <= 0: raise ValueError except ValueError: await message.answer("❌ Введите корректное количество дней (целое число больше 0):") return await state.update_data(duration_days=days) await state.set_state(TariffCreateState.price) await message.answer( "💰 Введите цену тарифа в рублях (например: 150)\n\n" "Будет показано клиенту при выборе тарифа", reply_markup=build_cancel_kb(), ) @router.message(TariffCreateState.price, IsAdminFilter()) async def process_tariff_price(message: Message, state: FSMContext): try: price = int(message.text.strip()) if price < 0: raise ValueError except ValueError: await message.answer("❌ Введите корректную цену (целое число 0 или больше):") return await state.update_data(price_rub=price) await state.set_state(TariffCreateState.traffic) await message.answer( "📦 Введите лимит трафика в ГБ (например: 100, 0 — безлимит):", reply_markup=build_cancel_kb(), ) @router.message(TariffCreateState.traffic, IsAdminFilter()) async def process_tariff_traffic(message: Message, state: FSMContext): try: traffic = int(message.text.strip()) if traffic < 0: raise ValueError except ValueError: await message.answer("❌ Введите корректный лимит трафика (целое число 0 или больше):") return await state.update_data(traffic_limit=traffic if traffic > 0 else None) await state.set_state(TariffCreateState.device_limit) await message.answer( "📱 Введите лимит устройств (HWID) для тарифа (например: 3, 0 — безлимит):", reply_markup=build_cancel_kb(), ) @router.message(TariffCreateState.device_limit, IsAdminFilter()) async def process_tariff_device_limit(message: Message, state: FSMContext, session: AsyncSession): try: device_limit = int(message.text.strip()) if device_limit < 0: raise ValueError except ValueError: await message.answer("❌ Введите корректный лимит устройств (целое число 0 или больше):") return data = await state.get_data() new_tariff = await create_tariff( session, { "name": data["name"], "group_code": data["group_code"], "duration_days": data["duration_days"], "price_rub": data["price_rub"], "traffic_limit": data["traffic_limit"], "device_limit": device_limit if device_limit > 0 else None, }, ) await state.set_state(TariffCreateState.confirm_more) await message.answer( f"✅ Тариф {new_tariff.name} добавлен в группу {data['group_code']}.\n\n" "➕ Хотите добавить ещё один тариф в эту группу?", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton(text="✅ Да", callback_data="add_more_tariff"), InlineKeyboardButton(text="❌ Нет", callback_data="done_tariff_group"), ] ] ), ) @router.callback_query(F.data == "add_more_tariff", IsAdminFilter()) async def handle_add_more_tariff(callback: CallbackQuery, state: FSMContext): await state.set_state(TariffCreateState.name) await callback.message.edit_text("📝 Введите название следующего тарифа:", reply_markup=build_cancel_kb()) @router.callback_query(F.data == "done_tariff_group", IsAdminFilter()) async def handle_done_tariff_group(callback: CallbackQuery, state: FSMContext): await state.clear() await callback.message.edit_text("✅ Группа тарифов успешно завершена.", reply_markup=build_tariff_menu_kb()) @router.callback_query(F.data == "cancel_tariff_creation", IsAdminFilter()) async def cancel_tariff_creation(callback: CallbackQuery, state: FSMContext): await state.clear() await callback.message.edit_text("❌ Создание тарифа отменено.", reply_markup=build_tariff_menu_kb()) @router.callback_query(AdminTariffCallback.filter(F.action == "list"), IsAdminFilter()) async def show_tariff_groups(callback: CallbackQuery, session: AsyncSession): result = await session.execute( select(distinct(Tariff.group_code)).where(Tariff.group_code.isnot(None)).order_by(Tariff.group_code) ) groups = [row[0] for row in result.fetchall()] if not groups: await callback.message.edit_text("❌ Нет сохранённых тарифов.", reply_markup=build_tariff_menu_kb()) return special_groups = { "discounts": "🔻 Скидки", "discounts_max": "🔻 Макс. скидки", "gifts": "🎁 Подарки", "trial": "🚀 Пробный период", } text = "📋 Выберите тарифную группу:\n\n" text += "Специальные группы:\n" for code, label in special_groups.items(): status = "✅ создана" if code in groups else "❌ не создана" text += f"{label} — {code}{status}\n" text += "\n" await callback.message.edit_text(text, reply_markup=build_tariff_groups_kb(groups)) def tariff_to_dict(tariff: Tariff) -> dict: return { "id": tariff.id, "name": tariff.name, "price_rub": tariff.price_rub, "group_code": tariff.group_code, "subgroup_title": tariff.subgroup_title, } @router.callback_query(AdminTariffCallback.filter(F.action.startswith("group|")), IsAdminFilter()) async def show_tariffs_in_group(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): group_code = callback_data.action.split("|", 1)[1] result = await session.execute(select(Tariff).where(Tariff.group_code == group_code).order_by(Tariff.id)) tariffs = result.scalars().all() if not tariffs: await callback.message.edit_text("❌ В этой группе пока нет тарифов.") return tariff_dicts = [tariff_to_dict(t) for t in tariffs] await callback.message.edit_text( f"📦 Тарифы группы: {group_code}", reply_markup=build_tariff_list_kb(tariff_dicts), ) @router.callback_query(AdminTariffCallback.filter(F.action.startswith("view|")), IsAdminFilter()) async def view_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): tariff_id = int(callback_data.action.split("|", 1)[1]) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) tariff = result.scalar_one_or_none() if not tariff: await callback.message.edit_text("❌ Тариф не найден.") return text, markup = render_tariff_card(tariff) await callback.message.edit_text(text=text, reply_markup=markup) @router.callback_query(AdminTariffCallback.filter(F.action.startswith("delete|")), IsAdminFilter()) async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminTariffCallback, session: AsyncSession): tariff_id = int(callback_data.action.split("|", 1)[1]) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) tariff = result.scalar_one_or_none() if not tariff: await callback.message.edit_text("❌ Тариф не найден.") return group_code = tariff.group_code if group_code == "gifts": gift_check = await session.execute(select(Gift).where(Gift.tariff_id == tariff_id).limit(1)) if gift_check.scalar_one_or_none(): result = await session.execute(select(Tariff).where(Tariff.group_code == "gifts", Tariff.id != tariff_id)) other_tariffs = result.scalars().all() if not other_tariffs: await callback.message.edit_text( "❌ Нельзя удалить тариф — он используется в подарках, а других тарифов в группе 'gifts' нет.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack() ) ] ] ), ) return builder = InlineKeyboardBuilder() for t in other_tariffs: builder.button( text=f"{t.name} — {t.price_rub}₽", callback_data=f"confirm_delete_tariff_with_replace|{tariff_id}|{t.id}", ) builder.button(text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()) await callback.message.edit_text( "Этот тариф используется в подарках.\n\n" "Выберите тариф, на который заменить его во всех подарках перед удалением:", reply_markup=builder.as_markup(), ) return await callback.message.edit_text( "⚠️ Вы уверены, что хотите удалить этот тариф?", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"), InlineKeyboardButton( text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack() ), ] ] ), ) @router.callback_query(F.data.startswith("confirm_delete_tariff_with_replace|"), IsAdminFilter()) async def delete_tariff_with_gift_replacement(callback: CallbackQuery, session: AsyncSession): _, tariff_id_str, replacement_id_str = callback.data.split("|") tariff_id = int(tariff_id_str) replacement_id = int(replacement_id_str) await session.execute(update(Gift).where(Gift.tariff_id == tariff_id).values(tariff_id=replacement_id)) await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None)) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) tariff = result.scalar_one_or_none() if not tariff: await callback.message.edit_text("❌ Тариф не найден.") return group_code = tariff.group_code await session.execute(delete(Tariff).where(Tariff.id == tariff_id)) result = await session.execute(select(Tariff).where(Tariff.group_code == group_code)) remaining_tariffs = result.scalars().all() if not remaining_tariffs: await session.execute(update(Server).where(Server.tariff_group == group_code).values(tariff_group=None)) await session.commit() await callback.message.edit_text("🗑 Тариф удалён. Все подарки обновлены.", reply_markup=build_tariff_menu_kb()) @router.callback_query(AdminTariffCallback.filter(F.action.startswith("edit|")), IsAdminFilter()) async def start_edit_tariff(callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext): tariff_id = int(callback_data.action.split("|")[1]) await state.update_data(tariff_id=tariff_id) await state.set_state(TariffEditState.choosing_field) await callback.message.edit_text( "✏️ Что вы хотите изменить?", reply_markup=build_edit_tariff_fields_kb(tariff_id), ) @router.callback_query(F.data.startswith("confirm_delete_tariff|"), IsAdminFilter()) async def delete_tariff(callback: CallbackQuery, session: AsyncSession): tariff_id = int(callback.data.split("|", 1)[1]) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) tariff = result.scalar_one_or_none() if not tariff: await callback.message.edit_text("❌ Тариф не найден.") return group_code = tariff.group_code await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None)) await session.execute(delete(Tariff).where(Tariff.id == tariff_id)) result = await session.execute(select(Tariff).where(Tariff.group_code == group_code)) remaining_tariffs = result.scalars().all() if not remaining_tariffs: await session.execute(update(Server).where(Server.tariff_group == group_code).values(tariff_group=None)) await session.commit() await callback.message.edit_text("🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb()) @router.callback_query(F.data.startswith("edit_field|"), IsAdminFilter()) async def ask_new_value(callback: CallbackQuery, state: FSMContext): _, _tariff_id, field = callback.data.split("|") await state.update_data(field=field) await state.set_state(TariffEditState.editing_value) field_names = { "name": "название тарифа", "duration_days": "длительность в днях", "price_rub": "цену в рублях", "traffic_limit": "лимит трафика в ГБ (0 — безлимит)", "device_limit": "лимит устройств (0 — безлимит)", } await callback.message.edit_text( f"✏️ Введите новое значение для {field_names.get(field, field)}:", reply_markup=build_cancel_kb(), ) @router.message(TariffEditState.editing_value, IsAdminFilter()) async def apply_edit(message: Message, state: FSMContext, session: AsyncSession): data = await state.get_data() tariff_id = data["tariff_id"] field = data["field"] value = message.text.strip() result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) tariff = result.scalar_one_or_none() if not tariff: await message.answer("❌ Тариф не найден.") await state.clear() return if field == "name": is_valid, error_msg = validate_tariff_name(value) if not is_valid: await message.answer( f"❌ {error_msg}\n\nПовторите ввод:", reply_markup=build_cancel_kb(), ) return if field in ["duration_days", "price_rub", "traffic_limit", "device_limit"]: try: num = int(value) if num < 0: raise ValueError if field in ["traffic_limit", "device_limit"]: value = num if num > 0 else None else: value = num except ValueError: await message.answer("❌ Введите корректное число.") return setattr(tariff, field, value) tariff.updated_at = datetime.utcnow() await session.commit() await state.clear() text, markup = render_tariff_card(tariff) await message.answer(text=text, reply_markup=markup) @router.callback_query(F.data.startswith("toggle_active|"), IsAdminFilter()) async def toggle_tariff_status(callback: CallbackQuery, session: AsyncSession): tariff_id = int(callback.data.split("|")[1]) result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) tariff = result.scalar_one_or_none() if not tariff: await callback.message.edit_text("❌ Тариф не найден.") return tariff.is_active = not tariff.is_active await session.commit() text, markup = render_tariff_card(tariff) await callback.message.edit_text(text=text, reply_markup=markup) @router.callback_query(AdminTariffCallback.filter(F.action.startswith("create|")), IsAdminFilter()) async def start_tariff_creation_existing_group( callback: CallbackQuery, callback_data: AdminTariffCallback, state: FSMContext ): group_code = callback_data.action.split("|", 1)[1] await state.update_data(group_code=group_code) await state.set_state(TariffCreateState.name) await callback.message.edit_text( f"📦 Добавление нового тарифа в группу {group_code}\n\n📝 Введите название тарифа:", reply_markup=build_cancel_kb(), ) def render_tariff_card(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]: traffic_text = f"{tariff.traffic_limit} ГБ" if tariff.traffic_limit else "Безлимит" device_text = f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит" text = ( f"📄 Тариф: {tariff.name}\n\n" f"📁 Группа: {tariff.group_code}\n" f"📅 Длительность: {tariff.duration_days} дней\n" f"💰 Стоимость: {tariff.price_rub}₽\n" f"📦 Трафик: {traffic_text}\n" f"📱 Устройств: {device_text}\n" f"{'✅ Активен' if tariff.is_active else '⛔ Отключен'}" ) return text, build_single_tariff_kb(tariff.id) @router.callback_query(F.data.startswith("start_subgrouping|"), IsAdminFilter()) async def start_subgrouping(callback: CallbackQuery, state: FSMContext, session: AsyncSession): group_code = callback.data.split("|", 1)[1] result = await session.execute( select(Tariff) .where(Tariff.group_code == group_code, (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == ""))) .order_by(Tariff.id) ) tariffs = result.scalars().all() if not tariffs: await callback.message.edit_text( "❌ Нет доступных тарифов для группировки.\n\nВсе тарифы уже находятся в подгруппах.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() ) ] ] ), ) return await state.set_state(TariffSubgroupState.selecting_tariffs) await state.update_data(group_code=group_code, selected_tariff_ids=[]) builder = InlineKeyboardBuilder() for tariff in tariffs: builder.row(InlineKeyboardButton(text=f"{tariff.name}", callback_data=f"sub_select|{tariff.id}")) builder.row( InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"), InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping"), ) await callback.message.edit_text( "Выберите тарифы, которые нужно объединить в подгруппу:", reply_markup=builder.as_markup() ) @router.callback_query(F.data.startswith("sub_select|"), TariffSubgroupState.selecting_tariffs, IsAdminFilter()) async def toggle_tariff_subgroup_selection(callback: CallbackQuery, state: FSMContext, session: AsyncSession): tariff_id = int(callback.data.split("|")[1]) data = await state.get_data() selected = set(data.get("selected_tariff_ids", [])) if tariff_id in selected: selected.remove(tariff_id) else: selected.add(tariff_id) await state.update_data(selected_tariff_ids=list(selected)) group_code = data["group_code"] result = await session.execute( select(Tariff) .where(Tariff.group_code == group_code, (Tariff.subgroup_title.is_(None) | (Tariff.subgroup_title == ""))) .order_by(Tariff.id) ) tariffs = result.scalars().all() builder = InlineKeyboardBuilder() for tariff in tariffs: is_selected = tariff.id in selected prefix = "✅ " if is_selected else "" builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.name}", callback_data=f"sub_select|{tariff.id}")) builder.row( InlineKeyboardButton(text="➡️ Продолжить", callback_data="subgroup_continue"), InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping"), ) await callback.message.edit_reply_markup(reply_markup=builder.as_markup()) @router.callback_query( F.data == "subgroup_continue", TariffSubgroupState.selecting_tariffs, IsAdminFilter(), ) async def ask_subgroup_title(callback: CallbackQuery, state: FSMContext): data = await state.get_data() if not data.get("selected_tariff_ids"): await callback.answer("Выберите хотя бы один тариф", show_alert=True) return await state.set_state(TariffSubgroupState.entering_subgroup_title) keyboard = InlineKeyboardMarkup( inline_keyboard=[[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping")]] ) await callback.message.edit_text( "📁 Введите название новой подгруппы:", reply_markup=keyboard, ) @router.message(TariffSubgroupState.entering_subgroup_title, IsAdminFilter()) async def apply_subgroup_title(message: Message, state: FSMContext, session: AsyncSession): title = message.text.strip() is_valid, error_msg = validate_subgroup_title(title) if not is_valid: await message.answer( f"❌ {error_msg}\n\nПовторите ввод:", reply_markup=InlineKeyboardMarkup( inline_keyboard=[[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_subgrouping")]] ), ) return data = await state.get_data() selected_ids = data.get("selected_tariff_ids", []) if not selected_ids: await message.answer("❌ Нет выбранных тарифов.") await state.clear() return await session.execute( update(Tariff).where(Tariff.id.in_(selected_ids)).values(subgroup_title=title, updated_at=datetime.utcnow()) ) await session.commit() await state.clear() await message.answer( f"✅ {len(selected_ids)} тарифов сгруппированы в подгруппу: {title}.", reply_markup=build_tariff_menu_kb(), ) @router.callback_query(F.data == "cancel_subgrouping", IsAdminFilter()) async def cancel_subgrouping(callback: CallbackQuery, state: FSMContext): await state.clear() await callback.message.edit_text("❌ Группировка в подгруппу отменена.", reply_markup=build_tariff_menu_kb()) @router.callback_query(F.data.startswith("view_subgroup|"), IsAdminFilter()) async def view_subgroup_tariffs(callback: CallbackQuery, session: AsyncSession): _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return result = await session.execute( select(Tariff) .where(Tariff.group_code == group_code, Tariff.subgroup_title == subgroup_title) .order_by(Tariff.id) ) tariffs = result.scalars().all() if not tariffs: await callback.message.edit_text("❌ В этой подгруппе пока нет тарифов.") return tariffs_dicts = [tariff_to_dict(t) for t in tariffs] builder = InlineKeyboardBuilder() for t in tariffs_dicts: title = f"{t['name']} — {t['price_rub']}₽" builder.row( InlineKeyboardButton( text=title, callback_data=AdminTariffCallback(action=f"view|{t['id']}").pack(), ) ) builder.row( InlineKeyboardButton( text="📝 Переименовать подгруппу", callback_data=f"rename_subgroup|{subgroup_hash}|{group_code}", ) ) builder.row( InlineKeyboardButton( text="✏️ Редактировать подгруппу", callback_data=f"edit_subgroup_tariffs|{subgroup_hash}|{group_code}", ) ) builder.row( InlineKeyboardButton( text="🗑 Удалить подгруппу", callback_data=f"delete_subgroup|{subgroup_hash}|{group_code}", ) ) builder.row( InlineKeyboardButton( text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack(), ) ) await callback.message.edit_text( f"📂 Подгруппа: {subgroup_title}", reply_markup=builder.as_markup(), ) @router.callback_query(F.data.startswith("rename_subgroup|"), IsAdminFilter()) async def start_rename_subgroup(callback: CallbackQuery, state: FSMContext, session: AsyncSession): _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return await state.update_data( subgroup_title=subgroup_title, group_code=group_code, subgroup_hash=subgroup_hash, ) await state.set_state(SubgroupEditState.entering_new_title) await callback.message.edit_text( f"📝 Введите новое название подгруппы:\n{subgroup_title}\n\nИли нажмите Отмена.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] ] ), ) @router.message(SubgroupEditState.entering_new_title, IsAdminFilter()) async def save_new_subgroup_title(message: Message, state: FSMContext, session: AsyncSession): new_title = message.text.strip() is_valid, error_msg = validate_subgroup_title(new_title) if not is_valid: data = await state.get_data() subgroup_hash = data.get("subgroup_hash") group_code = data.get("group_code") await message.answer( f"❌ {error_msg}\n\nПовторите ввод:", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" ) ] ] ), ) return data = await state.get_data() old_title = data["subgroup_title"] group_code = data["group_code"] await session.execute( update(Tariff) .where( Tariff.group_code == group_code, Tariff.subgroup_title == old_title, ) .values(subgroup_title=new_title) ) await session.commit() await state.clear() create_subgroup_hash(new_title, group_code) await message.answer( f"✅ Подгруппа {old_title} переименована в {new_title}.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() ) ] ] ), ) @router.callback_query(F.data.startswith("delete_subgroup|"), IsAdminFilter()) async def confirm_delete_subgroup(callback: CallbackQuery, state: FSMContext, session: AsyncSession): _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return await state.update_data( subgroup_title=subgroup_title, group_code=group_code, subgroup_hash=subgroup_hash, ) await state.set_state(SubgroupEditState.confirming_deletion) await callback.message.edit_text( f"❗ Вы уверены, что хотите удалить подгруппу {subgroup_title}?\n" "Это удалит поле `subgroup_title` у всех связанных тарифов.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton(text="✅ Удалить", callback_data="confirm_subgroup_deletion"), InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"), ] ] ), ) @router.callback_query(F.data == "confirm_subgroup_deletion", SubgroupEditState.confirming_deletion, IsAdminFilter()) async def perform_subgroup_deletion(callback: CallbackQuery, state: FSMContext, session: AsyncSession): data = await state.get_data() subgroup_title = data["subgroup_title"] group_code = data["group_code"] await session.execute( update(Tariff) .where(Tariff.group_code == group_code, Tariff.subgroup_title == subgroup_title) .values(subgroup_title=None) ) await session.commit() await state.clear() await callback.message.edit_text( f"✅ Подгруппа {subgroup_title} удалена.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="⬅️ Назад", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack() ) ] ] ), ) @router.callback_query(F.data.startswith("edit_subgroup_tariffs|"), IsAdminFilter()) async def start_edit_subgroup_tariffs(callback: CallbackQuery, state: FSMContext, session: AsyncSession): _, subgroup_hash, group_code = callback.data.split("|", 2) subgroup_title = await find_subgroup_by_hash(session, subgroup_hash, group_code) if not subgroup_title: await callback.message.edit_text("❌ Подгруппа не найдена.") return result = await session.execute( select(Tariff) .where( Tariff.group_code == group_code, or_(Tariff.subgroup_title == subgroup_title, Tariff.subgroup_title.is_(None), Tariff.subgroup_title == ""), ) .order_by(Tariff.id) ) all_tariffs_to_show = result.scalars().all() subgroup_tariff_ids = {t.id for t in all_tariffs_to_show if t.subgroup_title == subgroup_title} if not all_tariffs_to_show: await callback.message.edit_text( "❌ Нет доступных тарифов для редактирования.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}")] ] ), ) return await state.set_state(SubgroupEditState.editing_tariffs) await state.update_data( subgroup_title=subgroup_title, group_code=group_code, subgroup_hash=subgroup_hash, selected_tariff_ids=list(subgroup_tariff_ids), ) builder = InlineKeyboardBuilder() for tariff in all_tariffs_to_show: is_in_subgroup = tariff.id in subgroup_tariff_ids prefix = "✅ " if is_in_subgroup else "" builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.name}", callback_data=f"edit_sub_toggle|{tariff.id}")) builder.row( InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"), ) await callback.message.edit_text( f"✏️ Редактирование подгруппы: {subgroup_title}\n\n" "✅ - тарифы в подгруппе\n\n" "Нажмите на тариф, чтобы добавить/убрать его:", reply_markup=builder.as_markup(), ) @router.callback_query(F.data.startswith("edit_sub_toggle|"), SubgroupEditState.editing_tariffs, IsAdminFilter()) async def toggle_tariff_in_subgroup_edit(callback: CallbackQuery, state: FSMContext, session: AsyncSession): tariff_id = int(callback.data.split("|")[1]) data = await state.get_data() selected_ids = set(data.get("selected_tariff_ids", [])) if tariff_id in selected_ids: selected_ids.remove(tariff_id) else: selected_ids.add(tariff_id) await state.update_data(selected_tariff_ids=list(selected_ids)) subgroup_title = data["subgroup_title"] group_code = data["group_code"] subgroup_hash = data["subgroup_hash"] result = await session.execute( select(Tariff) .where( Tariff.group_code == group_code, or_(Tariff.subgroup_title == subgroup_title, Tariff.subgroup_title.is_(None), Tariff.subgroup_title == ""), ) .order_by(Tariff.id) ) all_tariffs_to_show = result.scalars().all() builder = InlineKeyboardBuilder() for tariff in all_tariffs_to_show: is_selected = tariff.id in selected_ids prefix = "✅ " if is_selected else "" builder.row(InlineKeyboardButton(text=f"{prefix}{tariff.name}", callback_data=f"edit_sub_toggle|{tariff.id}")) builder.row( InlineKeyboardButton(text="💾 Сохранить", callback_data="edit_sub_save"), InlineKeyboardButton(text="❌ Отмена", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}"), ) await callback.message.edit_reply_markup(reply_markup=builder.as_markup()) @router.callback_query(F.data == "edit_sub_save", SubgroupEditState.editing_tariffs, IsAdminFilter()) async def save_subgroup_tariffs_changes(callback: CallbackQuery, state: FSMContext, session: AsyncSession): data = await state.get_data() subgroup_title = data["subgroup_title"] group_code = data["group_code"] subgroup_hash = data["subgroup_hash"] selected_tariff_ids = set(data.get("selected_tariff_ids", [])) result = await session.execute( select(Tariff).where(Tariff.group_code == group_code, Tariff.subgroup_title == subgroup_title) ) current_subgroup_tariffs = result.scalars().all() current_tariff_ids = {t.id for t in current_subgroup_tariffs} to_add = selected_tariff_ids - current_tariff_ids to_remove = current_tariff_ids - selected_tariff_ids if to_remove: await session.execute( update(Tariff).where(Tariff.id.in_(to_remove)).values(subgroup_title=None, updated_at=datetime.utcnow()) ) if to_add: await session.execute( update(Tariff) .where(Tariff.id.in_(to_add)) .values(subgroup_title=subgroup_title, updated_at=datetime.utcnow()) ) await session.commit() await state.clear() if not selected_tariff_ids: await callback.message.edit_text( f"✅ Подгруппа {subgroup_title} была расформирована.", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="⬅️ Назад к группе тарифов", callback_data=AdminTariffCallback(action=f"group|{group_code}").pack(), ) ] ] ), ) return changes_text = [] if to_add: added_names = [] for tariff_id in to_add: result = await session.execute(select(Tariff.name).where(Tariff.id == tariff_id)) name = result.scalar_one() if name: added_names.append(name) changes_text.append(f"➕ Добавлено: {', '.join(added_names)}") if to_remove: removed_names = [] for tariff_id in to_remove: result = await session.execute(select(Tariff.name).where(Tariff.id == tariff_id)) name = result.scalar_one() if name: removed_names.append(name) changes_text.append(f"➖ Удалено: {', '.join(removed_names)}") if not changes_text: changes_text.append("Изменений не было") await callback.message.edit_text( f"✅ Подгруппа обновлена: {subgroup_title}\n\n{chr(10).join(changes_text)}", reply_markup=InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text="⬅️ Назад к подгруппе", callback_data=f"view_subgroup|{subgroup_hash}|{group_code}" ) ] ] ), )