fix: media group photos saving, add photos to user notifications, admin info in transaction details, remove deep link fallback

- Fix media group collection: use module-level dicts instead of dialog_data
  to avoid race conditions between concurrent handler calls
- Add photo/media group sending in user notifications (add/remove hours)
- Add admin username with emoji to all transaction detail windows
- Add emoji prefixes to detail content fields
- Support multiple photos via JSON array in photo_file_id column
- Change photo_file_id column from String(512) to Text
- Add get_photo_file_ids() helper with backward compat for single file_id
- Add 'All photos' button for media groups in detail views
- Remove /start photo_ deep link fallback handler
- Add Alembic migration for column type change
This commit is contained in:
2026-07-15 00:45:58 +03:00
parent 8526440d14
commit 53f30c923b
14 changed files with 997 additions and 110 deletions
@@ -1,10 +1,13 @@
from aiogram import Bot
from aiogram.enums import ContentType
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.widgets.text import Format, Const
from aiogram_dialog.widgets.kbd import SwitchTo, Button, ScrollingGroup, Select, Row, Group
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.media import DynamicMedia
from aiogram_dialog.api.entities.media import MediaAttachment, MediaId
from dishka import FromDishka
from dishka.integrations.aiogram_dialog import inject
@@ -444,12 +447,12 @@ async def get_room_history_data(
room_id = dialog_manager.dialog_data.get("selected_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)
if not room:
return {"history_content": "Ошибка: комната не найдена"}
return {"history_content": "Ошибка: комната не найдена", "transactions": [], "has_transactions": False}
transactions = await transactions_repository.get_room_transactions(room_id)
last_10 = transactions[:10]
@@ -458,18 +461,115 @@ async def get_room_history_data(
history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n<i>История операций пуста</i>"
else:
history_text = f"<blockquote>📜 <b>История операций</b></blockquote>\n\n<b>Комната:</b> {room.number}\n\n"
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}<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"
return {"history_content": history_text}
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,
):
dialog_manager.dialog_data["selected_transaction_id"] = int(item_id)
await dialog_manager.switch_to(AdminMenuSG.room_history_detail)
@inject
async def on_show_room_all_photos(
callback: CallbackQuery,
button: Button,
dialog_manager: DialogManager,
transactions_repository: FromDishka[HoursTransactionsRepository],
**kwargs,
):
tx_id = dialog_manager.dialog_data.get("selected_transaction_id")
if not tx_id:
return
tx = await transactions_repository.get_transaction_by_id(int(tx_id))
if not tx:
return
photo_file_ids = HoursTransactionsRepository.get_photo_file_ids(tx)
if len(photo_file_ids) <= 1:
return
bot: Bot = kwargs["bot"]
media = [InputMediaPhoto(media=file_id) for file_id in photo_file_ids]
try:
await bot.send_media_group(callback.message.chat.id, media=media)
except Exception:
pass
@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")
if not tx_id:
return {"detail_content": "Ошибка: транзакция не выбрана", "photo_media": None, "has_multiple_photos": False, "photo_count": 0}
tx = await transactions_repository.get_transaction_by_id(int(tx_id))
if not tx:
return {"detail_content": "Ошибка: транзакция не найдена", "photo_media": None, "has_multiple_photos": False, "photo_count": 0}
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
if photo_file_ids:
photo_media = MediaAttachment(
type=ContentType.PHOTO,
file_id=MediaId(file_id=photo_file_ids[0]),
)
return {
"detail_content": content,
"photo_media": photo_media,
"has_multiple_photos": len(photo_file_ids) > 1,
"photo_count": len(photo_file_ids),
}
rooms_select_floor_window = Window(
@@ -688,6 +788,19 @@ create_room_confirm_window = Window(
room_history_window = Window(
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(
Const("◀️ Назад"),
id="back_to_room_info",
@@ -696,3 +809,21 @@ room_history_window = Window(
state=AdminMenuSG.room_history,
getter=get_room_history_data,
)
room_history_detail_window = Window(
DynamicMedia("photo_media"),
Format("{detail_content}"),
Button(
Format("📷 Все фото ({photo_count})"),
id="show_room_all_photos_btn",
on_click=on_show_room_all_photos,
when="has_multiple_photos",
),
SwitchTo(
Const("◀️ К списку"),
id="back_to_room_history_from_detail",
state=AdminMenuSG.room_history,
),
state=AdminMenuSG.room_history_detail,
getter=get_room_transaction_detail_data,
)