from aiogram.types import Message, CallbackQuery from aiogram.utils.markdown import html_decoration as hd from aiogram_dialog import Window, DialogManager from aiogram_dialog.widgets.text import Format, Const from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, ScrollingGroup, Select, Group from aiogram_dialog.widgets.input import MessageInput from magic_filter import F from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject from dutylog.application.bot.user_dialogs.states import AdminMenuSG from dutylog.infrastructure.database.repositories.residents_repository import ( ResidentsRepository, ) from dutylog.infrastructure.database.repositories.rooms_repository import ( RoomsRepository, ) from dutylog.infrastructure.database.repositories.floors_repository import ( FloorsRepository, ) from dutylog.infrastructure.database.repositories.users_repository import ( UsersRepository, ) from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( HoursTransactionsRepository, ) from dutylog.infrastructure.utils.datetime import msk_now @inject async def get_residents_list_data( residents_repository: FromDishka[ResidentsRepository], rooms_repository: FromDishka[RoomsRepository], floors_repository: FromDishka[FloorsRepository], users_repository: FromDishka[UsersRepository], **kwargs, ): all_residents = await residents_repository.get_all_residents() residents_with_rooms = [] for resident in all_residents: room = await rooms_repository.get_room_by_id(resident.room) if room: floor = await floors_repository.get_floor_by_id(room.on_floor) floor_number = floor.number if floor else 999999 room_number = room.number else: floor_number = 999999 room_number = 999999 is_admin = False if resident.user_entity: user = await users_repository.get_user_by_id(resident.user_entity) if user and user.is_admin: is_admin = True residents_with_rooms.append((resident, floor_number, room_number, is_admin)) residents_with_rooms.sort(key=lambda x: (x[1], x[2])) residents_data = [] for resident, floor_number, room_number, is_admin in residents_with_rooms: status = "🟢" if resident.is_busy else "⚪️" name = resident.real_name if resident.real_name else "Без имени" admin_badge = " 👑" if is_admin else "" residents_data.append( (f"{name} | Комната {room_number} | {status}{admin_badge}", resident.id) ) content = f"""
🏠 Резиденты
Всего резидентов: {len(all_residents)} Выберите резидента для просмотра информации: """ return { "content": content, "residents": residents_data, } @inject async def get_resident_info_data( dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], rooms_repository: FromDishka[RoomsRepository], users_repository: FromDishka[UsersRepository], **kwargs, ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") if not resident_id: return {"info_content": "Ошибка: резидент не выбран", "is_busy": False, "is_admin": False, "from_search": False, "from_filter": False} resident = await residents_repository.get_resident_by_id(resident_id) if not resident: return {"info_content": "Ошибка: резидент не найден", "is_busy": False, "is_admin": False, "from_search": False, "from_filter": False} room = await rooms_repository.get_room_by_id(resident.room) room_number = room.number if room else "???" name = hd.quote(resident.real_name) if resident.real_name else "Без имени" status = "🟢 Занят" if resident.is_busy else "⚪️ Свободен" is_admin = False user_info = "Не привязан" if resident.user_entity: user = await users_repository.get_user_by_id(resident.user_entity) if user: if user.username: username = f"@{hd.quote(user.username)}" else: username = f"ID: {user.id}" first_name = hd.quote(user.first_name) if user.first_name else "Без имени" user_info = f"{first_name} ({username})" is_admin = user.is_admin admin_badge = " 👑" if is_admin else "" hours_info = "" if not is_admin: hours_info = f""" 🟢 Отработанные часы: {resident.inactive_hours} ч 🔴 Неотработанные часы: {resident.active_hours} ч """ else: hours_info = "\n
👑 Это администратор
" info_content = f"""
👤 Информация о резиденте{admin_badge}
ID: {resident.id} Имя: {name} Комната: {room_number} Статус: {status} Пользователь: {user_info} {hours_info}""" from_search = dialog_manager.dialog_data.get("from_search", False) from_filter = dialog_manager.dialog_data.get("from_filter", False) return { "info_content": info_content, "is_busy": resident.is_busy, "is_admin": is_admin, "from_search": from_search, "from_filter": from_filter, } async def on_resident_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["selected_resident_id"] = int(item_id) dialog_manager.dialog_data["from_search"] = False dialog_manager.dialog_data["from_filter"] = False await dialog_manager.switch_to(AdminMenuSG.resident_info) async def on_add_resident( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.create_resident_name) async def on_filter_residents( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.residents_filter_select) async def on_search_residents( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.residents_search_input) @inject async def on_logout_resident_confirm( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") if resident_id: await residents_repository.unbind_user_from_resident(resident_id) await dialog_manager.switch_to(AdminMenuSG.resident_info) async def on_logout_resident_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.resident_info) @inject async def on_delete_resident_confirm( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") if resident_id: await residents_repository.delete_resident(resident_id) await callback.answer("✅ Резидент удален!") await dialog_manager.switch_to(AdminMenuSG.residents) async def on_delete_resident_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.resident_info) async def on_rebind_resident( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.resident_rebind_floor) @inject async def get_rebind_floors_data( floors_repository: FromDishka[FloorsRepository], **kwargs, ): all_floors = await floors_repository.get_all_floors() all_floors.sort(key=lambda f: f.number) return { "floors": [(f.id, f"Этаж {f.number}") for f in all_floors], } async def on_rebind_floor_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["rebind_floor_id"] = int(item_id) await dialog_manager.switch_to(AdminMenuSG.resident_rebind_room) @inject async def get_rebind_rooms_data( dialog_manager: DialogManager, rooms_repository: FromDishka[RoomsRepository], **kwargs, ): floor_id = dialog_manager.dialog_data.get("rebind_floor_id") if not floor_id: return {"rooms": []} rooms = await rooms_repository.get_rooms_by_floor(floor_id) rooms.sort(key=lambda r: r.number) return { "rooms": [(r.id, str(r.number)) for r in rooms], } async def on_rebind_room_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["rebind_room_id"] = int(item_id) await dialog_manager.switch_to(AdminMenuSG.resident_rebind_confirm) @inject async def get_rebind_confirm_data( dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], rooms_repository: FromDishka[RoomsRepository], **kwargs, ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") new_room_id = dialog_manager.dialog_data.get("rebind_room_id") resident = await residents_repository.get_resident_by_id(resident_id) if resident_id else None new_room = await rooms_repository.get_room_by_id(new_room_id) if new_room_id else None old_room = await rooms_repository.get_room_by_id(resident.room) if resident else None resident_name = resident.real_name if resident and resident.real_name else "???" old_room_number = old_room.number if old_room else "???" new_room_number = new_room.number if new_room else "???" return { "resident_name": resident_name, "old_room_number": old_room_number, "new_room_number": new_room_number, } @inject async def on_rebind_confirm( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") new_room_id = dialog_manager.dialog_data.get("rebind_room_id") if resident_id and new_room_id: resident = await residents_repository.get_resident_by_id(resident_id) if resident: await residents_repository.update_resident_room(resident_id, new_room_id) await callback.answer("✅ Резидент перепривязан к новой комнате!") await dialog_manager.switch_to(AdminMenuSG.resident_info) async def on_rebind_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.resident_info) async def on_resident_name_input( message: Message, widget: MessageInput, dialog_manager: DialogManager, ): if not message.text or len(message.text.strip()) < 2: await message.answer("⚠️ Пожалуйста, введите корректное имя и фамилию") return dialog_manager.dialog_data["new_resident_name"] = message.text.strip() await dialog_manager.switch_to(AdminMenuSG.create_resident_floor) @inject async def get_create_resident_floors_data( floors_repository: FromDishka[FloorsRepository], **kwargs, ): all_floors = await floors_repository.get_all_floors() all_floors.sort(key=lambda f: f.number) return { "floors": [(f.id, f"Этаж {f.number}") for f in all_floors], } async def on_create_resident_floor_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["new_resident_floor_id"] = int(item_id) await dialog_manager.switch_to(AdminMenuSG.create_resident_room) @inject async def get_create_resident_rooms_data( dialog_manager: DialogManager, rooms_repository: FromDishka[RoomsRepository], **kwargs, ): floor_id = dialog_manager.dialog_data.get("new_resident_floor_id") if not floor_id: return {"rooms": []} rooms = await rooms_repository.get_rooms_by_floor(floor_id) rooms.sort(key=lambda r: r.number) return { "rooms": [(r.id, str(r.number)) for r in rooms], } async def on_create_resident_room_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["new_resident_room_id"] = int(item_id) await dialog_manager.switch_to(AdminMenuSG.create_resident_confirm) async def get_create_resident_confirm_data( dialog_manager: DialogManager, **kwargs, ): name = dialog_manager.dialog_data.get("new_resident_name", "???") return {"resident_name": name} @inject async def on_create_resident_confirm( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], ): name = dialog_manager.dialog_data.get("new_resident_name") room_id = dialog_manager.dialog_data.get("new_resident_room_id") if name and room_id: await residents_repository.create_resident( room_id=room_id, real_name=name, ) await callback.answer("✅ Резидент создан!") await dialog_manager.switch_to(AdminMenuSG.residents) async def on_create_resident_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.residents) async def on_search_input( message: Message, widget: MessageInput, dialog_manager: DialogManager, ): if not message.text or len(message.text.strip()) < 1: await message.answer("⚠️ Пожалуйста, введите поисковый запрос") return query = message.text.strip() if query.startswith("@"): query = query[1:] dialog_manager.dialog_data["search_query"] = query dialog_manager.dialog_data["is_search_active"] = True await dialog_manager.switch_to(AdminMenuSG.residents_search_results) @inject async def get_search_results_data( dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], rooms_repository: FromDishka[RoomsRepository], floors_repository: FromDishka[FloorsRepository], users_repository: FromDishka[UsersRepository], **kwargs, ): query = dialog_manager.dialog_data.get("search_query", "") residents, search_type = await residents_repository.search_residents( query, users_repository ) if not residents: return { "content": f"""
🔍 Результаты поиска
Запрос: {query} ❌ Ничего не найдено """, "residents": [], "has_results": False, } residents_with_rooms = [] for resident in residents: room = await rooms_repository.get_room_by_id(resident.room) if room: floor = await floors_repository.get_floor_by_id(room.on_floor) floor_number = floor.number if floor else 999999 room_number = room.number else: floor_number = 999999 room_number = 999999 is_admin = False if resident.user_entity: user = await users_repository.get_user_by_id(resident.user_entity) if user and user.is_admin: is_admin = True residents_with_rooms.append((resident, floor_number, room_number, is_admin)) residents_with_rooms.sort(key=lambda x: (x[1], x[2])) residents_data = [] for resident, floor_number, room_number, is_admin in residents_with_rooms: status = "🟢" if resident.is_busy else "⚪️" name = resident.real_name if resident.real_name else "Без имени" admin_badge = " 👑" if is_admin else "" residents_data.append( (f"{name} | Комната {room_number} | {status}{admin_badge}", resident.id) ) content = f"""
🔍 Результаты поиска по {search_type}
Найдено резидентов: {len(residents)} Выберите резидента для просмотра информации: """ return { "content": content, "residents": residents_data, "has_results": True, } async def on_search_resident_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["selected_resident_id"] = int(item_id) dialog_manager.dialog_data["from_search"] = True dialog_manager.dialog_data["from_filter"] = False await dialog_manager.switch_to(AdminMenuSG.resident_info) @inject async def get_resident_history_data( dialog_manager: DialogManager, residents_repository: FromDishka[ResidentsRepository], transactions_repository: FromDishka[HoursTransactionsRepository], **kwargs, ): resident_id = dialog_manager.dialog_data.get("selected_resident_id") if not resident_id: return {"history_content": "Ошибка: резидент не выбран"} resident = await residents_repository.get_resident_by_id(resident_id) if not resident: return {"history_content": "Ошибка: резидент не найден"} transactions = await transactions_repository.get_resident_history(resident_id) transactions_sorted = sorted(transactions, key=lambda x: x.created_at) last_10 = transactions_sorted[-10:] resident_name = resident.real_name if resident.real_name else "Без имени" if not last_10: history_text = f"""
📜 История операций
Резидент: {resident_name} История операций пуста """ else: history_text = f"""
📜 История операций
Резидент: {resident_name} """ for tx in last_10: operation = "Начислено" if tx.transaction_type == "increase" else "Списано" emoji = "+" if tx.transaction_type == "increase" else "−" msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None) date_str = msk_time.strftime("%d.%m.%Y %H:%M") remark_text = f"\n💬 {tx.remark}" if tx.remark else "" history_text += f"
{operation} {emoji}{tx.amount} ч\n📅 {date_str}{remark_text}
\n" return {"history_content": history_text} residents_list_window = Window( Format("{content}"), Row( Button( Const("🔍 Поиск"), id="search_residents_btn", on_click=on_search_residents, ), Button( Const("🔽 Фильтр"), id="filter_residents_btn", on_click=on_filter_residents, ), ), ScrollingGroup( Select( Format("{item[0]}"), id="residents_select", item_id_getter=lambda x: x[1], items="residents", on_click=on_resident_selected, ), id="residents_scroll", width=1, height=7, ), Button( Const("➕ Добавить резидента"), id="add_resident_btn", on_click=on_add_resident, ), SwitchTo( Const("◀️ Назад"), id="back_to_admin_menu", state=AdminMenuSG.main, ), state=AdminMenuSG.residents, getter=get_residents_list_data, ) resident_info_window = Window( Format("{info_content}"), Row( Button( Const("Добавить часы"), id="add_hours_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.add_hours_select), when=~F["is_admin"], ), Button( Const("Отнять часы"), id="remove_hours_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.remove_hours_select), when=~F["is_admin"], ), ), Button( Const("📜 История"), id="resident_history_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_history), when=~F["is_admin"], ), Button( Const("🔄 Перепривязать к комнате"), id="rebind_resident_btn", on_click=on_rebind_resident, when=~F["is_admin"], ), Button( Const("🚪 Разлогинить"), id="logout_resident_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_logout_confirm), when=F["is_busy"] & ~F["is_admin"], ), Button( Const("🗑 Удалить резидента"), id="delete_resident_btn", on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_delete_confirm), when=~F["is_admin"], ), SwitchTo( Const("◀️ Назад к результатам поиска"), id="back_to_search_results", state=AdminMenuSG.residents_search_results, when="from_search", ), SwitchTo( Const("◀️ Назад к результатам фильтра"), id="back_to_filter_results", state=AdminMenuSG.residents_filtered_results, when="from_filter", ), SwitchTo( Const("◀️ Назад к списку"), id="back_to_residents_list", state=AdminMenuSG.residents, when=~F["from_search"] & ~F["from_filter"], ), state=AdminMenuSG.resident_info, getter=get_resident_info_data, ) resident_logout_confirm_window = Window( Const( "
⚠️ Подтверждение
\n\nВы уверены, что хотите разлогинить этого резидента?" ), Row( Button( Const("✅ Да"), id="confirm_logout", on_click=on_logout_resident_confirm, ), Button( Const("❌ Нет"), id="cancel_logout", on_click=on_logout_resident_cancel, ), ), state=AdminMenuSG.resident_logout_confirm, ) create_resident_name_window = Window( Const("
Создание резидента
\n\nВведите имя и фамилию резидента:"), MessageInput(on_resident_name_input), SwitchTo( Const("◀️ Отмена"), id="cancel_create_resident_name", state=AdminMenuSG.residents, ), state=AdminMenuSG.create_resident_name, ) create_resident_floor_window = Window( Const("
🏢 Выбор этажа
\n\nВыберите этаж для нового резидента:"), Group( Select( Format("{item[1]}"), id="create_resident_floor_select", item_id_getter=lambda x: x[0], items="floors", on_click=on_create_resident_floor_selected, ), width=2, ), SwitchTo( Const("◀️ Назад"), id="back_to_create_name", state=AdminMenuSG.create_resident_name, ), state=AdminMenuSG.create_resident_floor, getter=get_create_resident_floors_data, ) create_resident_room_window = Window( Const("
🚪 Выбор комнаты
\n\nВыберите комнату для нового резидента:"), Group( Select( Format("{item[1]}"), id="create_resident_room_select", item_id_getter=lambda x: x[0], items="rooms", on_click=on_create_resident_room_selected, ), width=3, ), SwitchTo( Const("◀️ Назад"), id="back_to_create_floor", state=AdminMenuSG.create_resident_floor, ), state=AdminMenuSG.create_resident_room, getter=get_create_resident_rooms_data, ) create_resident_confirm_window = Window( Format("
Подтверждение
\n\nСоздать резидента {resident_name}?"), Row( Button( Const("✅ Да"), id="confirm_create_resident", on_click=on_create_resident_confirm, ), Button( Const("❌ Нет"), id="cancel_create_resident", on_click=on_create_resident_cancel, ), ), state=AdminMenuSG.create_resident_confirm, getter=get_create_resident_confirm_data, ) search_input_window = Window( Const("
🔍 Поиск резидентов
\n\n
Введите номер комнаты, имя резидента или username пользователя. Можно указать только часть имени или username для поиска.
"), MessageInput(on_search_input), SwitchTo( Const("◀️ Отмена"), id="cancel_search", state=AdminMenuSG.residents, ), state=AdminMenuSG.residents_search_input, ) search_results_window = Window( Format("{content}"), ScrollingGroup( Select( Format("{item[0]}"), id="search_residents_select", item_id_getter=lambda x: x[1], items="residents", on_click=on_search_resident_selected, ), id="search_residents_scroll", width=1, height=7, when="has_results", ), SwitchTo( Const("🔍 Новый поиск"), id="new_search", state=AdminMenuSG.residents_search_input, ), SwitchTo( Const("◀️ К списку резидентов"), id="back_to_residents_from_search", state=AdminMenuSG.residents, ), state=AdminMenuSG.residents_search_results, getter=get_search_results_data, ) resident_delete_confirm_window = Window( Const( "
⚠️ Подтверждение удаления
\n\nВы уверены, что хотите удалить этого резидента? Это действие необратимо!" ), Row( Button( Const("✅ Да, удалить"), id="confirm_delete", on_click=on_delete_resident_confirm, ), Button( Const("❌ Отмена"), id="cancel_delete", on_click=on_delete_resident_cancel, ), ), state=AdminMenuSG.resident_delete_confirm, ) resident_rebind_floor_window = Window( Const("
🏢 Перепривязка резидента
\n\nВыберите новый этаж:"), Group( Select( Format("{item[1]}"), id="rebind_floor_select", item_id_getter=lambda x: x[0], items="floors", on_click=on_rebind_floor_selected, ), width=2, ), SwitchTo( Const("◀️ Отмена"), id="cancel_rebind_floor", state=AdminMenuSG.resident_info, ), state=AdminMenuSG.resident_rebind_floor, getter=get_rebind_floors_data, ) resident_rebind_room_window = Window( Const("
🚪 Перепривязка резидента
\n\nВыберите новую комнату:"), Group( Select( Format("{item[1]}"), id="rebind_room_select", item_id_getter=lambda x: x[0], items="rooms", on_click=on_rebind_room_selected, ), width=3, ), SwitchTo( Const("◀️ Назад"), id="back_to_rebind_floor", state=AdminMenuSG.resident_rebind_floor, ), state=AdminMenuSG.resident_rebind_room, getter=get_rebind_rooms_data, ) resident_rebind_confirm_window = Window( Format("
Подтверждение перепривязки
\n\nПерепривязать резидента {resident_name} из комнаты {old_room_number} в комнату {new_room_number}?"), Row( Button( Const("✅ Да"), id="confirm_rebind", on_click=on_rebind_confirm, ), Button( Const("❌ Нет"), id="cancel_rebind", on_click=on_rebind_cancel, ), ), state=AdminMenuSG.resident_rebind_confirm, getter=get_rebind_confirm_data, ) resident_history_window = Window( Format("{history_content}"), SwitchTo( Const("◀️ Назад"), id="back_to_resident_info", state=AdminMenuSG.resident_info, ), state=AdminMenuSG.resident_history, getter=get_resident_history_data, )