mirror of
https://github.com/koloideal/DutyLog.git
synced 2026-08-08 18:11:14 +03:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a5e6cb7a8 | |||
| f775a34885 | |||
| f9d65b159a | |||
| 78930ab920 | |||
| 061c9c9ad6 | |||
| d8e6cead6e | |||
| ae940a58e6 | |||
| 504c454afb | |||
| 16e9d7f124 | |||
| c325586ef9 | |||
| ccd06c99b0 | |||
| 6d5608343a | |||
| 05b4a79b40 | |||
| 53f30c923b |
@@ -0,0 +1,25 @@
|
|||||||
|
"""add_photo_file_id_to_hours_transactions
|
||||||
|
|
||||||
|
Revision ID: a1b2c3d4e5f6
|
||||||
|
Revises: 4fe7f71301e7
|
||||||
|
Create Date: 2026-07-14 23:41:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = 'a1b2c3d4e5f6'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '4fe7f71301e7'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('hours_transactions', sa.Column('photo_file_id', sa.String(length=512), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('hours_transactions', 'photo_file_id')
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""change_photo_file_id_to_text
|
||||||
|
|
||||||
|
Revision ID: b2c3d4e5f6g7
|
||||||
|
Revises: a1b2c3d4e5f6
|
||||||
|
Create Date: 2026-07-15 00:35:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = 'b2c3d4e5f6g7'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = 'a1b2c3d4e5f6'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
'hours_transactions',
|
||||||
|
'photo_file_id',
|
||||||
|
existing_type=sa.String(length=512),
|
||||||
|
type_=sa.Text(),
|
||||||
|
existing_nullable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
'hours_transactions',
|
||||||
|
'photo_file_id',
|
||||||
|
existing_type=sa.Text(),
|
||||||
|
type_=sa.String(length=512),
|
||||||
|
existing_nullable=True,
|
||||||
|
)
|
||||||
@@ -13,6 +13,7 @@ from dutylog.application.bot.user_dialogs import main_menu_dialog
|
|||||||
from dutylog.application.bot.admin_dialogs import admin_menu_dialog
|
from dutylog.application.bot.admin_dialogs import admin_menu_dialog
|
||||||
from dutylog.application.bot.user_dialogs.registration_dialog import registration_dialog
|
from dutylog.application.bot.user_dialogs.registration_dialog import registration_dialog
|
||||||
from dutylog.application.bot.user_dialogs.user_menu.feedback import feedback_router
|
from dutylog.application.bot.user_dialogs.user_menu.feedback import feedback_router
|
||||||
|
from dutylog.application.bot.middlewares.album import AlbumMiddleware
|
||||||
from dutylog.infrastructure.ioc import (
|
from dutylog.infrastructure.ioc import (
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
DatabaseProvider,
|
DatabaseProvider,
|
||||||
@@ -37,6 +38,8 @@ async def main():
|
|||||||
|
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
dp.message.outer_middleware(AlbumMiddleware())
|
||||||
|
|
||||||
container = make_async_container(
|
container = make_async_container(
|
||||||
ConfigProvider(),
|
ConfigProvider(),
|
||||||
DatabaseProvider(),
|
DatabaseProvider(),
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ from dutylog.application.bot.admin_dialogs.residents_management import (
|
|||||||
residents_list_window,
|
residents_list_window,
|
||||||
resident_info_window,
|
resident_info_window,
|
||||||
resident_history_window,
|
resident_history_window,
|
||||||
|
resident_history_detail_window,
|
||||||
|
resident_history_search_input_window,
|
||||||
|
resident_history_search_results_window,
|
||||||
resident_logout_confirm_window,
|
resident_logout_confirm_window,
|
||||||
resident_delete_confirm_window,
|
resident_delete_confirm_window,
|
||||||
resident_rebind_floor_window,
|
resident_rebind_floor_window,
|
||||||
@@ -36,6 +39,7 @@ from dutylog.application.bot.admin_dialogs.rooms_management import (
|
|||||||
rooms_list_window,
|
rooms_list_window,
|
||||||
room_info_window,
|
room_info_window,
|
||||||
room_history_window,
|
room_history_window,
|
||||||
|
room_history_detail_window,
|
||||||
room_delete_confirm_window,
|
room_delete_confirm_window,
|
||||||
room_add_hours_select_window,
|
room_add_hours_select_window,
|
||||||
room_add_hours_custom_window,
|
room_add_hours_custom_window,
|
||||||
@@ -80,6 +84,8 @@ from dutylog.application.bot.creator_dialogs.admins_management import (
|
|||||||
)
|
)
|
||||||
from dutylog.application.bot.creator_dialogs.transactions_history import (
|
from dutylog.application.bot.creator_dialogs.transactions_history import (
|
||||||
transactions_history_window,
|
transactions_history_window,
|
||||||
|
transactions_history_detail_window,
|
||||||
|
transactions_history_delete_confirm_window,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +94,9 @@ admin_menu_dialog = Dialog(
|
|||||||
residents_list_window,
|
residents_list_window,
|
||||||
resident_info_window,
|
resident_info_window,
|
||||||
resident_history_window,
|
resident_history_window,
|
||||||
|
resident_history_detail_window,
|
||||||
|
resident_history_search_input_window,
|
||||||
|
resident_history_search_results_window,
|
||||||
resident_logout_confirm_window,
|
resident_logout_confirm_window,
|
||||||
resident_delete_confirm_window,
|
resident_delete_confirm_window,
|
||||||
resident_rebind_floor_window,
|
resident_rebind_floor_window,
|
||||||
@@ -113,6 +122,7 @@ admin_menu_dialog = Dialog(
|
|||||||
rooms_list_window,
|
rooms_list_window,
|
||||||
room_info_window,
|
room_info_window,
|
||||||
room_history_window,
|
room_history_window,
|
||||||
|
room_history_detail_window,
|
||||||
room_delete_confirm_window,
|
room_delete_confirm_window,
|
||||||
room_add_hours_select_window,
|
room_add_hours_select_window,
|
||||||
room_add_hours_custom_window,
|
room_add_hours_custom_window,
|
||||||
@@ -141,4 +151,6 @@ admin_menu_dialog = Dialog(
|
|||||||
add_admin_select_user_window,
|
add_admin_select_user_window,
|
||||||
add_admin_confirm_window,
|
add_admin_confirm_window,
|
||||||
transactions_history_window,
|
transactions_history_window,
|
||||||
|
transactions_history_detail_window,
|
||||||
|
transactions_history_delete_confirm_window,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
||||||
from aiogram.types import Message, CallbackQuery
|
from aiogram.types import Message, CallbackQuery, InputMediaPhoto
|
||||||
from aiogram_dialog import Window, DialogManager
|
from aiogram_dialog import Window, DialogManager
|
||||||
from aiogram_dialog.widgets.text import Format, Const
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, Select, Group
|
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, Select, Group
|
||||||
@@ -101,11 +103,28 @@ async def on_add_hours_remark_input(
|
|||||||
widget: MessageInput,
|
widget: MessageInput,
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
):
|
):
|
||||||
if not message.text or len(message.text.strip()) < 1:
|
album = dialog_manager.middleware_data.get("album")
|
||||||
await message.answer("⚠️ Пожалуйста, введите причину добавления часов")
|
photo_file_ids = []
|
||||||
|
remark = None
|
||||||
|
|
||||||
|
if album:
|
||||||
|
for msg in album:
|
||||||
|
if msg.photo:
|
||||||
|
photo_file_ids.append(msg.photo[-1].file_id)
|
||||||
|
if msg.caption and msg.caption.strip():
|
||||||
|
remark = msg.caption.strip()
|
||||||
|
elif message.photo:
|
||||||
|
photo_file_ids = [message.photo[-1].file_id]
|
||||||
|
remark = message.caption.strip() if message.caption and message.caption.strip() else None
|
||||||
|
elif message.text and len(message.text.strip()) > 0:
|
||||||
|
remark = message.text.strip()
|
||||||
|
|
||||||
|
if not photo_file_ids and not remark:
|
||||||
|
await message.answer("⚠️ Пожалуйста, отправьте текст или фото с подписью")
|
||||||
return
|
return
|
||||||
|
|
||||||
dialog_manager.dialog_data["hours_remark"] = message.text.strip()
|
dialog_manager.dialog_data["hours_remark"] = remark
|
||||||
|
dialog_manager.dialog_data["photo_file_ids"] = photo_file_ids
|
||||||
await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm)
|
await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm)
|
||||||
|
|
||||||
|
|
||||||
@@ -114,11 +133,28 @@ async def on_remove_hours_remark_input(
|
|||||||
widget: MessageInput,
|
widget: MessageInput,
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
):
|
):
|
||||||
if message.text and len(message.text.strip()) > 0:
|
album = dialog_manager.middleware_data.get("album")
|
||||||
dialog_manager.dialog_data["hours_remark"] = message.text.strip()
|
photo_file_ids = []
|
||||||
else:
|
remark = None
|
||||||
dialog_manager.dialog_data["hours_remark"] = None
|
|
||||||
|
|
||||||
|
if album:
|
||||||
|
for msg in album:
|
||||||
|
if msg.photo:
|
||||||
|
photo_file_ids.append(msg.photo[-1].file_id)
|
||||||
|
if msg.caption and msg.caption.strip():
|
||||||
|
remark = msg.caption.strip()
|
||||||
|
elif message.photo:
|
||||||
|
photo_file_ids = [message.photo[-1].file_id]
|
||||||
|
remark = message.caption.strip() if message.caption and message.caption.strip() else None
|
||||||
|
elif message.text and len(message.text.strip()) > 0:
|
||||||
|
remark = message.text.strip()
|
||||||
|
|
||||||
|
if not photo_file_ids and not remark:
|
||||||
|
await message.answer("⚠️ Пожалуйста, отправьте текст или фото с подписью")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog_manager.dialog_data["hours_remark"] = remark
|
||||||
|
dialog_manager.dialog_data["photo_file_ids"] = photo_file_ids
|
||||||
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
|
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
|
||||||
|
|
||||||
|
|
||||||
@@ -128,6 +164,7 @@ async def on_skip_remark(
|
|||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
):
|
):
|
||||||
dialog_manager.dialog_data["hours_remark"] = None
|
dialog_manager.dialog_data["hours_remark"] = None
|
||||||
|
dialog_manager.dialog_data["photo_file_ids"] = []
|
||||||
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
|
await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm)
|
||||||
|
|
||||||
|
|
||||||
@@ -137,10 +174,13 @@ async def get_hours_confirm_data(
|
|||||||
):
|
):
|
||||||
hours = dialog_manager.dialog_data.get("selected_hours", 0)
|
hours = dialog_manager.dialog_data.get("selected_hours", 0)
|
||||||
remark = dialog_manager.dialog_data.get("hours_remark")
|
remark = dialog_manager.dialog_data.get("hours_remark")
|
||||||
|
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
|
||||||
|
has_photo = bool(photo_file_ids)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"hours": hours,
|
"hours": hours,
|
||||||
"remark": remark if remark else "Не указана",
|
"remark": remark if remark else "Не указана",
|
||||||
|
"has_photo": f"✅ Да ({len(photo_file_ids)} шт.)" if has_photo else "❌ Нет",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -160,12 +200,15 @@ async def on_add_hours_confirm(
|
|||||||
admin_id = callback.from_user.id
|
admin_id = callback.from_user.id
|
||||||
|
|
||||||
if resident_id and hours:
|
if resident_id and hours:
|
||||||
|
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
|
||||||
|
photo_file_id = json.dumps(photo_file_ids) if photo_file_ids else None
|
||||||
await transactions_repository.add_hours(
|
await transactions_repository.add_hours(
|
||||||
resident_id=resident_id,
|
resident_id=resident_id,
|
||||||
amount=hours,
|
amount=hours,
|
||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
resident = await residents_repository.get_resident_by_id(resident_id)
|
resident = await residents_repository.get_resident_by_id(resident_id)
|
||||||
@@ -176,7 +219,14 @@ async def on_add_hours_confirm(
|
|||||||
notification_text = f"<blockquote>➕ <b>Начислены часы</b></blockquote>\n\n<b>Количество:</b> <code>{hours}</code> ч\n<b>Причина:</b> {remark}\n<b>Администратор:</b> {admin_username}\n\n<b>Всего неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
notification_text = f"<blockquote>➕ <b>Начислены часы</b></blockquote>\n\n<b>Количество:</b> <code>{hours}</code> ч\n<b>Причина:</b> {remark}\n<b>Администратор:</b> {admin_username}\n\n<b>Всего неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await bot.send_message(resident.user_entity, notification_text)
|
if photo_file_ids:
|
||||||
|
if len(photo_file_ids) == 1:
|
||||||
|
await bot.send_photo(resident.user_entity, photo_file_ids[0], caption=notification_text)
|
||||||
|
else:
|
||||||
|
media = [InputMediaPhoto(media=fid, caption=notification_text if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
|
||||||
|
await bot.send_media_group(resident.user_entity, media=media)
|
||||||
|
else:
|
||||||
|
await bot.send_message(resident.user_entity, notification_text)
|
||||||
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -208,11 +258,14 @@ async def on_remove_hours_confirm(
|
|||||||
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
|
||||||
|
photo_file_id = json.dumps(photo_file_ids) if photo_file_ids else None
|
||||||
await transactions_repository.move_hours_to_completed(
|
await transactions_repository.move_hours_to_completed(
|
||||||
resident_id=resident_id,
|
resident_id=resident_id,
|
||||||
amount=hours,
|
amount=hours,
|
||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
resident = await residents_repository.get_resident_by_id(resident_id)
|
resident = await residents_repository.get_resident_by_id(resident_id)
|
||||||
@@ -232,7 +285,14 @@ async def on_remove_hours_confirm(
|
|||||||
notification_text += f"<b>Администратор:</b> {admin_username}\n\n<b>Осталось неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
notification_text += f"<b>Администратор:</b> {admin_username}\n\n<b>Осталось неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await bot.send_message(resident.user_entity, notification_text)
|
if photo_file_ids:
|
||||||
|
if len(photo_file_ids) == 1:
|
||||||
|
await bot.send_photo(resident.user_entity, photo_file_ids[0], caption=notification_text)
|
||||||
|
else:
|
||||||
|
media = [InputMediaPhoto(media=fid, caption=notification_text if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
|
||||||
|
await bot.send_media_group(resident.user_entity, media=media)
|
||||||
|
else:
|
||||||
|
await bot.send_message(resident.user_entity, notification_text)
|
||||||
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -322,7 +382,7 @@ remove_hours_custom_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_hours_remark_window = Window(
|
add_hours_remark_window = Window(
|
||||||
Const("<blockquote>📝 <b>Причина добавления часов</b></blockquote>\n\n<blockquote>Укажите причину добавления часов (обязательно).</blockquote>"),
|
Const("<blockquote>📝 <b>Причина добавления часов</b></blockquote>\n\n<blockquote>Отправьте текст или фото с подписью.\nФото будет сохранено как описание операции.</blockquote>"),
|
||||||
MessageInput(on_add_hours_remark_input),
|
MessageInput(on_add_hours_remark_input),
|
||||||
SwitchTo(
|
SwitchTo(
|
||||||
Const("◀️ Назад"),
|
Const("◀️ Назад"),
|
||||||
@@ -333,7 +393,7 @@ add_hours_remark_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
remove_hours_remark_window = Window(
|
remove_hours_remark_window = Window(
|
||||||
Const("<blockquote>📝 <b>Причина снятия часов</b></blockquote>\n\n<blockquote>Укажите причину снятия часов (необязательно).</blockquote>"),
|
Const("<blockquote>📝 <b>Причина снятия часов</b></blockquote>\n\n<blockquote>Отправьте текст или фото с подписью.\nФото будет сохранено как описание операции.\nМожно пропустить, нажав кнопку ниже.</blockquote>"),
|
||||||
MessageInput(on_remove_hours_remark_input),
|
MessageInput(on_remove_hours_remark_input),
|
||||||
Button(
|
Button(
|
||||||
Const("⏭ Пропустить"),
|
Const("⏭ Пропустить"),
|
||||||
@@ -349,7 +409,7 @@ remove_hours_remark_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_hours_confirm_window = Window(
|
add_hours_confirm_window = Window(
|
||||||
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}"),
|
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}\n<b>Фото:</b> {has_photo}"),
|
||||||
Row(
|
Row(
|
||||||
Button(
|
Button(
|
||||||
Const("✅ Да"),
|
Const("✅ Да"),
|
||||||
@@ -367,7 +427,7 @@ add_hours_confirm_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
remove_hours_confirm_window = Window(
|
remove_hours_confirm_window = Window(
|
||||||
Format("<blockquote>➖ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите отнять <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}"),
|
Format("<blockquote>➖ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите отнять <code>{hours}</code> часов?\n\n<b>Причина:</b> {remark}\n<b>Фото:</b> {has_photo}"),
|
||||||
Row(
|
Row(
|
||||||
Button(
|
Button(
|
||||||
Const("✅ Да"),
|
Const("✅ Да"),
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
from aiogram.types import Message, CallbackQuery
|
from aiogram import Bot
|
||||||
|
from aiogram.enums import ContentType
|
||||||
|
from aiogram.types import Message, CallbackQuery, InputMediaPhoto, User
|
||||||
from aiogram.utils.markdown import html_decoration as hd
|
from aiogram.utils.markdown import html_decoration as hd
|
||||||
from aiogram_dialog import Window, DialogManager
|
from aiogram_dialog import Window, DialogManager
|
||||||
from aiogram_dialog.widgets.text import Format, Const
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, ScrollingGroup, Select, Group
|
from aiogram_dialog.widgets.kbd import Row, SwitchTo, Button, ScrollingGroup, Select, Group
|
||||||
from aiogram_dialog.widgets.input import MessageInput
|
from aiogram_dialog.widgets.input import MessageInput
|
||||||
|
from aiogram_dialog.widgets.media import DynamicMedia
|
||||||
|
from aiogram_dialog.api.entities.media import MediaAttachment, MediaId
|
||||||
from magic_filter import F
|
from magic_filter import F
|
||||||
from dishka import FromDishka
|
from dishka import FromDishka
|
||||||
from dishka.integrations.aiogram_dialog import inject
|
from dishka.integrations.aiogram_dialog import inject
|
||||||
@@ -24,6 +28,7 @@ from dutylog.infrastructure.database.repositories.users_repository import (
|
|||||||
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
|
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
|
||||||
HoursTransactionsRepository,
|
HoursTransactionsRepository,
|
||||||
)
|
)
|
||||||
|
from dutylog.infrastructure.utils.config import Config
|
||||||
from dutylog.infrastructure.utils.datetime import msk_now
|
from dutylog.infrastructure.utils.datetime import msk_now
|
||||||
|
|
||||||
|
|
||||||
@@ -448,10 +453,13 @@ async def on_create_resident_cancel(
|
|||||||
await dialog_manager.switch_to(AdminMenuSG.residents)
|
await dialog_manager.switch_to(AdminMenuSG.residents)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
async def on_search_input(
|
async def on_search_input(
|
||||||
message: Message,
|
message: Message,
|
||||||
widget: MessageInput,
|
widget: MessageInput,
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
):
|
):
|
||||||
if not message.text or len(message.text.strip()) < 1:
|
if not message.text or len(message.text.strip()) < 1:
|
||||||
await message.answer("⚠️ Пожалуйста, введите поисковый запрос")
|
await message.answer("⚠️ Пожалуйста, введите поисковый запрос")
|
||||||
@@ -463,7 +471,16 @@ async def on_search_input(
|
|||||||
|
|
||||||
dialog_manager.dialog_data["search_query"] = query
|
dialog_manager.dialog_data["search_query"] = query
|
||||||
dialog_manager.dialog_data["is_search_active"] = True
|
dialog_manager.dialog_data["is_search_active"] = True
|
||||||
await dialog_manager.switch_to(AdminMenuSG.residents_search_results)
|
|
||||||
|
residents, _ = await residents_repository.search_residents(query, users_repository)
|
||||||
|
|
||||||
|
if len(residents) == 1:
|
||||||
|
dialog_manager.dialog_data["selected_resident_id"] = residents[0].id
|
||||||
|
dialog_manager.dialog_data["from_search"] = True
|
||||||
|
dialog_manager.dialog_data["from_filter"] = False
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_info)
|
||||||
|
else:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.residents_search_results)
|
||||||
|
|
||||||
|
|
||||||
@inject
|
@inject
|
||||||
@@ -562,16 +579,16 @@ async def get_resident_history_data(
|
|||||||
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
|
|
||||||
if not resident_id:
|
if not resident_id:
|
||||||
return {"history_content": "Ошибка: резидент не выбран"}
|
return {"history_content": "Ошибка: резидент не выбран", "transactions": [], "has_transactions": False}
|
||||||
|
|
||||||
resident = await residents_repository.get_resident_by_id(resident_id)
|
resident = await residents_repository.get_resident_by_id(resident_id)
|
||||||
|
|
||||||
if not resident:
|
if not resident:
|
||||||
return {"history_content": "Ошибка: резидент не найден"}
|
return {"history_content": "Ошибка: резидент не найден", "transactions": [], "has_transactions": False}
|
||||||
|
|
||||||
transactions = await transactions_repository.get_resident_history(resident_id)
|
transactions = await transactions_repository.get_resident_history(resident_id)
|
||||||
transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
|
transactions_sorted = sorted(transactions, key=lambda x: x.created_at, reverse=True)
|
||||||
last_10 = transactions_sorted[-10:]
|
last_10 = transactions_sorted[:10]
|
||||||
|
|
||||||
resident_name = resident.real_name if resident.real_name else "Без имени"
|
resident_name = resident.real_name if resident.real_name else "Без имени"
|
||||||
|
|
||||||
@@ -590,18 +607,206 @@ async def get_resident_history_data(
|
|||||||
<b>Резидент:</b> {resident_name}
|
<b>Резидент:</b> {resident_name}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for tx in last_10:
|
|
||||||
|
tx_items = []
|
||||||
|
for tx in last_10:
|
||||||
|
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")
|
||||||
|
photo_mark = " 📷" if tx.photo_file_id else ""
|
||||||
|
label = f"{emoji}{tx.amount} ч | {date_str}{photo_mark}"
|
||||||
|
tx_items.append((label, str(tx.id)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"history_content": history_text,
|
||||||
|
"transactions": tx_items,
|
||||||
|
"has_transactions": len(tx_items) > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_transaction_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
item_id: str,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["selected_transaction_id"] = int(item_id)
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(item_id))
|
||||||
|
if tx:
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
if len(photo_file_ids) > 1:
|
||||||
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
|
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
|
||||||
emoji = "+" 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)
|
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
|
||||||
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
|
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
|
||||||
|
admin_info = ""
|
||||||
|
if tx.admin_id:
|
||||||
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
|
if admin:
|
||||||
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
caption = f"📋 <b>Детали транзакции</b>\n\n⚙️ <b>Операция:</b> {operation}\n🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч\n📅 <b>Дата:</b> {date_str}{remark_text}{admin_info}"
|
||||||
|
bot: Bot = dialog_manager.middleware_data.get("bot")
|
||||||
|
try:
|
||||||
|
media = [InputMediaPhoto(media=fid, caption=caption if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
|
||||||
|
await bot.send_media_group(callback.message.chat.id, media=media)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_history_detail)
|
||||||
|
|
||||||
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n"
|
|
||||||
|
|
||||||
return {"history_content": history_text}
|
@inject
|
||||||
|
async def get_transaction_detail_data(
|
||||||
|
event_from_user: User,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
config: FromDishka[Config],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
tx_id = dialog_manager.dialog_data.get("selected_transaction_id")
|
||||||
|
|
||||||
|
if not tx_id:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не выбрана", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(tx_id))
|
||||||
|
|
||||||
|
if not tx:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не найдена", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
admin_info = ""
|
||||||
|
if tx.admin_id:
|
||||||
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
|
if admin:
|
||||||
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
<blockquote>📋 <b>Детали транзакции</b></blockquote>
|
||||||
|
|
||||||
|
⚙️ <b>Операция:</b> {operation}
|
||||||
|
🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч
|
||||||
|
📅 <b>Дата:</b> {date_str}{remark_text}{admin_info}
|
||||||
|
"""
|
||||||
|
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
photo_media = None
|
||||||
|
has_multiple_photos = len(photo_file_ids) > 1
|
||||||
|
if photo_file_ids and not has_multiple_photos:
|
||||||
|
photo_media = MediaAttachment(
|
||||||
|
type=ContentType.PHOTO,
|
||||||
|
file_id=MediaId(file_id=photo_file_ids[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_multiple_photos:
|
||||||
|
detail_content = ""
|
||||||
|
else:
|
||||||
|
detail_content = content
|
||||||
|
|
||||||
|
is_creator = event_from_user.id == config.bot.creator_id
|
||||||
|
|
||||||
|
return {
|
||||||
|
"detail_content": detail_content,
|
||||||
|
"photo_media": photo_media,
|
||||||
|
"has_multiple_photos": has_multiple_photos,
|
||||||
|
"is_creator": is_creator,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def on_search_transactions(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_history_search_input)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_resident_delete_transaction_click(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["delete_source"] = "resident_history"
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.transactions_history_delete_confirm)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_transaction_search_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["transaction_search_query"] = message.text.strip()
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_history_search_results)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_transaction_search_results_data(
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
resident_id = dialog_manager.dialog_data.get("selected_resident_id")
|
||||||
|
query = dialog_manager.dialog_data.get("transaction_search_query", "")
|
||||||
|
|
||||||
|
if not resident_id:
|
||||||
|
return {"content": "Ошибка: резидент не выбран", "search_transactions": [], "has_search_results": False}
|
||||||
|
|
||||||
|
resident = await residents_repository.get_resident_by_id(resident_id)
|
||||||
|
resident_name = resident.real_name if resident and resident.real_name else "Без имени"
|
||||||
|
|
||||||
|
transactions = await transactions_repository.search_resident_transactions(resident_id, query)
|
||||||
|
|
||||||
|
if not transactions:
|
||||||
|
content = f"""
|
||||||
|
<blockquote>🔍 <b>Поиск по транзакциям</b></blockquote>
|
||||||
|
|
||||||
|
<b>Резидент:</b> {resident_name}
|
||||||
|
<b>Запрос:</b> <code>{query}</code>
|
||||||
|
|
||||||
|
❌ Ничего не найдено
|
||||||
|
"""
|
||||||
|
return {"content": content, "search_transactions": [], "has_search_results": False}
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
<blockquote>🔍 <b>Поиск по транзакциям</b></blockquote>
|
||||||
|
|
||||||
|
<b>Резидент:</b> {resident_name}
|
||||||
|
<b>Запрос:</b> <code>{query}</code>
|
||||||
|
<b>Найдено:</b> <code>{len(transactions)}</code>
|
||||||
|
|
||||||
|
"""
|
||||||
|
search_transactions = []
|
||||||
|
for tx in transactions:
|
||||||
|
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")
|
||||||
|
photo_mark = " 📷" if tx.photo_file_id else ""
|
||||||
|
label = f"{emoji}{tx.amount} ч | {date_str}{photo_mark}"
|
||||||
|
search_transactions.append((label, str(tx.id)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content": content,
|
||||||
|
"search_transactions": search_transactions,
|
||||||
|
"has_search_results": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
residents_list_window = Window(
|
residents_list_window = Window(
|
||||||
@@ -654,37 +859,42 @@ resident_info_window = Window(
|
|||||||
when=~F["is_admin"],
|
when=~F["is_admin"],
|
||||||
),
|
),
|
||||||
Button(
|
Button(
|
||||||
Const("➖️ Отнять"),
|
Const("➖ Отнять"),
|
||||||
id="remove_hours_btn",
|
id="remove_hours_btn",
|
||||||
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.remove_hours_select),
|
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.remove_hours_select),
|
||||||
when=~F["is_admin"],
|
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(
|
Button(
|
||||||
Const("🔄 Перепривязать к комнате"),
|
Const("🔄 Перепривязать к комнате"),
|
||||||
id="rebind_resident_btn",
|
id="rebind_resident_btn",
|
||||||
on_click=on_rebind_resident,
|
on_click=on_rebind_resident,
|
||||||
when=~F["is_admin"],
|
when=~F["is_admin"],
|
||||||
),
|
),
|
||||||
|
Button(
|
||||||
|
Const("🚪 Разлогинить"),
|
||||||
|
id="logout_resident_btn",
|
||||||
|
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_logout_confirm),
|
||||||
|
when=F["is_busy"] & ~F["is_admin"],
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
Button(
|
Button(
|
||||||
Const("🚪 Разлогинить"),
|
Const("📜 История"),
|
||||||
id="logout_resident_btn",
|
id="resident_history_btn",
|
||||||
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_logout_confirm),
|
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_history),
|
||||||
when=F["is_busy"] & ~F["is_admin"],
|
when=~F["is_admin"],
|
||||||
),
|
),
|
||||||
Button(
|
Button(
|
||||||
Const("🗑 Удалить"),
|
Const("🗑 Удалить"),
|
||||||
id="delete_resident_btn",
|
id="delete_resident_btn",
|
||||||
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_delete_confirm),
|
on_click=lambda c, b, m: m.switch_to(AdminMenuSG.resident_delete_confirm),
|
||||||
when=~F["is_admin"],
|
when=~F["is_admin"],
|
||||||
)
|
),
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("🔍 Поиск резидента"),
|
||||||
|
id="search_from_resident_info_btn",
|
||||||
|
on_click=on_search_residents,
|
||||||
),
|
),
|
||||||
SwitchTo(
|
SwitchTo(
|
||||||
Const("◀️ Назад к результатам поиска"),
|
Const("◀️ Назад к результатам поиска"),
|
||||||
@@ -919,6 +1129,24 @@ resident_rebind_confirm_window = Window(
|
|||||||
|
|
||||||
resident_history_window = Window(
|
resident_history_window = Window(
|
||||||
Format("{history_content}"),
|
Format("{history_content}"),
|
||||||
|
ScrollingGroup(
|
||||||
|
Select(
|
||||||
|
Format("{item[0]}"),
|
||||||
|
id="transactions_select",
|
||||||
|
item_id_getter=lambda x: x[1],
|
||||||
|
items="transactions",
|
||||||
|
on_click=on_transaction_selected,
|
||||||
|
),
|
||||||
|
id="transactions_scroll",
|
||||||
|
width=1,
|
||||||
|
height=8,
|
||||||
|
when="has_transactions",
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("🔍 Поиск по транзакциям"),
|
||||||
|
id="search_transactions_btn",
|
||||||
|
on_click=on_search_transactions,
|
||||||
|
),
|
||||||
SwitchTo(
|
SwitchTo(
|
||||||
Const("◀️ Назад"),
|
Const("◀️ Назад"),
|
||||||
id="back_to_resident_info",
|
id="back_to_resident_info",
|
||||||
@@ -927,3 +1155,63 @@ resident_history_window = Window(
|
|||||||
state=AdminMenuSG.resident_history,
|
state=AdminMenuSG.resident_history,
|
||||||
getter=get_resident_history_data,
|
getter=get_resident_history_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
resident_history_detail_window = Window(
|
||||||
|
DynamicMedia("photo_media", when=F["photo_media"]),
|
||||||
|
Format("{detail_content}", when=F["detail_content"]),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("🗑 Удалить"),
|
||||||
|
id="resident_delete_transaction_btn",
|
||||||
|
on_click=on_resident_delete_transaction_click,
|
||||||
|
when=F["is_creator"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ К списку"),
|
||||||
|
id="back_to_history_from_detail",
|
||||||
|
state=AdminMenuSG.resident_history,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.resident_history_detail,
|
||||||
|
getter=get_transaction_detail_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
resident_history_search_input_window = Window(
|
||||||
|
Const("<blockquote>🔍 <b>Поиск по транзакциям</b></blockquote>\n\n<blockquote>Введите поисковый запрос:\n• Текст для поиска по описанию\n• Число для поиска по количеству часов</blockquote>"),
|
||||||
|
MessageInput(on_transaction_search_input),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ Назад к истории"),
|
||||||
|
id="back_to_history_from_search_input",
|
||||||
|
state=AdminMenuSG.resident_history,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.resident_history_search_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
resident_history_search_results_window = Window(
|
||||||
|
Format("{content}"),
|
||||||
|
ScrollingGroup(
|
||||||
|
Select(
|
||||||
|
Format("{item[0]}"),
|
||||||
|
id="search_transactions_select",
|
||||||
|
item_id_getter=lambda x: x[1],
|
||||||
|
items="search_transactions",
|
||||||
|
on_click=on_transaction_selected,
|
||||||
|
),
|
||||||
|
id="search_transactions_scroll",
|
||||||
|
width=1,
|
||||||
|
height=8,
|
||||||
|
when="has_search_results",
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("🔍 Новый поиск"),
|
||||||
|
id="new_transaction_search",
|
||||||
|
state=AdminMenuSG.resident_history_search_input,
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ К истории"),
|
||||||
|
id="back_to_history_from_search_results",
|
||||||
|
state=AdminMenuSG.resident_history,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.resident_history_search_results,
|
||||||
|
getter=get_transaction_search_results_data,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
|
from aiogram.enums import ContentType
|
||||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
||||||
from aiogram.types import Message, CallbackQuery
|
from aiogram.types import Message, CallbackQuery, InputMediaPhoto
|
||||||
from aiogram_dialog import Window, DialogManager
|
from aiogram_dialog import Window, DialogManager
|
||||||
from aiogram_dialog.widgets.text import Format, Const
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
from aiogram_dialog.widgets.kbd import SwitchTo, Button, ScrollingGroup, Select, Row, Group
|
from aiogram_dialog.widgets.kbd import SwitchTo, Button, ScrollingGroup, Select, Row, Group
|
||||||
from aiogram_dialog.widgets.input import MessageInput
|
from aiogram_dialog.widgets.input import MessageInput
|
||||||
|
from aiogram_dialog.widgets.media import DynamicMedia
|
||||||
|
from aiogram_dialog.api.entities.media import MediaAttachment, MediaId
|
||||||
|
from magic_filter import F
|
||||||
from dishka import FromDishka
|
from dishka import FromDishka
|
||||||
from dishka.integrations.aiogram_dialog import inject
|
from dishka.integrations.aiogram_dialog import inject
|
||||||
|
|
||||||
@@ -311,11 +317,28 @@ async def on_room_remark_input(
|
|||||||
widget: MessageInput,
|
widget: MessageInput,
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
):
|
):
|
||||||
if message.text and message.text.strip():
|
album = dialog_manager.middleware_data.get("album")
|
||||||
dialog_manager.dialog_data["remark"] = message.text.strip()
|
photo_file_ids = []
|
||||||
else:
|
remark = None
|
||||||
dialog_manager.dialog_data["remark"] = None
|
|
||||||
|
|
||||||
|
if album:
|
||||||
|
for msg in album:
|
||||||
|
if msg.photo:
|
||||||
|
photo_file_ids.append(msg.photo[-1].file_id)
|
||||||
|
if msg.caption and msg.caption.strip():
|
||||||
|
remark = msg.caption.strip()
|
||||||
|
elif message.photo:
|
||||||
|
photo_file_ids = [message.photo[-1].file_id]
|
||||||
|
remark = message.caption.strip() if message.caption and message.caption.strip() else None
|
||||||
|
elif message.text and message.text.strip():
|
||||||
|
remark = message.text.strip()
|
||||||
|
|
||||||
|
if not photo_file_ids and not remark:
|
||||||
|
await message.answer("⚠️ Пожалуйста, отправьте текст или фото с подписью")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog_manager.dialog_data["remark"] = remark
|
||||||
|
dialog_manager.dialog_data["photo_file_ids"] = photo_file_ids
|
||||||
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
|
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
|
||||||
|
|
||||||
|
|
||||||
@@ -325,6 +348,7 @@ async def on_room_skip_remark(
|
|||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
):
|
):
|
||||||
dialog_manager.dialog_data["remark"] = None
|
dialog_manager.dialog_data["remark"] = None
|
||||||
|
dialog_manager.dialog_data["photo_file_ids"] = []
|
||||||
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
|
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
|
||||||
|
|
||||||
|
|
||||||
@@ -335,10 +359,13 @@ async def get_room_hours_confirm_data(
|
|||||||
hours = dialog_manager.dialog_data.get("selected_hours", 0)
|
hours = dialog_manager.dialog_data.get("selected_hours", 0)
|
||||||
remark = dialog_manager.dialog_data.get("remark", "")
|
remark = dialog_manager.dialog_data.get("remark", "")
|
||||||
remark_text = f"\n\n<b>Примечание:</b> {remark}" if remark else ""
|
remark_text = f"\n\n<b>Примечание:</b> {remark}" if remark else ""
|
||||||
|
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
|
||||||
|
has_photo = bool(photo_file_ids)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"hours": hours,
|
"hours": hours,
|
||||||
"remark_text": remark_text,
|
"remark_text": remark_text,
|
||||||
|
"has_photo": f"✅ Да ({len(photo_file_ids)} шт.)" if has_photo else "❌ Нет",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -360,12 +387,15 @@ async def on_room_add_hours_confirm(
|
|||||||
admin_id = callback.from_user.id
|
admin_id = callback.from_user.id
|
||||||
|
|
||||||
if room_id and hours:
|
if room_id and hours:
|
||||||
|
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
|
||||||
|
photo_file_id = json.dumps(photo_file_ids) if photo_file_ids else None
|
||||||
results = await transactions_repository.add_hours_to_room(
|
results = await transactions_repository.add_hours_to_room(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
amount=hours,
|
amount=hours,
|
||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
for transaction, resident in results:
|
for transaction, resident in results:
|
||||||
@@ -374,11 +404,15 @@ async def on_room_add_hours_confirm(
|
|||||||
if user:
|
if user:
|
||||||
try:
|
try:
|
||||||
remark_text = f"\n💬 <i>{remark}</i>" if remark else ""
|
remark_text = f"\n💬 <i>{remark}</i>" if remark else ""
|
||||||
await bot.send_message(
|
notification_text = f"<blockquote>➕ <b>Уведомление</b></blockquote>\n\nВашей комнате начислено <b>+{hours}</b> ч{remark_text}"
|
||||||
user.id,
|
if photo_file_ids:
|
||||||
f"<blockquote>� <b>Уведомление</b></blockq uote>\n\n"
|
if len(photo_file_ids) == 1:
|
||||||
f"Вашей комнате начислено <b>+{hours}</b> ч{remark_text}"
|
await bot.send_photo(user.id, photo_file_ids[0], caption=notification_text)
|
||||||
)
|
else:
|
||||||
|
media = [InputMediaPhoto(media=fid, caption=notification_text if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
|
||||||
|
await bot.send_media_group(user.id, media=media)
|
||||||
|
else:
|
||||||
|
await bot.send_message(user.id, notification_text)
|
||||||
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -444,12 +478,12 @@ async def get_room_history_data(
|
|||||||
room_id = dialog_manager.dialog_data.get("selected_room_id")
|
room_id = dialog_manager.dialog_data.get("selected_room_id")
|
||||||
|
|
||||||
if not room_id:
|
if not room_id:
|
||||||
return {"history_content": "Ошибка: комната не выбрана"}
|
return {"history_content": "Ошибка: комната не выбрана", "transactions": [], "has_transactions": False}
|
||||||
|
|
||||||
room = await rooms_repository.get_room_by_id(room_id)
|
room = await rooms_repository.get_room_by_id(room_id)
|
||||||
|
|
||||||
if not room:
|
if not room:
|
||||||
return {"history_content": "Ошибка: комната не найдена"}
|
return {"history_content": "Ошибка: комната не найдена", "transactions": [], "has_transactions": False}
|
||||||
|
|
||||||
transactions = await transactions_repository.get_room_transactions(room_id)
|
transactions = await transactions_repository.get_room_transactions(room_id)
|
||||||
last_10 = transactions[:10]
|
last_10 = transactions[:10]
|
||||||
@@ -458,18 +492,116 @@ async def get_room_history_data(
|
|||||||
history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n<i>История операций пуста</i>"
|
history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n<i>История операций пуста</i>"
|
||||||
else:
|
else:
|
||||||
history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n"
|
history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n"
|
||||||
for tx in last_10:
|
|
||||||
|
tx_items = []
|
||||||
|
for tx in last_10:
|
||||||
|
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")
|
||||||
|
photo_mark = " 📷" if tx.photo_file_id else ""
|
||||||
|
label = f"{emoji}{tx.amount} ч | {date_str}{photo_mark}"
|
||||||
|
tx_items.append((label, str(tx.id)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"history_content": history_text,
|
||||||
|
"transactions": tx_items,
|
||||||
|
"has_transactions": len(tx_items) > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_room_transaction_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
item_id: str,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["selected_transaction_id"] = int(item_id)
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(item_id))
|
||||||
|
if tx:
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
if len(photo_file_ids) > 1:
|
||||||
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
|
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
|
||||||
emoji = "+" 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)
|
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
|
||||||
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
|
||||||
|
admin_info = ""
|
||||||
|
if tx.admin_id:
|
||||||
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
|
if admin:
|
||||||
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
caption = f"📋 <b>Детали транзакции</b>\n\n⚙️ <b>Операция:</b> {operation}\n🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч\n📅 <b>Дата:</b> {date_str}{remark_text}{admin_info}"
|
||||||
|
bot: Bot = dialog_manager.middleware_data.get("bot")
|
||||||
|
try:
|
||||||
|
media = [InputMediaPhoto(media=fid, caption=caption if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
|
||||||
|
await bot.send_media_group(callback.message.chat.id, media=media)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.room_history_detail)
|
||||||
|
|
||||||
remark_text = f"\n💬 <i>{tx.remark}<t/i>" if tx.remark else ""
|
|
||||||
|
|
||||||
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч\n📅 {date_str}{remark_text}</blockquote>\n"
|
@inject
|
||||||
|
async def get_room_transaction_detail_data(
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
tx_id = dialog_manager.dialog_data.get("selected_transaction_id")
|
||||||
|
|
||||||
return {"history_content": history_text}
|
if not tx_id:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не выбрана", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(tx_id))
|
||||||
|
|
||||||
|
if not tx:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не найдена", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
admin_info = ""
|
||||||
|
if tx.admin_id:
|
||||||
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
|
if admin:
|
||||||
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
<blockquote>📋 <b>Детали транзакции</b></blockquote>
|
||||||
|
|
||||||
|
⚙️ <b>Операция:</b> {operation}
|
||||||
|
🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч
|
||||||
|
📅 <b>Дата:</b> {date_str}{remark_text}{admin_info}
|
||||||
|
"""
|
||||||
|
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
photo_media = None
|
||||||
|
has_multiple_photos = len(photo_file_ids) > 1
|
||||||
|
if photo_file_ids and not has_multiple_photos:
|
||||||
|
photo_media = MediaAttachment(
|
||||||
|
type=ContentType.PHOTO,
|
||||||
|
file_id=MediaId(file_id=photo_file_ids[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_multiple_photos:
|
||||||
|
detail_content = ""
|
||||||
|
else:
|
||||||
|
detail_content = content
|
||||||
|
|
||||||
|
return {
|
||||||
|
"detail_content": detail_content,
|
||||||
|
"photo_media": photo_media,
|
||||||
|
"has_multiple_photos": has_multiple_photos,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
rooms_select_floor_window = Window(
|
rooms_select_floor_window = Window(
|
||||||
@@ -524,7 +656,7 @@ rooms_list_window = Window(
|
|||||||
room_info_window = Window(
|
room_info_window = Window(
|
||||||
Format("{info_content}"),
|
Format("{info_content}"),
|
||||||
Button(
|
Button(
|
||||||
Const("Добавить часы"),
|
Const("➕ Добавить часы"),
|
||||||
id="room_add_hours_btn",
|
id="room_add_hours_btn",
|
||||||
on_click=on_room_add_hours_click,
|
on_click=on_room_add_hours_click,
|
||||||
),
|
),
|
||||||
@@ -603,7 +735,7 @@ room_add_hours_custom_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
room_add_hours_remark_window = Window(
|
room_add_hours_remark_window = Window(
|
||||||
Const("<blockquote>💬 <b>Примечание</b></blockquote>\n\nВведите примечание к операции (или пропустите):"),
|
Const("<blockquote>💬 <b>Примечание</b></blockquote>\n\nВведите примечание к операции или отправьте фото/медиагруппу с подписью (или пропустите):"),
|
||||||
MessageInput(on_room_remark_input),
|
MessageInput(on_room_remark_input),
|
||||||
Button(
|
Button(
|
||||||
Const("⏭ Пропустить"),
|
Const("⏭ Пропустить"),
|
||||||
@@ -619,7 +751,7 @@ room_add_hours_remark_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
room_add_hours_confirm_window = Window(
|
room_add_hours_confirm_window = Window(
|
||||||
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?{remark_text}"),
|
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?{remark_text}\n\n📷 <b>Фото:</b> {has_photo}"),
|
||||||
Row(
|
Row(
|
||||||
Button(
|
Button(
|
||||||
Const("✅ Да"),
|
Const("✅ Да"),
|
||||||
@@ -688,6 +820,19 @@ create_room_confirm_window = Window(
|
|||||||
|
|
||||||
room_history_window = Window(
|
room_history_window = Window(
|
||||||
Format("{history_content}"),
|
Format("{history_content}"),
|
||||||
|
ScrollingGroup(
|
||||||
|
Select(
|
||||||
|
Format("{item[0]}"),
|
||||||
|
id="room_transactions_select",
|
||||||
|
item_id_getter=lambda x: x[1],
|
||||||
|
items="transactions",
|
||||||
|
on_click=on_room_transaction_selected,
|
||||||
|
),
|
||||||
|
id="room_transactions_scroll",
|
||||||
|
width=1,
|
||||||
|
height=8,
|
||||||
|
when="has_transactions",
|
||||||
|
),
|
||||||
SwitchTo(
|
SwitchTo(
|
||||||
Const("◀️ Назад"),
|
Const("◀️ Назад"),
|
||||||
id="back_to_room_info",
|
id="back_to_room_info",
|
||||||
@@ -696,3 +841,15 @@ room_history_window = Window(
|
|||||||
state=AdminMenuSG.room_history,
|
state=AdminMenuSG.room_history,
|
||||||
getter=get_room_history_data,
|
getter=get_room_history_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
room_history_detail_window = Window(
|
||||||
|
DynamicMedia("photo_media"),
|
||||||
|
Format("{detail_content}", when=F["detail_content"]),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ К списку"),
|
||||||
|
id="back_to_room_history_from_detail",
|
||||||
|
state=AdminMenuSG.room_history,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.room_history_detail,
|
||||||
|
getter=get_room_transaction_detail_data,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
from dutylog.application.bot.creator_dialogs.transactions_history import (
|
from dutylog.application.bot.creator_dialogs.transactions_history import (
|
||||||
transactions_history_window,
|
transactions_history_window,
|
||||||
|
transactions_history_detail_window,
|
||||||
|
transactions_history_delete_confirm_window,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = ["transactions_history_window"]
|
__all__ = [
|
||||||
|
"transactions_history_window",
|
||||||
|
"transactions_history_detail_window",
|
||||||
|
"transactions_history_delete_confirm_window",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
from aiogram_dialog import Window
|
from aiogram import Bot
|
||||||
|
from aiogram.enums import ContentType
|
||||||
|
from aiogram.types import CallbackQuery, InputMediaPhoto
|
||||||
|
from aiogram_dialog import Window, DialogManager
|
||||||
from aiogram_dialog.widgets.text import Format, Const
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
from aiogram_dialog.widgets.kbd import SwitchTo
|
from aiogram_dialog.widgets.kbd import SwitchTo, ScrollingGroup, Select, Button, Row
|
||||||
|
from aiogram_dialog.widgets.media import DynamicMedia
|
||||||
|
from aiogram_dialog.api.entities.media import MediaAttachment, MediaId
|
||||||
|
from magic_filter import F
|
||||||
from dishka import FromDishka
|
from dishka import FromDishka
|
||||||
from dishka.integrations.aiogram_dialog import inject
|
from dishka.integrations.aiogram_dialog import inject
|
||||||
|
|
||||||
@@ -31,44 +37,207 @@ async def get_transactions_history_data(
|
|||||||
all_transactions = await transactions_repository.get_all_transactions()
|
all_transactions = await transactions_repository.get_all_transactions()
|
||||||
all_transactions.sort(key=lambda t: t.created_at, reverse=True)
|
all_transactions.sort(key=lambda t: t.created_at, reverse=True)
|
||||||
|
|
||||||
recent_transactions = all_transactions[:8]
|
recent_transactions = all_transactions[:40]
|
||||||
|
|
||||||
if not recent_transactions:
|
if not recent_transactions:
|
||||||
content = "<blockquote>📜 <b>История транзакций</b></blockquote>\n\n<i>Нет транзакций</i>"
|
content = "<blockquote>📜 <b>История транзакций</b></blockquote>\n\n<i>Нет транзакций</i>"
|
||||||
else:
|
else:
|
||||||
content = "<blockquote>📜 <b>История транзакций</b></blockquote>\n\n<i>Последние 8 транзакций:</i>\n\n"
|
content = "<blockquote>📜 <b>История транзакций</b></blockquote>\n\n<i>Последние 40 транзакций:</i>\n\n"
|
||||||
|
|
||||||
for tx in recent_transactions:
|
tx_items = []
|
||||||
resident = await residents_repository.get_by_id(tx.resident_id)
|
for tx in recent_transactions:
|
||||||
if not resident:
|
resident = await residents_repository.get_by_id(tx.resident_id)
|
||||||
continue
|
if not resident:
|
||||||
|
continue
|
||||||
|
|
||||||
room = await rooms_repository.get_by_id(resident.room)
|
room = await rooms_repository.get_by_id(resident.room)
|
||||||
room_number = room.number if room else "???"
|
room_number = room.number if room else "???"
|
||||||
|
|
||||||
|
emoji = "+" if tx.transaction_type == "increase" else "−"
|
||||||
|
room_mark = " 🚪" if tx.per_room else ""
|
||||||
|
|
||||||
|
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
|
||||||
|
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
photo_mark = " 📷" if tx.photo_file_id else ""
|
||||||
|
label = f"{emoji}{tx.amount} ч | {date_str}{photo_mark}"
|
||||||
|
tx_items.append((label, str(tx.id)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"content": content,
|
||||||
|
"transactions": tx_items,
|
||||||
|
"has_transactions": len(tx_items) > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_creator_transaction_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager,
|
||||||
|
item_id: str,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["selected_transaction_id"] = int(item_id)
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(item_id))
|
||||||
|
if tx:
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
if len(photo_file_ids) > 1:
|
||||||
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
|
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
|
||||||
emoji = "+" if tx.transaction_type == "increase" else "−"
|
emoji = "+" if tx.transaction_type == "increase" else "−"
|
||||||
room_mark = " 🚪" if tx.per_room else ""
|
room_mark = " 🚪" if tx.per_room else ""
|
||||||
|
|
||||||
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
|
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
|
||||||
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
resident = await residents_repository.get_by_id(tx.resident_id)
|
||||||
|
resident_name = resident.real_name if resident and resident.real_name else "Без имени"
|
||||||
|
room = await rooms_repository.get_by_id(resident.room) if resident else None
|
||||||
|
room_number = room.number if room else "???"
|
||||||
admin_info = ""
|
admin_info = ""
|
||||||
if tx.admin_id:
|
if tx.admin_id:
|
||||||
admin = await users_repository.get_user_by_id(tx.admin_id)
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
if admin:
|
if admin:
|
||||||
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
admin_info = f"\n👨💼 {admin_name}"
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
|
||||||
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
|
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
|
||||||
|
caption = f"📋 <b>Детали транзакции</b>\n\n⚙️ <b>Операция:</b> {operation}\n🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч{room_mark}\n👤 <b>Резидент:</b> {resident_name} (к. {room_number}){admin_info}\n📅 <b>Дата:</b> {date_str}{remark_text}"
|
||||||
|
bot: Bot = dialog_manager.middleware_data.get("bot")
|
||||||
|
try:
|
||||||
|
media = [InputMediaPhoto(media=fid, caption=caption if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
|
||||||
|
await bot.send_media_group(callback.message.chat.id, media=media)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.transactions_history_detail)
|
||||||
|
|
||||||
content += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч{room_mark}\n👤 {resident.real_name or 'Без имени'} (к. {room_number}){admin_info}\n📅 {date_str}{remark_text}</blockquote>\n"
|
|
||||||
|
|
||||||
return {"content": content}
|
@inject
|
||||||
|
async def on_delete_transaction_click(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["delete_source"] = "transactions_history"
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.transactions_history_delete_confirm)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_delete_transaction_confirm(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
):
|
||||||
|
tx_id = dialog_manager.dialog_data.get("selected_transaction_id")
|
||||||
|
if tx_id:
|
||||||
|
await transactions_repository.delete_transaction(int(tx_id))
|
||||||
|
await callback.answer("✅ Транзакция удалена", show_alert=True)
|
||||||
|
source = dialog_manager.dialog_data.pop("delete_source", "transactions_history")
|
||||||
|
if source == "resident_history":
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_history)
|
||||||
|
else:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.transactions_history)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_delete_transaction_back(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
dialog_manager: DialogManager,
|
||||||
|
):
|
||||||
|
source = dialog_manager.dialog_data.pop("delete_source", "transactions_history")
|
||||||
|
if source == "resident_history":
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.resident_history_detail)
|
||||||
|
else:
|
||||||
|
await dialog_manager.switch_to(AdminMenuSG.transactions_history_detail)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_transaction_detail_data(
|
||||||
|
dialog_manager,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
**kwargs,
|
||||||
|
) -> dict:
|
||||||
|
tx_id = dialog_manager.dialog_data.get("selected_transaction_id")
|
||||||
|
|
||||||
|
if not tx_id:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не выбрана", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(tx_id))
|
||||||
|
|
||||||
|
if not tx:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не найдена", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
operation = "Начислено" if tx.transaction_type == "increase" else "Списано"
|
||||||
|
emoji = "+" if tx.transaction_type == "increase" else "−"
|
||||||
|
room_mark = " 🚪" if tx.per_room else ""
|
||||||
|
|
||||||
|
msk_time = tx.created_at.astimezone(msk_now().tzinfo).replace(tzinfo=None)
|
||||||
|
date_str = msk_time.strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
resident = await residents_repository.get_by_id(tx.resident_id)
|
||||||
|
resident_name = resident.real_name if resident and resident.real_name else "Без имени"
|
||||||
|
room = await rooms_repository.get_by_id(resident.room) if resident else None
|
||||||
|
room_number = room.number if room else "???"
|
||||||
|
|
||||||
|
admin_info = ""
|
||||||
|
if tx.admin_id:
|
||||||
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
|
if admin:
|
||||||
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
|
||||||
|
remark_text = f"\n💬 <i>{tx.remark}</i>" if tx.remark else ""
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
<blockquote>📋 <b>Детали транзакции</b></blockquote>
|
||||||
|
|
||||||
|
⚙️ <b>Операция:</b> {operation}
|
||||||
|
🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч{room_mark}
|
||||||
|
👤 <b>Резидент:</b> {resident_name} (к. {room_number}){admin_info}
|
||||||
|
📅 <b>Дата:</b> {date_str}{remark_text}
|
||||||
|
"""
|
||||||
|
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
photo_media = None
|
||||||
|
has_multiple_photos = len(photo_file_ids) > 1
|
||||||
|
if photo_file_ids and not has_multiple_photos:
|
||||||
|
photo_media = MediaAttachment(
|
||||||
|
type=ContentType.PHOTO,
|
||||||
|
file_id=MediaId(file_id=photo_file_ids[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_multiple_photos:
|
||||||
|
detail_content = ""
|
||||||
|
else:
|
||||||
|
detail_content = content
|
||||||
|
|
||||||
|
return {
|
||||||
|
"detail_content": detail_content,
|
||||||
|
"photo_media": photo_media,
|
||||||
|
"has_multiple_photos": has_multiple_photos,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
transactions_history_window = Window(
|
transactions_history_window = Window(
|
||||||
Format("{content}"),
|
Format("{content}"),
|
||||||
|
ScrollingGroup(
|
||||||
|
Select(
|
||||||
|
Format("{item[0]}"),
|
||||||
|
id="creator_transactions_select",
|
||||||
|
item_id_getter=lambda x: x[1],
|
||||||
|
items="transactions",
|
||||||
|
on_click=on_creator_transaction_selected,
|
||||||
|
),
|
||||||
|
id="creator_transactions_scroll",
|
||||||
|
width=1,
|
||||||
|
height=8,
|
||||||
|
when="has_transactions",
|
||||||
|
),
|
||||||
SwitchTo(
|
SwitchTo(
|
||||||
Const("◀️ Назад"),
|
Const("◀️ Назад"),
|
||||||
id="back_to_main",
|
id="back_to_main",
|
||||||
@@ -77,3 +246,39 @@ transactions_history_window = Window(
|
|||||||
state=AdminMenuSG.transactions_history,
|
state=AdminMenuSG.transactions_history,
|
||||||
getter=get_transactions_history_data,
|
getter=get_transactions_history_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
transactions_history_detail_window = Window(
|
||||||
|
DynamicMedia("photo_media", when=F["photo_media"]),
|
||||||
|
Format("{detail_content}", when=F["detail_content"]),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("🗑 Удалить"),
|
||||||
|
id="delete_transaction_btn",
|
||||||
|
on_click=on_delete_transaction_click,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ К списку"),
|
||||||
|
id="back_to_transactions_from_detail",
|
||||||
|
state=AdminMenuSG.transactions_history,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.transactions_history_detail,
|
||||||
|
getter=get_transaction_detail_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
transactions_history_delete_confirm_window = Window(
|
||||||
|
Const("<blockquote>🗑 <b>Удаление транзакции</b></blockquote>\n\n⚠️ Вы уверены, что хотите удалить эту транзакцию?\nЭто действие нельзя отменить."),
|
||||||
|
Row(
|
||||||
|
Button(
|
||||||
|
Const("✅ Да, удалить"),
|
||||||
|
id="confirm_delete_transaction_btn",
|
||||||
|
on_click=on_delete_transaction_confirm,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("◀️ Назад"),
|
||||||
|
id="back_to_transaction_detail",
|
||||||
|
on_click=on_delete_transaction_back,
|
||||||
|
),
|
||||||
|
state=AdminMenuSG.transactions_history_delete_confirm,
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import asyncio
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumMiddleware(BaseMiddleware):
|
||||||
|
ALBUM_DATA: dict[str, list[Message]] = {}
|
||||||
|
|
||||||
|
def __init__(self, delay: float = 0.6) -> None:
|
||||||
|
self.delay = delay
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[Message, dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: Message,
|
||||||
|
data: dict[str, Any],
|
||||||
|
) -> Any:
|
||||||
|
if not event.media_group_id:
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ALBUM_DATA[event.media_group_id].append(event)
|
||||||
|
return
|
||||||
|
except KeyError:
|
||||||
|
self.ALBUM_DATA[event.media_group_id] = [event]
|
||||||
|
await asyncio.sleep(self.delay)
|
||||||
|
data["album"] = self.ALBUM_DATA.pop(event.media_group_id)
|
||||||
|
return await handler(event, data)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from aiogram_dialog import Dialog
|
from aiogram_dialog import Dialog
|
||||||
|
|
||||||
from dutylog.application.bot.user_dialogs.user_menu.main_menu import main_menu_window
|
from dutylog.application.bot.user_dialogs.user_menu.main_menu import main_menu_window
|
||||||
from dutylog.application.bot.user_dialogs.user_menu.history import history_window
|
from dutylog.application.bot.user_dialogs.user_menu.history import history_window, history_detail_window
|
||||||
from dutylog.application.bot.user_dialogs.user_menu.top_residents import top_residents_window
|
from dutylog.application.bot.user_dialogs.user_menu.top_residents import top_residents_window
|
||||||
from dutylog.application.bot.user_dialogs.user_menu.faq import faq_window
|
from dutylog.application.bot.user_dialogs.user_menu.faq import faq_window
|
||||||
from dutylog.application.bot.user_dialogs.user_menu.feedback import feedback_window
|
from dutylog.application.bot.user_dialogs.user_menu.feedback import feedback_window
|
||||||
@@ -10,6 +10,7 @@ from dutylog.application.bot.user_dialogs.user_menu.feedback import feedback_win
|
|||||||
main_menu_dialog = Dialog(
|
main_menu_dialog = Dialog(
|
||||||
main_menu_window,
|
main_menu_window,
|
||||||
history_window,
|
history_window,
|
||||||
|
history_detail_window,
|
||||||
top_residents_window,
|
top_residents_window,
|
||||||
faq_window,
|
faq_window,
|
||||||
feedback_window,
|
feedback_window,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from aiogram.fsm.state import State, StatesGroup
|
|||||||
class MainMenuSG(StatesGroup):
|
class MainMenuSG(StatesGroup):
|
||||||
main = State()
|
main = State()
|
||||||
history = State()
|
history = State()
|
||||||
|
history_detail = State()
|
||||||
top_residents = State()
|
top_residents = State()
|
||||||
faq = State()
|
faq = State()
|
||||||
feedback = State()
|
feedback = State()
|
||||||
@@ -19,6 +20,9 @@ class AdminMenuSG(StatesGroup):
|
|||||||
residents_filtered_results = State()
|
residents_filtered_results = State()
|
||||||
resident_info = State()
|
resident_info = State()
|
||||||
resident_history = State()
|
resident_history = State()
|
||||||
|
resident_history_search_input = State()
|
||||||
|
resident_history_search_results = State()
|
||||||
|
resident_history_detail = State()
|
||||||
resident_logout_confirm = State()
|
resident_logout_confirm = State()
|
||||||
resident_delete_confirm = State()
|
resident_delete_confirm = State()
|
||||||
resident_rebind_floor = State()
|
resident_rebind_floor = State()
|
||||||
@@ -44,6 +48,7 @@ class AdminMenuSG(StatesGroup):
|
|||||||
rooms_list = State()
|
rooms_list = State()
|
||||||
room_info = State()
|
room_info = State()
|
||||||
room_history = State()
|
room_history = State()
|
||||||
|
room_history_detail = State()
|
||||||
room_delete_confirm = State()
|
room_delete_confirm = State()
|
||||||
room_add_hours_select = State()
|
room_add_hours_select = State()
|
||||||
room_add_hours_custom = State()
|
room_add_hours_custom = State()
|
||||||
@@ -67,6 +72,8 @@ class AdminMenuSG(StatesGroup):
|
|||||||
add_admin_select_user = State()
|
add_admin_select_user = State()
|
||||||
add_admin_confirm = State()
|
add_admin_confirm = State()
|
||||||
transactions_history = State()
|
transactions_history = State()
|
||||||
|
transactions_history_detail = State()
|
||||||
|
transactions_history_delete_confirm = State()
|
||||||
|
|
||||||
|
|
||||||
class CreatorMenuSG(StatesGroup):
|
class CreatorMenuSG(StatesGroup):
|
||||||
@@ -76,6 +83,7 @@ class CreatorMenuSG(StatesGroup):
|
|||||||
add_admin_select_user = State()
|
add_admin_select_user = State()
|
||||||
add_admin_confirm = State()
|
add_admin_confirm = State()
|
||||||
transactions_history = State()
|
transactions_history = State()
|
||||||
|
transactions_history_detail = State()
|
||||||
top_residents = State()
|
top_residents = State()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
from aiogram.types import User
|
from aiogram import Bot
|
||||||
|
from aiogram.enums import ContentType
|
||||||
|
from aiogram.types import User, CallbackQuery, InputMediaPhoto
|
||||||
from aiogram_dialog import Window
|
from aiogram_dialog import Window
|
||||||
from aiogram_dialog.widgets.text import Format, Const
|
from aiogram_dialog.widgets.text import Format, Const
|
||||||
from aiogram_dialog.widgets.kbd import Back
|
from aiogram_dialog.widgets.kbd import Back, ScrollingGroup, Select, SwitchTo, Button
|
||||||
|
from aiogram_dialog.widgets.media import DynamicMedia
|
||||||
|
from aiogram_dialog.api.entities.media import MediaAttachment, MediaId
|
||||||
|
from magic_filter import F
|
||||||
from dishka import FromDishka
|
from dishka import FromDishka
|
||||||
from dishka.integrations.aiogram_dialog import inject
|
from dishka.integrations.aiogram_dialog import inject
|
||||||
|
|
||||||
@@ -15,6 +20,9 @@ from dutylog.infrastructure.database.repositories.rooms_repository import (
|
|||||||
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
|
from dutylog.infrastructure.database.repositories.hours_transactions_repository import (
|
||||||
HoursTransactionsRepository,
|
HoursTransactionsRepository,
|
||||||
)
|
)
|
||||||
|
from dutylog.infrastructure.database.repositories.users_repository import (
|
||||||
|
UsersRepository,
|
||||||
|
)
|
||||||
from dutylog.infrastructure.utils.datetime import msk_now
|
from dutylog.infrastructure.utils.datetime import msk_now
|
||||||
|
|
||||||
|
|
||||||
@@ -25,39 +33,164 @@ async def get_history_data(
|
|||||||
rooms_repository: FromDishka[RoomsRepository],
|
rooms_repository: FromDishka[RoomsRepository],
|
||||||
transactions_repository: FromDishka[HoursTransactionsRepository],
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> dict[str, str]:
|
) -> dict:
|
||||||
resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
|
resident = await residents_repository.get_resident_by_user_id(event_from_user.id)
|
||||||
|
|
||||||
if not resident:
|
if not resident:
|
||||||
history_text = "<blockquote>📜 <b>История операций</b></blockquote>\n\n⚠️ <i>Профиль не найден</i>"
|
history_text = "<blockquote>📜 <b>История операций</b></blockquote>\n\n⚠️ <i>Профиль не найден</i>"
|
||||||
|
return {"history_content": history_text, "transactions": [], "has_transactions": False}
|
||||||
|
|
||||||
|
transactions = await transactions_repository.get_resident_history(resident.id)
|
||||||
|
transactions_sorted = sorted(transactions, key=lambda x: x.created_at, reverse=True)
|
||||||
|
last_10 = transactions_sorted[:10]
|
||||||
|
|
||||||
|
if not last_10:
|
||||||
|
history_text = "📜 <b>История операций</b>\n\n<b>👤 Ваши операции:</b>\n<i>История операций пуста</i>\n\n"
|
||||||
|
return {"history_content": history_text, "transactions": [], "has_transactions": False}
|
||||||
|
|
||||||
|
history_text = "📜 <b>История операций</b>\n\n<b>👤 Ваши операции:</b>\n\n"
|
||||||
|
|
||||||
|
tx_items = []
|
||||||
|
for tx in last_10:
|
||||||
|
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")
|
||||||
|
photo_mark = " 📷" if tx.photo_file_id else ""
|
||||||
|
label = f"{emoji}{tx.amount} ч | {date_str}{photo_mark}"
|
||||||
|
tx_items.append((label, str(tx.id)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"history_content": history_text,
|
||||||
|
"transactions": tx_items,
|
||||||
|
"has_transactions": len(tx_items) > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def on_user_transaction_selected(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Select,
|
||||||
|
dialog_manager,
|
||||||
|
item_id: str,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
):
|
||||||
|
dialog_manager.dialog_data["selected_transaction_id"] = int(item_id)
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(item_id))
|
||||||
|
if tx:
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
if len(photo_file_ids) > 1:
|
||||||
|
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 ""
|
||||||
|
admin_info = ""
|
||||||
|
if tx.admin_id:
|
||||||
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
|
if admin:
|
||||||
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
caption = f"📋 <b>Детали транзакции</b>\n\n⚙️ <b>Операция:</b> {operation}\n🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч\n📅 <b>Дата:</b> {date_str}{remark_text}{admin_info}"
|
||||||
|
bot: Bot = dialog_manager.middleware_data.get("bot")
|
||||||
|
try:
|
||||||
|
media = [InputMediaPhoto(media=fid, caption=caption if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
|
||||||
|
await bot.send_media_group(callback.message.chat.id, media=media)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await dialog_manager.switch_to(MainMenuSG.history_detail)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def get_history_detail_data(
|
||||||
|
event_from_user: User,
|
||||||
|
dialog_manager,
|
||||||
|
transactions_repository: FromDishka[HoursTransactionsRepository],
|
||||||
|
residents_repository: FromDishka[ResidentsRepository],
|
||||||
|
users_repository: FromDishka[UsersRepository],
|
||||||
|
**kwargs,
|
||||||
|
) -> dict:
|
||||||
|
tx_id = dialog_manager.dialog_data.get("selected_transaction_id")
|
||||||
|
|
||||||
|
if not tx_id:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не выбрана", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
tx = await transactions_repository.get_transaction_by_id(int(tx_id))
|
||||||
|
|
||||||
|
if not tx:
|
||||||
|
return {"detail_content": "Ошибка: транзакция не найдена", "photo_media": None, "has_multiple_photos": False}
|
||||||
|
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
admin_info = ""
|
||||||
|
if tx.admin_id:
|
||||||
|
admin = await users_repository.get_user_by_id(tx.admin_id)
|
||||||
|
if admin:
|
||||||
|
admin_name = f"@{admin.username}" if admin.username else admin.first_name or f"ID: {admin.id}"
|
||||||
|
admin_info = f"\n👨💼 <b>Администратор:</b> {admin_name}"
|
||||||
|
|
||||||
|
content = f"""
|
||||||
|
<blockquote>📋 <b>Детали транзакции</b></blockquote>
|
||||||
|
|
||||||
|
⚙️ <b>Операция:</b> {operation}
|
||||||
|
🔢 <b>Количество:</b> {emoji}<code>{tx.amount}</code> ч
|
||||||
|
📅 <b>Дата:</b> {date_str}{remark_text}{admin_info}
|
||||||
|
"""
|
||||||
|
|
||||||
|
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
|
||||||
|
photo_media = None
|
||||||
|
has_multiple_photos = len(photo_file_ids) > 1
|
||||||
|
if photo_file_ids and not has_multiple_photos:
|
||||||
|
photo_media = MediaAttachment(
|
||||||
|
type=ContentType.PHOTO,
|
||||||
|
file_id=MediaId(file_id=photo_file_ids[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_multiple_photos:
|
||||||
|
detail_content = ""
|
||||||
else:
|
else:
|
||||||
transactions = await transactions_repository.get_resident_history(resident.id)
|
detail_content = content
|
||||||
transactions_sorted = sorted(transactions, key=lambda x: x.created_at)
|
|
||||||
last_10 = transactions_sorted[-10:]
|
|
||||||
|
|
||||||
if not last_10:
|
return {
|
||||||
history_text = "📜 <b>История операций</b>\n\n<b>👤 Ваши операции:</b>\n<i>История операций пуста</i>\n\n"
|
"detail_content": detail_content,
|
||||||
else:
|
"photo_media": photo_media,
|
||||||
history_text = "📜 <b>История операций</b>\n\n<b>👤 Ваши операции:</b>\n\n"
|
"has_multiple_photos": has_multiple_photos,
|
||||||
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 ""
|
|
||||||
|
|
||||||
room_mark = " 🚪" if tx.per_room else ""
|
|
||||||
|
|
||||||
history_text += f"<blockquote><b>{operation}</b> {emoji}<code>{tx.amount}</code> ч{room_mark}\n📅 {date_str}{remark_text}</blockquote>\n"
|
|
||||||
|
|
||||||
return {"history_content": history_text}
|
|
||||||
|
|
||||||
|
|
||||||
history_window = Window(
|
history_window = Window(
|
||||||
Format("{history_content}"),
|
Format("{history_content}"),
|
||||||
|
ScrollingGroup(
|
||||||
|
Select(
|
||||||
|
Format("{item[0]}"),
|
||||||
|
id="user_transactions_select",
|
||||||
|
item_id_getter=lambda x: x[1],
|
||||||
|
items="transactions",
|
||||||
|
on_click=on_user_transaction_selected,
|
||||||
|
),
|
||||||
|
id="user_transactions_scroll",
|
||||||
|
width=1,
|
||||||
|
height=8,
|
||||||
|
when="has_transactions",
|
||||||
|
),
|
||||||
Back(Const("◀️ Назад")),
|
Back(Const("◀️ Назад")),
|
||||||
state=MainMenuSG.history,
|
state=MainMenuSG.history,
|
||||||
getter=get_history_data,
|
getter=get_history_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
history_detail_window = Window(
|
||||||
|
DynamicMedia("photo_media"),
|
||||||
|
Format("{detail_content}", when=F["detail_content"]),
|
||||||
|
SwitchTo(
|
||||||
|
Const("◀️ К списку"),
|
||||||
|
id="back_to_history_from_detail",
|
||||||
|
state=MainMenuSG.history,
|
||||||
|
),
|
||||||
|
state=MainMenuSG.history_detail,
|
||||||
|
getter=get_history_detail_data,
|
||||||
|
)
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ class HoursTransactionsDAO:
|
|||||||
)
|
)
|
||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def search_by_remark_or_amount(
|
||||||
|
self, resident_id: int, query: str
|
||||||
|
) -> list[HoursTransaction]:
|
||||||
|
conditions = [HoursTransaction.resident_id == resident_id]
|
||||||
|
if query.isdigit():
|
||||||
|
amount = int(query)
|
||||||
|
conditions.append(HoursTransaction.amount == amount)
|
||||||
|
else:
|
||||||
|
conditions.append(HoursTransaction.remark.ilike(f"%{query}%"))
|
||||||
|
|
||||||
|
result = await self.session.execute(
|
||||||
|
select(HoursTransaction)
|
||||||
|
.where(*conditions)
|
||||||
|
.order_by(HoursTransaction.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
async def create(self, transaction: HoursTransaction) -> HoursTransaction:
|
async def create(self, transaction: HoursTransaction) -> HoursTransaction:
|
||||||
self.session.add(transaction)
|
self.session.add(transaction)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from sqlalchemy import BigInteger, Boolean, Integer, String, DateTime, ForeignKey
|
from sqlalchemy import BigInteger, Boolean, Integer, String, DateTime, ForeignKey, Text
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from dutylog.infrastructure.database.models.base import Base
|
from dutylog.infrastructure.database.models.base import Base
|
||||||
@@ -26,6 +26,7 @@ class HoursTransaction(Base):
|
|||||||
BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
BigInteger, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||||
)
|
)
|
||||||
remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
remark: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||||
|
photo_file_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
per_room: Mapped[bool] = mapped_column(
|
per_room: Mapped[bool] = mapped_column(
|
||||||
Boolean, default=False, server_default="false"
|
Boolean, default=False, server_default="false"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from dutylog.infrastructure.database.dao.hours_transactions_dao import (
|
from dutylog.infrastructure.database.dao.hours_transactions_dao import (
|
||||||
HoursTransactionsDAO,
|
HoursTransactionsDAO,
|
||||||
)
|
)
|
||||||
@@ -29,6 +31,7 @@ class HoursTransactionsRepository:
|
|||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
remark: str | None = None,
|
remark: str | None = None,
|
||||||
per_room: bool = False,
|
per_room: bool = False,
|
||||||
|
photo_file_id: str | None = None,
|
||||||
) -> tuple[HoursTransaction, Resident | None]:
|
) -> tuple[HoursTransaction, Resident | None]:
|
||||||
transaction = HoursTransaction(
|
transaction = HoursTransaction(
|
||||||
resident_id=resident_id,
|
resident_id=resident_id,
|
||||||
@@ -37,6 +40,7 @@ class HoursTransactionsRepository:
|
|||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
per_room=per_room,
|
per_room=per_room,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
transaction = await self.transactions_dao.create(transaction)
|
transaction = await self.transactions_dao.create(transaction)
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ class HoursTransactionsRepository:
|
|||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
remark: str | None = None,
|
remark: str | None = None,
|
||||||
per_room: bool = False,
|
per_room: bool = False,
|
||||||
|
photo_file_id: str | None = None,
|
||||||
) -> tuple[HoursTransaction, Resident | None]:
|
) -> tuple[HoursTransaction, Resident | None]:
|
||||||
transaction = HoursTransaction(
|
transaction = HoursTransaction(
|
||||||
resident_id=resident_id,
|
resident_id=resident_id,
|
||||||
@@ -71,6 +76,7 @@ class HoursTransactionsRepository:
|
|||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
per_room=per_room,
|
per_room=per_room,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
transaction = await self.transactions_dao.create(transaction)
|
transaction = await self.transactions_dao.create(transaction)
|
||||||
|
|
||||||
@@ -96,6 +102,7 @@ class HoursTransactionsRepository:
|
|||||||
admin_id: int | None = None,
|
admin_id: int | None = None,
|
||||||
remark: str | None = None,
|
remark: str | None = None,
|
||||||
per_room: bool = False,
|
per_room: bool = False,
|
||||||
|
photo_file_id: str | None = None,
|
||||||
) -> tuple[HoursTransaction, Resident | None]:
|
) -> tuple[HoursTransaction, Resident | None]:
|
||||||
"""Перемещает часы из неотработанных в отработанные"""
|
"""Перемещает часы из неотработанных в отработанные"""
|
||||||
transaction = HoursTransaction(
|
transaction = HoursTransaction(
|
||||||
@@ -105,6 +112,7 @@ class HoursTransactionsRepository:
|
|||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
per_room=per_room,
|
per_room=per_room,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
transaction = await self.transactions_dao.create(transaction)
|
transaction = await self.transactions_dao.create(transaction)
|
||||||
|
|
||||||
@@ -123,6 +131,11 @@ class HoursTransactionsRepository:
|
|||||||
async def get_resident_history(self, resident_id: int) -> list[HoursTransaction]:
|
async def get_resident_history(self, resident_id: int) -> list[HoursTransaction]:
|
||||||
return await self.transactions_dao.get_by_resident_id(resident_id)
|
return await self.transactions_dao.get_by_resident_id(resident_id)
|
||||||
|
|
||||||
|
async def search_resident_transactions(
|
||||||
|
self, resident_id: int, query: str
|
||||||
|
) -> list[HoursTransaction]:
|
||||||
|
return await self.transactions_dao.search_by_remark_or_amount(resident_id, query)
|
||||||
|
|
||||||
async def get_all_transactions(self) -> list[HoursTransaction]:
|
async def get_all_transactions(self) -> list[HoursTransaction]:
|
||||||
return await self.transactions_dao.get_all()
|
return await self.transactions_dao.get_all()
|
||||||
|
|
||||||
@@ -131,6 +144,26 @@ class HoursTransactionsRepository:
|
|||||||
) -> HoursTransaction | None:
|
) -> HoursTransaction | None:
|
||||||
return await self.transactions_dao.get_by_id(transaction_id)
|
return await self.transactions_dao.get_by_id(transaction_id)
|
||||||
|
|
||||||
|
async def delete_transaction(self, transaction_id: int) -> None:
|
||||||
|
tx = await self.transactions_dao.get_by_id(transaction_id)
|
||||||
|
if tx:
|
||||||
|
resident = await self.residents_dao.get_by_id(tx.resident_id)
|
||||||
|
if resident:
|
||||||
|
if tx.transaction_type == TransactionType.INCREASE.value:
|
||||||
|
new_active = max(0, resident.active_hours - tx.amount)
|
||||||
|
await self.residents_dao.update(
|
||||||
|
tx.resident_id, active_hours=new_active
|
||||||
|
)
|
||||||
|
elif tx.transaction_type == TransactionType.DECREASE.value:
|
||||||
|
new_active = resident.active_hours + tx.amount
|
||||||
|
new_inactive = max(0, resident.inactive_hours - tx.amount)
|
||||||
|
await self.residents_dao.update(
|
||||||
|
tx.resident_id,
|
||||||
|
active_hours=new_active,
|
||||||
|
inactive_hours=new_inactive,
|
||||||
|
)
|
||||||
|
await self.transactions_dao.delete(transaction_id)
|
||||||
|
|
||||||
async def get_by_period(self, start_date, end_date) -> list[HoursTransaction]:
|
async def get_by_period(self, start_date, end_date) -> list[HoursTransaction]:
|
||||||
return await self.transactions_dao.get_by_period(start_date, end_date)
|
return await self.transactions_dao.get_by_period(start_date, end_date)
|
||||||
|
|
||||||
@@ -141,6 +174,7 @@ class HoursTransactionsRepository:
|
|||||||
admin_id: int | None = None,
|
admin_id: int | None = None,
|
||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
remark: str | None = None,
|
remark: str | None = None,
|
||||||
|
photo_file_id: str | None = None,
|
||||||
) -> list[tuple[HoursTransaction, Resident | None]]:
|
) -> list[tuple[HoursTransaction, Resident | None]]:
|
||||||
residents = await self.residents_dao.get_by_room(room_id)
|
residents = await self.residents_dao.get_by_room(room_id)
|
||||||
results = []
|
results = []
|
||||||
@@ -158,6 +192,7 @@ class HoursTransactionsRepository:
|
|||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
per_room=True,
|
per_room=True,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
@@ -174,3 +209,15 @@ class HoursTransactionsRepository:
|
|||||||
|
|
||||||
all_transactions.sort(key=lambda t: t.created_at, reverse=True)
|
all_transactions.sort(key=lambda t: t.created_at, reverse=True)
|
||||||
return all_transactions
|
return all_transactions
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_photo_file_ids(tx: HoursTransaction) -> list[str]:
|
||||||
|
if not tx.photo_file_id:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
ids = json.loads(tx.photo_file_id)
|
||||||
|
if isinstance(ids, list):
|
||||||
|
return ids
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
return [tx.photo_file_id]
|
||||||
|
|||||||
Reference in New Issue
Block a user