""" Расписание отключений — вода, свет, отопление Только для администраторов """ import logging from datetime import datetime, timedelta from aiogram import Router, F from aiogram.types import Message, CallbackQuery from aiogram.filters import Command, StateFilter from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from database.db import AsyncSessionLocal from database.models import Schedule, User from utils.decorators import admin_only from keyboards.inline import get_schedule_menu, get_schedule_item_menu import config logger = logging.getLogger(__name__) router = Router() # ============================================================================= # FSM для создания расписания # ============================================================================= class ScheduleState(StatesGroup): waiting_for_type = State() waiting_for_title = State() waiting_for_start = State() waiting_for_end = State() waiting_for_description = State() # ============================================================================= # СОЗДАНИЕ РАСПИСАНИЯ # ============================================================================= @router.message(Command('schedule_add')) @admin_only async def cmd_schedule_add(message: Message, state: FSMContext): """ Добавить событие в расписание Использование: /schedule_add """ await message.answer( '📅 Добавление события в расписание\n\n' 'Выберите тип отключения:\n\n' '1️⃣ 💧 Вода\n' '2️⃣ ⚡ Электричество\n' '3️⃣ 🔥 Отопление\n' '4️⃣ 🔶 Газ\n' '5️⃣ 🛗 Лифт\n' '6️⃣ 📋 Общее\n\n' 'Или отправьте номер типа (1-6):', reply_markup=get_schedule_menu(), parse_mode='HTML' ) await state.set_state(ScheduleState.waiting_for_type) @router.callback_query(F.data.startswith('schedule_type_')) async def callback_schedule_type(callback: CallbackQuery, state: FSMContext): """Выбор типа отключения""" type_map = { 'schedule_type_water': ('water', '💧 Вода'), 'schedule_type_electricity': ('electricity', '⚡ Электричество'), 'schedule_type_heating': ('heating', '🔥 Отопление'), 'schedule_type_gas': ('gas', '🔶 Газ'), 'schedule_type_elevator': ('elevator', '🛗 Лифт'), 'schedule_type_general': ('general', '📋 Общее'), } type_key, type_name = type_map.get(callback.data, ('general', '📋 Общее')) await state.update_data(schedule_type=type_key, type_name=type_name) await callback.message.answer( f'✅ Тип: {type_name}\n\n' 'Теперь отправьте название события:\n' 'Например: "Плановое отключение воды"', parse_mode='HTML' ) await state.set_state(ScheduleState.waiting_for_title) await callback.answer() @router.message(ScheduleState.waiting_for_type) async def process_schedule_type(message: Message, state: FSMContext): """Выбор типа отключения текстом""" type_map = { '1': ('water', '💧 Вода'), '2': ('electricity', '⚡ Электричество'), '3': ('heating', '🔥 Отопление'), '4': ('gas', '🔶 Газ'), '5': ('elevator', '🛗 Лифт'), '6': ('general', '📋 Общее'), } text = message.text.strip() if text in type_map: type_key, type_name = type_map[text] await state.update_data(schedule_type=type_key, type_name=type_name) await message.answer( f'✅ Тип: {type_name}\n\n' 'Теперь отправьте название события:\n' 'Например: "Плановое отключение воды"', parse_mode='HTML' ) await state.set_state(ScheduleState.waiting_for_title) else: await message.answer('❌ Неверный тип. Выберите из меню или отправьте номер 1-6:') @router.message(ScheduleState.waiting_for_title) async def process_schedule_title(message: Message, state: FSMContext): """Получили название события""" title = message.text await state.update_data(title=title) await message.answer( f'✅ Название: {title}\n\n' 'Теперь отправьте дату и время НАЧАЛА в формате:\n' 'ДД.ММ.ГГГГ ЧЧ:ММ\n\n' 'Например: 27.02.2026 10:00', parse_mode='HTML' ) @router.message(ScheduleState.waiting_for_start) async def process_schedule_start(message: Message, state: FSMContext): """Получили время начала""" text = message.text.strip() try: start_time = datetime.strptime(text, '%d.%m.%Y %H:%M') except ValueError: await message.answer( '❌ Неверный формат даты.\n' 'Используйте: ДД.ММ.ГГГГ ЧЧ:ММ\n\n' 'Например: 27.02.2026 10:00', parse_mode='HTML' ) return if start_time < datetime.utcnow(): await message.answer('❌ Время начала должно быть в будущем!') return await state.update_data(start_time=start_time) await message.answer( f'✅ Начало: {start_time.strftime("%d.%m.%Y %H:%M")}\n\n' 'Теперь отправьте дату и время ОКОНЧАНИЯ:\n' 'ДД.ММ.ГГГГ ЧЧ:ММ', parse_mode='HTML' ) await state.set_state(ScheduleState.waiting_for_end) @router.message(ScheduleState.waiting_for_end) async def process_schedule_end(message: Message, state: FSMContext): """Получили время окончания""" text = message.text.strip() try: end_time = datetime.strptime(text, '%d.%m.%Y %H:%M') except ValueError: await message.answer( '❌ Неверный формат даты.\n' 'Используйте: ДД.ММ.ГГГГ ЧЧ:ММ', parse_mode='HTML' ) return data = await state.get_data() start_time = data.get('start_time') if end_time <= start_time: await message.answer('❌ Время окончания должно быть позже времени начала!') return await state.update_data(end_time=end_time) await message.answer( f'✅ Окончание: {end_time.strftime("%d.%m.%Y %H:%M")}\n\n' 'Отправьте описание (необязательно) или нажмите "Пропустить":', reply_markup=get_schedule_item_menu(skip=True), parse_mode='HTML' ) await state.set_state(ScheduleState.waiting_for_description) @router.message(ScheduleState.waiting_for_description) async def process_schedule_description(message: Message, state: FSMContext): """Получили описание или пропустили""" if message.text and message.text.lower() != 'пропустить': description = message.text else: description = None await state.update_data(description=description) # Сохраняем событие в БД data = await state.get_data() async with AsyncSessionLocal() as session: schedule = Schedule( schedule_type=data['schedule_type'], title=data['title'], description=description, start_time=data['start_time'], end_time=data['end_time'], created_by=message.from_user.id, ) session.add(schedule) await session.commit() await session.refresh(schedule) # Формируем сообщение о создании type_name = data.get('type_name', '📋 Общее') duration = (data['end_time'] - data['start_time']).total_seconds() / 3600 await message.answer( f'✅ Событие добавлено в расписание!\n\n' f'{type_name} — {data["title"]}\n\n' f'📅 Начало: {data["start_time"].strftime("%d.%m.%Y %H:%M")}\n' f'📅 Окончание: {data["end_time"].strftime("%d.%m.%Y %H:%M")}\n' f'⏱️ Длительность: {duration:.1f} ч.\n' f'🔖 ID: {schedule.schedule_id}\n\n' f'Для просмотра: /график', parse_mode='HTML' ) await state.clear() # ============================================================================= # ПРОСМОТР РАСПИСАНИЯ # ============================================================================= @router.message(Command('график')) @router.message(Command('schedule')) async def cmd_schedule(message: Message): """Показать расписание отключений""" async with AsyncSessionLocal() as session: # Получаем активные будущие события stmt = ( select(Schedule) .where(Schedule.is_active == True) .where(Schedule.end_time > datetime.utcnow()) .order_by(Schedule.start_time) .limit(10) ) result = await session.execute(stmt) schedules = list(result.scalars().all()) if not schedules: await message.answer('✅ Нет запланированных отключений') return text = '📅 Расписание отключений\n\n' for i, s in enumerate(schedules, 1): emoji = s.get_type_emoji() type_name = { 'water': 'Вода', 'electricity': 'Электричество', 'heating': 'Отопление', 'gas': 'Газ', 'elevator': 'Лифт', 'general': 'Общее' }.get(s.schedule_type, 'Общее') duration = (s.end_time - s.start_time).total_seconds() / 3600 text += f'{i}. {emoji} {s.title}\n' text += f' └─ Тип: {type_name}\n' text += f' └─ Начало: {s.start_time.strftime("%d.%m.%Y %H:%M")}\n' text += f' └─ Окончание: {s.end_time.strftime("%d.%m.%Y %H:%M")}\n' text += f' └─ Длительность: {duration:.1f} ч.\n' if s.description: text += f' └─ Описание: {s.description[:100]}\n' text += '\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # УДАЛЕНИЕ СОБЫТИЯ # ============================================================================= @router.message(Command('schedule_del')) @admin_only async def cmd_schedule_del(message: Message): """ Удалить событие из расписания Использование: /schedule_del """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/schedule_del <ID>\n\n' 'ID можно узнать из /график', parse_mode='HTML' ) return try: schedule_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID') return async with AsyncSessionLocal() as session: schedule = await session.get(Schedule, schedule_id) if not schedule: await message.answer(f'❌ Событие {schedule_id} не найдено') return schedule.is_active = False await session.commit() await message.answer(f'✅ Событие "{schedule.title}" удалено из расписания') # ============================================================================= # НАПОМИНАНИЯ # ============================================================================= async def check_and_send_reminders(bot): """Проверить и отправить напоминания о предстоящих отключениях""" async with AsyncSessionLocal() as session: # Находим события которые начнутся через 1 час stmt = ( select(Schedule) .where(Schedule.is_active == True) .where(Schedule.reminder_sent == False) .where(Schedule.start_time > datetime.utcnow()) .where(Schedule.start_time <= datetime.utcnow() + timedelta(hours=1)) ) result = await session.execute(stmt) schedules = list(result.scalars().all()) for schedule in schedules: # Отправляем напоминание в чат в тему "Важные объявления" try: emoji = schedule.get_type_emoji() duration = (schedule.end_time - schedule.start_time).total_seconds() / 3600 await bot.send_message( chat_id=config.ADMIN_CHAT_ID, message_thread_id=config.get_topic_id('general'), # В общую тему text=f'⏰ Напоминание: отключение через 1 час!\n\n' f'{emoji} {schedule.title}\n\n' f'📅 Начало: {schedule.start_time.strftime("%d.%m.%Y %H:%M")}\n' f'📅 Окончание: {schedule.end_time.strftime("%d.%m.%Y %H:%M")}\n' f'⏱️ Длительность: {duration:.1f} ч.\n\n' f'Не забудьте сделать запасы!', parse_mode='HTML' ) # Помечаем что напоминание отправлено schedule.reminder_sent = True await session.commit() logger.info(f'Напоминание об отключении "{schedule.title}" отправлено') except Exception as e: logger.error(f'Не удалось отправить напоминание: {e}')