This commit is contained in:
2026-03-01 14:19:05 +03:00
parent 8f7b8df096
commit aff088723a
13 changed files with 495 additions and 9 deletions
@@ -35,11 +35,17 @@ from dutylog.application.bot.user_dialogs.admin_dialogs.rooms_management import
create_room_input_window,
create_room_confirm_window,
)
from dutylog.application.bot.user_dialogs.admin_dialogs.reporting_period_management import (
reporting_period_window,
next_period_confirm_window,
)
from dutylog.application.bot.user_dialogs.admin_dialogs.hours_management import (
add_hours_select_window,
remove_hours_select_window,
add_hours_custom_window,
remove_hours_custom_window,
add_hours_remark_window,
remove_hours_remark_window,
add_hours_confirm_window,
remove_hours_confirm_window,
)
@@ -59,6 +65,8 @@ admin_menu_dialog = Dialog(
remove_hours_select_window,
add_hours_custom_window,
remove_hours_custom_window,
add_hours_remark_window,
remove_hours_remark_window,
add_hours_confirm_window,
remove_hours_confirm_window,
create_resident_name_window,
@@ -75,6 +83,8 @@ admin_menu_dialog = Dialog(
create_room_select_floor_window,
create_room_input_window,
create_room_confirm_window,
reporting_period_window,
next_period_confirm_window,
statistics_window,
broadcast_window,
broadcast_confirm_window,
@@ -50,9 +50,9 @@ async def on_hours_selected(
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)
await dialog_manager.switch_to(AdminMenuSG.add_hours_remark)
else:
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
await dialog_manager.switch_to(AdminMenuSG.remove_hours_remark)
async def on_custom_hours_click(
@@ -84,19 +84,59 @@ async def on_custom_hours_input(
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)
await dialog_manager.switch_to(AdminMenuSG.add_hours_remark)
else:
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
await dialog_manager.switch_to(AdminMenuSG.remove_hours_remark)
except ValueError:
await message.answer("⚠️ Пожалуйста, введите корректное число")
async def on_add_hours_remark_input(
message: Message,
widget: MessageInput,
dialog_manager: DialogManager,
):
if not message.text or len(message.text.strip()) < 1:
await message.answer("⚠️ Пожалуйста, введите причину добавления часов")
return
dialog_manager.dialog_data["hours_remark"] = message.text.strip()
await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm)
async def on_remove_hours_remark_input(
message: Message,
widget: MessageInput,
dialog_manager: DialogManager,
):
if message.text and len(message.text.strip()) > 0:
dialog_manager.dialog_data["hours_remark"] = message.text.strip()
else:
dialog_manager.dialog_data["hours_remark"] = None
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
async def on_skip_remark(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
dialog_manager.dialog_data["hours_remark"] = None
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
async def get_hours_confirm_data(
dialog_manager: DialogManager,
**kwargs,
):
hours = dialog_manager.dialog_data.get("selected_hours", 0)
return {"hours": hours}
remark = dialog_manager.dialog_data.get("hours_remark")
return {
"hours": hours,
"remark": remark if remark else "Не указана",
}
@inject
@@ -108,6 +148,7 @@ async def on_add_hours_confirm(
):
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
hours = dialog_manager.dialog_data.get("selected_hours")
remark = dialog_manager.dialog_data.get("hours_remark")
admin_id = callback.from_user.id
if resident_id and hours:
@@ -116,6 +157,7 @@ async def on_add_hours_confirm(
amount=hours,
admin_id=admin_id,
is_active=True,
remark=remark,
)
await dialog_manager.switch_to(AdminMenuSG.resident_info)
@@ -131,6 +173,7 @@ async def on_remove_hours_confirm(
):
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
hours = dialog_manager.dialog_data.get("selected_hours")
remark = dialog_manager.dialog_data.get("hours_remark")
admin_id = callback.from_user.id
if resident_id and hours:
@@ -147,6 +190,7 @@ async def on_remove_hours_confirm(
resident_id=resident_id,
amount=hours,
admin_id=admin_id,
remark=remark,
)
await dialog_manager.switch_to(AdminMenuSG.resident_info)
@@ -234,8 +278,35 @@ remove_hours_custom_window = Window(
state=AdminMenuSG.remove_hours_custom,
)
add_hours_remark_window = Window(
Const("<blockquote>📝 <b>Причина добавления часов</b></blockquote>\n\n<blockquote>Укажите причину добавления часов (обязательно).</blockquote>"),
MessageInput(on_add_hours_remark_input),
SwitchTo(
Const("◀️ Назад"),
id="back_to_add_hours_select",
state=AdminMenuSG.add_hours_select,
),
state=AdminMenuSG.add_hours_remark,
)
remove_hours_remark_window = Window(
Const("<blockquote>📝 <b>Причина снятия часов</b></blockquote>\n\n<blockquote>Укажите причину снятия часов (необязательно).</blockquote>"),
MessageInput(on_remove_hours_remark_input),
Button(
Const("⏭ Пропустить"),
id="skip_remark_btn",
on_click=on_skip_remark,
),
SwitchTo(
Const("◀️ Назад"),
id="back_to_remove_hours_select",
state=AdminMenuSG.remove_hours_select,
),
state=AdminMenuSG.remove_hours_remark,
)
add_hours_confirm_window = Window(
Format("<blockquote> <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?"),
Format("<blockquote> <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}"),
Row(
Button(
Const("✅ Да"),
@@ -253,7 +324,7 @@ add_hours_confirm_window = Window(
)
remove_hours_confirm_window = Window(
Format("<blockquote> <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите отнять <code>{hours}</code> часов?"),
Format("<blockquote> <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите отнять <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}"),
Row(
Button(
Const("✅ Да"),
@@ -4,6 +4,7 @@ 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 datetime import datetime, timedelta
from dutylog.application.bot.user_dialogs.states import AdminMenuSG
from dutylog.infrastructure.database.repositories.users_repository import (
@@ -12,13 +13,33 @@ from dutylog.infrastructure.database.repositories.users_repository import (
from dutylog.infrastructure.database.repositories.residents_repository import (
ResidentsRepository,
)
from dutylog.infrastructure.database.repositories.reporting_periods_repository import (
ReportingPeriodsRepository,
)
from dutylog.infrastructure.utils.config import Config
MONTH_NAMES = {
1: "Январь",
2: "Февраль",
3: "Март",
4: "Апрель",
5: "Май",
6: "Июнь",
7: "Июль",
8: "Август",
9: "Сентябрь",
10: "Октябрь",
11: "Ноябрь",
12: "Декабрь",
}
@inject
async def get_admin_menu_data(
event_from_user: User,
users_repository: FromDishka[UsersRepository],
reporting_periods_repository: FromDishka[ReportingPeriodsRepository],
config: FromDishka[Config],
**kwargs,
):
@@ -36,11 +57,32 @@ async def get_admin_menu_data(
else:
greeting = "👨‍💼 <b>Администратор</b>"
active_period = await reporting_periods_repository.get_active_period()
if active_period:
start_date = active_period.start_date
next_day = start_date + timedelta(days=1)
reporting_month = MONTH_NAMES[next_day.month]
reporting_year = next_day.year
days_passed = (datetime.now().date() - start_date).days
period_info = f"""
━━━━━━━━━━━━━━━━━━━━
📅 <b>Активный отчётный период</b>
<blockquote>Месяц: <b>{reporting_month} {reporting_year}</b>
Начало: <code>{start_date.strftime('%d.%m.%Y')}</code>
Прошло дней: <code>{days_passed}</code></blockquote>
"""
else:
period_info = ""
content = f"""
{greeting}
<blockquote>📋 <b>Панель управления</b></blockquote>
{period_info}
Выберите действие:
"""
@@ -91,6 +133,10 @@ async def on_floors_click(callback, button, dialog_manager):
await dialog_manager.switch_to(AdminMenuSG.floors)
async def on_reporting_period_click(callback, button, dialog_manager):
await dialog_manager.switch_to(AdminMenuSG.reporting_period)
main_menu_window = Window(
Format("{content}"),
SwitchTo(
@@ -110,6 +156,11 @@ main_menu_window = Window(
on_click=on_floors_click,
),
),
Button(
Const("📅 Отчётный период"),
id="reporting_period_btn",
on_click=on_reporting_period_click,
),
SwitchTo(
Const("📊 Статистика"),
id="stats_btn",
@@ -0,0 +1,203 @@
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, Row
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.infrastructure.database.repositories.reporting_periods_repository import (
ReportingPeriodsRepository,
)
MONTH_NAMES = {
1: "Январь",
2: "Февраль",
3: "Март",
4: "Апрель",
5: "Май",
6: "Июнь",
7: "Июль",
8: "Август",
9: "Сентябрь",
10: "Октябрь",
11: "Ноябрь",
12: "Декабрь",
}
async def on_reporting_period_click(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
await dialog_manager.switch_to(AdminMenuSG.reporting_period)
@inject
async def get_reporting_period_data(
reporting_periods_repository: FromDishka[ReportingPeriodsRepository],
**kwargs,
):
active_period = await reporting_periods_repository.get_active_period()
if active_period:
start_date = active_period.start_date
next_day = start_date + timedelta(days=1)
reporting_month = MONTH_NAMES[next_day.month]
reporting_year = next_day.year
days_passed = (datetime.now().date() - start_date).days
content = f"""
<blockquote>📅 <b>Отчётный период</b></blockquote>
<b>Статус:</b> 🟢 Активен
<b>Отчётный месяц:</b> <code>{reporting_month} {reporting_year}</code>
<b>Дата начала:</b> <code>{start_date.strftime('%d.%m.%Y')}</code>
<b>Прошло дней:</b> <code>{days_passed}</code>
━━━━━━━━━━━━━━━━━━━━
Используйте кнопки ниже для управления периодом.
"""
has_active = True
else:
content = """
<blockquote>📅 <b>Отчётный период</b></blockquote>
<b>Статус:</b> ⚪️ Нет активного периода
━━━━━━━━━━━━━━━━━━━━
Создайте новый отчётный период, чтобы начать учёт дежурств.
"""
has_active = False
return {
"content": content,
"has_active": has_active,
}
async def on_next_period_click(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
await dialog_manager.switch_to(AdminMenuSG.next_period_confirm)
async def on_make_report_click(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
await callback.answer("⚠️ Функционал в разработке", show_alert=True)
@inject
async def get_next_period_confirm_data(
reporting_periods_repository: FromDishka[ReportingPeriodsRepository],
**kwargs,
):
active_period = await reporting_periods_repository.get_active_period()
if active_period:
start_date = active_period.start_date
next_day = start_date + timedelta(days=1)
reporting_month = MONTH_NAMES[next_day.month]
reporting_year = next_day.year
content = f"""
<blockquote>⚠️ <b>Подтверждение</b></blockquote>
Вы уверены, что хотите начать следующий отчётный период?
<b>Текущий период:</b>
• Месяц: <code>{reporting_month} {reporting_year}</code>
• Начало: <code>{start_date.strftime('%d.%m.%Y')}</code>
Текущий период будет закрыт с датой окончания <code>{datetime.now().date().strftime('%d.%m.%Y')}</code>, и будет создан новый период.
"""
else:
content = f"""
<blockquote>⚠️ <b>Подтверждение</b></blockquote>
Вы уверены, что хотите создать новый отчётный период?
Период начнётся с <code>{datetime.now().date().strftime('%d.%m.%Y')}</code>.
"""
return {"content": content}
@inject
async def on_next_period_confirm(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
reporting_periods_repository: FromDishka[ReportingPeriodsRepository],
):
active_period = await reporting_periods_repository.get_active_period()
current_date = datetime.now().date()
if active_period:
await reporting_periods_repository.close_period(active_period.id, current_date)
await reporting_periods_repository.create_period(current_date)
await callback.answer("✅ Новый отчётный период создан!")
await dialog_manager.switch_to(AdminMenuSG.reporting_period)
async def on_next_period_cancel(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
):
await dialog_manager.switch_to(AdminMenuSG.reporting_period)
reporting_period_window = Window(
Format("{content}"),
Button(
Const("➡️ Следующий период"),
id="next_period_btn",
on_click=on_next_period_click,
),
Button(
Const("📊 Сделать отчёт"),
id="make_report_btn",
on_click=on_make_report_click,
when="has_active",
),
SwitchTo(
Const("◀️ Назад"),
id="back_to_admin_menu_from_period",
state=AdminMenuSG.main,
),
state=AdminMenuSG.reporting_period,
getter=get_reporting_period_data,
)
next_period_confirm_window = Window(
Format("{content}"),
Row(
Button(
Const("✅ Да"),
id="confirm_next_period",
on_click=on_next_period_confirm,
),
Button(
Const("❌ Нет"),
id="cancel_next_period",
on_click=on_next_period_cancel,
),
),
state=AdminMenuSG.next_period_confirm,
getter=get_next_period_confirm_data,
)
@@ -22,7 +22,9 @@ class AdminMenuSG(StatesGroup):
remove_hours_select = State()
add_hours_custom = State()
remove_hours_custom = State()
add_hours_remark = State()
add_hours_confirm = State()
remove_hours_remark = State()
remove_hours_confirm = State()
create_resident_name = State()
create_resident_floor = State()
@@ -38,6 +40,8 @@ class AdminMenuSG(StatesGroup):
create_room_select_floor = State()
create_room_input = State()
create_room_confirm = State()
reporting_period = State()
next_period_confirm = State()
statistics = State()
broadcast = State()
broadcast_confirm = State()