diff --git a/src/dutylog/application/__main__.py b/src/dutylog/application/__main__.py
index 606b0dd..b05e501 100644
--- a/src/dutylog/application/__main__.py
+++ b/src/dutylog/application/__main__.py
@@ -11,6 +11,7 @@ from dishka.integrations.aiogram import setup_dishka
from dutylog.application.bot.user_handlers import router as user_router
from dutylog.application.bot.user_dialogs import main_menu_dialog
from dutylog.application.bot.admin_dialogs import admin_menu_dialog
+from dutylog.application.bot.creator_dialogs import creator_menu_dialog
from dutylog.application.bot.user_dialogs.registration_dialog import registration_dialog
from dutylog.infrastructure.ioc import (
ConfigProvider,
@@ -46,6 +47,7 @@ async def main():
dp.include_router(user_router)
dp.include_router(main_menu_dialog)
dp.include_router(admin_menu_dialog)
+ dp.include_router(creator_menu_dialog)
dp.include_router(registration_dialog)
setup_dialogs(dp)
diff --git a/src/dutylog/application/bot/admin_dialogs/main_menu.py b/src/dutylog/application/bot/admin_dialogs/main_menu.py
index 4c75de1..79e9212 100644
--- a/src/dutylog/application/bot/admin_dialogs/main_menu.py
+++ b/src/dutylog/application/bot/admin_dialogs/main_menu.py
@@ -6,7 +6,7 @@ from dishka import FromDishka
from dishka.integrations.aiogram_dialog import inject
from datetime import datetime, timedelta
-from dutylog.application.bot.user_dialogs.states import AdminMenuSG
+from dutylog.application.bot.user_dialogs.states import AdminMenuSG, CreatorMenuSG
from dutylog.infrastructure.database.repositories.users_repository import (
UsersRepository,
)
@@ -86,7 +86,7 @@ async def get_admin_menu_data(
Выберите действие:
"""
- return {"content": content}
+ return {"content": content, "is_creator": is_creator}
@inject
@@ -137,6 +137,10 @@ async def on_reporting_period_click(callback, button, dialog_manager):
await dialog_manager.switch_to(AdminMenuSG.reporting_period)
+async def on_admins_click(callback, button, dialog_manager):
+ await dialog_manager.start(CreatorMenuSG.admins_list)
+
+
main_menu_window = Window(
Format("{content}"),
SwitchTo(
@@ -161,6 +165,12 @@ main_menu_window = Window(
id="reporting_period_btn",
on_click=on_reporting_period_click,
),
+ Button(
+ Const("👨💼 Админы"),
+ id="admins_btn",
+ on_click=on_admins_click,
+ when="is_creator",
+ ),
SwitchTo(
Const("📊 Статистика"),
id="stats_btn",
diff --git a/src/dutylog/application/bot/admin_dialogs/reporting_period_management.py b/src/dutylog/application/bot/admin_dialogs/reporting_period_management.py
index e287736..f6f62fc 100644
--- a/src/dutylog/application/bot/admin_dialogs/reporting_period_management.py
+++ b/src/dutylog/application/bot/admin_dialogs/reporting_period_management.py
@@ -63,8 +63,6 @@ async def get_reporting_period_data(
Дата начала: {start_date.strftime('%d.%m.%Y')}
Прошло дней: {days_passed}
-━━━━━━━━━━━━━━━━━━━━
-
Используйте кнопки ниже для управления периодом.
"""
has_active = True
@@ -74,8 +72,6 @@ async def get_reporting_period_data(
Статус: ⚪️ Нет активного периода
-━━━━━━━━━━━━━━━━━━━━
-
Создайте новый отчётный период, чтобы начать учёт дежурств.
"""
has_active = False
diff --git a/src/dutylog/application/bot/creator_dialogs/__init__.py b/src/dutylog/application/bot/creator_dialogs/__init__.py
new file mode 100644
index 0000000..4fa302d
--- /dev/null
+++ b/src/dutylog/application/bot/creator_dialogs/__init__.py
@@ -0,0 +1,5 @@
+from dutylog.application.bot.creator_dialogs.creator_menu_dialog import (
+ creator_menu_dialog,
+)
+
+__all__ = ["creator_menu_dialog"]
diff --git a/src/dutylog/application/bot/creator_dialogs/admins_management.py b/src/dutylog/application/bot/creator_dialogs/admins_management.py
new file mode 100644
index 0000000..893a050
--- /dev/null
+++ b/src/dutylog/application/bot/creator_dialogs/admins_management.py
@@ -0,0 +1,374 @@
+from aiogram.types import CallbackQuery
+from aiogram_dialog import Window, DialogManager
+from aiogram_dialog.widgets.text import Format, Const
+from aiogram_dialog.widgets.kbd import SwitchTo, Button, Select, ScrollingGroup, Row
+from dishka import FromDishka
+from dishka.integrations.aiogram_dialog import inject
+
+from dutylog.application.bot.user_dialogs.states import CreatorMenuSG, AdminMenuSG
+from dutylog.infrastructure.database.repositories.users_repository import UsersRepository
+
+
+@inject
+async def get_admins_list_data(
+ users_repository: FromDishka[UsersRepository],
+ **kwargs,
+) -> dict[str, str | list[tuple[str, int]]]:
+ all_users = await users_repository.get_all_users()
+ admins = [u for u in all_users if u.is_admin]
+
+ if not admins:
+ content = """
+
👨💼 Администраторы+ +⚠️ Нет администраторов в системе. +""" + admins_list = [] + else: + content = f""" +
👨💼 Администраторы+ +Всего администраторов:
{len(admins)}
+
+Выберите администратора для просмотра информации:
+"""
+ admins_list = []
+ for admin in admins:
+ display_name = f"@{admin.username}" if admin.username else f"ID: {admin.id}"
+ if admin.first_name:
+ display_name = f"{admin.first_name} ({display_name})"
+ admins_list.append((display_name, admin.id))
+
+ return {
+ "content": content,
+ "admins": admins_list,
+ }
+
+
+async def on_admin_selected(
+ callback: CallbackQuery,
+ widget,
+ dialog_manager: DialogManager,
+ item_id: str,
+) -> None:
+ dialog_manager.dialog_data["selected_admin_id"] = int(item_id)
+ await dialog_manager.switch_to(CreatorMenuSG.admin_info)
+
+
+async def on_add_admin_click(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+) -> None:
+ await dialog_manager.switch_to(CreatorMenuSG.add_admin_select_user)
+
+
+@inject
+async def get_admin_info_data(
+ users_repository: FromDishka[UsersRepository],
+ dialog_manager: DialogManager,
+ **kwargs,
+) -> dict[str, str]:
+ admin_id = dialog_manager.dialog_data.get("selected_admin_id")
+ if not admin_id:
+ return {"content": "⚠️ Администратор не выбран"}
+
+ admin = await users_repository.get_user_by_id(int(admin_id))
+
+ if not admin:
+ return {"content": "⚠️ Администратор не найден"}
+
+ username = f"@{admin.username}" if admin.username else "—"
+ first_name = admin.first_name or "—"
+ last_name = admin.last_name or "—"
+
+ content = f"""
+👨💼 Информация об администраторе+ +ID:
{admin.id}
+Username: {username}
+Имя: {first_name}
+Фамилия: {last_name}
+
+Дата добавления: {admin.created_at.strftime('%d.%m.%Y %H:%M')}
+"""
+
+ return {"content": content}
+
+
+async def on_remove_admin_click(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+) -> None:
+ await dialog_manager.switch_to(CreatorMenuSG.remove_admin_confirm)
+
+
+@inject
+async def get_remove_admin_confirm_data(
+ users_repository: FromDishka[UsersRepository],
+ dialog_manager: DialogManager,
+ **kwargs,
+) -> dict[str, str]:
+ admin_id = dialog_manager.dialog_data.get("selected_admin_id")
+ if not admin_id:
+ return {"content": "⚠️ Администратор не выбран"}
+
+ admin = await users_repository.get_user_by_id(int(admin_id))
+
+ if not admin:
+ return {"content": "⚠️ Администратор не найден"}
+
+ username = f"@{admin.username}" if admin.username else f"ID: {admin.id}"
+ display_name = admin.first_name or username
+
+ content = f"""
+⚠️ Подтверждение удаления+ +Вы уверены, что хотите удалить администратора {display_name}? + +Пользователь потеряет права администратора. +""" + + return {"content": content} + + +@inject +async def on_remove_admin_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + users_repository: FromDishka[UsersRepository], +) -> None: + admin_id = dialog_manager.dialog_data.get("selected_admin_id") + if not admin_id: + await callback.answer("⚠️ Администратор не выбран", show_alert=True) + return + + await users_repository.update_user(int(admin_id), is_admin=False) + + await callback.answer("✅ Администратор удалён!") + await dialog_manager.switch_to(CreatorMenuSG.admins_list) + + +async def on_remove_admin_cancel( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +) -> None: + await dialog_manager.switch_to(CreatorMenuSG.admin_info) + + +@inject +async def get_add_admin_select_user_data( + users_repository: FromDishka[UsersRepository], + **kwargs, +) -> dict[str, str | list[tuple[str, int]]]: + all_users = await users_repository.get_all_users() + non_admin_users = [u for u in all_users if not u.is_admin] + + if not non_admin_users: + content = """ +
➕ Добавить администратора+ +⚠️ Нет пользователей, которых можно сделать администраторами. + +Все пользователи уже являются администраторами. +""" + users_list = [] + else: + content = f""" +
➕ Добавить администратора+ +Всего пользователей:
{len(non_admin_users)}
+
+Выберите пользователя для назначения администратором:
+"""
+ users_list = []
+ for user in non_admin_users:
+ display_name = f"@{user.username}" if user.username else f"ID: {user.id}"
+ if user.first_name:
+ display_name = f"{user.first_name} ({display_name})"
+ users_list.append((display_name, user.id))
+
+ return {
+ "content": content,
+ "users": users_list,
+ }
+
+
+async def on_user_selected(
+ callback: CallbackQuery,
+ widget,
+ dialog_manager: DialogManager,
+ item_id: str,
+) -> None:
+ dialog_manager.dialog_data["selected_user_id"] = int(item_id)
+ await dialog_manager.switch_to(CreatorMenuSG.add_admin_confirm)
+
+
+@inject
+async def get_add_admin_confirm_data(
+ users_repository: FromDishka[UsersRepository],
+ dialog_manager: DialogManager,
+ **kwargs,
+) -> dict[str, str]:
+ user_id = dialog_manager.dialog_data.get("selected_user_id")
+ if not user_id:
+ return {"content": "⚠️ Пользователь не выбран"}
+
+ user = await users_repository.get_user_by_id(int(user_id))
+
+ if not user:
+ return {"content": "⚠️ Пользователь не найден"}
+
+ username = f"@{user.username}" if user.username else f"ID: {user.id}"
+ display_name = user.first_name or username
+
+ content = f"""
+⚠️ Подтверждение+ +Вы уверены, что хотите назначить {display_name} администратором? + +Пользователь получит доступ к панели управления. +""" + + return {"content": content} + + +@inject +async def on_add_admin_confirm( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, + users_repository: FromDishka[UsersRepository], +) -> None: + user_id = dialog_manager.dialog_data.get("selected_user_id") + if not user_id: + await callback.answer("⚠️ Пользователь не выбран", show_alert=True) + return + + await users_repository.update_user(int(user_id), is_admin=True) + + await callback.answer("✅ Администратор добавлен!") + await dialog_manager.switch_to(CreatorMenuSG.admins_list) + + +async def on_add_admin_cancel( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +) -> None: + await dialog_manager.switch_to(CreatorMenuSG.add_admin_select_user) + + +async def on_back_to_main( + callback: CallbackQuery, + button: Button, + dialog_manager: DialogManager, +) -> None: + await dialog_manager.done() + + +admins_list_window = Window( + Format("{content}"), + ScrollingGroup( + Select( + Format("{item[0]}"), + id="admin_select", + item_id_getter=lambda x: x[1], + items="admins", + on_click=on_admin_selected, + ), + id="admins_scroll", + width=1, + height=8, + ), + Button( + Const("➕ Добавить администратора"), + id="add_admin_btn", + on_click=on_add_admin_click, + ), + Button( + Const("◀️ Назад"), + id="back_to_main_from_admins", + on_click=on_back_to_main, + ), + state=CreatorMenuSG.admins_list, + getter=get_admins_list_data, +) + +admin_info_window = Window( + Format("{content}"), + Button( + Const("🗑 Удалить администратора"), + id="remove_admin_btn", + on_click=on_remove_admin_click, + ), + SwitchTo( + Const("◀️ Назад"), + id="back_to_admins_list", + state=CreatorMenuSG.admins_list, + ), + state=CreatorMenuSG.admin_info, + getter=get_admin_info_data, +) + +remove_admin_confirm_window = Window( + Format("{content}"), + Row( + Button( + Const("✅ Да"), + id="confirm_remove_admin", + on_click=on_remove_admin_confirm, + ), + Button( + Const("❌ Нет"), + id="cancel_remove_admin", + on_click=on_remove_admin_cancel, + ), + ), + state=CreatorMenuSG.remove_admin_confirm, + getter=get_remove_admin_confirm_data, +) + +add_admin_select_user_window = Window( + Format("{content}"), + ScrollingGroup( + Select( + Format("{item[0]}"), + id="user_select", + item_id_getter=lambda x: x[1], + items="users", + on_click=on_user_selected, + ), + id="users_scroll", + width=1, + height=8, + ), + SwitchTo( + Const("◀️ Назад"), + id="back_to_admins_from_add", + state=CreatorMenuSG.admins_list, + ), + state=CreatorMenuSG.add_admin_select_user, + getter=get_add_admin_select_user_data, +) + +add_admin_confirm_window = Window( + Format("{content}"), + Row( + Button( + Const("✅ Да"), + id="confirm_add_admin", + on_click=on_add_admin_confirm, + ), + Button( + Const("❌ Нет"), + id="cancel_add_admin", + on_click=on_add_admin_cancel, + ), + ), + state=CreatorMenuSG.add_admin_confirm, + getter=get_add_admin_confirm_data, +) diff --git a/src/dutylog/application/bot/creator_dialogs/creator_menu_dialog.py b/src/dutylog/application/bot/creator_dialogs/creator_menu_dialog.py new file mode 100644 index 0000000..cedc4a1 --- /dev/null +++ b/src/dutylog/application/bot/creator_dialogs/creator_menu_dialog.py @@ -0,0 +1,18 @@ +from aiogram_dialog import Dialog + +from dutylog.application.bot.creator_dialogs.admins_management import ( + admins_list_window, + admin_info_window, + remove_admin_confirm_window, + add_admin_select_user_window, + add_admin_confirm_window, +) + + +creator_menu_dialog = Dialog( + admins_list_window, + admin_info_window, + remove_admin_confirm_window, + add_admin_select_user_window, + add_admin_confirm_window, +) diff --git a/src/dutylog/application/bot/user_dialogs/registration_dialog.py b/src/dutylog/application/bot/user_dialogs/registration_dialog.py index f8056d5..edee3dd 100644 --- a/src/dutylog/application/bot/user_dialogs/registration_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/registration_dialog.py @@ -116,19 +116,11 @@ async def on_resident_selected( widget: Select, dialog_manager: DialogManager, item_id: str, - residents_repository: FromDishka[ResidentsRepository], - users_repository: FromDishka[UsersRepository], + residents_repository: FromDishka[ResidentsRepository] ): user_id = callback.from_user.id resident_id = int(item_id) - await users_repository.get_or_create_user( - user_id=user_id, - username=callback.from_user.username, - first_name=callback.from_user.first_name, - last_name=callback.from_user.last_name, - ) - await residents_repository.bind_user_to_resident(resident_id, user_id) await callback.answer("✅ Регистрация успешна!") diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py index a1563bc..bb84ef6 100644 --- a/src/dutylog/application/bot/user_dialogs/states.py +++ b/src/dutylog/application/bot/user_dialogs/states.py @@ -49,6 +49,14 @@ class AdminMenuSG(StatesGroup): broadcast_confirm = State() +class CreatorMenuSG(StatesGroup): + admins_list = State() + admin_info = State() + remove_admin_confirm = State() + add_admin_select_user = State() + add_admin_confirm = State() + + class RegistrationSG(StatesGroup): select_floor = State() select_room = State() diff --git a/src/dutylog/application/bot/user_handlers.py b/src/dutylog/application/bot/user_handlers.py index b54807e..a69d764 100644 --- a/src/dutylog/application/bot/user_handlers.py +++ b/src/dutylog/application/bot/user_handlers.py @@ -29,7 +29,13 @@ async def start_handler( config: FromDishka[Config], ): assert message.from_user is not None - user = await users_repository.get_user_by_id(message.from_user.id) + + user = await users_repository.get_or_create_user( + user_id=message.from_user.id, + username=message.from_user.username, + first_name=message.from_user.first_name, + last_name=message.from_user.last_name, + ) is_creator = message.from_user.id == config.bot.creator_id is_admin = user.is_admin if user else False diff --git a/src/dutylog/infrastructure/database/repositories/users_repository.py b/src/dutylog/infrastructure/database/repositories/users_repository.py index f79e154..fefa6de 100644 --- a/src/dutylog/infrastructure/database/repositories/users_repository.py +++ b/src/dutylog/infrastructure/database/repositories/users_repository.py @@ -50,3 +50,6 @@ class UsersRepository: async def get_all_users(self) -> list[User]: return await self.users_dao.get_all() + + async def update_user(self, user_id: int, **kwargs) -> User | None: + return await self.users_dao.update(user_id, **kwargs) diff --git a/test_report_2026-03-01_2026-03-01.xlsx b/test_report_2026-03-01_2026-03-01.xlsx deleted file mode 100644 index 47a9a97..0000000 Binary files a/test_report_2026-03-01_2026-03-01.xlsx and /dev/null differ diff --git a/test_report_generation.py b/test_report_generation.py deleted file mode 100644 index fbd57c8..0000000 --- a/test_report_generation.py +++ /dev/null @@ -1,79 +0,0 @@ -import asyncio -from datetime import date - -from dutylog.infrastructure.database.config import create_engine, create_session_maker -from dutylog.infrastructure.database.dao.hours_transactions_dao import HoursTransactionsDAO -from dutylog.infrastructure.database.dao.residents_dao import ResidentsDAO -from dutylog.infrastructure.database.dao.rooms_dao import RoomsDAO -from dutylog.infrastructure.database.dao.floors_dao import FloorsDAO -from dutylog.infrastructure.database.dao.reporting_periods_dao import ReportingPeriodsDAO -from dutylog.infrastructure.database.dao.users_dao import UsersDAO -from dutylog.infrastructure.database.repositories.hours_transactions_repository import ( - HoursTransactionsRepository, -) -from dutylog.infrastructure.database.repositories.residents_repository import ResidentsRepository -from dutylog.infrastructure.database.repositories.rooms_repository import RoomsRepository -from dutylog.infrastructure.database.repositories.floors_repository import FloorsRepository -from dutylog.infrastructure.database.repositories.reporting_periods_repository import ( - ReportingPeriodsRepository, -) -from dutylog.infrastructure.database.repositories.users_repository import UsersRepository -from dutylog.infrastructure.utils.config import load_config -from dutylog.services.report_service import ReportService - - -async def test_report_generation() -> None: - config = load_config() - engine = create_engine(config.database.url) - session_maker = create_session_maker(engine) - - async with session_maker() as session: - hours_transactions_dao = HoursTransactionsDAO(session) - residents_dao = ResidentsDAO(session) - rooms_dao = RoomsDAO(session) - floors_dao = FloorsDAO(session) - reporting_periods_dao = ReportingPeriodsDAO(session) - users_dao = UsersDAO(session) - - hours_transactions_repository = HoursTransactionsRepository( - hours_transactions_dao, residents_dao - ) - residents_repository = ResidentsRepository(residents_dao) - rooms_repository = RoomsRepository(rooms_dao) - floors_repository = FloorsRepository(floors_dao) - reporting_periods_repository = ReportingPeriodsRepository(reporting_periods_dao) - users_repository = UsersRepository(users_dao) - - report_service = ReportService( - hours_transactions_repository, - residents_repository, - rooms_repository, - floors_repository, - users_repository, - ) - - active_period = await reporting_periods_repository.get_active_period() - - if not active_period: - print("⚠️ Нет активного периода") - return - - print(f"📅 Активный период: с {active_period.start_date}") - - end_date = active_period.end_date if active_period.end_date else date.today() - - print(f"📊 Генерирую отчёт за период {active_period.start_date} - {end_date}") - - report_file = await report_service.generate_period_report( - active_period.start_date, end_date - ) - - filename = f"test_report_{active_period.start_date}_{end_date}.xlsx" - with open(filename, "wb") as f: - f.write(report_file.read()) - - print(f"✅ Отчёт сохранён в файл: {filename}") - - -if __name__ == "__main__": - asyncio.run(test_report_generation())