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 449352f..2745455 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 @@ -15,6 +15,11 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.residents_management imp search_input_window, search_results_window, ) +from dutylog.application.bot.user_dialogs.admin_dialogs.residents_filter import ( + filter_select_window, + filter_hours_input_window, + filtered_results_window, +) from dutylog.application.bot.user_dialogs.admin_dialogs.hours_management import ( add_hours_select_window, remove_hours_select_window, @@ -49,4 +54,7 @@ admin_menu_dialog = Dialog( broadcast_confirm_window, search_input_window, search_results_window, + filter_select_window, + filter_hours_input_window, + filtered_results_window, ) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_filter.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_filter.py new file mode 100644 index 0000000..c23fd93 --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_filter.py @@ -0,0 +1,223 @@ +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 SwitchTo, Button, ScrollingGroup, 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.rooms_repository import ( + RoomsRepository, +) + + +async def on_filter_residents( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.residents_filter_select) + + +async def get_filter_options_data(**kwargs): + filter_options = [ + ("busy", "🟢 Только занятые"), + ("free", "⚪️ Только свободные"), + ("hours_more", "🔴 Неотработанных часов > X"), + ("hours_less", "🔴 Неотработанных часов < X"), + ("worked_more", "🟢 Отработанных часов > X"), + ("worked_less", "🟢 Отработанных часов < X"), + ] + return {"filter_options": filter_options} + + +async def on_filter_selected( + callback: CallbackQuery, + widget: Select, + dialog_manager: DialogManager, + item_id: str, +): + dialog_manager.dialog_data["filter_type"] = item_id + + if item_id in ["hours_more", "hours_less", "worked_more", "worked_less"]: + await dialog_manager.switch_to(AdminMenuSG.residents_filter_hours_input) + else: + dialog_manager.dialog_data["filter_hours"] = None + await dialog_manager.switch_to(AdminMenuSG.residents_filtered_results) + + +async def on_filter_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["filter_hours"] = hours + await dialog_manager.switch_to(AdminMenuSG.residents_filtered_results) + except ValueError: + await message.answer("⚠️ Пожалуйста, введите корректное число") + + +@inject +async def get_filtered_results_data( + dialog_manager: DialogManager, + residents_repository: FromDishka[ResidentsRepository], + rooms_repository: FromDishka[RoomsRepository], + **kwargs, +): + filter_type = dialog_manager.dialog_data.get("filter_type", "") + filter_hours = dialog_manager.dialog_data.get("filter_hours") + + all_residents = await residents_repository.get_all_residents() + + if filter_type == "busy": + filtered = [r for r in all_residents if r.is_busy] + filter_description = "занятым резидентам" + elif filter_type == "free": + filtered = [r for r in all_residents if not r.is_busy] + filter_description = "свободным резидентам" + elif filter_type == "hours_more": + filtered = [r for r in all_residents if r.active_hours > filter_hours] + filter_description = f"неотработанных часов > {filter_hours}" + elif filter_type == "hours_less": + filtered = [r for r in all_residents if r.active_hours < filter_hours] + filter_description = f"неотработанных часов < {filter_hours}" + elif filter_type == "worked_more": + filtered = [r for r in all_residents if r.inactive_hours > filter_hours] + filter_description = f"отработанных часов > {filter_hours}" + elif filter_type == "worked_less": + filtered = [r for r in all_residents if r.inactive_hours < filter_hours] + filter_description = f"отработанных часов < {filter_hours}" + else: + filtered = all_residents + filter_description = "всем резидентам" + + if not filtered: + return { + "content": f""" +
🔽 Результаты фильтрации
+ +Фильтр: {filter_description} + +❌ Ничего не найдено +""", + "residents": [], + "has_results": False, + } + + residents_with_rooms = [] + for resident in filtered: + 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""" +
🔽 Результаты фильтрации по {filter_description}
+ +Найдено резидентов: {len(filtered)} + +Выберите резидента для просмотра информации: +""" + + return { + "content": content, + "residents": residents_data, + "has_results": True, + } + + +async def on_filtered_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_filter"] = True + await dialog_manager.switch_to(AdminMenuSG.resident_info) + + +filter_select_window = Window( + Const("
🔽 Фильтрация резидентов
\n\n
Выберите критерий фильтрации для отображения нужных резидентов.
"), + Group( + Select( + Format("{item[1]}"), + id="filter_select", + item_id_getter=lambda x: x[0], + items="filter_options", + on_click=on_filter_selected, + ), + width=1, + ), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_filter", + state=AdminMenuSG.residents, + ), + state=AdminMenuSG.residents_filter_select, + getter=get_filter_options_data, +) + +filter_hours_input_window = Window( + Const("
🔽 Фильтрация по часам
\n\n
Введите количество часов для фильтрации.
"), + MessageInput(on_filter_hours_input), + SwitchTo( + Const("◀️ Назад"), + id="back_to_filter_select", + state=AdminMenuSG.residents_filter_select, + ), + state=AdminMenuSG.residents_filter_hours_input, +) + +filtered_results_window = Window( + Format("{content}"), + ScrollingGroup( + Select( + Format("{item[0]}"), + id="filtered_residents_select", + item_id_getter=lambda x: x[1], + items="residents", + on_click=on_filtered_resident_selected, + ), + id="filtered_residents_scroll", + width=1, + height=7, + when="has_results", + ), + SwitchTo( + Const("🔽 Новый фильтр"), + id="new_filter", + state=AdminMenuSG.residents_filter_select, + ), + SwitchTo( + Const("◀️ К списку резидентов"), + id="back_to_residents_from_filter", + state=AdminMenuSG.residents, + ), + state=AdminMenuSG.residents_filtered_results, + getter=get_filtered_results_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 index c9ec5f9..27109aa 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/residents_management.py @@ -72,12 +72,12 @@ async def get_resident_info_data( resident_id = dialog_manager.dialog_data.get("selected_resident_id") if not resident_id: - return {"info_content": "Ошибка: резидент не выбран", "is_busy": False, "from_search": False} + return {"info_content": "Ошибка: резидент не выбран", "is_busy": 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, "from_search": False} + return {"info_content": "Ошибка: резидент не найден", "is_busy": False, "from_search": False, "from_filter": False} room = await rooms_repository.get_room_by_id(resident.room) room_number = room.number if room else "???" @@ -111,11 +111,13 @@ async def get_resident_info_data( """ 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, "from_search": from_search, + "from_filter": from_filter, } @@ -127,6 +129,7 @@ async def on_resident_selected( ): 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) @@ -143,7 +146,7 @@ async def on_filter_residents( button: Button, dialog_manager: DialogManager, ): - await callback.answer("⚠️ Функционал в разработке", show_alert=True) + await dialog_manager.switch_to(AdminMenuSG.residents_filter_select) async def on_search_residents( @@ -363,6 +366,7 @@ async def on_search_resident_selected( ): 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) @@ -427,16 +431,22 @@ resident_info_window = Window( when="is_busy", ), SwitchTo( - Const("◀️ Назад к результатам"), + 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"], + when=~F["from_search"] & ~F["from_filter"], ), state=AdminMenuSG.resident_info, getter=get_resident_info_data, diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py index 45b6b51..8ce8bde 100644 --- a/src/dutylog/application/bot/user_dialogs/states.py +++ b/src/dutylog/application/bot/user_dialogs/states.py @@ -12,6 +12,9 @@ class AdminMenuSG(StatesGroup): residents = State() residents_search_input = State() residents_search_results = State() + residents_filter_select = State() + residents_filter_hours_input = State() + residents_filtered_results = State() resident_info = State() resident_logout_confirm = State() add_hours_select = State()