This commit is contained in:
2026-03-01 16:19:55 +03:00
parent cf0b0309fc
commit 3d0dd57752
6 changed files with 128 additions and 18 deletions
@@ -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.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.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 from dutylog.application.bot.user_dialogs.user_menu.faq import faq_window
main_menu_dialog = Dialog( main_menu_dialog = Dialog(
main_menu_window, main_menu_window,
history_window, history_window,
top_residents_window,
faq_window, faq_window,
) )
@@ -4,6 +4,7 @@ from aiogram.fsm.state import State, StatesGroup
class MainMenuSG(StatesGroup): class MainMenuSG(StatesGroup):
main = State() main = State()
history = State() history = State()
top_residents = State()
faq = State() faq = State()
@@ -1,8 +1,7 @@
from aiogram.types import User from aiogram.types import User
from aiogram_dialog import Window 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.kbd import Back
from aiogram_dialog.widgets.text import Const
from dishka import FromDishka from dishka import FromDishka
from dishka.integrations.aiogram_dialog import inject 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 ( from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
HoursTransactionsRepository, HoursTransactionsRepository,
) )
from dutylog.infrastructure.utils.datetime import msk_now
@inject @inject
@@ -21,19 +21,19 @@ async def get_history_data(
residents_repository: FromDishka[ResidentsRepository], residents_repository: FromDishka[ResidentsRepository],
transactions_repository: FromDishka[HoursTransactionsRepository], transactions_repository: FromDishka[HoursTransactionsRepository],
**kwargs, **kwargs,
): ) -> dict[str, str]:
resident = await residents_repository.get_resident_by_user_id(event_from_user.id) resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
if not resident: if not resident:
history_text = """ history_text = """
<blockquote>📜 <b>История операций</b></blockquote> <blockquote>📜 <b>История операций</b></blockquote>
<i>Профиль не найден</i> ⚠️ <i>Профиль не найден</i>
""" """
else: else:
transactions = await transactions_repository.get_resident_history(resident.id) transactions = await transactions_repository.get_resident_history(resident.id)
transactions_sorted = sorted(transactions, key=lambda x: x.created_at) transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
last_10 = transactions_sorted[:10] last_10 = transactions_sorted[-10:]
if not last_10: if not last_10:
history_text = """ history_text = """
@@ -42,20 +42,20 @@ async def get_history_data(
<i>История операций пуста</i> <i>История операций пуста</i>
""" """
else: else:
history_lines = [] history_text = """
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} <code>{tx.amount}</code> ч • <i>{date_str}</i>"
)
history_text = f"""
<blockquote>📜 <b>История операций</b></blockquote> <blockquote>📜 <b>История операций</b></blockquote>
{"".join(f"{line}\n" for line in history_lines)}
<i>Показаны последние 10 операций</i>
""" """
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💬 <i>{tx.remark}</i>" if tx.remark else ""
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n"
return {"history_content": history_text} return {"history_content": history_text}
@@ -108,6 +108,12 @@ main_menu_window = Window(
state=MainMenuSG.history, state=MainMenuSG.history,
when="has_resident", when="has_resident",
), ),
SwitchTo(
Const("🏆 Топ общежития"),
id="top_btn",
state=MainMenuSG.top_residents,
when="is_regular_user",
),
SwitchTo( SwitchTo(
Const("❓ FAQ"), Const("❓ FAQ"),
id="faq_btn", id="faq_btn",
@@ -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 = """
<blockquote>🏆 <b>Топ общежития</b></blockquote>
⚠️ Нет данных для отображения топа.
"""
else:
content = """
<blockquote>🏆 <b>Топ общежития</b></blockquote>
"""
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"<blockquote>{medals[idx]} <b>{name}</b> — <code>{total_hours}</code> ч</blockquote>\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,
)
+39 -2
View File
@@ -1,7 +1,8 @@
from aiogram import Router from aiogram import Router
from aiogram.filters import CommandStart 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 import DialogManager, StartMode
from aiogram_dialog.api.exceptions import UnknownIntent
from dishka import FromDishka from dishka import FromDishka
from dutylog.application.bot.user_dialogs.states import ( from dutylog.application.bot.user_dialogs.states import (
@@ -27,7 +28,7 @@ async def start_handler(
users_repository: FromDishka[UsersRepository], users_repository: FromDishka[UsersRepository],
residents_repository: FromDishka[ResidentsRepository], residents_repository: FromDishka[ResidentsRepository],
config: FromDishka[Config], config: FromDishka[Config],
): ) -> None:
assert message.from_user is not None assert message.from_user is not None
user = await users_repository.get_or_create_user( user = await users_repository.get_or_create_user(
@@ -53,3 +54,39 @@ async def start_handler(
return return
await dialog_manager.start(RegistrationSG.select_floor, mode=StartMode.RESET_STACK) 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)