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):