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)