This commit is contained in:
2026-03-04 01:43:20 +03:00
parent 9b004c3a86
commit e796a5bfdd
10 changed files with 179 additions and 610 deletions
@@ -7,6 +7,7 @@ from dutylog.application.bot.admin_dialogs.main_menu import (
from dutylog.application.bot.admin_dialogs.residents_management import (
residents_list_window,
resident_info_window,
resident_history_window,
resident_logout_confirm_window,
resident_delete_confirm_window,
resident_rebind_floor_window,
@@ -34,6 +35,7 @@ from dutylog.application.bot.admin_dialogs.rooms_management import (
rooms_select_floor_window,
rooms_list_window,
room_info_window,
room_history_window,
room_delete_confirm_window,
room_add_hours_select_window,
room_remove_hours_select_window,
@@ -76,6 +78,7 @@ admin_menu_dialog = Dialog(
main_menu_window,
residents_list_window,
resident_info_window,
resident_history_window,
resident_logout_confirm_window,
resident_delete_confirm_window,
resident_rebind_floor_window,
@@ -100,6 +103,7 @@ admin_menu_dialog = Dialog(
rooms_select_floor_window,
rooms_list_window,
room_info_window,
room_history_window,
room_delete_confirm_window,
room_add_hours_select_window,
room_remove_hours_select_window,
@@ -7,73 +7,48 @@ from dutylog.application.bot.user_dialogs.states import AdminMenuSG
async def get_admin_faq_data(**kwargs) -> dict[str, str]:
content = """
<blockquote>❓ <b>Гайд по админке</b></blockquote>
<blockquote>❓ <b>О боте DutyLog</b></blockquote>
Привет! Ты теперь админ, и это круто 😎
Давай разберёмся, что тут к чему.
<b>DutyLog</b> — система учёта дежурных часов для общежития.
<blockquote><b>🏠 Резиденты — твоя главная тусовка</b>
<b>Основные возможности:</b>
Здесь живут все жители общаги. Можешь:
Искать кого угодно — по имени, комнате или нику в телеге
Фильтровать по часам (кто должник, а кто молодец)
Начислять или снимать часы (не забывай писать за что!)
Добавлять новых людей или удалять старых
Отвязывать юзеров от резидентов (если кто-то съехал)</blockquote>
<blockquote><b>👥 Управление резидентами</b>
Регистрация жителей общежития
Привязка к комнатам и пользователям Telegram
Учёт отработанных и неотработанных часов
Поиск по имени, комнате или username
Фильтрация по количеству часов</blockquote>
<blockquote><b>🚪 Комнаты и 🏢 Этажи</b>
<blockquote><b>🏢 Структура общежития</b>
• Управление этажами и комнатами
• Учёт часов как для резидентов, так и для комнат
• Просмотр жителей каждой комнаты</blockquote>
Тут всё просто — структура общежития.
Добавляй этажи, создавай комнаты, привязывай их друг к другу.
Без этого резидентов не создать!</blockquote>
<blockquote><b>⏰ Учёт часов</b>
• Начисление неотработанных часов
• Списание часов (перевод в отработанные)
• История всех операций с примечаниями
• Уведомления резидентам об изменениях</blockquote>
<blockquote><b>📅 Отчётный период — важная штука!</b>
<blockquote><b>📅 Отчётные периоды</b>
• Создание периодов учёта (обычно месяц)
• Автоматическое закрытие предыдущего периода
• Генерация Excel-отчётов за период
• 4 листа в отчёте: начисления/списания для резидентов и комнат</blockquote>
Система работает по месяцам. Один период = один месяц учёта.
<blockquote><b>📊 Статистика и отчёты</b>
• Общая статистика по резидентам и комнатам
• Количество пользователей и админов
• Детальные отчёты в формате Excel
• История операций для каждого резидента и комнаты</blockquote>
Как это работает:
Создаёшь новый период → старый автоматом закрывается
Дата начала = когда создал
Дата конца = когда создал следующий
<blockquote><b>📢 Дополнительно</b>
Рассылка сообщений всем пользователям
Разграничение прав (админы/пользователи)
Админы не участвуют в учёте часов</blockquote>
Зачем это нужно? Чтобы потом сделать красивый отчёт за месяц и показать всем, кто сколько отработал.</blockquote>
<blockquote><b>📊 Отчёты — твоя суперсила</b>
После закрытия периода можешь сгенерить Excel-файл.
В нём будет:
• Все начисления и списания за месяц
• Кто и когда начислил/снял часы
• Примечания (поэтому их важно писать!)
• Автоматический подсчёт итогов
Отчёт можно скинуть старосте или куратору — всё наглядно.</blockquote>
<blockquote><b>📊 Статистика</b>
Быстрый взгляд на цифры:
• Сколько всего людей в системе
• Сколько админов (ты не один!)
• Общая сумма часов по всем резидентам</blockquote>
<blockquote><b>📢 Рассылка</b>
Нужно всем что-то сообщить? Жми сюда.
Сообщение улетит всем пользователям бота.
Можно использовать HTML для красоты.</blockquote>
<blockquote><b>💡 Лайфхаки:</b>
• <b>Всегда пиши примечание</b> при начислении/списании часов
Через месяц никто не вспомнит, за что было
• <b>Делай отчёты регулярно</b>
Это твоя страховка и архив данных
• <b>Проверяй статистику</b> перед закрытием периода
Вдруг что-то забыл начислить?</blockquote>
Вопросы? Пиши создателю бота 👑
<b>Для вопросов и предложений обращайтесь к создателю бота.</b>
"""
return {"content": content}
@@ -20,6 +20,10 @@ from dutylog.infrastructure.database.repositories.floors_repository import (
from dutylog.infrastructure.database.repositories.users_repository import (
UsersRepository,
)
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
HoursTransactionsRepository,
)
from dutylog.infrastructure.utils.datetime import msk_now
@inject
@@ -546,6 +550,58 @@ async def on_search_resident_selected(
await dialog_manager.switch_to(AdminMenuSG.resident_info)
@inject
async def get_resident_history_data(
dialog_manager: DialogManager,
residents_repository: FromDishka[ResidentsRepository],
transactions_repository: FromDishka[HoursTransactionsRepository],
**kwargs,
):
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
if not resident_id:
return {"history_content": "Ошибка: резидент не выбран"}
resident = await residents_repository.get_resident_by_id(resident_id)
if not resident:
return {"history_content": "Ошибка: резидент не найден"}
transactions = await transactions_repository.get_resident_history(resident_id)
transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
last_10 = transactions_sorted[-10:]
resident_name = resident.real_name if resident.real_name else "Без имени"
if not last_10:
history_text = f"""
<blockquote>📜 <b>История операций</b></blockquote>
<b>Резидент:</b> {resident_name}
<i>История операций пуста</i>
"""
else:
history_text = f"""
<blockquote>📜 <b>История операций</b></blockquote>
<b>Резидент:</b> {resident_name}
"""
for tx in last_10:
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
emoji = "+" if tx.transaction_type == "increase" else ""
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n"
return {"history_content": history_text}
residents_list_window = Window(
Format("{content}"),
Row(
@@ -602,6 +658,12 @@ resident_info_window = Window(
when=~F["is_admin"],
),
),
Button(
Const("📜 История"),
id="resident_history_btn",
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_history),
when=~F["is_admin"],
),
Button(
Const("🔄 Перепривязать к комнате"),
id="rebind_resident_btn",
@@ -850,3 +912,14 @@ resident_rebind_confirm_window = Window(
state=AdminMenuSG.resident_rebind_confirm,
getter=get_rebind_confirm_data,
)
resident_history_window = Window(
Format("{history_content}"),
SwitchTo(
Const("◀️ Назад"),
id="back_to_resident_info",
state=AdminMenuSG.resident_info,
),
state=AdminMenuSG.resident_history,
getter=get_resident_history_data,
)
@@ -22,6 +22,7 @@ from dutylog.infrastructure.database.repositories.room_hours_transactions_reposi
from dutylog.infrastructure.database.repositories.users_repository import (
UsersRepository,
)
from dutylog.infrastructure.utils.datetime import msk_now
async def on_rooms_click(
@@ -533,6 +534,56 @@ async def on_delete_room_cancel(
await dialog_manager.switch_to(AdminMenuSG.room_info)
@inject
async def get_room_history_data(
dialog_manager: DialogManager,
rooms_repository: FromDishka[RoomsRepository],
room_transactions_repository: FromDishka[RoomHoursTransactionsRepository],
**kwargs,
):
room_id = dialog_manager.dialog_data.get("selected_room_id")
if not room_id:
return {"history_content": "Ошибка: комната не выбрана"}
room = await rooms_repository.get_room_by_id(room_id)
if not room:
return {"history_content": "Ошибка: комната не найдена"}
transactions = await room_transactions_repository.get_room_history(room_id)
transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
last_10 = transactions_sorted[-10:]
if not last_10:
history_text = f"""
<blockquote>📜 <b>История операций</b></blockquote>
<b>Комната:</b> {room.number}
<i>История операций пуста</i>
"""
else:
history_text = f"""
<blockquote>📜 <b>История операций</b></blockquote>
<b>Комната:</b> {room.number}
"""
for tx in last_10:
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
emoji = "+" if tx.transaction_type == "increase" else ""
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n"
return {"history_content": history_text}
rooms_select_floor_window = Window(
Format("{content}"),
Group(
@@ -596,6 +647,11 @@ room_info_window = Window(
on_click=on_room_remove_hours_click,
),
),
Button(
Const("📜 История"),
id="room_history_btn",
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.room_history),
),
Button(
Const("🗑 Удалить комнату"),
id="delete_room_btn",
@@ -819,3 +875,14 @@ create_room_confirm_window = Window(
state=AdminMenuSG.create_room_confirm,
getter=get_create_room_confirm_data,
)
room_history_window = Window(
Format("{history_content}"),
SwitchTo(
Const("◀️ Назад"),
id="back_to_room_info",
state=AdminMenuSG.room_info,
),
state=AdminMenuSG.room_history,
getter=get_room_history_data,
)
@@ -17,6 +17,7 @@ class AdminMenuSG(StatesGroup):
residents_filter_hours_input = State()
residents_filtered_results = State()
resident_info = State()
resident_history = State()
resident_logout_confirm = State()
resident_delete_confirm = State()
resident_rebind_floor = State()
@@ -41,6 +42,7 @@ class AdminMenuSG(StatesGroup):
rooms_select_floor = State()
rooms_list = State()
room_info = State()
room_history = State()
room_delete_confirm = State()
room_add_hours_select = State()
room_remove_hours_select = State()