Merge pull request #1407 from Fr1ngg/g0v1v5-bedolaga/add-button-to-close-all-open-tickets

Add admin bulk close control for support tickets
This commit is contained in:
Egor
2025-10-19 02:27:39 +03:00
committed by GitHub
6 changed files with 118 additions and 1 deletions

View File

@@ -274,6 +274,30 @@ class TicketCRUD:
db, ticket_id, TicketStatus.CLOSED.value, datetime.utcnow()
)
@staticmethod
async def close_all_open_tickets(
db: AsyncSession,
) -> List[int]:
"""Закрыть все открытые тикеты. Возвращает список идентификаторов закрытых тикетов."""
open_statuses = [TicketStatus.OPEN.value, TicketStatus.ANSWERED.value]
result = await db.execute(
select(Ticket.id).where(Ticket.status.in_(open_statuses))
)
ticket_ids = result.scalars().all()
if not ticket_ids:
return []
now = datetime.utcnow()
await db.execute(
update(Ticket)
.where(Ticket.id.in_(ticket_ids))
.values(status=TicketStatus.CLOSED.value, closed_at=now, updated_at=now)
)
await db.commit()
return ticket_ids
@staticmethod
async def add_support_audit(
db: AsyncSession,

View File

@@ -207,6 +207,7 @@ async def show_support_audit(
'close_ticket': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_TICKET", "Закрытие тикета"),
'block_user_timed': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_TIMED", "Блокировка (время)"),
'block_user_perm': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_PERM", "Блокировка (навсегда)"),
'close_all_tickets': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_ALL_TICKETS", "Массовое закрытие тикетов"),
'unblock_user': texts.t("ADMIN_SUPPORT_AUDIT_ACTION_UNBLOCK", "Снятие блока"),
}
action_text = action_map.get(log.action, log.action)
@@ -215,6 +216,8 @@ async def show_support_audit(
extra = ""
if log.action == 'block_user_timed' and 'minutes' in details:
extra = f" ({details['minutes']} мин)"
elif log.action == 'close_all_tickets' and 'count' in details:
extra = f" ({details['count']})"
lines.append(f"{ts}{role} <code>{log.actor_telegram_id}</code> — {action_text}{ticket_part}{extra}")
# keyboard with pagination

View File

@@ -461,6 +461,81 @@ async def mark_ticket_as_answered(
)
async def close_all_open_admin_tickets(
callback: types.CallbackQuery,
db_user: User,
db: AsyncSession
):
"""Закрыть все открытые тикеты."""
if not (settings.is_admin(callback.from_user.id) or SupportSettingsService.is_moderator(callback.from_user.id)):
texts = get_texts(db_user.language)
await callback.answer(texts.ACCESS_DENIED, show_alert=True)
return
texts = get_texts(db_user.language)
try:
closed_ticket_ids = await TicketCRUD.close_all_open_tickets(db)
except Exception as error:
logger.error("Error closing all open tickets: %s", error)
await callback.answer(
texts.t("TICKET_UPDATE_ERROR", "❌ Ошибка при обновлении тикета."),
show_alert=True
)
return
closed_count = len(closed_ticket_ids)
if closed_count == 0:
await callback.answer(
texts.t("ADMIN_CLOSE_ALL_OPEN_TICKETS_EMPTY", " Нет открытых тикетов для закрытия."),
show_alert=True
)
return
try:
is_moderator = (
not settings.is_admin(callback.from_user.id)
and SupportSettingsService.is_moderator(callback.from_user.id)
)
await TicketCRUD.add_support_audit(
db,
actor_user_id=db_user.id if db_user else None,
actor_telegram_id=callback.from_user.id,
is_moderator=is_moderator,
action="close_all_tickets",
ticket_id=None,
target_user_id=None,
details={
"count": closed_count,
"ticket_ids": closed_ticket_ids,
}
)
except Exception as audit_error:
logger.warning("Failed to add support audit for bulk close: %s", audit_error)
# Обновляем список тикетов
await show_admin_tickets(callback, db_user, db)
success_text = texts.t(
"ADMIN_CLOSE_ALL_OPEN_TICKETS_SUCCESS",
"✅ Закрыто открытых тикетов: {count}"
).format(count=closed_count)
notification_keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text="🗑 Удалить", callback_data="admin_support_delete_msg")]]
)
try:
await callback.message.answer(success_text, reply_markup=notification_keyboard)
except Exception:
# Если не удалось отправить отдельное сообщение, пробуем ответить алертом
try:
await callback.answer(success_text, show_alert=True)
except Exception:
pass
async def close_admin_ticket(
callback: types.CallbackQuery,
db_user: User,
@@ -935,7 +1010,8 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_admin_tickets, F.data == "admin_tickets")
dp.callback_query.register(show_admin_tickets, F.data == "admin_tickets_scope_open")
dp.callback_query.register(show_admin_tickets, F.data == "admin_tickets_scope_closed")
dp.callback_query.register(close_all_open_admin_tickets, F.data == "admin_tickets_close_all_open")
dp.callback_query.register(view_admin_ticket, F.data.startswith("admin_view_ticket_"))
# Ответы на тикеты

