diff --git a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py
index c85fd2a..0aa6f33 100644
--- a/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py
+++ b/src/dutylog/application/bot/user_dialogs/admin_dialogs/admin_menu_dialog.py
@@ -18,6 +18,9 @@ from dutylog.infrastructure.database.repositories.residents_repository import (
from dutylog.infrastructure.database.repositories.rooms_repository import (
RoomsRepository,
)
+from dutylog.infrastructure.database.repositories.floors_repository import (
+ FloorsRepository,
+)
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
HoursTransactionsRepository,
)
@@ -84,8 +87,8 @@ async def get_statistics_data(
━━━━━━━━━━━━━━━━━━━━
-🟢 Всего активных часов: {total_active_hours} ч
-🔴 Всего неактивных часов: {total_inactive_hours} ч
+🟢 Всего отработанных часов: {total_inactive_hours} ч
+🔴 Всего неотработанных часов: {total_active_hours} ч
📊 Общий итог: {total_active_hours + total_inactive_hours} ч
"""
@@ -244,9 +247,8 @@ async def get_resident_info_data(
━━━━━━━━━━━━━━━━━━━━
-🟢 Активные часы: {resident.active_hours} ч
-🔴 Неактивные часы: {resident.inactive_hours} ч
-📊 Всего часов: {resident.active_hours + resident.inactive_hours} ч
+🟢 Отработанные часы: {resident.inactive_hours} ч
+🔴 Неотработанные часы: {resident.active_hours} ч
"""
return {
@@ -270,7 +272,108 @@ async def on_add_resident(
button: Button,
dialog_manager: DialogManager,
):
- await callback.answer("⚠️ Функционал в разработке", show_alert=True)
+ await dialog_manager.switch_to(AdminMenuSG.create_resident_name)
+
+
+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_filter_residents(
@@ -289,6 +392,22 @@ async def on_search_residents(
await callback.answer("⚠️ Функционал в разработке", show_alert=True)
+async def on_rooms_click(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+):
+ await callback.answer("⚠️ Функционал в разработке", show_alert=True)
+
+
+async def on_floors_click(
+ callback: CallbackQuery,
+ button: Button,
+ dialog_manager: DialogManager,
+):
+ await callback.answer("⚠️ Функционал в разработке", show_alert=True)
+
+
@inject
async def on_logout_resident_confirm(
callback: CallbackQuery,
@@ -408,6 +527,7 @@ async def on_add_hours_confirm(
admin_id = callback.from_user.id
if resident_id and hours:
+ # Добавляем часы к неотработанным (active_hours)
await transactions_repository.add_hours(
resident_id=resident_id,
amount=hours,
@@ -424,17 +544,27 @@ async def on_remove_hours_confirm(
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:
- await transactions_repository.remove_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,
- is_active=True,
)
await dialog_manager.switch_to(AdminMenuSG.resident_info)
@@ -456,6 +586,18 @@ admin_menu_dialog = Dialog(
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",
@@ -512,12 +654,12 @@ admin_menu_dialog = Dialog(
Format("{info_content}"),
Row(
Button(
- Const("➕ Добавить часы"),
+ Const("Добавить часы"),
id="add_hours_btn",
on_click=on_add_hours_click,
),
Button(
- Const("➖ Отнять часы"),
+ Const("Отнять часы"),
id="remove_hours_btn",
on_click=on_remove_hours_click,
),
@@ -658,6 +800,73 @@ admin_menu_dialog = Dialog(
state=AdminMenuSG.remove_hours_confirm,
getter=get_hours_confirm_data,
),
+ Window(
+ Const("
➕ Создание резидента\n\nВведите имя и фамилию резидента:"), + MessageInput(on_resident_name_input), + SwitchTo( + Const("◀️ Отмена"), + id="cancel_create_resident_name", + state=AdminMenuSG.residents, + ), + state=AdminMenuSG.create_resident_name, + ), + Window( + Const("
🏢 Выбор этажа\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, + ), + Window( + Const("
🚪 Выбор комнаты\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, + ), + Window( + Format("
✅ Подтверждение\n\nСоздать резидента {resident_name}?"), + 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, + ), Window( Format("{stats_content}"), SwitchTo(Const("◀️ Назад"), id="back_from_stats", state=AdminMenuSG.main), diff --git a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py index 7f84eb2..89c6f22 100644 --- a/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py +++ b/src/dutylog/application/bot/user_dialogs/main_menu_dialog.py @@ -81,9 +81,9 @@ async def get_main_menu_data( ⏰ Ваши часы дежурств -
🟢 Отработанные часы:""" has_resident = True else: @@ -120,7 +120,8 @@ async def get_history_data( """ else: transactions = await transactions_repository.get_resident_history(resident.id) - last_10 = transactions[:10] + transactions_sorted = sorted(transactions, key=lambda x: x.created_at) + last_10 = transactions_sorted[:10] if not last_10: history_text = """ @@ -131,7 +132,7 @@ async def get_history_data( else: history_lines = [] for tx in last_10: - emoji = "➕" if tx.transaction_type == "increase" else "➖" + emoji = "+" if tx.transaction_type == "increase" else "-" date_str = tx.created_at.strftime("%d.%m.%Y %H:%M") history_lines.append( f"{emoji}{resident.active_hours}ч +🟢 Отработанные часы:+🔴 Неотработанные часы:{resident.inactive_hours}ч ━━━━━━━━━━━━━━━━ -🔴 Неотработанные часы:{resident.inactive_hours}ч{resident.active_hours}ч
{tx.amount} ч • {date_str}"
diff --git a/src/dutylog/application/bot/user_dialogs/states.py b/src/dutylog/application/bot/user_dialogs/states.py
index 8e54ad2..4e5dce7 100644
--- a/src/dutylog/application/bot/user_dialogs/states.py
+++ b/src/dutylog/application/bot/user_dialogs/states.py
@@ -18,6 +18,10 @@ class AdminMenuSG(StatesGroup):
remove_hours_custom = State()
add_hours_confirm = State()
remove_hours_confirm = State()
+ create_resident_name = State()
+ create_resident_floor = State()
+ create_resident_room = State()
+ create_resident_confirm = State()
statistics = State()
broadcast = State()
broadcast_confirm = State()
diff --git a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py
index 13561ec..d9a2abb 100644
--- a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py
+++ b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py
@@ -78,6 +78,33 @@ class HoursTransactionsRepository:
return transaction, resident
+ async def move_hours_to_completed(
+ self,
+ resident_id: int,
+ amount: int,
+ admin_id: int | None = None,
+ ) -> tuple[HoursTransaction, Resident | None]:
+ """Перемещает часы из неотработанных в отработанные"""
+ transaction = HoursTransaction(
+ resident_id=resident_id,
+ transaction_type=TransactionType.DECREASE.value,
+ amount=amount,
+ admin_id=admin_id,
+ )
+ transaction = await self.transactions_dao.create(transaction)
+
+ resident = await self.residents_dao.get_by_id(resident_id)
+ if resident:
+ new_active = max(0, resident.active_hours - amount)
+ new_inactive = resident.inactive_hours + amount
+ resident = await self.residents_dao.update(
+ resident_id,
+ active_hours=new_active,
+ inactive_hours=new_inactive
+ )
+
+ return transaction, resident
+
async def get_resident_history(self, resident_id: int) -> list[HoursTransaction]:
return await self.transactions_dao.get_by_resident_id(resident_id)