mirror of
https://github.com/koloideal/DutyLog.git
synced 2026-06-10 10:25:29 +03:00
update
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
|||||||
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.exceptions import TelegramForbiddenError, TelegramBadRequest
|
||||||
|
from aiogram_dialog import Window, DialogManager
|
||||||
|
from aiogram_dialog.widgets.text import Const
|
||||||
|
from aiogram_dialog.widgets.kbd import Row, 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 callback.message.delete()
|
||||||
|
await dialog_manager.start(AdminMenuSG.main)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_broadcast_cancel(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.main)
|
||||||
|
|
||||||
|
|
||||||
|
broadcast_window = Window(
|
||||||
|
Const(
|
||||||
|
"<blockquote>📢 <b>Рассылка</b></blockquote>\n\nОтправьте сообщение, которое хотите разослать всем пользователям:"
|
||||||
|
),
|
||||||
|
MessageInput(on_broadcast_message),
|
||||||
|
SwitchTo(Const("◀️ Отмена"), id="cancel_broadcast", state=AdminMenuSG.main),
|
||||||
|
state=AdminMenuSG.broadcast,
|
||||||
|
)
|
||||||
|
|
||||||
|
broadcast_confirm_window = Window(
|
||||||
|
Const(
|
||||||
|
"<blockquote>📢 <b>Подтверждение рассылки</b></blockquote>\n\n⚠️ Вы уверены, что хотите отправить это сообщение всем пользователям?"
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
from aiogram_dialog import Window, DialogManager
|
||||||
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
|
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, Select, Group
|
||||||
|
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.residents_repository import (
|
||||||
|
ResidentsRepository,
|
||||||
|
)
|
||||||
|
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
|
||||||
|
HoursTransactionsRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_add_hours_click(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.add_hours_select)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_remove_hours_click(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.remove_hours_select)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_hours_select_data(**kwargs):
|
||||||
|
hours_options = [
|
||||||
|
(5, "5"), (10, "10"), (15, "15"), (20, "20"),
|
||||||
|
(25, "25"), (30, "30"), (35, "35"), (40, "40"),
|
||||||
|
(45, "45"), (50, "50"), (55, "55"), (60, "60"),
|
||||||
|
(65, "65"), (70, "70"), (75, "75"), (80, "80"),
|
||||||
|
]
|
||||||
|
return {"hours_options": hours_options}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_hours_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
item_id: str,
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["selected_hours"] = int(item_id)
|
||||||
|
|
||||||
|
if dialog_manager.current_context().state == AdminMenuSG.add_hours_select:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm)
|
||||||
|
else:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_custom_hours_click(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
if dialog_manager.current_context().state == AdminMenuSG.add_hours_select:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.add_hours_custom)
|
||||||
|
else:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.remove_hours_custom)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_custom_hours_input(
|
||||||
|
message: Message,
|
||||||
|
widget: MessageInput,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
if not message.text:
|
||||||
|
await message.answer("⚠️ Пожалуйста, введите число")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
hours = int(message.text)
|
||||||
|
if hours <= 0:
|
||||||
|
await message.answer("⚠️ Количество часов должно быть положительным числом")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog_manager.dialog_data["selected_hours"] = hours
|
||||||
|
|
||||||
|
if dialog_manager.current_context().state == AdminMenuSG.add_hours_custom:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm)
|
||||||
|
else:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
|
||||||
|
except ValueError:
|
||||||
|
await message.answer("⚠️ Пожалуйста, введите корректное число")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_hours_confirm_data(
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
hours = dialog_manager.dialog_data.get("selected_hours", 0)
|
||||||
|
return {"hours": hours}
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_add_hours_confirm(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
):
|
||||||
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
|
hours = dialog_manager.dialog_data.get("selected_hours")
|
||||||
|
admin_id = callback.from_user.id
|
||||||
|
|
||||||
|
if resident_id and hours:
|
||||||
|
await transactions_repository.add_hours(
|
||||||
|
resident_id=resident_id,
|
||||||
|
amount=hours,
|
||||||
|
admin_id=admin_id,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_remove_hours_confirm(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
):
|
||||||
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
|
hours = dialog_manager.dialog_data.get("selected_hours")
|
||||||
|
admin_id = callback.from_user.id
|
||||||
|
|
||||||
|
if resident_id and hours:
|
||||||
|
resident = await residents_repository.get_resident_by_id(resident_id)
|
||||||
|
if resident and resident.active_hours < hours:
|
||||||
|
await callback.answer(
|
||||||
|
f"⚠️ Недостаточно часов! У резидента {resident.active_hours} неотработанных ч, а вы пытаетесь отнять {hours} ч",
|
||||||
|
show_alert=True
|
||||||
|
)
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
return
|
||||||
|
|
||||||
|
await transactions_repository.move_hours_to_completed(
|
||||||
|
resident_id=resident_id,
|
||||||
|
amount=hours,
|
||||||
|
admin_id=admin_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_hours_cancel(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
|
||||||
|
|
||||||
|
add_hours_select_window = Window(
|
||||||
|
Const("<blockquote>➕ <b>Добавить часы</b></blockquote>\n\nВыберите количество часов:"),
|
||||||
|
Group(
|
||||||
|
Select(
|
||||||
|
Format("{item[1]} ч"),
|
||||||
|
id="hours_select_add",
|
||||||
|
item_id_getter=lambda x: x[0],
|
||||||
|
items="hours_options",
|
||||||
|
on_click=on_hours_selected,
|
||||||
|
),
|
||||||
|
width=4,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("✏️ Ввести свое количество"),
|
||||||
|
id="custom_hours_add_btn",
|
||||||
|
on_click=on_custom_hours_click,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Отмена"),
|
||||||
|
id="cancel_add_hours",
|
||||||
|
state=AdminMenuSG.resident_info,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.add_hours_select,
|
||||||
|
getter=get_hours_select_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_hours_select_window = Window(
|
||||||
|
Const("<blockquote>➖ <b>Отнять часы</b></blockquote>\n\nВыберите количество часов:"),
|
||||||
|
Group(
|
||||||
|
Select(
|
||||||
|
Format("{item[1]} ч"),
|
||||||
|
id="hours_select_remove",
|
||||||
|
item_id_getter=lambda x: x[0],
|
||||||
|
items="hours_options",
|
||||||
|
on_click=on_hours_selected,
|
||||||
|
),
|
||||||
|
width=4,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("✏️ Ввести свое количество"),
|
||||||
|
id="custom_hours_remove_btn",
|
||||||
|
on_click=on_custom_hours_click,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Отмена"),
|
||||||
|
id="cancel_remove_hours",
|
||||||
|
state=AdminMenuSG.resident_info,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.remove_hours_select,
|
||||||
|
getter=get_hours_select_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_hours_custom_window = Window(
|
||||||
|
Const("<blockquote>✏️ <b>Добавить часы</b></blockquote>\n\nВведите количество часов:"),
|
||||||
|
MessageInput(on_custom_hours_input),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Отмена"),
|
||||||
|
id="cancel_custom_add",
|
||||||
|
state=AdminMenuSG.add_hours_select,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.add_hours_custom,
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_hours_custom_window = Window(
|
||||||
|
Const("<blockquote>✏️ <b>Отнять часы</b></blockquote>\n\nВведите количество часов:"),
|
||||||
|
MessageInput(on_custom_hours_input),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Отмена"),
|
||||||
|
id="cancel_custom_remove",
|
||||||
|
state=AdminMenuSG.remove_hours_select,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.remove_hours_custom,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_hours_confirm_window = Window(
|
||||||
|
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?"),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("✅ Да"),
|
||||||
|
id="confirm_add_hours",
|
||||||
|
on_click=on_add_hours_confirm,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("❌ Нет"),
|
||||||
|
id="cancel_add_hours_confirm",
|
||||||
|
on_click=on_hours_cancel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.add_hours_confirm,
|
||||||
|
getter=get_hours_confirm_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_hours_confirm_window = Window(
|
||||||
|
Format("<blockquote>➖ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите отнять <code>{hours}</code> часов?"),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("✅ Да"),
|
||||||
|
id="confirm_remove_hours",
|
||||||
|
on_click=on_remove_hours_confirm,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("❌ Нет"),
|
||||||
|
id="cancel_remove_hours_confirm",
|
||||||
|
on_click=on_hours_cancel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.remove_hours_confirm,
|
||||||
|
getter=get_hours_confirm_data,
|
||||||
|
)
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
from aiogram.types import User
|
||||||
|
from aiogram_dialog import Window, DialogManager
|
||||||
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
|
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_admin_menu_data(
|
||||||
|
event_from_user: User,
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
config: FromDishka[Config],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
user = await users_repository.get_or_create_user(
|
||||||
|
user_id=event_from_user.id,
|
||||||
|
username=event_from_user.username,
|
||||||
|
first_name=event_from_user.first_name,
|
||||||
|
last_name=event_from_user.last_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_creator = event_from_user.id == config.bot.creator_id
|
||||||
|
|
||||||
|
if is_creator:
|
||||||
|
greeting = "👑 <b>Создатель</b>"
|
||||||
|
else:
|
||||||
|
greeting = "👨💼 <b>Администратор</b>"
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
{greeting}
|
||||||
|
|
||||||
|
<blockquote>📋 <b>Панель управления</b></blockquote>
|
||||||
|
|
||||||
|
Выберите действие:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {"content": content}
|
||||||
|
|
||||||
|
|
||||||
|
@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_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"""
|
||||||
|
<blockquote>📊 <b>Статистика системы</b></blockquote>
|
||||||
|
|
||||||
|
👥 <b>Всего пользователей:</b> <code>{total_users}</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_inactive_hours}</code> ч
|
||||||
|
🔴 <b>Всего неотработанных часов:</b> <code>{total_active_hours}</code> ч
|
||||||
|
📊 <b>Общий итог:</b> <code>{total_active_hours + total_inactive_hours}</code> ч
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {"stats_content": stats_text}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_rooms_click(callback, button, dialog_manager):
|
||||||
|
await callback.answer("⚠️ Функционал в разработке", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_floors_click(callback, button, dialog_manager):
|
||||||
|
await callback.answer("⚠️ Функционал в разработке", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
main_menu_window = Window(
|
||||||
|
Format("{content}"),
|
||||||
|
SwitchTo(
|
||||||
|
Const("🏠 Резиденты"),
|
||||||
|
id="residents_btn",
|
||||||
|
state=AdminMenuSG.residents,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("🚪 Комнаты"),
|
||||||
|
id="rooms_btn",
|
||||||
|
on_click=on_rooms_click,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("🏢 Этажи"),
|
||||||
|
id="floors_btn",
|
||||||
|
on_click=on_floors_click,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("📊 Статистика"),
|
||||||
|
id="stats_btn",
|
||||||
|
state=AdminMenuSG.statistics,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("📢 Рассылка"),
|
||||||
|
id="broadcast_btn",
|
||||||
|
state=AdminMenuSG.broadcast,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.main,
|
||||||
|
getter=get_admin_menu_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
statistics_window = Window(
|
||||||
|
Format("{stats_content}"),
|
||||||
|
SwitchTo(Const("◀️ Назад"), id="back_from_stats", state=AdminMenuSG.main),
|
||||||
|
state=AdminMenuSG.statistics,
|
||||||
|
getter=get_statistics_data,
|
||||||
|
)
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
from aiogram.types import Message, CallbackQuery
|
||||||
|
from aiogram_dialog import Window, DialogManager
|
||||||
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
|
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, ScrollingGroup, Select, Group
|
||||||
|
from aiogram_dialog.widgets.input import MessageInput
|
||||||
|
from magic_filter import F
|
||||||
|
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.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.users_repository import (
|
||||||
|
UsersRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_residents_list_data(
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
all_residents = await residents_repository.get_all_residents()
|
||||||
|
|
||||||
|
residents_with_rooms = []
|
||||||
|
for resident in all_residents:
|
||||||
|
room = await rooms_repository.get_room_by_id(resident.room)
|
||||||
|
room_number = room.number if room else 999999
|
||||||
|
residents_with_rooms.append((resident, room_number))
|
||||||
|
|
||||||
|
residents_with_rooms.sort(key=lambda x: x[1])
|
||||||
|
|
||||||
|
residents_data = []
|
||||||
|
for resident, room_number in residents_with_rooms:
|
||||||
|
status = "🟢" if resident.is_busy else "⚪️"
|
||||||
|
name = resident.real_name if resident.real_name else "Без имени"
|
||||||
|
|
||||||
|
residents_data.append(
|
||||||
|
(f"{name} | Комната {room_number} | {status}", resident.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
<blockquote>🏠 <b>Резиденты</b></blockquote>
|
||||||
|
|
||||||
|
<b>Всего резидентов:</b> <code>{len(all_residents)}</code>
|
||||||
|
|
||||||
|
Выберите резидента для просмотра информации:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content": content,
|
||||||
|
"residents": residents_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_resident_info_data(
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
|
|
||||||
|
if not resident_id:
|
||||||
|
return {"info_content": "Ошибка: резидент не выбран", "is_busy": False, "from_search": False}
|
||||||
|
|
||||||
|
resident = await residents_repository.get_resident_by_id(resident_id)
|
||||||
|
|
||||||
|
if not resident:
|
||||||
|
return {"info_content": "Ошибка: резидент не найден", "is_busy": False, "from_search": False}
|
||||||
|
|
||||||
|
room = await rooms_repository.get_room_by_id(resident.room)
|
||||||
|
room_number = room.number if room else "???"
|
||||||
|
|
||||||
|
name = resident.real_name if resident.real_name else "Без имени"
|
||||||
|
status = "🟢 Занят" if resident.is_busy else "⚪️ Свободен"
|
||||||
|
|
||||||
|
user_info = "Не привязан"
|
||||||
|
if resident.user_entity:
|
||||||
|
user = await users_repository.get_user_by_id(resident.user_entity)
|
||||||
|
if user:
|
||||||
|
if user.username:
|
||||||
|
username = f"@{user.username}"
|
||||||
|
else:
|
||||||
|
username = f"ID: {user.id}"
|
||||||
|
user_info = f"{user.first_name} ({username})"
|
||||||
|
|
||||||
|
info_content = f"""
|
||||||
|
<blockquote>👤 <b>Информация о резиденте</b></blockquote>
|
||||||
|
|
||||||
|
<b>ID:</b> <code>{resident.id}</code>
|
||||||
|
<b>Имя:</b> {name}
|
||||||
|
<b>Комната:</b> <code>{room_number}</code>
|
||||||
|
<b>Статус:</b> {status}
|
||||||
|
<b>Пользователь:</b> {user_info}
|
||||||
|
|
||||||
|
━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
🟢 <b>Отработанные часы:</b> <code>{resident.inactive_hours}</code> ч
|
||||||
|
🔴 <b>Неотработанные часы:</b> <code>{resident.active_hours}</code> ч
|
||||||
|
"""
|
||||||
|
|
||||||
|
from_search = dialog_manager.dialog_data.get("from_search", False)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"info_content": info_content,
|
||||||
|
"is_busy": resident.is_busy,
|
||||||
|
"from_search": from_search,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_resident_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
item_id: str,
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["selected_resident_id"] = int(item_id)
|
||||||
|
dialog_manager.dialog_data["from_search"] = False
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_add_resident(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.create_resident_name)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_filter_residents(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await callback.answer("⚠️ Функционал в разработке", show_alert=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_search_residents(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.residents_search_input)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_logout_resident_confirm(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
):
|
||||||
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
|
|
||||||
|
if resident_id:
|
||||||
|
await residents_repository.unbind_user_from_resident(resident_id)
|
||||||
|
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_logout_resident_cancel(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_resident_name_input(
|
||||||
|
message: Message,
|
||||||
|
widget: MessageInput,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
if not message.text or len(message.text.strip()) < 2:
|
||||||
|
await message.answer("⚠️ Пожалуйста, введите корректное имя и фамилию")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog_manager.dialog_data["new_resident_name"] = message.text.strip()
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.create_resident_floor)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_create_resident_floors_data(
|
||||||
|
floors_repository: FromDishka[FloorsRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
all_floors = await floors_repository.get_all_floors()
|
||||||
|
all_floors.sort(key=lambda f: f.number)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"floors": [(f.id, f"Этаж {f.number}") for f in all_floors],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_create_resident_floor_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
item_id: str,
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["new_resident_floor_id"] = int(item_id)
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.create_resident_room)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_create_resident_rooms_data(
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
floor_id = dialog_manager.dialog_data.get("new_resident_floor_id")
|
||||||
|
|
||||||
|
if not floor_id:
|
||||||
|
return {"rooms": []}
|
||||||
|
|
||||||
|
rooms = await rooms_repository.get_rooms_by_floor(floor_id)
|
||||||
|
rooms.sort(key=lambda r: r.number)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"rooms": [(r.id, str(r.number)) for r in rooms],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_create_resident_room_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
item_id: str,
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["new_resident_room_id"] = int(item_id)
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.create_resident_confirm)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_create_resident_confirm_data(
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
name = dialog_manager.dialog_data.get("new_resident_name", "???")
|
||||||
|
return {"resident_name": name}
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_create_resident_confirm(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
):
|
||||||
|
name = dialog_manager.dialog_data.get("new_resident_name")
|
||||||
|
room_id = dialog_manager.dialog_data.get("new_resident_room_id")
|
||||||
|
|
||||||
|
if name and room_id:
|
||||||
|
await residents_repository.create_resident(
|
||||||
|
room_id=room_id,
|
||||||
|
real_name=name,
|
||||||
|
)
|
||||||
|
await callback.answer("✅ Резидент создан!")
|
||||||
|
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.residents)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_create_resident_cancel(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.residents)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_search_input(
|
||||||
|
message: Message,
|
||||||
|
widget: MessageInput,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
if not message.text or len(message.text.strip()) < 1:
|
||||||
|
await message.answer("⚠️ Пожалуйста, введите поисковый запрос")
|
||||||
|
return
|
||||||
|
|
||||||
|
query = message.text.strip()
|
||||||
|
if query.startswith("@"):
|
||||||
|
query = query[1:]
|
||||||
|
|
||||||
|
dialog_manager.dialog_data["search_query"] = query
|
||||||
|
dialog_manager.dialog_data["is_search_active"] = True
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.residents_search_results)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_search_results_data(
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
query = dialog_manager.dialog_data.get("search_query", "")
|
||||||
|
|
||||||
|
residents, search_type = await residents_repository.search_residents(
|
||||||
|
query, users_repository
|
||||||
|
)
|
||||||
|
|
||||||
|
if not residents:
|
||||||
|
return {
|
||||||
|
"content": f"""
|
||||||
|
<blockquote>🔍 <b>Результаты поиска</b></blockquote>
|
||||||
|
|
||||||
|
<b>Запрос:</b> <code>{query}</code>
|
||||||
|
|
||||||
|
❌ Ничего не найдено
|
||||||
|
""",
|
||||||
|
"residents": [],
|
||||||
|
"has_results": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
residents_with_rooms = []
|
||||||
|
for resident in residents:
|
||||||
|
room = await rooms_repository.get_room_by_id(resident.room)
|
||||||
|
room_number = room.number if room else 999999
|
||||||
|
residents_with_rooms.append((resident, room_number))
|
||||||
|
|
||||||
|
residents_with_rooms.sort(key=lambda x: x[1])
|
||||||
|
|
||||||
|
residents_data = []
|
||||||
|
for resident, room_number in residents_with_rooms:
|
||||||
|
status = "🟢" if resident.is_busy else "⚪️"
|
||||||
|
name = resident.real_name if resident.real_name else "Без имени"
|
||||||
|
|
||||||
|
residents_data.append(
|
||||||
|
(f"{name} | Комната {room_number} | {status}", resident.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
<blockquote>🔍 <b>Результаты поиска по {search_type}</b></blockquote>
|
||||||
|
|
||||||
|
<b>Найдено резидентов:</b> <code>{len(residents)}</code>
|
||||||
|
|
||||||
|
Выберите резидента для просмотра информации:
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content": content,
|
||||||
|
"residents": residents_data,
|
||||||
|
"has_results": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_search_resident_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
item_id: str,
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["selected_resident_id"] = int(item_id)
|
||||||
|
dialog_manager.dialog_data["from_search"] = True
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
|
||||||
|
|
||||||
|
residents_list_window = Window(
|
||||||
|
Format("{content}"),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("🔍 Поиск"),
|
||||||
|
id="search_residents_btn",
|
||||||
|
on_click=on_search_residents,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("🔽 Фильтр"),
|
||||||
|
id="filter_residents_btn",
|
||||||
|
on_click=on_filter_residents,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ScrollingGroup(
|
||||||
|
Select(
|
||||||
|
Format("{item[0]}"),
|
||||||
|
id="residents_select",
|
||||||
|
item_id_getter=lambda x: x[1],
|
||||||
|
items="residents",
|
||||||
|
on_click=on_resident_selected,
|
||||||
|
),
|
||||||
|
id="residents_scroll",
|
||||||
|
width=1,
|
||||||
|
height=7,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("➕ Добавить резидента"),
|
||||||
|
id="add_resident_btn",
|
||||||
|
on_click=on_add_resident,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Назад"),
|
||||||
|
id="back_to_admin_menu",
|
||||||
|
state=AdminMenuSG.main,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.residents,
|
||||||
|
getter=get_residents_list_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
resident_info_window = Window(
|
||||||
|
Format("{info_content}"),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("Добавить часы"),
|
||||||
|
id="add_hours_btn",
|
||||||
|
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.add_hours_select),
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("Отнять часы"),
|
||||||
|
id="remove_hours_btn",
|
||||||
|
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.remove_hours_select),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("🚪 Разлогинить"),
|
||||||
|
id="logout_resident_btn",
|
||||||
|
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_logout_confirm),
|
||||||
|
when="is_busy",
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Назад к результатам"),
|
||||||
|
id="back_to_search_results",
|
||||||
|
state=AdminMenuSG.residents_search_results,
|
||||||
|
when="from_search",
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Назад к списку"),
|
||||||
|
id="back_to_residents_list",
|
||||||
|
state=AdminMenuSG.residents,
|
||||||
|
when=~F["from_search"],
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.resident_info,
|
||||||
|
getter=get_resident_info_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
resident_logout_confirm_window = Window(
|
||||||
|
Const(
|
||||||
|
"<blockquote>⚠️ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите разлогинить этого резидента?"
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("✅ Да"),
|
||||||
|
id="confirm_logout",
|
||||||
|
on_click=on_logout_resident_confirm,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("❌ Нет"),
|
||||||
|
id="cancel_logout",
|
||||||
|
on_click=on_logout_resident_cancel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.resident_logout_confirm,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_resident_name_window = Window(
|
||||||
|
Const("<blockquote>➕ <b>Создание резидента</b></blockquote>\n\nВведите имя и фамилию резидента:"),
|
||||||
|
MessageInput(on_resident_name_input),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Отмена"),
|
||||||
|
id="cancel_create_resident_name",
|
||||||
|
state=AdminMenuSG.residents,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.create_resident_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_resident_floor_window = Window(
|
||||||
|
Const("<blockquote>🏢 <b>Выбор этажа</b></blockquote>\n\nВыберите этаж для нового резидента:"),
|
||||||
|
Group(
|
||||||
|
Select(
|
||||||
|
Format("{item[1]}"),
|
||||||
|
id="create_resident_floor_select",
|
||||||
|
item_id_getter=lambda x: x[0],
|
||||||
|
items="floors",
|
||||||
|
on_click=on_create_resident_floor_selected,
|
||||||
|
),
|
||||||
|
width=2,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Назад"),
|
||||||
|
id="back_to_create_name",
|
||||||
|
state=AdminMenuSG.create_resident_name,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.create_resident_floor,
|
||||||
|
getter=get_create_resident_floors_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_resident_room_window = Window(
|
||||||
|
Const("<blockquote>🚪 <b>Выбор комнаты</b></blockquote>\n\nВыберите комнату для нового резидента:"),
|
||||||
|
Group(
|
||||||
|
Select(
|
||||||
|
Format("{item[1]}"),
|
||||||
|
id="create_resident_room_select",
|
||||||
|
item_id_getter=lambda x: x[0],
|
||||||
|
items="rooms",
|
||||||
|
on_click=on_create_resident_room_selected,
|
||||||
|
),
|
||||||
|
width=3,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Назад"),
|
||||||
|
id="back_to_create_floor",
|
||||||
|
state=AdminMenuSG.create_resident_floor,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.create_resident_room,
|
||||||
|
getter=get_create_resident_rooms_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_resident_confirm_window = Window(
|
||||||
|
Format("<blockquote>✅ <b>Подтверждение</b></blockquote>\n\nСоздать резидента <b>{resident_name}</b>?"),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("✅ Да"),
|
||||||
|
id="confirm_create_resident",
|
||||||
|
on_click=on_create_resident_confirm,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("❌ Нет"),
|
||||||
|
id="cancel_create_resident",
|
||||||
|
on_click=on_create_resident_cancel,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.create_resident_confirm,
|
||||||
|
getter=get_create_resident_confirm_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
search_input_window = Window(
|
||||||
|
Const("<blockquote>🔍 <b>Поиск резидентов</b></blockquote>\n\n<blockquote>Введите номер комнаты, имя резидента или username пользователя. Можно указать только часть имени или username для поиска.</blockquote>"),
|
||||||
|
MessageInput(on_search_input),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Отмена"),
|
||||||
|
id="cancel_search",
|
||||||
|
state=AdminMenuSG.residents,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.residents_search_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
search_results_window = Window(
|
||||||
|
Format("{content}"),
|
||||||
|
ScrollingGroup(
|
||||||
|
Select(
|
||||||
|
Format("{item[0]}"),
|
||||||
|
id="search_residents_select",
|
||||||
|
item_id_getter=lambda x: x[1],
|
||||||
|
items="residents",
|
||||||
|
on_click=on_search_resident_selected,
|
||||||
|
),
|
||||||
|
id="search_residents_scroll",
|
||||||
|
width=1,
|
||||||
|
height=7,
|
||||||
|
when="has_results",
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("🔍 Новый поиск"),
|
||||||
|
id="new_search",
|
||||||
|
state=AdminMenuSG.residents_search_input,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ К списку резидентов"),
|
||||||
|
id="back_to_residents_from_search",
|
||||||
|
state=AdminMenuSG.residents,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.residents_search_results,
|
||||||
|
getter=get_search_results_data,
|
||||||
|
)
|
||||||
@@ -1,196 +1,12 @@
|
|||||||
from aiogram.types import User
|
from aiogram_dialog import Dialog
|
||||||
from aiogram_dialog import Dialog, Window, DialogManager
|
|
||||||
from aiogram_dialog.widgets.text import Format, Const
|
|
||||||
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
|
from dutylog.application.bot.user_dialogs.user_menu.main_menu import main_menu_window
|
||||||
from dutylog.infrastructure.database.repositories.users_repository import (
|
from dutylog.application.bot.user_dialogs.user_menu.history import history_window
|
||||||
UsersRepository,
|
from dutylog.application.bot.user_dialogs.user_menu.faq import faq_window
|
||||||
)
|
|
||||||
from dutylog.infrastructure.database.repositories.residents_repository import (
|
|
||||||
ResidentsRepository,
|
|
||||||
)
|
|
||||||
from dutylog.infrastructure.database.repositories.rooms_repository import (
|
|
||||||
RoomsRepository,
|
|
||||||
)
|
|
||||||
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
|
|
||||||
HoursTransactionsRepository,
|
|
||||||
)
|
|
||||||
from dutylog.infrastructure.utils.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
@inject
|
|
||||||
async def get_main_menu_data(
|
|
||||||
event_from_user: User,
|
|
||||||
users_repository: FromDishka[UsersRepository],
|
|
||||||
residents_repository: FromDishka[ResidentsRepository],
|
|
||||||
rooms_repository: FromDishka[RoomsRepository],
|
|
||||||
config: FromDishka[Config],
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
user = await users_repository.get_or_create_user(
|
|
||||||
user_id=event_from_user.id,
|
|
||||||
username=event_from_user.username,
|
|
||||||
first_name=event_from_user.first_name,
|
|
||||||
last_name=event_from_user.last_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
is_creator = event_from_user.id == config.bot.creator_id
|
|
||||||
is_admin = user.is_admin
|
|
||||||
|
|
||||||
if is_creator:
|
|
||||||
greeting = "👑 <b>Создатель</b>"
|
|
||||||
elif is_admin:
|
|
||||||
greeting = "👨💼 <b>Администратор</b>"
|
|
||||||
else:
|
|
||||||
resident = await residents_repository.get_resident_by_user_id(
|
|
||||||
event_from_user.id
|
|
||||||
)
|
|
||||||
if resident:
|
|
||||||
room = await rooms_repository.get_room_by_id(resident.room)
|
|
||||||
room_number = room.number if room else "???"
|
|
||||||
real_name = (
|
|
||||||
resident.real_name if resident.real_name else event_from_user.first_name
|
|
||||||
)
|
|
||||||
greeting = (
|
|
||||||
f"👋 <b>Привет, {real_name}!</b>\n🚪 Комната <code>{room_number}</code>"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
greeting = f"👋 <b>Привет, {event_from_user.first_name}!</b>"
|
|
||||||
|
|
||||||
if not is_admin and not is_creator:
|
|
||||||
resident = await residents_repository.get_resident_by_user_id(
|
|
||||||
event_from_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not resident:
|
|
||||||
content = f"""
|
|
||||||
{greeting}
|
|
||||||
|
|
||||||
<blockquote>⚠️ <b>Профиль не найден</b></blockquote>
|
|
||||||
|
|
||||||
Вы еще не привязаны к резиденту.
|
|
||||||
Обратитесь к администратору для регистрации.
|
|
||||||
"""
|
|
||||||
has_resident = False
|
|
||||||
else:
|
|
||||||
content = f"""
|
|
||||||
{greeting}
|
|
||||||
|
|
||||||
⏰ <b>Ваши часы дежурств</b>
|
|
||||||
|
|
||||||
<blockquote>🟢 <b>Отработанные часы:</b> <code>{resident.inactive_hours}</code> ч
|
|
||||||
━━━━━━━━━━━━━━━━
|
|
||||||
🔴 Неотработанные часы: <code>{resident.active_hours}</code> ч</blockquote>
|
|
||||||
"""
|
|
||||||
has_resident = True
|
|
||||||
else:
|
|
||||||
content = f"""
|
|
||||||
{greeting}
|
|
||||||
|
|
||||||
<blockquote>📋 <b>Панель управления</b></blockquote>
|
|
||||||
|
|
||||||
Добро пожаловать в систему учета дежурств!
|
|
||||||
"""
|
|
||||||
has_resident = False
|
|
||||||
|
|
||||||
return {
|
|
||||||
"content": content,
|
|
||||||
"is_regular_user": not is_admin and not is_creator,
|
|
||||||
"has_resident": has_resident,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@inject
|
|
||||||
async def get_history_data(
|
|
||||||
event_from_user: User,
|
|
||||||
residents_repository: FromDishka[ResidentsRepository],
|
|
||||||
transactions_repository: FromDishka[HoursTransactionsRepository],
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
|
|
||||||
|
|
||||||
if not resident:
|
|
||||||
history_text = """
|
|
||||||
<blockquote>📜 <b>История операций</b></blockquote>
|
|
||||||
|
|
||||||
<i>Профиль не найден</i>
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
transactions = await transactions_repository.get_resident_history(resident.id)
|
|
||||||
transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
|
|
||||||
last_10 = transactions_sorted[:10]
|
|
||||||
|
|
||||||
if not last_10:
|
|
||||||
history_text = """
|
|
||||||
<blockquote>📜 <b>История операций</b></blockquote>
|
|
||||||
|
|
||||||
<i>История операций пуста</i>
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
history_lines = []
|
|
||||||
for tx in last_10:
|
|
||||||
emoji = "+" if tx.transaction_type == "increase" else "-"
|
|
||||||
date_str = tx.created_at.strftime("%d.%m.%Y %H:%M")
|
|
||||||
history_lines.append(
|
|
||||||
f"{emoji} <code>{tx.amount}</code> ч • <i>{date_str}</i>"
|
|
||||||
)
|
|
||||||
|
|
||||||
history_text = f"""
|
|
||||||
<blockquote>📜 <b>История операций</b></blockquote>
|
|
||||||
|
|
||||||
{"".join(f"{line}\n" for line in history_lines)}
|
|
||||||
<i>Показаны последние 10 операций</i>
|
|
||||||
"""
|
|
||||||
|
|
||||||
return {"history_content": history_text}
|
|
||||||
|
|
||||||
|
|
||||||
main_menu_dialog = Dialog(
|
main_menu_dialog = Dialog(
|
||||||
Window(
|
main_menu_window,
|
||||||
Format("{content}"),
|
history_window,
|
||||||
SwitchTo(
|
faq_window,
|
||||||
Const("📜 История"),
|
|
||||||
id="history_btn",
|
|
||||||
state=MainMenuSG.history,
|
|
||||||
when="has_resident",
|
|
||||||
),
|
|
||||||
SwitchTo(
|
|
||||||
Const("❓ FAQ"),
|
|
||||||
id="faq_btn",
|
|
||||||
state=MainMenuSG.faq,
|
|
||||||
when="is_regular_user",
|
|
||||||
),
|
|
||||||
state=MainMenuSG.main,
|
|
||||||
getter=get_main_menu_data,
|
|
||||||
),
|
|
||||||
Window(
|
|
||||||
Format("{history_content}"),
|
|
||||||
Back(Const("◀️ Назад")),
|
|
||||||
state=MainMenuSG.history,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from aiogram_dialog import Window
|
||||||
|
from aiogram_dialog.widgets.text import Const
|
||||||
|
from aiogram_dialog.widgets.kbd import SwitchTo
|
||||||
|
|
||||||
|
from dutylog.application.bot.user_dialogs.states import MainMenuSG
|
||||||
|
|
||||||
|
|
||||||
|
faq_window = 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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from aiogram.types import User
|
||||||
|
from aiogram_dialog import Window
|
||||||
|
from aiogram_dialog.widgets.text import Format
|
||||||
|
from aiogram_dialog.widgets.kbd import Back
|
||||||
|
from aiogram_dialog.widgets.text import Const
|
||||||
|
from dishka import FromDishka
|
||||||
|
from dishka.integrations.aiogram_dialog import inject
|
||||||
|
|
||||||
|
from dutylog.application.bot.user_dialogs.states import MainMenuSG
|
||||||
|
from dutylog.infrastructure.database.repositories.residents_repository import (
|
||||||
|
ResidentsRepository,
|
||||||
|
)
|
||||||
|
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
|
||||||
|
HoursTransactionsRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_history_data(
|
||||||
|
event_from_user: User,
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
|
||||||
|
|
||||||
|
if not resident:
|
||||||
|
history_text = """
|
||||||
|
<blockquote>📜 <b>История операций</b></blockquote>
|
||||||
|
|
||||||
|
<i>Профиль не найден</i>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
transactions = await transactions_repository.get_resident_history(resident.id)
|
||||||
|
transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
|
||||||
|
last_10 = transactions_sorted[:10]
|
||||||
|
|
||||||
|
if not last_10:
|
||||||
|
history_text = """
|
||||||
|
<blockquote>📜 <b>История операций</b></blockquote>
|
||||||
|
|
||||||
|
<i>История операций пуста</i>
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
history_lines = []
|
||||||
|
for tx in last_10:
|
||||||
|
emoji = "+" if tx.transaction_type == "increase" else "-"
|
||||||
|
date_str = tx.created_at.strftime("%d.%m.%Y %H:%M")
|
||||||
|
history_lines.append(
|
||||||
|
f"{emoji} <code>{tx.amount}</code> ч • <i>{date_str}</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
history_text = f"""
|
||||||
|
<blockquote>📜 <b>История операций</b></blockquote>
|
||||||
|
|
||||||
|
{"".join(f"{line}\n" for line in history_lines)}
|
||||||
|
<i>Показаны последние 10 операций</i>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {"history_content": history_text}
|
||||||
|
|
||||||
|
|
||||||
|
history_window = Window(
|
||||||
|
Format("{history_content}"),
|
||||||
|
Back(Const("◀️ Назад")),
|
||||||
|
state=MainMenuSG.history,
|
||||||
|
getter=get_history_data,
|
||||||
|
)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
from aiogram.types import User
|
||||||
|
from aiogram_dialog import Window, DialogManager
|
||||||
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
|
from aiogram_dialog.widgets.kbd import SwitchTo
|
||||||
|
from dishka import FromDishka
|
||||||
|
from dishka.integrations.aiogram_dialog import inject
|
||||||
|
|
||||||
|
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.rooms_repository import (
|
||||||
|
RoomsRepository,
|
||||||
|
)
|
||||||
|
from dutylog.infrastructure.utils.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_main_menu_data(
|
||||||
|
event_from_user: User,
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
|
config: FromDishka[Config],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
user = await users_repository.get_or_create_user(
|
||||||
|
user_id=event_from_user.id,
|
||||||
|
username=event_from_user.username,
|
||||||
|
first_name=event_from_user.first_name,
|
||||||
|
last_name=event_from_user.last_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_creator = event_from_user.id == config.bot.creator_id
|
||||||
|
is_admin = user.is_admin
|
||||||
|
|
||||||
|
if is_creator:
|
||||||
|
greeting = "👑 <b>Создатель</b>"
|
||||||
|
elif is_admin:
|
||||||
|
greeting = "👨💼 <b>Администратор</b>"
|
||||||
|
else:
|
||||||
|
resident = await residents_repository.get_resident_by_user_id(
|
||||||
|
event_from_user.id
|
||||||
|
)
|
||||||
|
if resident:
|
||||||
|
room = await rooms_repository.get_room_by_id(resident.room)
|
||||||
|
room_number = room.number if room else "???"
|
||||||
|
real_name = (
|
||||||
|
resident.real_name if resident.real_name else event_from_user.first_name
|
||||||
|
)
|
||||||
|
greeting = (
|
||||||
|
f"👋 <b>Привет, {real_name}!</b>\n🚪 Комната <code>{room_number}</code>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
greeting = f"👋 <b>Привет, {event_from_user.first_name}!</b>"
|
||||||
|
|
||||||
|
if not is_admin and not is_creator:
|
||||||
|
resident = await residents_repository.get_resident_by_user_id(
|
||||||
|
event_from_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not resident:
|
||||||
|
content = f"""
|
||||||
|
{greeting}
|
||||||
|
|
||||||
|
<blockquote>⚠️ <b>Профиль не найден</b></blockquote>
|
||||||
|
|
||||||
|
Вы еще не привязаны к резиденту.
|
||||||
|
Обратитесь к администратору для регистрации.
|
||||||
|
"""
|
||||||
|
has_resident = False
|
||||||
|
else:
|
||||||
|
content = f"""
|
||||||
|
{greeting}
|
||||||
|
|
||||||
|
⏰ <b>Ваши часы дежурств</b>
|
||||||
|
|
||||||
|
<blockquote>🟢 <b>Отработанные часы:</b> <code>{resident.inactive_hours}</code> ч
|
||||||
|
━━━━━━━━━━━━━━━━
|
||||||
|
🔴 Неотработанные часы: <code>{resident.active_hours}</code> ч</blockquote>
|
||||||
|
"""
|
||||||
|
has_resident = True
|
||||||
|
else:
|
||||||
|
content = f"""
|
||||||
|
{greeting}
|
||||||
|
|
||||||
|
<blockquote>📋 <b>Панель управления</b></blockquote>
|
||||||
|
|
||||||
|
Добро пожаловать в систему учета дежурств!
|
||||||
|
"""
|
||||||
|
has_resident = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content": content,
|
||||||
|
"is_regular_user": not is_admin and not is_creator,
|
||||||
|
"has_resident": has_resident,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main_menu_window = Window(
|
||||||
|
Format("{content}"),
|
||||||
|
SwitchTo(
|
||||||
|
Const("📜 История"),
|
||||||
|
id="history_btn",
|
||||||
|
state=MainMenuSG.history,
|
||||||
|
when="has_resident",
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("❓ FAQ"),
|
||||||
|
id="faq_btn",
|
||||||
|
state=MainMenuSG.faq,
|
||||||
|
when="is_regular_user",
|
||||||
|
),
|
||||||
|
state=MainMenuSG.main,
|
||||||
|
getter=get_main_menu_data,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user