This commit is contained in:
2026-02-28 10:10:50 +03:00
parent 44182955aa
commit 381093dcec
4 changed files with 145 additions and 57 deletions
@@ -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 import Dialog, Window, DialogManager
from aiogram_dialog.widgets.text import Format, Const 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 import FromDishka
from dishka.integrations.aiogram_dialog import inject from dishka.integrations.aiogram_dialog import inject
from dutylog.application.bot.user_dialogs.states import AdminMenuSG from dutylog.application.bot.user_dialogs.states import AdminMenuSG
from dutylog.infrastructure.database.repositories.users_repository import UsersRepository 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 from dutylog.infrastructure.utils.config import Config
@@ -42,50 +46,21 @@ async def get_admin_menu_data(
return {"content": content} 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 = """
<blockquote>👥 <b>Список пользователей</b></blockquote>
<i>Пользователи не найдены</i>
"""
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"• <b>{name}</b>{admin_badge}\n"
f" 🟢 <code>{user.active_hours}</code> ч | 🔴 <code>{user.inactive_hours}</code> ч"
)
users_text = f"""
<blockquote>👥 <b>Список пользователей</b></blockquote>
{"".join(f"{line}\n\n" for line in users_lines)}
<i>Всего пользователей: {len(all_users)}</i>
"""
return {"users_content": users_text}
@inject @inject
async def get_statistics_data( async def get_statistics_data(
users_repository: FromDishka[UsersRepository], users_repository: FromDishka[UsersRepository],
residents_repository: FromDishka[ResidentsRepository],
**kwargs, **kwargs,
): ):
all_users = await users_repository.get_all_users() all_users = await users_repository.get_all_users()
all_residents = await residents_repository.get_all_residents()
total_users = len(all_users) total_users = len(all_users)
total_active_hours = sum(user.active_hours for user in all_users) total_residents = len(all_residents)
total_inactive_hours = sum(user.inactive_hours for user in all_users) busy_residents = len([r for r in all_residents if r.is_busy])
admins_count = len([user for user in all_users if user.is_admin]) 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""" stats_text = f"""
<blockquote>📊 <b>Статистика системы</b></blockquote> <blockquote>📊 <b>Статистика системы</b></blockquote>
@@ -93,6 +68,10 @@ async def get_statistics_data(
👥 <b>Всего пользователей:</b> <code>{total_users}</code> 👥 <b>Всего пользователей:</b> <code>{total_users}</code>
👨‍💼 <b>Администраторов:</b> <code>{admins_count}</code> 👨‍💼 <b>Администраторов:</b> <code>{admins_count}</code>
🏠 <b>Всего резидентов:</b> <code>{total_residents}</code>
✅ <b>Привязано к пользователям:</b> <code>{busy_residents}</code>
❌ <b>Свободных:</b> <code>{total_residents - busy_residents}</code>
━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━
🟢 <b>Всего активных часов:</b> <code>{total_active_hours}</code> ч 🟢 <b>Всего активных часов:</b> <code>{total_active_hours}</code> ч
@@ -103,13 +82,87 @@ async def get_statistics_data(
return {"stats_content": stats_text} 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"""
<blockquote>📢 <b>Результаты рассылки</b></blockquote>
✅ <b>Успешно отправлено:</b> <code>{success_count}</code>
❌ <b>Не удалось отправить:</b> <code>{failed_count}</code>
📊 <b>Всего пользователей:</b> <code>{len(all_users) - 1}</code>
"""
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( admin_menu_dialog = Dialog(
Window( Window(
Format("{content}"), Format("{content}"),
SwitchTo(
Const("🏠 Резиденты"),
id="residents_btn",
state=AdminMenuSG.residents,
),
SwitchTo( SwitchTo(
Const("👥 Пользователи"), Const("👥 Пользователи"),
id="users_btn", id="users_btn",
state=AdminMenuSG.users_list, state=AdminMenuSG.users,
), ),
SwitchTo( SwitchTo(
Const("📊 Статистика"), Const("📊 Статистика"),
@@ -125,20 +178,31 @@ admin_menu_dialog = Dialog(
getter=get_admin_menu_data, getter=get_admin_menu_data,
), ),
Window( Window(
Format("{users_content}"), Const("<blockquote>🏠 <b>Резиденты</b></blockquote>\n\n<i>Функционал в разработке</i>"),
Back(Const("◀️ Назад")), SwitchTo(Const("◀️ Назад"), id="back_from_residents", state=AdminMenuSG.main),
state=AdminMenuSG.users_list, state=AdminMenuSG.residents,
getter=get_users_list_data, ),
Window(
Const("<blockquote>👥 <b>Пользователи</b></blockquote>\n\n<i>Функционал в разработке</i>"),
SwitchTo(Const("◀️ Назад"), id="back_from_users", state=AdminMenuSG.main),
state=AdminMenuSG.users,
), ),
Window( Window(
Format("{stats_content}"), Format("{stats_content}"),
Back(Const("◀️ Назад")), SwitchTo(Const("◀️ Назад"), id="back_from_stats", state=AdminMenuSG.main),
state=AdminMenuSG.statistics, state=AdminMenuSG.statistics,
getter=get_statistics_data, getter=get_statistics_data,
), ),
Window( Window(
Const("<blockquote>📢 <b>Рассылка</b></blockquote>\n\n<i>Функционал в разработке</i>"), Const("<blockquote>📢 <b>Рассылка</b></blockquote>\n\nОтправьте сообщение, которое хотите разослать всем пользователям:"),
Back(Const("◀️ Назад")), MessageInput(on_broadcast_message),
SwitchTo(Const("◀️ Отмена"), id="cancel_broadcast", state=AdminMenuSG.main),
state=AdminMenuSG.broadcast, state=AdminMenuSG.broadcast,
), ),
Window(
Const("<blockquote>📢 <b>Подтверждение рассылки</b></blockquote>\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,
),
) )
@@ -1,11 +1,11 @@
from aiogram.types import User from aiogram.types import User
from aiogram_dialog import Dialog, Window, DialogManager from aiogram_dialog import Dialog, Window, DialogManager
from aiogram_dialog.widgets.text import Format, Const 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 import FromDishka
from dishka.integrations.aiogram_dialog import inject 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.users_repository import UsersRepository
from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository
from dutylog.infrastructure.database.repositories.hours_transactions_repository import HoursTransactionsRepository from dutylog.infrastructure.database.repositories.hours_transactions_repository import HoursTransactionsRepository
@@ -131,10 +131,10 @@ main_menu_dialog = Dialog(
state=MainMenuSG.history, state=MainMenuSG.history,
when="has_resident", when="has_resident",
), ),
Start( SwitchTo(
Const("🔄 Перерегистрация"), Const("❓ FAQ"),
id="reregister_btn", id="faq_btn",
state=RegistrationSG.select_floor, state=MainMenuSG.faq,
when="is_regular_user", when="is_regular_user",
), ),
state=MainMenuSG.main, state=MainMenuSG.main,
@@ -146,5 +146,26 @@ main_menu_dialog = Dialog(
state=MainMenuSG.history, state=MainMenuSG.history,
getter=get_history_data, getter=get_history_data,
), ),
Window(
Const("""<blockquote>❓ <b>Часто задаваемые вопросы</b></blockquote>
<b>Что это за система?</b>
<blockquote>Это система учета дежурств в общежитии. Здесь отображаются ваши отработанные и неотработанные часы дежурств.</blockquote>
<b>Что делать, если я зарегистрировался не под собой?</b>
<blockquote>⚠️ Перерегистрацию может выполнить только администратор. Обратитесь к администратору для исправления данных.</blockquote>
<b>Как начисляются часы?</b>
<blockquote>Часы начисляются и списываются администраторами системы. Все изменения отображаются в разделе "История".</blockquote>
<b>Что означают активные и неактивные часы?</b>
<blockquote>🟢 <b>Отработанные часы</b> - часы, которые вы уже отработали
🔴 <b>Неотработанные часы</b> - часы, которые вам еще предстоит отработать</blockquote>
<b>Как связаться с администратором?</b>
<blockquote>Обратитесь к старосте вашего этажа или в администрацию общежития.</blockquote>"""),
SwitchTo(Const("◀️ Назад"), id="back_to_main", state=MainMenuSG.main),
state=MainMenuSG.faq,
),
) )
@@ -2,7 +2,7 @@ from aiogram.types import CallbackQuery
from magic_filter import F from magic_filter import F
from aiogram_dialog import Dialog, Window, DialogManager from aiogram_dialog import Dialog, Window, DialogManager
from aiogram_dialog.widgets.text import Format, Const 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 import FromDishka
from dishka.integrations.aiogram_dialog import inject from dishka.integrations.aiogram_dialog import inject
@@ -118,7 +118,7 @@ async def on_resident_selected(
registration_dialog = Dialog( registration_dialog = Dialog(
Window( Window(
Const("<blockquote>🏢 <b>Выбор этажа</b></blockquote>\n\nВыберите этаж, на котором вы живете:", when="has_available"), Const("<blockquote>🏢 <b>Выбор этажа</b></blockquote>\n\n⚠️ <b>Внимание!</b> Перерегистрацию может выполнить только администратор. Выбирайте внимательно!\n\nВыберите этаж, на котором вы живете:", when="has_available"),
Const("<blockquote>⚠️ <b>Нет доступных резидентов</b></blockquote>\n\nВсе резиденты уже заняты.\nОбратитесь к администратору.", when=~F["has_available"]), Const("<blockquote>⚠️ <b>Нет доступных резидентов</b></blockquote>\n\nВсе резиденты уже заняты.\nОбратитесь к администратору.", when=~F["has_available"]),
Group( Group(
Select( Select(
@@ -146,7 +146,7 @@ registration_dialog = Dialog(
), ),
width=3, width=3,
), ),
Cancel(Const("◀️ Назад")), SwitchTo(Const("◀️ Назад"), id="back_to_floors", state=RegistrationSG.select_floor),
state=RegistrationSG.select_room, state=RegistrationSG.select_room,
getter=get_rooms_data, getter=get_rooms_data,
), ),
@@ -162,7 +162,7 @@ registration_dialog = Dialog(
), ),
width=1, width=1,
), ),
Cancel(Const("◀️ Назад")), SwitchTo(Const("◀️ Назад"), id="back_to_rooms", state=RegistrationSG.select_room),
state=RegistrationSG.select_resident, state=RegistrationSG.select_resident,
getter=get_residents_data, getter=get_residents_data,
), ),
@@ -4,13 +4,16 @@ from aiogram.fsm.state import State, StatesGroup
class MainMenuSG(StatesGroup): class MainMenuSG(StatesGroup):
main = State() main = State()
history = State() history = State()
faq = State()
class AdminMenuSG(StatesGroup): class AdminMenuSG(StatesGroup):
main = State() main = State()
users_list = State() residents = State()
users = State()
statistics = State() statistics = State()
broadcast = State() broadcast = State()
broadcast_confirm = State()
class RegistrationSG(StatesGroup): class RegistrationSG(StatesGroup):