diff --git a/src/dutylog/application/bot/admin_dialogs/admin_menu_dialog.py b/src/dutylog/application/bot/admin_dialogs/admin_menu_dialog.py
index 5a3494f..c7872a8 100644
--- a/src/dutylog/application/bot/admin_dialogs/admin_menu_dialog.py
+++ b/src/dutylog/application/bot/admin_dialogs/admin_menu_dialog.py
@@ -33,7 +33,16 @@ from dutylog.application.bot.admin_dialogs.floors_management import (
from dutylog.application.bot.admin_dialogs.rooms_management import (
rooms_select_floor_window,
rooms_list_window,
+ room_info_window,
room_delete_confirm_window,
+ room_add_hours_select_window,
+ room_remove_hours_select_window,
+ room_add_hours_custom_window,
+ room_remove_hours_custom_window,
+ room_add_hours_remark_window,
+ room_remove_hours_remark_window,
+ room_add_hours_confirm_window,
+ room_remove_hours_confirm_window,
create_room_select_floor_window,
create_room_input_window,
create_room_confirm_window,
@@ -90,7 +99,16 @@ admin_menu_dialog = Dialog(
create_floor_confirm_window,
rooms_select_floor_window,
rooms_list_window,
+ room_info_window,
room_delete_confirm_window,
+ room_add_hours_select_window,
+ room_remove_hours_select_window,
+ room_add_hours_custom_window,
+ room_remove_hours_custom_window,
+ room_add_hours_remark_window,
+ room_remove_hours_remark_window,
+ room_add_hours_confirm_window,
+ room_remove_hours_confirm_window,
create_room_select_floor_window,
create_room_input_window,
create_room_confirm_window,
diff --git a/src/dutylog/application/bot/admin_dialogs/residents_management.py b/src/dutylog/application/bot/admin_dialogs/residents_management.py
index 86e7039..e7d8b7d 100644
--- a/src/dutylog/application/bot/admin_dialogs/residents_management.py
+++ b/src/dutylog/application/bot/admin_dialogs/residents_management.py
@@ -111,8 +111,6 @@ async def get_resident_info_data(
Статус: {status}
Пользователь: {user_info}
-━━━━━━━━━━━━━━━━━━━━
-
🟢 Отработанные часы: {resident.inactive_hours} ч
🔴 Неотработанные часы: {resident.active_hours} ч
"""
diff --git a/src/dutylog/application/bot/admin_dialogs/rooms_management.py b/src/dutylog/application/bot/admin_dialogs/rooms_management.py
index 9d514d9..10bbf07 100644
--- a/src/dutylog/application/bot/admin_dialogs/rooms_management.py
+++ b/src/dutylog/application/bot/admin_dialogs/rooms_management.py
@@ -13,6 +13,15 @@ from dutylog.infrastructure.database.repositories.rooms_repository import (
from dutylog.infrastructure.database.repositories.floors_repository import (
FloorsRepository,
)
+from dutylog.infrastructure.database.repositories.residents_repository import (
+ ResidentsRepository,
+)
+from dutylog.infrastructure.database.repositories.room_hours_transactions_repository import (
+ RoomHoursTransactionsRepository,
+)
+from dutylog.infrastructure.database.repositories.users_repository import (
+ UsersRepository,
+)
async def on_rooms_click(
@@ -84,7 +93,7 @@ async def get_rooms_list_data(
Всего комнат: {len(rooms)}
-Выберите комнату для удаления:
+Выберите комнату для просмотра:
"""
return {
@@ -101,7 +110,7 @@ async def on_room_selected(
item_id: str,
):
dialog_manager.dialog_data["selected_room_id"] = int(item_id)
- await dialog_manager.switch_to(AdminMenuSG.room_delete_confirm)
+ await dialog_manager.switch_to(AdminMenuSG.room_info)
async def on_add_room_click(
@@ -207,6 +216,282 @@ async def on_create_room_cancel(
await dialog_manager.switch_to(AdminMenuSG.rooms_select_floor)
+@inject
+async def get_room_info_data(
+ dialog_manager: DialogManager,
+ rooms_repository: FromDishka[RoomsRepository],
+ floors_repository: FromDishka[FloorsRepository],
+ residents_repository: FromDishka[ResidentsRepository],
+ **kwargs,
+):
+ room_id = dialog_manager.dialog_data.get("selected_room_id")
+
+ if not room_id:
+ return {"info_content": "Ошибка: комната не выбрана"}
+
+ room = await rooms_repository.get_room_by_id(room_id)
+
+ if not room:
+ return {"info_content": "Ошибка: комната не найдена"}
+
+ floor = await floors_repository.get_floor_by_id(room.on_floor)
+ floor_number = floor.number if floor else "???"
+
+ # Получаем резидентов комнаты
+ residents = await residents_repository.get_residents_by_room(room_id)
+
+ residents_info = ""
+ if residents:
+ residents_info = "\nПроживающие:\n"
+ for resident in residents:
+ status = "🟢" if resident.is_busy else "⚪️"
+ name = resident.real_name if resident.real_name else "Без имени"
+ residents_info += f"{status} {name}\n"
+ else:
+ residents_info = "\nНет проживающих\n"
+
+ info_content = f"""
+
🚪 Информация о комнате+ +Номер:
{room.number}
+Этаж: {floor_number}
+{residents_info}
+🟢 Отработанные часы: {room.inactive_hours} ч
+🔴 Неотработанные часы: {room.active_hours} ч
+"""
+
+ return {
+ "info_content": info_content,
+ "has_residents": len(residents) > 0,
+ }
+
+
+async def on_room_add_hours_click(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+):
+ await dialog_manager.switch_to(AdminMenuSG.room_add_hours_select)
+
+
+async def on_room_remove_hours_click(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+):
+ await dialog_manager.switch_to(AdminMenuSG.room_remove_hours_select)
+
+
+async def get_room_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_room_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.room_add_hours_select:
+ await dialog_manager.switch_to(AdminMenuSG.room_add_hours_remark)
+ else:
+ await dialog_manager.switch_to(AdminMenuSG.room_remove_hours_remark)
+
+
+async def on_room_custom_hours_click(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+):
+ if dialog_manager.current_context().state == AdminMenuSG.room_add_hours_select:
+ await dialog_manager.switch_to(AdminMenuSG.room_add_hours_custom)
+ else:
+ await dialog_manager.switch_to(AdminMenuSG.room_remove_hours_custom)
+
+
+async def on_room_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.room_add_hours_custom:
+ await dialog_manager.switch_to(AdminMenuSG.room_add_hours_remark)
+ else:
+ await dialog_manager.switch_to(AdminMenuSG.room_remove_hours_remark)
+ except ValueError:
+ await message.answer("⚠️ Пожалуйста, введите корректное число")
+
+
+async def on_room_remark_input(
+ message: Message,
+ widget: MessageInput,
+ dialog_manager: DialogManager,
+):
+ if message.text and message.text.strip():
+ dialog_manager.dialog_data["remark"] = message.text.strip()
+ else:
+ dialog_manager.dialog_data["remark"] = None
+
+ if dialog_manager.current_context().state == AdminMenuSG.room_add_hours_remark:
+ await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
+ else:
+ await dialog_manager.switch_to(AdminMenuSG.room_remove_hours_confirm)
+
+
+async def on_room_skip_remark(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+):
+ dialog_manager.dialog_data["remark"] = None
+
+ if dialog_manager.current_context().state == AdminMenuSG.room_add_hours_remark:
+ await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
+ else:
+ await dialog_manager.switch_to(AdminMenuSG.room_remove_hours_confirm)
+
+
+async def get_room_hours_confirm_data(
+ dialog_manager: DialogManager,
+ **kwargs,
+):
+ hours = dialog_manager.dialog_data.get("selected_hours", 0)
+ remark = dialog_manager.dialog_data.get("remark", "")
+ remark_text = f"\n\nПримечание: {remark}" if remark else ""
+
+ return {
+ "hours": hours,
+ "remark_text": remark_text,
+ }
+
+
+@inject
+async def on_room_add_hours_confirm(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+ room_transactions_repository: FromDishka[RoomHoursTransactionsRepository],
+ residents_repository: FromDishka[ResidentsRepository],
+ users_repository: FromDishka[UsersRepository],
+ **kwargs,
+):
+ from aiogram import Bot
+ bot: Bot = dialog_manager.middleware_data.get("bot")
+
+ room_id = dialog_manager.dialog_data.get("selected_room_id")
+ hours = dialog_manager.dialog_data.get("selected_hours")
+ remark = dialog_manager.dialog_data.get("remark")
+ admin_id = callback.from_user.id
+
+ if room_id and hours:
+ transaction, _ = await room_transactions_repository.add_hours(
+ room_id=room_id,
+ amount=hours,
+ admin_id=admin_id,
+ is_active=True,
+ )
+
+ # Отправляем уведомления всем проживающим
+ residents = await residents_repository.get_residents_by_room(room_id)
+ for resident in residents:
+ if resident.user_entity:
+ user = await users_repository.get_user_by_id(resident.user_entity)
+ if user:
+ try:
+ remark_text = f"\n💬 {remark}" if remark else ""
+ await bot.send_message(
+ user.id,
+ f"📢 Уведомление\n\n" + f"Вашей комнате начислено +{hours} ч{remark_text}" + ) + except Exception: + pass + + await dialog_manager.switch_to(AdminMenuSG.room_info) + + +@inject +async def on_room_remove_hours_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + room_transactions_repository: FromDishka[RoomHoursTransactionsRepository], + rooms_repository: FromDishka[RoomsRepository], + residents_repository: FromDishka[ResidentsRepository], + users_repository: FromDishka[UsersRepository], + **kwargs, +): + from aiogram import Bot + bot: Bot = dialog_manager.middleware_data.get("bot") + + room_id = dialog_manager.dialog_data.get("selected_room_id") + hours = dialog_manager.dialog_data.get("selected_hours") + remark = dialog_manager.dialog_data.get("remark") + admin_id = callback.from_user.id + + if room_id and hours: + room = await rooms_repository.get_room_by_id(room_id) + if room and room.active_hours < hours: + await callback.answer( + f"⚠️ Недостаточно часов! У комнаты {room.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч", + show_alert=True + ) + await dialog_manager.switch_to(AdminMenuSG.room_info) + return + + await room_transactions_repository.move_hours_to_completed( + room_id=room_id, + amount=hours, + admin_id=admin_id, + ) + + # Отправляем уведомления всем проживающим + residents = await residents_repository.get_residents_by_room(room_id) + for resident in residents: + if resident.user_entity: + user = await users_repository.get_user_by_id(resident.user_entity) + if user: + try: + remark_text = f"\n💬 {remark}" if remark else "" + await bot.send_message( + user.id, + f"
📢 Уведомление\n\n" + f"С вашей комнаты списано -{hours} ч{remark_text}" + ) + except Exception: + pass + + await dialog_manager.switch_to(AdminMenuSG.room_info) + + +async def on_room_hours_cancel( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.room_info) + + @inject async def get_room_delete_confirm_data( dialog_manager: DialogManager, @@ -245,7 +530,7 @@ async def on_delete_room_cancel( button: Button, dialog_manager: DialogManager, ): - await dialog_manager.switch_to(AdminMenuSG.rooms_list) + await dialog_manager.switch_to(AdminMenuSG.room_info) rooms_select_floor_window = Window( @@ -297,6 +582,34 @@ rooms_list_window = Window( getter=get_rooms_list_data, ) +room_info_window = Window( + Format("{info_content}"), + Row( + Button( + Const("Добавить часы"), + id="room_add_hours_btn", + on_click=on_room_add_hours_click, + ), + Button( + Const("Отнять часы"), + id="room_remove_hours_btn", + on_click=on_room_remove_hours_click, + ), + ), + Button( + Const("🗑 Удалить комнату"), + id="delete_room_btn", + on_click=lambda c, b, m: m.switch_to(AdminMenuSG.room_delete_confirm), + ), + SwitchTo( + Const("◀️ Назад к списку"), + id="back_to_rooms_list", + state=AdminMenuSG.rooms_list, + ), + state=AdminMenuSG.room_info, + getter=get_room_info_data, +) + room_delete_confirm_window = Window( Format("
⚠️ Подтверждение удаления\n\nВы точно хотите удалить комнату
{room_number}?\nЭто действие необратимо и удалит всех резидентов в этой комнате!"),
Row(
@@ -315,6 +628,148 @@ room_delete_confirm_window = Window(
getter=get_room_delete_confirm_data,
)
+room_add_hours_select_window = Window(
+ Const("➕ Добавить часы комнате\n\nВыберите количество часов:"), + Group( + Select( + Format("{item[1]} ч"), + id="room_hours_select_add", + item_id_getter=lambda x: x[0], + items="hours_options", + on_click=on_room_hours_selected, + ), + width=4, + ), + Button( + Const("✏️ Ввести свое количество"), + id="room_custom_hours_add_btn", + on_click=on_room_custom_hours_click, + ), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_room_add_hours", + state=AdminMenuSG.room_info, + ), + state=AdminMenuSG.room_add_hours_select, + getter=get_room_hours_select_data, +) + +room_remove_hours_select_window = Window( + Const("
➖ Отнять часы у комнаты\n\nВыберите количество часов:"), + Group( + Select( + Format("{item[1]} ч"), + id="room_hours_select_remove", + item_id_getter=lambda x: x[0], + items="hours_options", + on_click=on_room_hours_selected, + ), + width=4, + ), + Button( + Const("✏️ Ввести свое количество"), + id="room_custom_hours_remove_btn", + on_click=on_room_custom_hours_click, + ), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_room_remove_hours", + state=AdminMenuSG.room_info, + ), + state=AdminMenuSG.room_remove_hours_select, + getter=get_room_hours_select_data, +) + +room_add_hours_custom_window = Window( + Const("
✏️ Добавить часы\n\nВведите количество часов:"), + MessageInput(on_room_custom_hours_input), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_room_custom_add", + state=AdminMenuSG.room_add_hours_select, + ), + state=AdminMenuSG.room_add_hours_custom, +) + +room_remove_hours_custom_window = Window( + Const("
✏️ Отнять часы\n\nВведите количество часов:"), + MessageInput(on_room_custom_hours_input), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_room_custom_remove", + state=AdminMenuSG.room_remove_hours_select, + ), + state=AdminMenuSG.room_remove_hours_custom, +) + +room_add_hours_remark_window = Window( + Const("
💬 Примечание\n\nВведите примечание к операции (или пропустите):"), + MessageInput(on_room_remark_input), + Button( + Const("⏭ Пропустить"), + id="skip_room_add_remark", + on_click=on_room_skip_remark, + ), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_room_add_remark", + state=AdminMenuSG.room_info, + ), + state=AdminMenuSG.room_add_hours_remark, +) + +room_remove_hours_remark_window = Window( + Const("
💬 Примечание\n\nВведите примечание к операции (или пропустите):"), + MessageInput(on_room_remark_input), + Button( + Const("⏭ Пропустить"), + id="skip_room_remove_remark", + on_click=on_room_skip_remark, + ), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_room_remove_remark", + state=AdminMenuSG.room_info, + ), + state=AdminMenuSG.room_remove_hours_remark, +) + +room_add_hours_confirm_window = Window( + Format("
➕ Подтверждение\n\nВы уверены, что хотите добавить
{hours} часов?{remark_text}"),
+ Row(
+ Button(
+ Const("✅ Да"),
+ id="confirm_room_add_hours",
+ on_click=on_room_add_hours_confirm,
+ ),
+ Button(
+ Const("❌ Нет"),
+ id="cancel_room_add_hours_confirm",
+ on_click=on_room_hours_cancel,
+ ),
+ ),
+ state=AdminMenuSG.room_add_hours_confirm,
+ getter=get_room_hours_confirm_data,
+)
+
+room_remove_hours_confirm_window = Window(
+ Format("➖ Подтверждение\n\nВы уверены, что хотите отнять
{hours} часов?{remark_text}"),
+ Row(
+ Button(
+ Const("✅ Да"),
+ id="confirm_room_remove_hours",
+ on_click=on_room_remove_hours_confirm,
+ ),
+ Button(
+ Const("❌ Нет"),
+ id="cancel_room_remove_hours_confirm",
+ on_click=on_room_hours_cancel,
+ ),
+ ),
+ state=AdminMenuSG.room_remove_hours_confirm,
+ getter=get_room_hours_confirm_data,
+)
+
create_room_select_floor_window = Window(
Format("{content}"),
Group(
diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py
index 882be5f..0e60b90 100644
--- a/src/dutylog/application/bot/user_dialogs/states.py
+++ b/src/dutylog/application/bot/user_dialogs/states.py
@@ -40,7 +40,16 @@ class AdminMenuSG(StatesGroup):
create_floor_confirm = State()
rooms_select_floor = State()
rooms_list = State()
+ room_info = State()
room_delete_confirm = State()
+ room_add_hours_select = State()
+ room_remove_hours_select = State()
+ room_add_hours_custom = State()
+ room_remove_hours_custom = State()
+ room_add_hours_remark = State()
+ room_add_hours_confirm = State()
+ room_remove_hours_remark = State()
+ room_remove_hours_confirm = State()
create_room_select_floor = State()
create_room_input = State()
create_room_confirm = State()