""" Events — календарь событий дома """ 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 database.db import AsyncSessionLocal from database.models import User from services.events import EventsService from utils.decorators import admin_only from keyboards.inline import get_events_keyboard, get_event_view_keyboard, get_main_menu from swarmlib.notifier import send_telegram from sqlalchemy import select logger = logging.getLogger(__name__) router = Router() # ============================================================================= # FSM для создания события # ============================================================================= class EventState(StatesGroup): waiting_for_title = State() waiting_for_date = State() waiting_for_location = State() waiting_for_description = State() # ============================================================================= # СОЗДАНИЕ СОБЫТИЯ # ============================================================================= @router.message(Command('event_add')) @router.message(Command('добавить_событие')) @admin_only async def cmd_event_add(message: Message, state: FSMContext): """ Добавить событие Использование: /event_add """ await message.answer( '📅 Создание события\n\n' 'Отправьте название события:\n' 'Например: "Общее собрание жильцов"', parse_mode='HTML' ) await state.set_state(EventState.waiting_for_title) @router.message(EventState.waiting_for_title) async def process_event_title(message: Message, state: FSMContext): """Получили название события""" await state.update_data(title=message.text) await message.answer( f'✅ Название: {message.text}\n\n' 'Отправьте дату и время события в формате:\n' 'ДД.ММ.ГГГГ ЧЧ:ММ\n\n' 'Например: 01.03.2026 19:00', parse_mode='HTML' ) await state.set_state(EventState.waiting_for_date) @router.message(EventState.waiting_for_date) async def process_event_date(message: Message, state: FSMContext): """Получили дату события""" text = message.text.strip() try: event_date = datetime.strptime(text, '%d.%m.%Y %H:%M') except ValueError: await message.answer( '❌ Неверный формат даты.\n' 'Используйте: ДД.ММ.ГГГГ ЧЧ:ММ', parse_mode='HTML' ) return if event_date < datetime.utcnow(): await message.answer('❌ Дата должна быть в будущем!') return await state.update_data(event_date=event_date) await message.answer( f'✅ Дата: {event_date.strftime("%d.%m.%Y %H:%M")}\n\n' 'Отправьте место проведения (необязательно) или "пропустить":\n' 'Например: "Холл 1 этажа"', parse_mode='HTML' ) await state.set_state(EventState.waiting_for_location) @router.message(EventState.waiting_for_location) async def process_event_location(message: Message, state: FSMContext): """Получили место проведения""" location = message.text if message.text.lower() != 'пропустить' else None await state.update_data(location=location) await message.answer( f'✅ Место: {location or "Не указано"}\n\n' 'Отправьте описание события (необязательно) или "пропустить":', parse_mode='HTML' ) await state.set_state(EventState.waiting_for_description) @router.message(EventState.waiting_for_description) async def process_event_description(message: Message, state: FSMContext): """Получили описание""" description = message.text if message.text.lower() != 'пропустить' else None await state.update_data(description=description) # Создаём событие data = await state.get_data() async with AsyncSessionLocal() as session: events_service = EventsService(session) event_id = await events_service.create_event( title=data['title'], event_date=data['event_date'], description=description, location=data.get('location'), created_by=message.from_user.id ) # Уведомляем админа через swarmlib send_telegram(f"📅 *Новое событие создано!*\n\n" f"Название: {data['title']}\n" f"Дата: {data['event_date'].strftime('%d.%m.%Y %H:%M')}\n" f"Автор: {message.from_user.full_name} (ID: {message.from_user.id})", parse_mode="markdown") await message.answer( f'✅ Событие создано!\n\n' f'📅 {data["title"]}\n' f'🗓️ {data["event_date"].strftime("%d.%m.%Y %H:%M")}\n' f'📍 {data.get("location") or "Место не указано"}\n' f'🔖 ID: {event_id}\n\n' f'Событие доступно в меню "События"', parse_mode='HTML' ) await state.clear() # ============================================================================= # ПРОСМОТР СОБЫТИЙ (INLINE) # ============================================================================= @router.message(Command('events')) @router.message(Command('события')) @router.callback_query(F.data == 'events') async def cmd_events(event): """Показать предстоящие события (Inline)""" async with AsyncSessionLocal() as session: events_service = EventsService(session) events = await events_service.get_upcoming_events(10) text = '📅 Предстоящие события\n\n' if not events: text += 'Событий пока нет. Следите за обновлениями!' else: for e in events: text += f'• {e["title"]} ({e["event_date"].strftime("%d.%m.%Y %H:%M")})\n' keyboard = get_events_keyboard(events) if isinstance(event, Message): await event.answer(text, reply_markup=keyboard, parse_mode='HTML') else: await event.message.edit_text(text, reply_markup=keyboard, parse_mode='HTML') @router.callback_query(F.data.startswith('event_view_')) async def cb_event_view(callback: CallbackQuery): """Детальный просмотр события""" event_id = int(callback.data.split('_')[2]) user_id = callback.from_user.id async with AsyncSessionLocal() as session: events_service = EventsService(session) event = await events_service.get_event(event_id) if not event: await callback.answer('❌ Событие не найдено', show_alert=True) return participants = await events_service.get_participants(event_id) is_participant = any(p['user_id'] == user_id for p in participants) # Проверяем админа для кнопки удаления stmt = select(User).where(User.user_id == user_id) user = (await session.execute(stmt)).scalar_one_or_none() is_admin = user.is_admin if user else False emoji = events_service.get_event_type_emoji(event['event_type']) text = ( f'{emoji} {event["title"]}\n\n' f'🗓️ Дата: {event["event_date"].strftime("%d.%m.%Y %H:%M")}\n' f'📍 Место: {event["location"] or "Не указано"}\n' f'👥 Участников: {event["participants_count"]}\n\n' f'📝 Описание:\n{event["description"] or "Нет описания"}' ) if participants: text += '\n\nУчастники:\n' for p in participants[:10]: text += f'• {p["name"]}\n' if len(participants) > 10: text += f'...и еще {len(participants)-10}' await callback.message.edit_text( text, reply_markup=get_event_view_keyboard(event_id, is_participant, is_admin), parse_mode='HTML' ) @router.callback_query(F.data.startswith('event_join_')) async def cb_event_join(callback: CallbackQuery): """Записаться на событие через кнопку""" event_id = int(callback.data.split('_')[2]) user_id = callback.from_user.id async with AsyncSessionLocal() as session: events_service = EventsService(session) success = await events_service.join_event(event_id, user_id) if success: await callback.answer('✅ Вы записались!', show_alert=False) # Обновляем сообщение await cb_event_view(callback) else: await callback.answer('ℹ️ Вы уже участвуете', show_alert=True) @router.callback_query(F.data.startswith('event_leave_')) async def cb_event_leave(callback: CallbackQuery): """Отписаться от события через кнопку""" event_id = int(callback.data.split('_')[2]) user_id = callback.from_user.id async with AsyncSessionLocal() as session: events_service = EventsService(session) await events_service.leave_event(event_id, user_id) await callback.answer('✅ Вы отписались', show_alert=False) await cb_event_view(callback) @router.callback_query(F.data.startswith('event_delete_')) @admin_only async def cb_event_delete(callback: CallbackQuery): """Удалить событие (Админ)""" event_id = int(callback.data.split('_')[2]) async with AsyncSessionLocal() as session: events_service = EventsService(session) event = await events_service.get_event(event_id) await events_service.cancel_event(event_id) send_telegram(f"🗑️ *Событие удалено!*\n\nID: {event_id}\nНазвание: {event['title'] if event else '???'}\nАдмин: {callback.from_user.full_name}", parse_mode="markdown") await callback.answer('🗑️ Событие удалено', show_alert=True) await cmd_events(callback) # ============================================================================= # УЧАСТИЕ В СОБЫТИИ (СТАРЫЕ КОМАНДЫ ДЛЯ СОВМЕСТИМОСТИ) # ============================================================================= @router.message(Command('join')) async def cmd_join(message: Message): """ Присоединиться к событию Использование: /join """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/join <event_id>', parse_mode='HTML' ) return try: event_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID события') return async with AsyncSessionLocal() as session: events_service = EventsService(session) event = await events_service.get_event(event_id) if not event: await message.answer('❌ Событие не найдено') return if not event['is_active']: await message.answer('❌ Событие отменено') return success = await events_service.join_event(event_id, message.from_user.id) if success: await message.answer( f'✅ Вы записались на событие!\n\n' f'📅 {event["title"]}\n' f'🗓️ {event["event_date"].strftime("%d.%m.%Y %H:%M")}\n' f'👥 Участников: {event["participants_count"] + 1}', parse_mode='HTML' ) else: await message.answer('ℹ️ Вы уже записаны на это событие') @router.message(Command('leave')) async def cmd_leave(message: Message): """ Отписаться от события Использование: /leave """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/leave <event_id>', parse_mode='HTML' ) return try: event_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID события') return async with AsyncSessionLocal() as session: events_service = EventsService(session) success = await events_service.leave_event(event_id, message.from_user.id) if success: await message.answer('✅ Вы отписались от события') else: await message.answer('ℹ️ Вы не записаны на это событие') # ============================================================================= # МОИ СОБЫТИЯ # ============================================================================= @router.message(Command('my_events')) @router.message(Command('мои_события')) async def cmd_my_events(message: Message): """Показать события пользователя""" async with AsyncSessionLocal() as session: events_service = EventsService(session) events = await events_service.get_user_events(message.from_user.id) if not events: await message.answer('📅 Вы не записаны на события\n\n' 'Посмотреть все: /events', parse_mode='HTML') return text = '📅 Ваши события\n\n' for event in events: date_str = event['event_date'].strftime('%d.%m.%Y %H:%M') text += f'📅 {event["title"]}\n' text += f' └─ {date_str}\n' text += f' └─ 📍 {event["location"] or "Место не указано"}\n\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # УЧАСТНИКИ СОБЫТИЯ # ============================================================================= @router.message(Command('participants')) @router.message(Command('участники')) @admin_only async def cmd_participants(message: Message): """ Показать участников события Использование: /participants """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/participants <event_id>', parse_mode='HTML' ) return try: event_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID события') return async with AsyncSessionLocal() as session: events_service = EventsService(session) participants = await events_service.get_participants(event_id) event = await events_service.get_event(event_id) if not participants: await message.answer('ℹ️ Нет участников') return text = f'👥 Участники: {event["title"]}\n\n' text += f'Всего: {len(participants)}\n\n' for p in participants: apt = f'(кв. {p["apartment"]})' if p['apartment'] else '' text += f'• {p["name"]} {apt}\n' await message.answer(text, parse_mode='HTML') # ============================================================================= # ОТМЕНА СОБЫТИЯ # ============================================================================= @router.message(Command('event_cancel')) @router.message(Command('отменить_событие')) @admin_only async def cmd_event_cancel(message: Message): """ Отменить событие Использование: /event_cancel """ args = message.text.split() if len(args) < 2: await message.answer( '❌ Использование:\n' '/event_cancel <event_id>', parse_mode='HTML' ) return try: event_id = int(args[1]) except ValueError: await message.answer('❌ Неверный ID события') return async with AsyncSessionLocal() as session: events_service = EventsService(session) success = await events_service.cancel_event(event_id) if success: await message.answer('✅ Событие отменено') else: await message.answer('❌ Событие не найдено')