From 381093dcec5fefac29c512ab6b206a53860f1ebc Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 28 Feb 2026 10:10:50 +0300 Subject: [PATCH] commit --- .../admin_dialogs/admin_menu_dialog.py | 156 ++++++++++++------ .../bot/user_dialogs/main_menu_dialog.py | 33 +++- .../bot/user_dialogs/registration_dialog.py | 8 +- .../application/bot/user_dialogs/states.py | 5 +- 4 files changed, 145 insertions(+), 57 deletions(-) diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py index f4899e8..ff61a4d 100644 --- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py @@ -1,12 +1,16 @@ -from aiogram.types import User +from aiogram.types import User, Message, CallbackQuery +from aiogram import Bot +from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest from aiogram_dialog import Dialog, Window, DialogManager from aiogram_dialog.widgets.text import Format, Const -from aiogram_dialog.widgets.kbd import SwitchTo, Back, Start +from aiogram_dialog.widgets.kbd import SwitchTo, Button +from aiogram_dialog.widgets.input import MessageInput 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.users_repository import UsersRepository +from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository from dutylog.infrastructure.utils.config import Config @@ -42,50 +46,21 @@ async def get_admin_menu_data( return {"content": content} -@inject -async def get_users_list_data( - users_repository: FromDishka[UsersRepository], - **kwargs, -): - all_users = await users_repository.get_all_users() - - if not all_users: - users_text = """ -
👥 Список пользователей
- -Пользователи не найдены -""" - else: - users_lines = [] - for user in all_users: - name = user.first_name or user.username or f"ID: {user.id}" - admin_badge = " 👨‍💼" if user.is_admin else "" - users_lines.append( - f"• {name}{admin_badge}\n" - f" 🟢 {user.active_hours} ч | 🔴 {user.inactive_hours} ч" - ) - - users_text = f""" -
👥 Список пользователей
- -{"".join(f"{line}\n\n" for line in users_lines)} -Всего пользователей: {len(all_users)} -""" - - return {"users_content": users_text} - - @inject async def get_statistics_data( users_repository: FromDishka[UsersRepository], + residents_repository: FromDishka[ResidentsRepository], **kwargs, ): all_users = await users_repository.get_all_users() + all_residents = await residents_repository.get_all_residents() total_users = len(all_users) - total_active_hours = sum(user.active_hours for user in all_users) - total_inactive_hours = sum(user.inactive_hours for user in all_users) - admins_count = len([user for user in all_users if user.is_admin]) + total_residents = len(all_residents) + busy_residents = len([r for r in all_residents if r.is_busy]) + total_active_hours = sum(r.active_hours for r in all_residents) + total_inactive_hours = sum(r.inactive_hours for r in all_residents) + admins_count = len([u for u in all_users if u.is_admin]) stats_text = f"""
📊 Статистика системы
@@ -93,6 +68,10 @@ async def get_statistics_data( 👥 Всего пользователей: {total_users} 👨‍💼 Администраторов: {admins_count} +🏠 Всего резидентов: {total_residents} +✅ Привязано к пользователям: {busy_residents} +❌ Свободных: {total_residents - busy_residents} + ━━━━━━━━━━━━━━━━━━━━ 🟢 Всего активных часов: {total_active_hours} ч @@ -103,13 +82,87 @@ async def get_statistics_data( return {"stats_content": stats_text} +async def on_broadcast_message( + message: Message, + widget: MessageInput, + dialog_manager: DialogManager, +): + dialog_manager.dialog_data["broadcast_message_id"] = message.message_id + dialog_manager.dialog_data["broadcast_chat_id"] = message.chat.id + + await message.copy_to(message.chat.id) + await dialog_manager.switch_to(AdminMenuSG.broadcast_confirm) + + +@inject +async def on_broadcast_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + users_repository: FromDishka[UsersRepository], +): + assert callback.message is not None + + bot: Bot = dialog_manager.middleware_data["bot"] + message_id = dialog_manager.dialog_data["broadcast_message_id"] + chat_id = dialog_manager.dialog_data["broadcast_chat_id"] + admin_id = callback.from_user.id + + all_users = await users_repository.get_all_users() + + success_count = 0 + failed_count = 0 + + for user in all_users: + if user.id == admin_id: + continue + + try: + await bot.copy_message( + chat_id=user.id, + from_chat_id=chat_id, + message_id=message_id, + ) + success_count += 1 + except TelegramForbiddenError: + failed_count += 1 + except TelegramBadRequest: + failed_count += 1 + except Exception: + failed_count += 1 + + result_text = f""" +
📢 Результаты рассылки
+ +✅ Успешно отправлено: {success_count} +❌ Не удалось отправить: {failed_count} +📊 Всего пользователей: {len(all_users) - 1} +""" + + await callback.message.answer(result_text) + await dialog_manager.switch_to(AdminMenuSG.main) + + +async def on_broadcast_cancel( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +): + await dialog_manager.switch_to(AdminMenuSG.main) + + admin_menu_dialog = Dialog( Window( Format("{content}"), + SwitchTo( + Const("🏠 Резиденты"), + id="residents_btn", + state=AdminMenuSG.residents, + ), SwitchTo( Const("👥 Пользователи"), id="users_btn", - state=AdminMenuSG.users_list, + state=AdminMenuSG.users, ), SwitchTo( Const("📊 Статистика"), @@ -125,20 +178,31 @@ admin_menu_dialog = Dialog( getter=get_admin_menu_data, ), Window( - Format("{users_content}"), - Back(Const("◀️ Назад")), - state=AdminMenuSG.users_list, - getter=get_users_list_data, + Const("
🏠 Резиденты
\n\nФункционал в разработке"), + SwitchTo(Const("◀️ Назад"), id="back_from_residents", state=AdminMenuSG.main), + state=AdminMenuSG.residents, + ), + Window( + Const("
👥 Пользователи
\n\nФункционал в разработке"), + SwitchTo(Const("◀️ Назад"), id="back_from_users", state=AdminMenuSG.main), + state=AdminMenuSG.users, ), Window( Format("{stats_content}"), - Back(Const("◀️ Назад")), + SwitchTo(Const("◀️ Назад"), id="back_from_stats", state=AdminMenuSG.main), state=AdminMenuSG.statistics, getter=get_statistics_data, ), Window( - Const("
📢 Рассылка
\n\nФункционал в разработке"), - Back(Const("◀️ Назад")), + Const("
📢 Рассылка
\n\nОтправьте сообщение, которое хотите разослать всем пользователям:"), + MessageInput(on_broadcast_message), + SwitchTo(Const("◀️ Отмена"), id="cancel_broadcast", state=AdminMenuSG.main), state=AdminMenuSG.broadcast, ), + Window( + Const("
📢 Подтверждение рассылки
\n\n⚠️ Вы уверены, что хотите отправить это сообщение всем пользователям?"), + Button(Const("✅ Да, отправить"), id="confirm_broadcast", on_click=on_broadcast_confirm), + Button(Const("❌ Нет, отменить"), id="cancel_broadcast_confirm", on_click=on_broadcast_cancel), + state=AdminMenuSG.broadcast_confirm, + ), ) 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 5002552..d1efc24 100644 --- a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py @@ -1,11 +1,11 @@ from aiogram.types import User from aiogram_dialog import Dialog, Window, DialogManager from aiogram_dialog.widgets.text import Format, Const -from aiogram_dialog.widgets.kbd import SwitchTo, Back, Start +from aiogram_dialog.widgets.kbd import SwitchTo, Back from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject -from dutylog.application.bot.user_dialogs.states import MainMenuSG, RegistrationSG +from dutylog.application.bot.user_dialogs.states import MainMenuSG from dutylog.infrastructure.database.repositories.users_repository import UsersRepository from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository from dutylog.infrastructure.database.repositories.hours_transactions_repository import HoursTransactionsRepository @@ -131,10 +131,10 @@ main_menu_dialog = Dialog( state=MainMenuSG.history, when="has_resident", ), - Start( - Const("🔄 Перерегистрация"), - id="reregister_btn", - state=RegistrationSG.select_floor, + SwitchTo( + Const("❓ FAQ"), + id="faq_btn", + state=MainMenuSG.faq, when="is_regular_user", ), state=MainMenuSG.main, @@ -146,5 +146,26 @@ main_menu_dialog = Dialog( state=MainMenuSG.history, getter=get_history_data, ), + Window( + Const("""
Часто задаваемые вопросы
+ +Что это за система? +
Это система учета дежурств в общежитии. Здесь отображаются ваши отработанные и неотработанные часы дежурств.
+ +Что делать, если я зарегистрировался не под собой? +
⚠️ Перерегистрацию может выполнить только администратор. Обратитесь к администратору для исправления данных.
+ +Как начисляются часы? +
Часы начисляются и списываются администраторами системы. Все изменения отображаются в разделе "История".
+ +Что означают активные и неактивные часы? +
🟢 Отработанные часы - часы, которые вы уже отработали +🔴 Неотработанные часы - часы, которые вам еще предстоит отработать
+ +Как связаться с администратором? +
Обратитесь к старосте вашего этажа или в администрацию общежития.
"""), + SwitchTo(Const("◀️ Назад"), id="back_to_main", state=MainMenuSG.main), + state=MainMenuSG.faq, + ), ) diff --git a/src/dutylog/application/bot/user_dialogs/registration_dialog.py b/src/dutylog/application/bot/user_dialogs/registration_dialog.py index a9f82e4..c79f111 100644 --- a/src/dutylog/application/bot/user_dialogs/registration_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/registration_dialog.py @@ -2,7 +2,7 @@ from aiogram.types import CallbackQuery from magic_filter import F from aiogram_dialog import Dialog, Window, DialogManager from aiogram_dialog.widgets.text import Format, Const -from aiogram_dialog.widgets.kbd import Select, Cancel, Group +from aiogram_dialog.widgets.kbd import Select, Group, SwitchTo from dishka import FromDishka from dishka.integrations.aiogram_dialog import inject @@ -118,7 +118,7 @@ async def on_resident_selected( registration_dialog = Dialog( Window( - Const("
🏢 Выбор этажа
\n\nВыберите этаж, на котором вы живете:", when="has_available"), + Const("
🏢 Выбор этажа
\n\n⚠️ Внимание! Перерегистрацию может выполнить только администратор. Выбирайте внимательно!\n\nВыберите этаж, на котором вы живете:", when="has_available"), Const("
⚠️ Нет доступных резидентов
\n\nВсе резиденты уже заняты.\nОбратитесь к администратору.", when=~F["has_available"]), Group( Select( @@ -146,7 +146,7 @@ registration_dialog = Dialog( ), width=3, ), - Cancel(Const("◀️ Назад")), + SwitchTo(Const("◀️ Назад"), id="back_to_floors", state=RegistrationSG.select_floor), state=RegistrationSG.select_room, getter=get_rooms_data, ), @@ -162,7 +162,7 @@ registration_dialog = Dialog( ), width=1, ), - Cancel(Const("◀️ Назад")), + SwitchTo(Const("◀️ Назад"), id="back_to_rooms", state=RegistrationSG.select_room), state=RegistrationSG.select_resident, getter=get_residents_data, ), diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py index 0037c38..0d6fb3c 100644 --- a/src/dutylog/application/bot/user_dialogs/states.py +++ b/src/dutylog/application/bot/user_dialogs/states.py @@ -4,13 +4,16 @@ from aiogram.fsm.state import State, StatesGroup class MainMenuSG(StatesGroup): main = State() history = State() + faq = State() class AdminMenuSG(StatesGroup): main = State() - users_list = State() + residents = State() + users = State() statistics = State() broadcast = State() + broadcast_confirm = State() class RegistrationSG(StatesGroup):