View File

@@ -2316,6 +2316,12 @@ def get_admin_tickets_keyboard(
keyboard.append(switch_row)
if open_rows and scope in ("all", "open"):
keyboard.append([
InlineKeyboardButton(
text=texts.t("ADMIN_CLOSE_ALL_OPEN_TICKETS", "🔒 Закрыть все открытые"),
callback_data="admin_tickets_close_all_open"
)
])
keyboard.append([InlineKeyboardButton(text=texts.t("OPEN_TICKETS_HEADER", "Открытые тикеты"), callback_data="noop")])
keyboard.extend(open_rows)
if closed_rows and scope in ("all", "closed"):

View File

@@ -581,8 +581,12 @@
"ADMIN_SUBSCRIPTIONS_PRICING": "⚙️ Pricing settings",
"ADMIN_SUPPORT_ASSIGN_MODERATOR_PROMPT": "🧑‍⚖️ <b>Assign moderator</b>\n\nSend the user's Telegram ID (number)",
"ADMIN_SUPPORT_AUDIT": "🧾 Moderator audit",
"ADMIN_CLOSE_ALL_OPEN_TICKETS": "🔒 Close all open tickets",
"ADMIN_CLOSE_ALL_OPEN_TICKETS_EMPTY": " No open tickets to close.",
"ADMIN_CLOSE_ALL_OPEN_TICKETS_SUCCESS": "✅ Closed open tickets: {count}",
"ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_PERM": "Permanent block",
"ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_TIMED": "Timed block",
"ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_ALL_TICKETS": "Mass close tickets",
"ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_TICKET": "Ticket closed",
"ADMIN_SUPPORT_AUDIT_ACTION_UNBLOCK": "Unblock",
"ADMIN_SUPPORT_AUDIT_EMPTY": "Nothing here yet",

View File

@@ -581,8 +581,12 @@
"ADMIN_SUBSCRIPTIONS_PRICING": "⚙️ Настройки цен",
"ADMIN_SUPPORT_ASSIGN_MODERATOR_PROMPT": "🧑‍⚖️ <b>Назначение модератора</b>\n\nОтправьте Telegram ID пользователя (число)",
"ADMIN_SUPPORT_AUDIT": "🧾 Аудит модераторов",
"ADMIN_CLOSE_ALL_OPEN_TICKETS": "🔒 Закрыть все открытые",
"ADMIN_CLOSE_ALL_OPEN_TICKETS_EMPTY": " Нет открытых тикетов для закрытия.",
"ADMIN_CLOSE_ALL_OPEN_TICKETS_SUCCESS": "✅ Закрыто открытых тикетов: {count}",
"ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_PERM": "Блокировка (навсегда)",
"ADMIN_SUPPORT_AUDIT_ACTION_BLOCK_TIMED": "Блокировка (время)",
"ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_ALL_TICKETS": "Массовое закрытие тикетов",
"ADMIN_SUPPORT_AUDIT_ACTION_CLOSE_TICKET": "Закрытие тикета",
"ADMIN_SUPPORT_AUDIT_ACTION_UNBLOCK": "Снятие блока",
"ADMIN_SUPPORT_AUDIT_EMPTY": "Пока пусто",