From f899eb9199392edd585812d50bf9db399072b4ef Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 1 Mar 2026 01:14:32 +0300 Subject: [PATCH] update --- .../admin_dialogs/admin_menu_dialog.py | 1072 +---------------- .../user_dialogs/admin_dialogs/broadcast.py | 109 ++ .../admin_dialogs/hours_management.py | 271 +++++ .../user_dialogs/admin_dialogs/main_menu.py | 132 ++ .../admin_dialogs/residents_management.py | 573 +++++++++ .../bot/user_dialogs/main_menu_dialog.py | 198 +-- .../bot/user_dialogs/user_menu/__init__.py | 0 .../bot/user_dialogs/user_menu/faq.py | 28 + .../bot/user_dialogs/user_menu/history.py | 68 ++ .../bot/user_dialogs/user_menu/main_menu.py | 119 ++ 10 files changed, 1350 insertions(+), 1220 deletions(-) create mode 100644 src/dutylog/application/bot/user_dialogs/admin_dialogs/broadcast.py create mode 100644 src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py create mode 100644 src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py create mode 100644 src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py create mode 100644 src/dutylog/application/bot/user_dialogs/user_menu/__init__.py create mode 100644 src/dutylog/application/bot/user_dialogs/user_menu/faq.py create mode 100644 src/dutylog/application/bot/user_dialogs/user_menu/history.py create mode 100644 src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py index 6e01344..449352f 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py @@ -1,1038 +1,52 @@ -from aiogram.types import User, Message, CallbackQuery -from aiogram import Bot -from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest -from aiogram_dialog import Dialog, 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 aiogram_dialog import Dialog -from dutylog.application.bot.user_dialogs.states import AdminMenuSG -from dutylog.infrastructure.database.repositories.users_repository import ( - UsersRepository, +from dutylog.application.bot.user_dialogs.admin_dialogs.main_menu import ( + main_menu_window, + statistics_window, ) -from dutylog.infrastructure.database.repositories.residents_repository import ( - ResidentsRepository, +from dutylog.application.bot.user_dialogs.admin_dialogs.residents_management import ( + residents_list_window, + resident_info_window, + resident_logout_confirm_window, + create_resident_name_window, + create_resident_floor_window, + create_resident_room_window, + create_resident_confirm_window, + search_input_window, + search_results_window, ) -from dutylog.infrastructure.database.repositories.rooms_repository import ( - RoomsRepository, +from dutylog.application.bot.user_dialogs.admin_dialogs.hours_management import ( + add_hours_select_window, + remove_hours_select_window, + add_hours_custom_window, + remove_hours_custom_window, + add_hours_confirm_window, + remove_hours_confirm_window, ) -from dutylog.infrastructure.database.repositories.floors_repository import ( - FloorsRepository, +from dutylog.application.bot.user_dialogs.admin_dialogs.broadcast import ( + broadcast_window, + broadcast_confirm_window, ) -from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( - HoursTransactionsRepository, -) -from dutylog.infrastructure.utils.config import Config - - -@inject -async def get_admin_menu_data( - event_from_user: User, - users_repository: FromDishka[UsersRepository], - config: FromDishka[Config], - **kwargs, -): - user = await users_repository.get_or_create_user( - user_id=event_from_user.id, - username=event_from_user.username, - first_name=event_from_user.first_name, - last_name=event_from_user.last_name, - ) - - is_creator = event_from_user.id == config.bot.creator_id - - if is_creator: - greeting = "👑 Создатель" - else: - greeting = "👨‍💼 Администратор" - - content = f""" -{greeting} - -
📋 Панель управления
- -Выберите действие: -""" - - return {"content": content} - - -@inject -async def get_statistics_data( - users_repository: FromDishka[UsersRepository], - residents_repository: FromDishka[ResidentsRepository], - **kwargs, -): - all_users = await users_repository.get_all_users() - all_residents = await residents_repository.get_all_residents() - - total_users = len(all_users) - total_residents = len(all_residents) - busy_residents = len([r for r in all_residents if r.is_busy]) - total_active_hours = sum(r.active_hours for r in all_residents) - total_inactive_hours = sum(r.inactive_hours for r in all_residents) - admins_count = len([u for u in all_users if u.is_admin]) - - stats_text = f""" -
📊 Статистика системы
- -👥 Всего пользователей: {total_users} -👨‍💼 Администраторов: {admins_count} - -🏠 Всего резидентов: {total_residents} -✅ Привязано к пользователям: {busy_residents} -❌ Свободных: {total_residents - busy_residents} - -━━━━━━━━━━━━━━━━━━━━ - -🟢 Всего отработанных часов: {total_inactive_hours} ч -🔴 Всего неотработанных часов: {total_active_hours} ч -📊 Общий итог: {total_active_hours + total_inactive_hours} ч -""" - - return {"stats_content": stats_text} - - -async def on_broadcast_message( - message: Message, - widget: MessageInput, - dialog_manager: DialogManager, -): - dialog_manager.dialog_data["broadcast_message_id"] = message.message_id - dialog_manager.dialog_data["broadcast_chat_id"] = message.chat.id - - await message.copy_to(message.chat.id) - await dialog_manager.switch_to(AdminMenuSG.broadcast_confirm) - - -@inject -async def on_broadcast_confirm( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, - users_repository: FromDishka[UsersRepository], -): - assert callback.message is not None - - bot: Bot = dialog_manager.middleware_data["bot"] - message_id = dialog_manager.dialog_data["broadcast_message_id"] - chat_id = dialog_manager.dialog_data["broadcast_chat_id"] - admin_id = callback.from_user.id - - all_users = await users_repository.get_all_users() - - success_count = 0 - failed_count = 0 - - for user in all_users: - if user.id == admin_id: - continue - - try: - await bot.copy_message( - chat_id=user.id, - from_chat_id=chat_id, - message_id=message_id, - ) - success_count += 1 - except TelegramForbiddenError: - failed_count += 1 - except TelegramBadRequest: - failed_count += 1 - - result_text = f""" -
📢 Результаты рассылки
- -✅ Успешно отправлено: {success_count} -❌ Не удалось отправить: {failed_count} -📊 Всего пользователей: {len(all_users) - 1} -""" - - await callback.message.answer(result_text) - await callback.message.delete() - await dialog_manager.start(AdminMenuSG.main) - - -async def on_broadcast_cancel( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await dialog_manager.switch_to(AdminMenuSG.main) - - -@inject -async def get_residents_list_data( - residents_repository: FromDishka[ResidentsRepository], - rooms_repository: FromDishka[RoomsRepository], - **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) - room_number = room.number if room else 999999 - residents_with_rooms.append((resident, room_number)) - - residents_with_rooms.sort(key=lambda x: x[1]) - - residents_data = [] - for resident, room_number in residents_with_rooms: - status = "🟢" if resident.is_busy else "⚪️" - name = resident.real_name if resident.real_name else "Без имени" - - residents_data.append( - (f"{name} | Комната {room_number} | {status}", 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, "from_search": False} - - resident = await residents_repository.get_resident_by_id(resident_id) - - if not resident: - return {"info_content": "Ошибка: резидент не найден", "is_busy": False, "from_search": False} - - room = await rooms_repository.get_room_by_id(resident.room) - room_number = room.number if room else "???" - - name = resident.real_name if resident.real_name else "Без имени" - status = "🟢 Занят" if resident.is_busy else "⚪️ Свободен" - - user_info = "Не привязан" - if resident.user_entity: - user = await users_repository.get_user_by_id(resident.user_entity) - if user: - if user.username: - username = f"@{user.username}" - else: - username = f"ID: {user.id}" - user_info = f"{user.first_name} ({username})" - - info_content = f""" -
👤 Информация о резиденте
- -ID: {resident.id} -Имя: {name} -Комната: {room_number} -Статус: {status} -Пользователь: {user_info} - -━━━━━━━━━━━━━━━━━━━━ - -🟢 Отработанные часы: {resident.inactive_hours} ч -🔴 Неотработанные часы: {resident.active_hours} ч -""" - - from_search = dialog_manager.dialog_data.get("from_search", False) - - return { - "info_content": info_content, - "is_busy": resident.is_busy, - "from_search": from_search, - } - - -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 - 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_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_filter_residents( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await callback.answer("⚠️ Функционал в разработке", show_alert=True) - - -async def on_search_residents( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await dialog_manager.switch_to(AdminMenuSG.residents_search_input) - - -async def on_rooms_click( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await callback.answer("⚠️ Функционал в разработке", show_alert=True) - - -async def on_floors_click( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await callback.answer("⚠️ Функционал в разработке", show_alert=True) - - -@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) - - -async def on_add_hours_click( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await dialog_manager.switch_to(AdminMenuSG.add_hours_select) - - -async def on_remove_hours_click( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await dialog_manager.switch_to(AdminMenuSG.remove_hours_select) - - -async def get_hours_select_data(**kwargs): - hours_options = [ - (5, "5"), (10, "10"), (15, "15"), (20, "20"), - (25, "25"), (30, "30"), (35, "35"), (40, "40"), - (45, "45"), (50, "50"), (55, "55"), (60, "60"), - (65, "65"), (70, "70"), (75, "75"), (80, "80"), - ] - return {"hours_options": hours_options} - - -async def on_hours_selected( - callback: CallbackQuery, - widget: Select, - dialog_manager: DialogManager, - item_id: str, -): - dialog_manager.dialog_data["selected_hours"] = int(item_id) - - if dialog_manager.current_context().state == AdminMenuSG.add_hours_select: - await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) - else: - await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) - - -async def on_custom_hours_click( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - if dialog_manager.current_context().state == AdminMenuSG.add_hours_select: - await dialog_manager.switch_to(AdminMenuSG.add_hours_custom) - else: - await dialog_manager.switch_to(AdminMenuSG.remove_hours_custom) - - -async def on_custom_hours_input( - message: Message, - widget: MessageInput, - dialog_manager: DialogManager, -): - if not message.text: - await message.answer("⚠️ Пожалуйста, введите число") - return - - try: - hours = int(message.text) - if hours <= 0: - await message.answer("⚠️ Количество часов должно быть положительным числом") - return - - dialog_manager.dialog_data["selected_hours"] = hours - - if dialog_manager.current_context().state == AdminMenuSG.add_hours_custom: - await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) - else: - await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) - except ValueError: - await message.answer("⚠️ Пожалуйста, введите корректное число") - - -async def get_hours_confirm_data( - dialog_manager: DialogManager, - **kwargs, -): - hours = dialog_manager.dialog_data.get("selected_hours", 0) - return {"hours": hours} - - -@inject -async def on_add_hours_confirm( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, - transactions_repository: FromDishka[HoursTransactionsRepository], -): - resident_id = dialog_manager.dialog_data.get("selected_resident_id") - hours = dialog_manager.dialog_data.get("selected_hours") - admin_id = callback.from_user.id - - if resident_id and hours: - # Добавляем часы к неотработанным (active_hours) - await transactions_repository.add_hours( - resident_id=resident_id, - amount=hours, - admin_id=admin_id, - is_active=True, - ) - - await dialog_manager.switch_to(AdminMenuSG.resident_info) - - -@inject -async def on_remove_hours_confirm( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, - transactions_repository: FromDishka[HoursTransactionsRepository], - residents_repository: FromDishka[ResidentsRepository], -): - resident_id = dialog_manager.dialog_data.get("selected_resident_id") - hours = dialog_manager.dialog_data.get("selected_hours") - admin_id = callback.from_user.id - - if resident_id and hours: - resident = await residents_repository.get_resident_by_id(resident_id) - if resident and resident.active_hours < hours: - await callback.answer( - f"⚠️ Недостаточно часов! У резидента {resident.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч", - show_alert=True - ) - await dialog_manager.switch_to(AdminMenuSG.resident_info) - return - - # Перемещаем часы из неотработанных в отработанные - await transactions_repository.move_hours_to_completed( - resident_id=resident_id, - amount=hours, - admin_id=admin_id, - ) - - await dialog_manager.switch_to(AdminMenuSG.resident_info) - - -async def on_hours_cancel( - callback: CallbackQuery, - button: Button, - dialog_manager: DialogManager, -): - await dialog_manager.switch_to(AdminMenuSG.resident_info) - - -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], - 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) - room_number = room.number if room else 999999 - residents_with_rooms.append((resident, room_number)) - - residents_with_rooms.sort(key=lambda x: x[1]) - - residents_data = [] - for resident, room_number in residents_with_rooms: - status = "🟢" if resident.is_busy else "⚪️" - name = resident.real_name if resident.real_name else "Без имени" - - residents_data.append( - (f"{name} | Комната {room_number} | {status}", 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 - await dialog_manager.switch_to(AdminMenuSG.resident_info) admin_menu_dialog = Dialog( - Window( - Format("{content}"), - SwitchTo( - Const("🏠 Резиденты"), - id="residents_btn", - state=AdminMenuSG.residents, - ), - Row( - Button( - Const("🚪 Комнаты"), - id="rooms_btn", - on_click=on_rooms_click, - ), - Button( - Const("🏢 Этажи"), - id="floors_btn", - on_click=on_floors_click, - ), - ), - SwitchTo( - Const("📊 Статистика"), - id="stats_btn", - state=AdminMenuSG.statistics, - ), - SwitchTo( - Const("📢 Рассылка"), - id="broadcast_btn", - state=AdminMenuSG.broadcast, - ), - state=AdminMenuSG.main, - getter=get_admin_menu_data, - ), - 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, - ), - Window( - Format("{info_content}"), - Row( - Button( - Const("Добавить часы"), - id="add_hours_btn", - on_click=on_add_hours_click, - ), - Button( - Const("Отнять часы"), - id="remove_hours_btn", - on_click=on_remove_hours_click, - ), - ), - Button( - Const("🚪 Разлогинить"), - id="logout_resident_btn", - on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_logout_confirm), - when="is_busy", - ), - SwitchTo( - Const("◀️ Назад к результатам"), - id="back_to_search_results", - state=AdminMenuSG.residents_search_results, - when="from_search", - ), - SwitchTo( - Const("◀️ Назад к списку"), - id="back_to_residents_list", - state=AdminMenuSG.residents, - when=~F["from_search"], - ), - state=AdminMenuSG.resident_info, - getter=get_resident_info_data, - ), - 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, - ), - Window( - Const("
Добавить часы
\n\nВыберите количество часов:"), - Group( - Select( - Format("{item[1]} ч"), - id="hours_select_add", - item_id_getter=lambda x: x[0], - items="hours_options", - on_click=on_hours_selected, - ), - width=4, - ), - Button( - Const("✏️ Ввести свое количество"), - id="custom_hours_add_btn", - on_click=on_custom_hours_click, - ), - SwitchTo( - Const("◀️ Отмена"), - id="cancel_add_hours", - state=AdminMenuSG.resident_info, - ), - state=AdminMenuSG.add_hours_select, - getter=get_hours_select_data, - ), - Window( - Const("
Отнять часы
\n\nВыберите количество часов:"), - Group( - Select( - Format("{item[1]} ч"), - id="hours_select_remove", - item_id_getter=lambda x: x[0], - items="hours_options", - on_click=on_hours_selected, - ), - width=4, - ), - Button( - Const("✏️ Ввести свое количество"), - id="custom_hours_remove_btn", - on_click=on_custom_hours_click, - ), - SwitchTo( - Const("◀️ Отмена"), - id="cancel_remove_hours", - state=AdminMenuSG.resident_info, - ), - state=AdminMenuSG.remove_hours_select, - getter=get_hours_select_data, - ), - Window( - Const("
✏️ Добавить часы
\n\nВведите количество часов:"), - MessageInput(on_custom_hours_input), - SwitchTo( - Const("◀️ Отмена"), - id="cancel_custom_add", - state=AdminMenuSG.add_hours_select, - ), - state=AdminMenuSG.add_hours_custom, - ), - Window( - Const("
✏️ Отнять часы
\n\nВведите количество часов:"), - MessageInput(on_custom_hours_input), - SwitchTo( - Const("◀️ Отмена"), - id="cancel_custom_remove", - state=AdminMenuSG.remove_hours_select, - ), - state=AdminMenuSG.remove_hours_custom, - ), - Window( - Format("
Подтверждение
\n\nВы уверены, что хотите добавить {hours} часов?"), - Row( - Button( - Const("✅ Да"), - id="confirm_add_hours", - on_click=on_add_hours_confirm, - ), - Button( - Const("❌ Нет"), - id="cancel_add_hours_confirm", - on_click=on_hours_cancel, - ), - ), - state=AdminMenuSG.add_hours_confirm, - getter=get_hours_confirm_data, - ), - Window( - Format("
Подтверждение
\n\nВы уверены, что хотите отнять {hours} часов?"), - Row( - Button( - Const("✅ Да"), - id="confirm_remove_hours", - on_click=on_remove_hours_confirm, - ), - Button( - Const("❌ Нет"), - id="cancel_remove_hours_confirm", - on_click=on_hours_cancel, - ), - ), - state=AdminMenuSG.remove_hours_confirm, - getter=get_hours_confirm_data, - ), - Window( - Const("
Создание резидента
\n\nВведите имя и фамилию резидента:"), - MessageInput(on_resident_name_input), - SwitchTo( - Const("◀️ Отмена"), - id="cancel_create_resident_name", - state=AdminMenuSG.residents, - ), - state=AdminMenuSG.create_resident_name, - ), - 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, - ), - 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, - ), - 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, - ), - Window( - Format("{stats_content}"), - SwitchTo(Const("◀️ Назад"), id="back_from_stats", state=AdminMenuSG.main), - state=AdminMenuSG.statistics, - getter=get_statistics_data, - ), - Window( - Const( - "
📢 Рассылка
\n\nОтправьте сообщение, которое хотите разослать всем пользователям:" - ), - MessageInput(on_broadcast_message), - SwitchTo(Const("◀️ Отмена"), id="cancel_broadcast", state=AdminMenuSG.main), - state=AdminMenuSG.broadcast, - ), - Window( - Const( - "
📢 Подтверждение рассылки
\n\n⚠️ Вы уверены, что хотите отправить это сообщение всем пользователям?" - ), - Row( - Button( - Const("✅ Да"), id="confirm_broadcast", on_click=on_broadcast_confirm - ), - Button( - Const("❌ Нет"), - id="cancel_broadcast_confirm", - on_click=on_broadcast_cancel, - ), - ), - state=AdminMenuSG.broadcast_confirm, - ), - Window( - Const("
🔍 Поиск резидентов
\n\nВведите номер комнаты, имя резидента или username пользователя. Можно указать только часть имени или username для поиска."), - MessageInput(on_search_input), - SwitchTo( - Const("◀️ Отмена"), - id="cancel_search", - state=AdminMenuSG.residents, - ), - state=AdminMenuSG.residents_search_input, - ), - 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, - ), + main_menu_window, + residents_list_window, + resident_info_window, + resident_logout_confirm_window, + add_hours_select_window, + remove_hours_select_window, + add_hours_custom_window, + remove_hours_custom_window, + add_hours_confirm_window, + remove_hours_confirm_window, + create_resident_name_window, + create_resident_floor_window, + create_resident_room_window, + create_resident_confirm_window, + statistics_window, + broadcast_window, + broadcast_confirm_window, + search_input_window, + search_results_window, ) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/broadcast.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/broadcast.py new file mode 100644 index 0000000..21649f5 --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/broadcast.py @@ -0,0 +1,109 @@ +from aiogram.types import Message, CallbackQuery +from aiogram import Bot +from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest +from aiogram_dialog import Window, DialogManager +from aiogram_dialog.widgets.text import Const +from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button +from aiogram_dialog.widgets.input import MessageInput +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.users_repository import ( + UsersRepository, +) + + +async def on_broadcast_message( + message: Message, + widget: MessageInput, + dialog_manager: DialogManager, +): + dialog_manager.dialog_data["broadcast_message_id"] = message.message_id + dialog_manager.dialog_data["broadcast_chat_id"] = message.chat.id + + await message.copy_to(message.chat.id) + await dialog_manager.switch_to(AdminMenuSG.broadcast_confirm) + + +@inject +async def on_broadcast_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + users_repository: FromDishka[UsersRepository], +): + assert callback.message is not None + + bot: Bot = dialog_manager.middleware_data["bot"] + message_id = dialog_manager.dialog_data["broadcast_message_id"] + chat_id = dialog_manager.dialog_data["broadcast_chat_id"] + admin_id = callback.from_user.id + + all_users = await users_repository.get_all_users() + + success_count = 0 + failed_count = 0 + + for user in all_users: + if user.id == admin_id: + continue + + try: + await bot.copy_message( + chat_id=user.id, + from_chat_id=chat_id, + message_id=message_id, + ) + success_count += 1 + except TelegramForbiddenError: + failed_count += 1 + except TelegramBadRequest: + failed_count += 1 + + result_text = f""" +
📢 Результаты рассылки
+ +✅ Успешно отправлено: {success_count} +❌ Не удалось отправить: {failed_count} +📊 Всего пользователей: {len(all_users) - 1} +""" + + await callback.message.answer(result_text) + await callback.message.delete() + await dialog_manager.start(AdminMenuSG.main) + + +async def on_broadcast_cancel( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.main) + + +broadcast_window = Window( + Const( + "
📢 Рассылка
\n\nОтправьте сообщение, которое хотите разослать всем пользователям:" + ), + MessageInput(on_broadcast_message), + SwitchTo(Const("◀️ Отмена"), id="cancel_broadcast", state=AdminMenuSG.main), + state=AdminMenuSG.broadcast, +) + +broadcast_confirm_window = Window( + Const( + "
📢 Подтверждение рассылки
\n\n⚠️ Вы уверены, что хотите отправить это сообщение всем пользователям?" + ), + Row( + Button( + Const("✅ Да"), id="confirm_broadcast", on_click=on_broadcast_confirm + ), + Button( + Const("❌ Нет"), + id="cancel_broadcast_confirm", + on_click=on_broadcast_cancel, + ), + ), + state=AdminMenuSG.broadcast_confirm, +) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py new file mode 100644 index 0000000..8145ab3 --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/hours_management.py @@ -0,0 +1,271 @@ +from aiogram.types import Message, CallbackQuery +from aiogram_dialog import Window, DialogManager +from aiogram_dialog.widgets.text import Format, Const +from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, Select, Group +from aiogram_dialog.widgets.input import MessageInput +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.hours_transactions_repository import ( + HoursTransactionsRepository, +) + + +async def on_add_hours_click( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.add_hours_select) + + +async def on_remove_hours_click( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.remove_hours_select) + + +async def get_hours_select_data(**kwargs): + hours_options = [ + (5, "5"), (10, "10"), (15, "15"), (20, "20"), + (25, "25"), (30, "30"), (35, "35"), (40, "40"), + (45, "45"), (50, "50"), (55, "55"), (60, "60"), + (65, "65"), (70, "70"), (75, "75"), (80, "80"), + ] + return {"hours_options": hours_options} + + +async def on_hours_selected( + callback: CallbackQuery, + widget: Select, + dialog_manager: DialogManager, + item_id: str, +): + dialog_manager.dialog_data["selected_hours"] = int(item_id) + + if dialog_manager.current_context().state == AdminMenuSG.add_hours_select: + await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) + else: + await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) + + +async def on_custom_hours_click( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + if dialog_manager.current_context().state == AdminMenuSG.add_hours_select: + await dialog_manager.switch_to(AdminMenuSG.add_hours_custom) + else: + await dialog_manager.switch_to(AdminMenuSG.remove_hours_custom) + + +async def on_custom_hours_input( + message: Message, + widget: MessageInput, + dialog_manager: DialogManager, +): + if not message.text: + await message.answer("⚠️ Пожалуйста, введите число") + return + + try: + hours = int(message.text) + if hours <= 0: + await message.answer("⚠️ Количество часов должно быть положительным числом") + return + + dialog_manager.dialog_data["selected_hours"] = hours + + if dialog_manager.current_context().state == AdminMenuSG.add_hours_custom: + await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) + else: + await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) + except ValueError: + await message.answer("⚠️ Пожалуйста, введите корректное число") + + +async def get_hours_confirm_data( + dialog_manager: DialogManager, + **kwargs, +): + hours = dialog_manager.dialog_data.get("selected_hours", 0) + return {"hours": hours} + + +@inject +async def on_add_hours_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + transactions_repository: FromDishka[HoursTransactionsRepository], +): + resident_id = dialog_manager.dialog_data.get("selected_resident_id") + hours = dialog_manager.dialog_data.get("selected_hours") + admin_id = callback.from_user.id + + if resident_id and hours: + await transactions_repository.add_hours( + resident_id=resident_id, + amount=hours, + admin_id=admin_id, + is_active=True, + ) + + await dialog_manager.switch_to(AdminMenuSG.resident_info) + + +@inject +async def on_remove_hours_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + transactions_repository: FromDishka[HoursTransactionsRepository], + residents_repository: FromDishka[ResidentsRepository], +): + resident_id = dialog_manager.dialog_data.get("selected_resident_id") + hours = dialog_manager.dialog_data.get("selected_hours") + admin_id = callback.from_user.id + + if resident_id and hours: + resident = await residents_repository.get_resident_by_id(resident_id) + if resident and resident.active_hours < hours: + await callback.answer( + f"⚠️ Недостаточно часов! У резидента {resident.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч", + show_alert=True + ) + await dialog_manager.switch_to(AdminMenuSG.resident_info) + return + + await transactions_repository.move_hours_to_completed( + resident_id=resident_id, + amount=hours, + admin_id=admin_id, + ) + + await dialog_manager.switch_to(AdminMenuSG.resident_info) + + +async def on_hours_cancel( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.resident_info) + + +add_hours_select_window = Window( + Const("
Добавить часы
\n\nВыберите количество часов:"), + Group( + Select( + Format("{item[1]} ч"), + id="hours_select_add", + item_id_getter=lambda x: x[0], + items="hours_options", + on_click=on_hours_selected, + ), + width=4, + ), + Button( + Const("✏️ Ввести свое количество"), + id="custom_hours_add_btn", + on_click=on_custom_hours_click, + ), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_add_hours", + state=AdminMenuSG.resident_info, + ), + state=AdminMenuSG.add_hours_select, + getter=get_hours_select_data, +) + +remove_hours_select_window = Window( + Const("
Отнять часы
\n\nВыберите количество часов:"), + Group( + Select( + Format("{item[1]} ч"), + id="hours_select_remove", + item_id_getter=lambda x: x[0], + items="hours_options", + on_click=on_hours_selected, + ), + width=4, + ), + Button( + Const("✏️ Ввести свое количество"), + id="custom_hours_remove_btn", + on_click=on_custom_hours_click, + ), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_remove_hours", + state=AdminMenuSG.resident_info, + ), + state=AdminMenuSG.remove_hours_select, + getter=get_hours_select_data, +) + +add_hours_custom_window = Window( + Const("
✏️ Добавить часы
\n\nВведите количество часов:"), + MessageInput(on_custom_hours_input), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_custom_add", + state=AdminMenuSG.add_hours_select, + ), + state=AdminMenuSG.add_hours_custom, +) + +remove_hours_custom_window = Window( + Const("
✏️ Отнять часы
\n\nВведите количество часов:"), + MessageInput(on_custom_hours_input), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_custom_remove", + state=AdminMenuSG.remove_hours_select, + ), + state=AdminMenuSG.remove_hours_custom, +) + +add_hours_confirm_window = Window( + Format("
Подтверждение
\n\nВы уверены, что хотите добавить {hours} часов?"), + Row( + Button( + Const("✅ Да"), + id="confirm_add_hours", + on_click=on_add_hours_confirm, + ), + Button( + Const("❌ Нет"), + id="cancel_add_hours_confirm", + on_click=on_hours_cancel, + ), + ), + state=AdminMenuSG.add_hours_confirm, + getter=get_hours_confirm_data, +) + +remove_hours_confirm_window = Window( + Format("
Подтверждение
\n\nВы уверены, что хотите отнять {hours} часов?"), + Row( + Button( + Const("✅ Да"), + id="confirm_remove_hours", + on_click=on_remove_hours_confirm, + ), + Button( + Const("❌ Нет"), + id="cancel_remove_hours_confirm", + on_click=on_hours_cancel, + ), + ), + state=AdminMenuSG.remove_hours_confirm, + getter=get_hours_confirm_data, +) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py new file mode 100644 index 0000000..89f3eee --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/main_menu.py @@ -0,0 +1,132 @@ +from aiogram.types import User +from aiogram_dialog import Window, DialogManager +from aiogram_dialog.widgets.text import Format, Const +from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button +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.users_repository import ( + UsersRepository, +) +from dutylog.infrastructure.database.repositories.residents_repository import ( + ResidentsRepository, +) +from dutylog.infrastructure.utils.config import Config + + +@inject +async def get_admin_menu_data( + event_from_user: User, + users_repository: FromDishka[UsersRepository], + config: FromDishka[Config], + **kwargs, +): + user = await users_repository.get_or_create_user( + user_id=event_from_user.id, + username=event_from_user.username, + first_name=event_from_user.first_name, + last_name=event_from_user.last_name, + ) + + is_creator = event_from_user.id == config.bot.creator_id + + if is_creator: + greeting = "👑 Создатель" + else: + greeting = "👨‍💼 Администратор" + + content = f""" +{greeting} + +
📋 Панель управления
+ +Выберите действие: +""" + + return {"content": content} + + +@inject +async def get_statistics_data( + users_repository: FromDishka[UsersRepository], + residents_repository: FromDishka[ResidentsRepository], + **kwargs, +): + all_users = await users_repository.get_all_users() + all_residents = await residents_repository.get_all_residents() + + total_users = len(all_users) + total_residents = len(all_residents) + busy_residents = len([r for r in all_residents if r.is_busy]) + total_active_hours = sum(r.active_hours for r in all_residents) + total_inactive_hours = sum(r.inactive_hours for r in all_residents) + admins_count = len([u for u in all_users if u.is_admin]) + + stats_text = f""" +
📊 Статистика системы
+ +👥 Всего пользователей: {total_users} +👨‍💼 Администраторов: {admins_count} + +🏠 Всего резидентов: {total_residents} +✅ Привязано к пользователям: {busy_residents} +❌ Свободных: {total_residents - busy_residents} + +━━━━━━━━━━━━━━━━━━━━ + +🟢 Всего отработанных часов: {total_inactive_hours} ч +🔴 Всего неотработанных часов: {total_active_hours} ч +📊 Общий итог: {total_active_hours + total_inactive_hours} ч +""" + + return {"stats_content": stats_text} + + +async def on_rooms_click(callback, button, dialog_manager): + await callback.answer("⚠️ Функционал в разработке", show_alert=True) + + +async def on_floors_click(callback, button, dialog_manager): + await callback.answer("⚠️ Функционал в разработке", show_alert=True) + + +main_menu_window = Window( + Format("{content}"), + SwitchTo( + Const("🏠 Резиденты"), + id="residents_btn", + state=AdminMenuSG.residents, + ), + Row( + Button( + Const("🚪 Комнаты"), + id="rooms_btn", + on_click=on_rooms_click, + ), + Button( + Const("🏢 Этажи"), + id="floors_btn", + on_click=on_floors_click, + ), + ), + SwitchTo( + Const("📊 Статистика"), + id="stats_btn", + state=AdminMenuSG.statistics, + ), + SwitchTo( + Const("📢 Рассылка"), + id="broadcast_btn", + state=AdminMenuSG.broadcast, + ), + state=AdminMenuSG.main, + getter=get_admin_menu_data, +) + +statistics_window = Window( + Format("{stats_content}"), + SwitchTo(Const("◀️ Назад"), id="back_from_stats", state=AdminMenuSG.main), + state=AdminMenuSG.statistics, + getter=get_statistics_data, +) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py new file mode 100644 index 0000000..c9ec5f9 --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py @@ -0,0 +1,573 @@ +from aiogram.types import Message, CallbackQuery +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, +) + + +@inject +async def get_residents_list_data( + residents_repository: FromDishka[ResidentsRepository], + rooms_repository: FromDishka[RoomsRepository], + **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) + room_number = room.number if room else 999999 + residents_with_rooms.append((resident, room_number)) + + residents_with_rooms.sort(key=lambda x: x[1]) + + residents_data = [] + for resident, room_number in residents_with_rooms: + status = "🟢" if resident.is_busy else "⚪️" + name = resident.real_name if resident.real_name else "Без имени" + + residents_data.append( + (f"{name} | Комната {room_number} | {status}", 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, "from_search": False} + + resident = await residents_repository.get_resident_by_id(resident_id) + + if not resident: + return {"info_content": "Ошибка: резидент не найден", "is_busy": False, "from_search": False} + + room = await rooms_repository.get_room_by_id(resident.room) + room_number = room.number if room else "???" + + name = resident.real_name if resident.real_name else "Без имени" + status = "🟢 Занят" if resident.is_busy else "⚪️ Свободен" + + user_info = "Не привязан" + if resident.user_entity: + user = await users_repository.get_user_by_id(resident.user_entity) + if user: + if user.username: + username = f"@{user.username}" + else: + username = f"ID: {user.id}" + user_info = f"{user.first_name} ({username})" + + info_content = f""" +
👤 Информация о резиденте
+ +ID: {resident.id} +Имя: {name} +Комната: {room_number} +Статус: {status} +Пользователь: {user_info} + +━━━━━━━━━━━━━━━━━━━━ + +🟢 Отработанные часы: {resident.inactive_hours} ч +🔴 Неотработанные часы: {resident.active_hours} ч +""" + + from_search = dialog_manager.dialog_data.get("from_search", False) + + return { + "info_content": info_content, + "is_busy": resident.is_busy, + "from_search": from_search, + } + + +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 + 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 callback.answer("⚠️ Функционал в разработке", show_alert=True) + + +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) + + +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], + 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) + room_number = room.number if room else 999999 + residents_with_rooms.append((resident, room_number)) + + residents_with_rooms.sort(key=lambda x: x[1]) + + residents_data = [] + for resident, room_number in residents_with_rooms: + status = "🟢" if resident.is_busy else "⚪️" + name = resident.real_name if resident.real_name else "Без имени" + + residents_data.append( + (f"{name} | Комната {room_number} | {status}", 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 + await dialog_manager.switch_to(AdminMenuSG.resident_info) + + +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), + ), + Button( + Const("Отнять часы"), + id="remove_hours_btn", + on_click=lambda c, b, m: m.switch_to(AdminMenuSG.remove_hours_select), + ), + ), + Button( + Const("🚪 Разлогинить"), + id="logout_resident_btn", + on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_logout_confirm), + when="is_busy", + ), + SwitchTo( + Const("◀️ Назад к результатам"), + id="back_to_search_results", + state=AdminMenuSG.residents_search_results, + when="from_search", + ), + SwitchTo( + Const("◀️ Назад к списку"), + id="back_to_residents_list", + state=AdminMenuSG.residents, + when=~F["from_search"], + ), + 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, +) diff --git a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py index 89c6f22..df960ca 100644 --- a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py @@ -1,196 +1,12 @@ -from aiogram.types import User -from aiogram_dialog import Dialog, Window, DialogManager -from aiogram_dialog.widgets.text import Format, Const -from aiogram_dialog.widgets.kbd import SwitchTo, Back -from dishka import FromDishka -from dishka.integrations.aiogram_dialog import inject +from aiogram_dialog import Dialog -from dutylog.application.bot.user_dialogs.states import MainMenuSG -from dutylog.infrastructure.database.repositories.users_repository import ( - UsersRepository, -) -from dutylog.infrastructure.database.repositories.residents_repository import ( - ResidentsRepository, -) -from dutylog.infrastructure.database.repositories.rooms_repository import ( - RoomsRepository, -) -from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( - HoursTransactionsRepository, -) -from dutylog.infrastructure.utils.config import Config - - -@inject -async def get_main_menu_data( - event_from_user: User, - users_repository: FromDishka[UsersRepository], - residents_repository: FromDishka[ResidentsRepository], - rooms_repository: FromDishka[RoomsRepository], - config: FromDishka[Config], - **kwargs, -): - user = await users_repository.get_or_create_user( - user_id=event_from_user.id, - username=event_from_user.username, - first_name=event_from_user.first_name, - last_name=event_from_user.last_name, - ) - - is_creator = event_from_user.id == config.bot.creator_id - is_admin = user.is_admin - - if is_creator: - greeting = "👑 Создатель" - elif is_admin: - greeting = "👨‍💼 Администратор" - else: - resident = await residents_repository.get_resident_by_user_id( - event_from_user.id - ) - if resident: - room = await rooms_repository.get_room_by_id(resident.room) - room_number = room.number if room else "???" - real_name = ( - resident.real_name if resident.real_name else event_from_user.first_name - ) - greeting = ( - f"👋 Привет, {real_name}!\n🚪 Комната {room_number}" - ) - else: - greeting = f"👋 Привет, {event_from_user.first_name}!" - - if not is_admin and not is_creator: - resident = await residents_repository.get_resident_by_user_id( - event_from_user.id - ) - - if not resident: - content = f""" -{greeting} - -
⚠️ Профиль не найден
- -Вы еще не привязаны к резиденту. -Обратитесь к администратору для регистрации. -""" - has_resident = False - else: - content = f""" -{greeting} - -⏰ Ваши часы дежурств - -
🟢 Отработанные часы: {resident.inactive_hours} ч -━━━━━━━━━━━━━━━━ -🔴 Неотработанные часы: {resident.active_hours} ч
-""" - has_resident = True - else: - content = f""" -{greeting} - -
📋 Панель управления
- -Добро пожаловать в систему учета дежурств! -""" - has_resident = False - - return { - "content": content, - "is_regular_user": not is_admin and not is_creator, - "has_resident": has_resident, - } - - -@inject -async def get_history_data( - event_from_user: User, - residents_repository: FromDishka[ResidentsRepository], - transactions_repository: FromDishka[HoursTransactionsRepository], - **kwargs, -): - resident = await residents_repository.get_resident_by_user_id(event_from_user.id) - - if not resident: - history_text = """ -
📜 История операций
- -Профиль не найден -""" - else: - transactions = await transactions_repository.get_resident_history(resident.id) - transactions_sorted = sorted(transactions, key=lambda x: x.created_at) - last_10 = transactions_sorted[:10] - - if not last_10: - history_text = """ -
📜 История операций
- -История операций пуста -""" - else: - history_lines = [] - for tx in last_10: - emoji = "+" if tx.transaction_type == "increase" else "-" - date_str = tx.created_at.strftime("%d.%m.%Y %H:%M") - history_lines.append( - f"{emoji} {tx.amount} ч • {date_str}" - ) - - history_text = f""" -
📜 История операций
- -{"".join(f"{line}\n" for line in history_lines)} -Показаны последние 10 операций -""" - - return {"history_content": history_text} +from dutylog.application.bot.user_dialogs.user_menu.main_menu import main_menu_window +from dutylog.application.bot.user_dialogs.user_menu.history import history_window +from dutylog.application.bot.user_dialogs.user_menu.faq import faq_window main_menu_dialog = Dialog( - Window( - Format("{content}"), - SwitchTo( - Const("📜 История"), - id="history_btn", - state=MainMenuSG.history, - when="has_resident", - ), - SwitchTo( - Const("❓ FAQ"), - id="faq_btn", - state=MainMenuSG.faq, - when="is_regular_user", - ), - state=MainMenuSG.main, - getter=get_main_menu_data, - ), - Window( - Format("{history_content}"), - Back(Const("◀️ Назад")), - state=MainMenuSG.history, - getter=get_history_data, - ), - Window( - Const("""
Часто задаваемые вопросы
- -Что это за система? -
Это система учета дежурств в общежитии. Здесь отображаются ваши отработанные и неотработанные часы дежурств.
- -Что делать, если я зарегистрировался не под собой? -
⚠️ Перерегистрацию может выполнить только администратор. Обратитесь к администратору для исправления данных.
- -Как начисляются часы? -
Часы начисляются и списываются администраторами системы. Все изменения отображаются в разделе "История".
- -Что означают активные и неактивные часы? -
🟢 Отработанные часы - часы, которые вы уже отработали -🔴 Неотработанные часы - часы, которые вам еще предстоит отработать
- -Как связаться с администратором? -
Обратитесь к старосте вашего этажа или в администрацию общежития.
"""), - SwitchTo(Const("◀️ Назад"), id="back_to_main", state=MainMenuSG.main), - state=MainMenuSG.faq, - ), + main_menu_window, + history_window, + faq_window, ) diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/__init__.py b/src/dutylog/application/bot/user_dialogs/user_menu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/faq.py b/src/dutylog/application/bot/user_dialogs/user_menu/faq.py new file mode 100644 index 0000000..fdab72b --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/user_menu/faq.py @@ -0,0 +1,28 @@ +from aiogram_dialog import Window +from aiogram_dialog.widgets.text import Const +from aiogram_dialog.widgets.kbd import SwitchTo + +from dutylog.application.bot.user_dialogs.states import MainMenuSG + + +faq_window = Window( + Const("""
Часто задаваемые вопросы
+ +Что это за система? +
Это система учета дежурств в общежитии. Здесь отображаются ваши отработанные и неотработанные часы дежурств.
+ +Что делать, если я зарегистрировался не под собой? +
⚠️ Перерегистрацию может выполнить только администратор. Обратитесь к администратору для исправления данных.
+ +Как начисляются часы? +
Часы начисляются и списываются администраторами системы. Все изменения отображаются в разделе "История".
+ +Что означают активные и неактивные часы? +
🟢 Отработанные часы - часы, которые вы уже отработали +🔴 Неотработанные часы - часы, которые вам еще предстоит отработать
+ +Как связаться с администратором? +
Обратитесь к старосте вашего этажа или в администрацию общежития.
"""), + SwitchTo(Const("◀️ Назад"), id="back_to_main", state=MainMenuSG.main), + state=MainMenuSG.faq, +) diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/history.py b/src/dutylog/application/bot/user_dialogs/user_menu/history.py new file mode 100644 index 0000000..248ac7d --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/user_menu/history.py @@ -0,0 +1,68 @@ +from aiogram.types import User +from aiogram_dialog import Window +from aiogram_dialog.widgets.text import Format +from aiogram_dialog.widgets.kbd import Back +from aiogram_dialog.widgets.text import Const +from dishka import FromDishka +from dishka.integrations.aiogram_dialog import inject + +from dutylog.application.bot.user_dialogs.states import MainMenuSG +from dutylog.infrastructure.database.repositories.residents_repository import ( + ResidentsRepository, +) +from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( + HoursTransactionsRepository, +) + + +@inject +async def get_history_data( + event_from_user: User, + residents_repository: FromDishka[ResidentsRepository], + transactions_repository: FromDishka[HoursTransactionsRepository], + **kwargs, +): + resident = await residents_repository.get_resident_by_user_id(event_from_user.id) + + if not resident: + history_text = """ +
📜 История операций
+ +Профиль не найден +""" + else: + transactions = await transactions_repository.get_resident_history(resident.id) + transactions_sorted = sorted(transactions, key=lambda x: x.created_at) + last_10 = transactions_sorted[:10] + + if not last_10: + history_text = """ +
📜 История операций
+ +История операций пуста +""" + else: + history_lines = [] + for tx in last_10: + emoji = "+" if tx.transaction_type == "increase" else "-" + date_str = tx.created_at.strftime("%d.%m.%Y %H:%M") + history_lines.append( + f"{emoji} {tx.amount} ч • {date_str}" + ) + + history_text = f""" +
📜 История операций
+ +{"".join(f"{line}\n" for line in history_lines)} +Показаны последние 10 операций +""" + + return {"history_content": history_text} + + +history_window = Window( + Format("{history_content}"), + Back(Const("◀️ Назад")), + state=MainMenuSG.history, + getter=get_history_data, +) diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py b/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py new file mode 100644 index 0000000..233e9f0 --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py @@ -0,0 +1,119 @@ +from aiogram.types import User +from aiogram_dialog import Window, DialogManager +from aiogram_dialog.widgets.text import Format, Const +from aiogram_dialog.widgets.kbd import SwitchTo +from dishka import FromDishka +from dishka.integrations.aiogram_dialog import inject + +from dutylog.application.bot.user_dialogs.states import MainMenuSG +from dutylog.infrastructure.database.repositories.users_repository import ( + UsersRepository, +) +from dutylog.infrastructure.database.repositories.residents_repository import ( + ResidentsRepository, +) +from dutylog.infrastructure.database.repositories.rooms_repository import ( + RoomsRepository, +) +from dutylog.infrastructure.utils.config import Config + + +@inject +async def get_main_menu_data( + event_from_user: User, + users_repository: FromDishka[UsersRepository], + residents_repository: FromDishka[ResidentsRepository], + rooms_repository: FromDishka[RoomsRepository], + config: FromDishka[Config], + **kwargs, +): + user = await users_repository.get_or_create_user( + user_id=event_from_user.id, + username=event_from_user.username, + first_name=event_from_user.first_name, + last_name=event_from_user.last_name, + ) + + is_creator = event_from_user.id == config.bot.creator_id + is_admin = user.is_admin + + if is_creator: + greeting = "👑 Создатель" + elif is_admin: + greeting = "👨‍💼 Администратор" + else: + resident = await residents_repository.get_resident_by_user_id( + event_from_user.id + ) + if resident: + room = await rooms_repository.get_room_by_id(resident.room) + room_number = room.number if room else "???" + real_name = ( + resident.real_name if resident.real_name else event_from_user.first_name + ) + greeting = ( + f"👋 Привет, {real_name}!\n🚪 Комната {room_number}" + ) + else: + greeting = f"👋 Привет, {event_from_user.first_name}!" + + if not is_admin and not is_creator: + resident = await residents_repository.get_resident_by_user_id( + event_from_user.id + ) + + if not resident: + content = f""" +{greeting} + +
⚠️ Профиль не найден
+ +Вы еще не привязаны к резиденту. +Обратитесь к администратору для регистрации. +""" + has_resident = False + else: + content = f""" +{greeting} + +⏰ Ваши часы дежурств + +
🟢 Отработанные часы: {resident.inactive_hours} ч +━━━━━━━━━━━━━━━━ +🔴 Неотработанные часы: {resident.active_hours} ч
+""" + has_resident = True + else: + content = f""" +{greeting} + +
📋 Панель управления
+ +Добро пожаловать в систему учета дежурств! +""" + has_resident = False + + return { + "content": content, + "is_regular_user": not is_admin and not is_creator, + "has_resident": has_resident, + } + + +main_menu_window = Window( + Format("{content}"), + SwitchTo( + Const("📜 История"), + id="history_btn", + state=MainMenuSG.history, + when="has_resident", + ), + SwitchTo( + Const("❓ FAQ"), + id="faq_btn", + state=MainMenuSG.faq, + when="is_regular_user", + ), + state=MainMenuSG.main, + getter=get_main_menu_data, +)