From 3d0dd5775225d4f9504ada9a21641c21a4ed50fc Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 1 Mar 2026 16:19:55 +0300 Subject: [PATCH] update --- .../bot/user_dialogs/main_menu_dialog.py | 2 + .../application/bot/user_dialogs/states.py | 1 + .../bot/user_dialogs/user_menu/history.py | 32 +++++----- .../bot/user_dialogs/user_menu/main_menu.py | 6 ++ .../user_dialogs/user_menu/top_residents.py | 64 +++++++++++++++++++ src/dutylog/application/bot/user_handlers.py | 41 +++++++++++- 6 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 src/dutylog/application/bot/user_dialogs/user_menu/top_residents.py 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 df960ca..db1d295 100644 --- a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py @@ -2,11 +2,13 @@ from aiogram_dialog import Dialog 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.top_residents import top_residents_window from dutylog.application.bot.user_dialogs.user_menu.faq import faq_window main_menu_dialog = Dialog( main_menu_window, history_window, + top_residents_window, faq_window, ) diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py index bb84ef6..e54428b 100644 --- a/src/dutylog/application/bot/user_dialogs/states.py +++ b/src/dutylog/application/bot/user_dialogs/states.py @@ -4,6 +4,7 @@ from aiogram.fsm.state import State, StatesGroup class MainMenuSG(StatesGroup): main = State() history = State() + top_residents = State() faq = State() diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/history.py b/src/dutylog/application/bot/user_dialogs/user_menu/history.py index 248ac7d..b39d01b 100644 --- a/src/dutylog/application/bot/user_dialogs/user_menu/history.py +++ b/src/dutylog/application/bot/user_dialogs/user_menu/history.py @@ -1,8 +1,7 @@ from aiogram.types import User from aiogram_dialog import Window -from aiogram_dialog.widgets.text import Format +from aiogram_dialog.widgets.text import Format, Const 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 @@ -13,6 +12,7 @@ from dutylog.infrastructure.database.repositories.residents_repository import ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( HoursTransactionsRepository, ) +from dutylog.infrastructure.utils.datetime import msk_now @inject @@ -21,19 +21,19 @@ async def get_history_data( residents_repository: FromDishka[ResidentsRepository], transactions_repository: FromDishka[HoursTransactionsRepository], **kwargs, -): +) -> dict[str, str]: 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] + last_10 = transactions_sorted[-10:] if not last_10: history_text = """ @@ -42,20 +42,20 @@ async def get_history_data( История операций пуста """ 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""" + history_text = """
📜 История операций
-{"".join(f"{line}\n" for line in history_lines)} -Показаны последние 10 операций """ + for tx in last_10: + operation = "Начислено" if tx.transaction_type == "increase" else "Списано" + emoji = "+" if tx.transaction_type == "increase" else "−" + + msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None) + date_str = msk_time.strftime("%d.%m.%Y %H:%M") + + remark_text = f"\n💬 {tx.remark}" if tx.remark else "" + + history_text += f"
{operation} {emoji}{tx.amount} ч\n📅 {date_str}{remark_text}
\n" return {"history_content": history_text} 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 index 233e9f0..f77dcc9 100644 --- a/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py +++ b/src/dutylog/application/bot/user_dialogs/user_menu/main_menu.py @@ -108,6 +108,12 @@ main_menu_window = Window( state=MainMenuSG.history, when="has_resident", ), + SwitchTo( + Const("🏆 Топ общежития"), + id="top_btn", + state=MainMenuSG.top_residents, + when="is_regular_user", + ), SwitchTo( Const("❓ FAQ"), id="faq_btn", diff --git a/src/dutylog/application/bot/user_dialogs/user_menu/top_residents.py b/src/dutylog/application/bot/user_dialogs/user_menu/top_residents.py new file mode 100644 index 0000000..df4df5e --- /dev/null +++ b/src/dutylog/application/bot/user_dialogs/user_menu/top_residents.py @@ -0,0 +1,64 @@ +from aiogram_dialog import Window +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.residents_repository import ( + ResidentsRepository, +) + + +@inject +async def get_top_residents_data( + residents_repository: FromDishka[ResidentsRepository], + **kwargs, +) -> dict[str, str]: + all_residents = await residents_repository.get_all_residents() + + residents_with_hours = [ + r for r in all_residents + if (r.inactive_hours + r.active_hours) > 0 + ] + + sorted_residents = sorted( + residents_with_hours, + key=lambda r: r.inactive_hours + r.active_hours, + reverse=True + ) + + top_residents = sorted_residents[:5] + + if not top_residents: + content = """ +
🏆 Топ общежития
+ +⚠️ Нет данных для отображения топа. +""" + else: + content = """ +
🏆 Топ общежития
+ +""" + medals = ["🥇", "🥈", "🥉", "4.", "5."] + + for idx, resident in enumerate(top_residents): + total_hours = resident.inactive_hours + resident.active_hours + name = resident.real_name or "Без имени" + + content += f"
{medals[idx]} {name}{total_hours} ч
\n" + + return {"content": content} + + +top_residents_window = Window( + Format("{content}"), + SwitchTo( + Const("◀️ Назад"), + id="back_to_main", + state=MainMenuSG.main, + ), + state=MainMenuSG.top_residents, + getter=get_top_residents_data, +) diff --git a/src/dutylog/application/bot/user_handlers.py b/src/dutylog/application/bot/user_handlers.py index a69d764..7199888 100644 --- a/src/dutylog/application/bot/user_handlers.py +++ b/src/dutylog/application/bot/user_handlers.py @@ -1,7 +1,8 @@ from aiogram import Router from aiogram.filters import CommandStart -from aiogram.types import Message +from aiogram.types import Message, CallbackQuery, ErrorEvent from aiogram_dialog import DialogManager, StartMode +from aiogram_dialog.api.exceptions import UnknownIntent from dishka import FromDishka from dutylog.application.bot.user_dialogs.states import ( @@ -27,7 +28,7 @@ async def start_handler( users_repository: FromDishka[UsersRepository], residents_repository: FromDishka[ResidentsRepository], config: FromDishka[Config], -): +) -> None: assert message.from_user is not None user = await users_repository.get_or_create_user( @@ -53,3 +54,39 @@ async def start_handler( return await dialog_manager.start(RegistrationSG.select_floor, mode=StartMode.RESET_STACK) + + +@router.error() +async def unknown_intent_handler( + event: ErrorEvent, + dialog_manager: DialogManager, + users_repository: FromDishka[UsersRepository], + residents_repository: FromDishka[ResidentsRepository], + config: FromDishka[Config], +) -> None: + if not isinstance(event.exception, UnknownIntent): + raise event.exception + + user_id = None + if event.update.message and event.update.message.from_user: + user_id = event.update.message.from_user.id + elif event.update.callback_query and event.update.callback_query.from_user: + user_id = event.update.callback_query.from_user.id + + if not user_id: + return + + user = await users_repository.get_user_by_id(user_id) + + is_creator = user_id == config.bot.creator_id + is_admin = user.is_admin if user else False + + if is_admin or is_creator: + await dialog_manager.start(AdminMenuSG.main, mode=StartMode.RESET_STACK) + return + + resident = await residents_repository.get_resident_by_user_id(user_id) + if resident: + await dialog_manager.start(MainMenuSG.main, mode=StartMode.RESET_STACK) + else: + await dialog_manager.start(RegistrationSG.select_floor, mode=StartMode.RESET_STACK)