This commit is contained in:
2026-03-01 15:58:47 +03:00
parent 1110d89bb0
commit cf0b0309fc
12 changed files with 430 additions and 95 deletions
+2
View File
@@ -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)
@@ -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",
@@ -63,8 +63,6 @@ async def get_reporting_period_data(
<b>Дата начала:</b> <code>{start_date.strftime('%d.%m.%Y')}</code>
<b>Прошло дней:</b> <code>{days_passed}</code>
━━━━━━━━━━━━━━━━━━━━
Используйте кнопки ниже для управления периодом.
"""
has_active = True
@@ -74,8 +72,6 @@ async def get_reporting_period_data(
<b>Статус:</b> ⚪️ Нет активного периода
━━━━━━━━━━━━━━━━━━━━
Создайте новый отчётный период, чтобы начать учёт дежурств.
"""
has_active = False
@@ -0,0 +1,5 @@
from dutylog.application.bot.creator_dialogs.creator_menu_dialog import (
creator_menu_dialog,
)
__all__ = ["creator_menu_dialog"]
@@ -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 = """
<blockquote>👨‍💼 <b>Администраторы</b></blockquote>
⚠️ Нет администраторов в системе.
"""
admins_list = []
else:
content = f"""
<blockquote>👨‍💼 <b>Администраторы</b></blockquote>
Всего администраторов: <code>{len(admins)}</code>
Выберите администратора для просмотра информации:
"""
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"""
<blockquote>👨‍💼 <b>Информация об администраторе</b></blockquote>
<b>ID:</b> <code>{admin.id}</code>
<b>Username:</b> {username}
<b>Имя:</b> <code>{first_name}</code>
<b>Фамилия:</b> <code>{last_name}</code>
<b>Дата добавления:</b> <code>{admin.created_at.strftime('%d.%m.%Y %H:%M')}</code>
"""
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"""
<blockquote>⚠️ <b>Подтверждение удаления</b></blockquote>
Вы уверены, что хотите удалить администратора <b>{display_name}</b>?
Пользователь потеряет права администратора.
"""
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 = """
<blockquote> <b>Добавить администратора</b></blockquote>
⚠️ Нет пользователей, которых можно сделать администраторами.
Все пользователи уже являются администраторами.
"""
users_list = []
else:
content = f"""
<blockquote> <b>Добавить администратора</b></blockquote>
Всего пользователей: <code>{len(non_admin_users)}</code>
Выберите пользователя для назначения администратором:
"""
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"""
<blockquote>⚠️ <b>Подтверждение</b></blockquote>
Вы уверены, что хотите назначить <b>{display_name}</b> администратором?
Пользователь получит доступ к панели управления.
"""
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,
)
@@ -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,
)
@@ -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("✅ Регистрация успешна!")
@@ -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()
+7 -1
View File
@@ -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
@@ -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)
Binary file not shown.
-79
View File
@@ -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())