import json from aiogram import Bot from aiogram.enums import ContentType from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter from aiogram.types import Message, CallbackQuery, InputMediaPhoto from aiogram_dialog import Window, DialogManager from aiogram_dialog.widgets.text import Format, Const from aiogram_dialog.widgets.kbd import SwitchTo, Button, ScrollingGroup, Select, Row, Group from aiogram_dialog.widgets.input import MessageInput from aiogram_dialog.widgets.media import DynamicMedia from aiogram_dialog.api.entities.media import MediaAttachment, MediaId 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.rooms_repository import ( RoomsRepository, ) from dutylog.infrastructure.database.repositories.floors_repository import ( FloorsRepository, ) from dutylog.infrastructure.database.repositories.residents_repository import ( ResidentsRepository, ) from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( HoursTransactionsRepository, ) from dutylog.infrastructure.database.repositories.users_repository import ( UsersRepository, ) from dutylog.infrastructure.utils.datetime import msk_now async def on_rooms_click( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.rooms_select_floor) @inject async def get_rooms_floors_data( floors_repository: FromDishka[FloorsRepository], **kwargs, ): all_floors = await floors_repository.get_all_floors() all_floors.sort(key=lambda f: f.number) floors_data = [(f"🏢 Этаж {f.number}", f.id) for f in all_floors] content = "
🚪 Комнаты\n\nВыберите этаж для просмотра комнат:" return { "content": content, "floors": floors_data, } async def on_rooms_floor_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["selected_floor_id"] = int(item_id) await dialog_manager.switch_to(AdminMenuSG.rooms_list) @inject async def get_rooms_list_data( dialog_manager: DialogManager, rooms_repository: FromDishka[RoomsRepository], floors_repository: FromDishka[FloorsRepository], **kwargs, ): floor_id = dialog_manager.dialog_data.get("selected_floor_id") if not floor_id: return { "content": "Ошибка: этаж не выбран", "rooms": [], "floor_number": "???", } floor = await floors_repository.get_floor_by_id(floor_id) floor_number = floor.number if floor else "???" rooms = await rooms_repository.get_rooms_by_floor(floor_id) rooms.sort(key=lambda r: r.number) rooms_data = [(f"🚪 Комната {r.number}", r.id) for r in rooms] content = f"
🚪 Комнаты на этаже {floor_number}\n\nВсего комнат:
{len(rooms)}\n\nВыберите комнату для просмотра:"
return {
"content": content,
"rooms": rooms_data,
"floor_number": floor_number,
}
async def on_room_selected(
callback: CallbackQuery,
widget: Select,
dialog_manager: DialogManager,
item_id: str,
):
dialog_manager.dialog_data["selected_room_id"] = int(item_id)
await dialog_manager.switch_to(AdminMenuSG.room_info)
async def on_add_room_click(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
await dialog_manager.switch_to(AdminMenuSG.create_room_select_floor)
@inject
async def get_create_room_floors_data(
floors_repository: FromDishka[FloorsRepository],
**kwargs,
):
all_floors = await floors_repository.get_all_floors()
all_floors.sort(key=lambda f: f.number)
floors_data = [(f"🏢 Этаж {f.number}", f.id) for f in all_floors]
content = "➕ Создание комнаты\n\nВыберите этаж для новой комнаты:" return { "content": content, "floors": floors_data, } async def on_create_room_floor_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, ): dialog_manager.dialog_data["new_room_floor_id"] = int(item_id) await dialog_manager.switch_to(AdminMenuSG.create_room_input) async def on_room_number_input( message: Message, widget: MessageInput, dialog_manager: DialogManager, ): if not message.text: await message.answer("⚠️ Пожалуйста, введите номер комнаты") return try: room_number = int(message.text) if room_number <= 0: await message.answer("⚠️ Номер комнаты должен быть положительным числом") return dialog_manager.dialog_data["new_room_number"] = room_number await dialog_manager.switch_to(AdminMenuSG.create_room_confirm) except ValueError: await message.answer("⚠️ Пожалуйста, введите корректное число") async def get_create_room_confirm_data( dialog_manager: DialogManager, **kwargs, ): room_number = dialog_manager.dialog_data.get("new_room_number", "???") return {"room_number": room_number} @inject async def on_create_room_confirm( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, rooms_repository: FromDishka[RoomsRepository], ): room_number = dialog_manager.dialog_data.get("new_room_number") floor_id = dialog_manager.dialog_data.get("new_room_floor_id") if room_number and floor_id: existing_room = await rooms_repository.get_room_by_number(room_number) if existing_room: await callback.answer( f"⚠️ Комната {room_number} уже существует!", show_alert=True ) await dialog_manager.switch_to(AdminMenuSG.rooms_select_floor) return await rooms_repository.create_room(room_number, floor_id) await callback.answer("✅ Комната создана!") await dialog_manager.switch_to(AdminMenuSG.rooms_select_floor) async def on_create_room_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): 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) total_active_hours = sum(r.active_hours for r in residents) total_inactive_hours = sum(r.inactive_hours for r in residents) 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} (
{resident.active_hours} ч)\n"
else:
residents_info = "\nНет проживающих\n"
info_content = f"🚪 Информация о комнате\n\nНомер:
{room.number}\nЭтаж: {floor_number}\n{residents_info}\n🟢 Отработанные часы (всего): {total_inactive_hours} ч\n🔴 Неотработанные часы (всего): {total_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 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)
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_remark)
async def on_room_custom_hours_click(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
await dialog_manager.switch_to(AdminMenuSG.room_add_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
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_remark)
except ValueError:
await message.answer("⚠️ Пожалуйста, введите корректное число")
async def on_room_remark_input(
message: Message,
widget: MessageInput,
dialog_manager: DialogManager,
):
album = dialog_manager.middleware_data.get("album")
photo_file_ids = []
remark = None
if album:
for msg in album:
if msg.photo:
photo_file_ids.append(msg.photo[-1].file_id)
if msg.caption and msg.caption.strip():
remark = msg.caption.strip()
elif message.photo:
photo_file_ids = [message.photo[-1].file_id]
remark = message.caption.strip() if message.caption and message.caption.strip() else None
elif message.text and message.text.strip():
remark = message.text.strip()
dialog_manager.dialog_data["remark"] = remark
dialog_manager.dialog_data["photo_file_ids"] = photo_file_ids
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
async def on_room_skip_remark(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
dialog_manager.dialog_data["remark"] = None
dialog_manager.dialog_data["photo_file_ids"] = []
await dialog_manager.switch_to(AdminMenuSG.room_add_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 ""
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
has_photo = bool(photo_file_ids)
return {
"hours": hours,
"remark_text": remark_text,
"has_photo": f"✅ Да ({len(photo_file_ids)} шт.)" if has_photo else "❌ Нет",
}
@inject
async def on_room_add_hours_confirm(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
transactions_repository: FromDishka[HoursTransactionsRepository],
residents_repository: FromDishka[ResidentsRepository],
users_repository: FromDishka[UsersRepository],
**kwargs,
):
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:
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
photo_file_id = json.dumps(photo_file_ids) if photo_file_ids else None
results = await transactions_repository.add_hours_to_room(
room_id=room_id,
amount=hours,
admin_id=admin_id,
is_active=True,
remark=remark,
photo_file_id=photo_file_id,
)
for transaction, resident in results:
if resident and 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 ""
notification_text = f"➕ Уведомление\n\nВашей комнате начислено +{hours} ч{remark_text}" if photo_file_ids: if len(photo_file_ids) == 1: await bot.send_photo(user.id, photo_file_ids[0], caption=notification_text) else: media = [InputMediaPhoto(media=fid, caption=notification_text if i == 0 else None) for i, fid in enumerate(photo_file_ids)] await bot.send_media_group(user.id, media=media) else: await bot.send_message(user.id, notification_text) except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter): 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, rooms_repository: FromDishka[RoomsRepository], **kwargs, ): room_id = dialog_manager.dialog_data.get("selected_room_id") if not room_id: return {"room_number": "???"} room = await rooms_repository.get_room_by_id(room_id) room_number = room.number if room else "???" return {"room_number": room_number} @inject async def on_delete_room_confirm( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, rooms_repository: FromDishka[RoomsRepository], ): room_id = dialog_manager.dialog_data.get("selected_room_id") if room_id: await rooms_repository.delete_room(room_id) await callback.answer("✅ Комната удалена!") await dialog_manager.switch_to(AdminMenuSG.rooms_list) async def on_delete_room_cancel( callback: CallbackQuery, button: Button, dialog_manager: DialogManager, ): await dialog_manager.switch_to(AdminMenuSG.room_info) @inject async def get_room_history_data( dialog_manager: DialogManager, rooms_repository: FromDishka[RoomsRepository], transactions_repository: FromDishka[HoursTransactionsRepository], **kwargs, ): room_id = dialog_manager.dialog_data.get("selected_room_id") if not room_id: return {"history_content": "Ошибка: комната не выбрана", "transactions": [], "has_transactions": False} room = await rooms_repository.get_room_by_id(room_id) if not room: return {"history_content": "Ошибка: комната не найдена", "transactions": [], "has_transactions": False} transactions = await transactions_repository.get_room_transactions(room_id) last_10 = transactions[:10] if not last_10: history_text = f"
📜 История операций\n\nКомната: {room.number}\n\nИстория операций пуста" else: history_text = f"
📜 История операций\n\nКомната: {room.number}\n\n" tx_items = [] for tx in last_10: emoji = "+" if tx.transaction_type == "increase" else "−" msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None) date_str = msk_time.strftime("%d.%m.%Y %H:%M") photo_mark = " 📷" if tx.photo_file_id else "" label = f"{emoji}{tx.amount} ч | {date_str}{photo_mark}" tx_items.append((label, str(tx.id))) return { "history_content": history_text, "transactions": tx_items, "has_transactions": len(tx_items) > 0, } @inject async def on_room_transaction_selected( callback: CallbackQuery, widget: Select, dialog_manager: DialogManager, item_id: str, transactions_repository: FromDishka[HoursTransactionsRepository], users_repository: FromDishka[UsersRepository], ): dialog_manager.dialog_data["selected_transaction_id"] = int(item_id) tx = await transactions_repository.get_transaction_by_id(int(item_id)) if tx: photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx) if len(photo_file_ids) > 1: operation = "Начислено" if tx.transaction_type == "increase" else "Списано" emoji = "+" if tx.transaction_type == "increase" else "−" msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None) date_str = msk_time.strftime("%d.%m.%Y %H:%M") remark_text = f"\n💬 {tx.remark}" if tx.remark else "" admin_info = "" if tx.admin_id: admin = await users_repository.get_user_by_id(tx.admin_id) if admin: admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}" admin_info = f"\n👨💼 Администратор: {admin_name}" caption = f"📋 Детали транзакции\n\n⚙️ Операция: {operation}\n🔢 Количество: {emoji}
{tx.amount} ч\n📅 Дата: {date_str}{remark_text}{admin_info}"
bot: Bot = dialog_manager.middleware_data.get("bot")
try:
media = [InputMediaPhoto(media=fid, caption=caption if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
await bot.send_media_group(callback.message.chat.id, media=media)
except Exception:
pass
await dialog_manager.switch_to(AdminMenuSG.room_history_detail)
@inject
async def get_room_transaction_detail_data(
dialog_manager: DialogManager,
transactions_repository: FromDishka[HoursTransactionsRepository],
users_repository: FromDishka[UsersRepository],
**kwargs,
):
tx_id = dialog_manager.dialog_data.get("selected_transaction_id")
if not tx_id:
return {"detail_content": "Ошибка: транзакция не выбрана", "photo_media": None, "has_multiple_photos": False}
tx = await transactions_repository.get_transaction_by_id(int(tx_id))
if not tx:
return {"detail_content": "Ошибка: транзакция не найдена", "photo_media": None, "has_multiple_photos": False}
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
emoji = "+" if tx.transaction_type == "increase" else "−"
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
remark_text = f"\n💬 {tx.remark}" if tx.remark else ""
admin_info = ""
if tx.admin_id:
admin = await users_repository.get_user_by_id(tx.admin_id)
if admin:
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
admin_info = f"\n👨💼 Администратор: {admin_name}"
content = f"""
📋 Детали транзакции⚙️ Операция: {operation} 🔢 Количество: {emoji}
{tx.amount} ч
📅 Дата: {date_str}{remark_text}{admin_info}
"""
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
photo_media = None
has_multiple_photos = len(photo_file_ids) > 1
if photo_file_ids and not has_multiple_photos:
photo_media = MediaAttachment(
type=ContentType.PHOTO,
file_id=MediaId(file_id=photo_file_ids[0]),
)
if has_multiple_photos:
detail_content = ""
else:
detail_content = content
return {
"detail_content": detail_content,
"photo_media": photo_media,
"has_multiple_photos": has_multiple_photos,
}
rooms_select_floor_window = Window(
Format("{content}"),
Group(
Select(
Format("{item[0]}"),
id="rooms_floors_select",
item_id_getter=lambda x: x[1],
items="floors",
on_click=on_rooms_floor_selected,
),
width=2,
),
Button(
Const("➕ Добавить комнату"),
id="add_room_btn",
on_click=on_add_room_click,
),
SwitchTo(
Const("◀️ Назад"),
id="back_to_admin_menu_from_rooms",
state=AdminMenuSG.main,
),
state=AdminMenuSG.rooms_select_floor,
getter=get_rooms_floors_data,
)
rooms_list_window = Window(
Format("{content}"),
ScrollingGroup(
Select(
Format("{item[0]}"),
id="rooms_select",
item_id_getter=lambda x: x[1],
items="rooms",
on_click=on_room_selected,
),
id="rooms_scroll",
width=1,
height=7,
),
SwitchTo(
Const("◀️ Назад к этажам"),
id="back_to_rooms_floors",
state=AdminMenuSG.rooms_select_floor,
),
state=AdminMenuSG.rooms_list,
getter=get_rooms_list_data,
)
room_info_window = Window(
Format("{info_content}"),
Button(
Const("Добавить часы"),
id="room_add_hours_btn",
on_click=on_room_add_hours_click,
),
Button(
Const("📜 История"),
id="room_history_btn",
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.room_history),
),
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(
Button(
Const("✅ Да, удалить"),
id="confirm_delete_room",
on_click=on_delete_room_confirm,
),
Button(
Const("❌ Отмена"),
id="cancel_delete_room",
on_click=on_delete_room_cancel,
),
),
state=AdminMenuSG.room_delete_confirm,
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_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_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_add_hours_confirm_window = Window( Format("
➕ Подтверждение\n\nВы уверены, что хотите добавить
{hours} часов?{remark_text}\n\n📷 Фото: {has_photo}"),
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,
)
create_room_select_floor_window = Window(
Format("{content}"),
Group(
Select(
Format("{item[0]}"),
id="create_room_floors_select",
item_id_getter=lambda x: x[1],
items="floors",
on_click=on_create_room_floor_selected,
),
width=2,
),
SwitchTo(
Const("◀️ Отмена"),
id="cancel_create_room_floor",
state=AdminMenuSG.rooms_select_floor,
),
state=AdminMenuSG.create_room_select_floor,
getter=get_create_room_floors_data,
)
create_room_input_window = Window(
Const("➕ Создание комнаты\n\nВведите номер комнаты:"), MessageInput(on_room_number_input), SwitchTo( Const("◀️ Назад"), id="back_to_create_room_floor", state=AdminMenuSG.create_room_select_floor, ), state=AdminMenuSG.create_room_input, ) create_room_confirm_window = Window( Format("
✅ Подтверждение\n\nСоздать комнату
{room_number}?"),
Row(
Button(
Const("✅ Да"),
id="confirm_create_room",
on_click=on_create_room_confirm,
),
Button(
Const("❌ Нет"),
id="cancel_create_room",
on_click=on_create_room_cancel,
),
),
state=AdminMenuSG.create_room_confirm,
getter=get_create_room_confirm_data,
)
room_history_window = Window(
Format("{history_content}"),
ScrollingGroup(
Select(
Format("{item[0]}"),
id="room_transactions_select",
item_id_getter=lambda x: x[1],
items="transactions",
on_click=on_room_transaction_selected,
),
id="room_transactions_scroll",
width=1,
height=8,
when="has_transactions",
),
SwitchTo(
Const("◀️ Назад"),
id="back_to_room_info",
state=AdminMenuSG.room_info,
),
state=AdminMenuSG.room_history,
getter=get_room_history_data,
)
room_history_detail_window = Window(
DynamicMedia("photo_media"),
Format("{detail_content}"),
SwitchTo(
Const("◀️ К списку"),
id="back_to_room_history_from_detail",
state=AdminMenuSG.room_history,
),
state=AdminMenuSG.room_history_detail,
getter=get_room_transaction_detail_data,
)