intra_max_chatbot/bot/handlers/view_tickets.py
2026-03-26 15:39:02 +03:00

253 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Обработчики просмотра заявок (Intradesk API).
Заявки загружаются из Intradesk API по нажатию кнопки — только незакрытые.
Навигация через callback payload:
"view_tickets" → список, страница 0
"itickets_page:{n}" → список, страница n
"iticket_detail:{id}" → карточка задачи (из кэша в context)
"main_menu" → в menu.py
"""
from __future__ import annotations
import re
import time
from maxapi.context import BaseContext
from maxapi.dispatcher import Router
from maxapi.filters import F
from maxapi.types.updates.message_callback import MessageCallback
from bot.filters import PayloadStartsWith
from bot.keyboards import iticket_detail_kb, menu_only_kb
from bot.send_helper import bot_reply
from database.connection import get_db
from database.queries.organizations import get_organization
from external.intradesk_api import get_client_open_tasks, get_task_last_comment
router = Router(router_id="view_tickets")
_PAGE_SIZE = 5
def _status_icon(status_name: str) -> str:
sn = status_name.lower()
if "выполнен" in sn or "закрыт" in sn:
return "🟢"
if "работ" in sn:
return "🟡"
if "открыт" in sn:
return "🔴"
return ""
def _fmt_date(iso: str) -> str:
"""'2026-03-26T09:57:11Z''26.03.2026'"""
if not iso or len(iso) < 10:
return iso or ""
y, m, d = iso[:10].split("-")
return f"{d}.{m}.{y}"
def _fmt_date_short(iso: str) -> str:
"""'2026-03-26''26.03'"""
if not iso or len(iso) < 10:
return iso or ""
y, m, d = iso[:10].split("-")
return f"{d}.{m}"
def _build_tasks_text(tasks: list[dict], page: int, total: int) -> str:
pages = max(1, -(-total // _PAGE_SIZE))
lines = [f"📋 Активные заявки (всего: {total}) — стр. {page + 1}/{pages}"]
for t in tasks:
icon = _status_icon(t.get("status_name") or "")
num = t.get("task_number") or t.get("id") or "?"
name = (t.get("name") or "")[:60]
created = _fmt_date(t.get("created_at") or "")
status = t.get("status_name") or ""
lines += [
"",
"" * 20,
f"📌 №{num} · {created}",
f"{name}",
f"{icon} {status}",
]
comment = t.get("last_comment")
if comment and comment.get("text"):
ctext = comment["text"][:80] + ("" if len(comment["text"]) > 80 else "")
author_short = (comment.get("author") or "").split()[0] # только фамилия
cdate = _fmt_date_short(comment.get("date") or "")
lines.append(f'💬 "{ctext}"{author_short} ({cdate})')
return "\n".join(lines)
async def _load_and_show(
event: MessageCallback,
bot_user: dict,
page: int,
context: BaseContext,
) -> None:
"""
Загрузить страницу заявок из API и показать.
Сразу редактирует сообщение на «⏳ Загрузка...», затем делает запрос к API.
Перед рендером проверяет load_token — если пользователь успел уйти
в другой флоу, контекст изменится и мы тихо выходим.
"""
org_id = bot_user.get("org_id")
intradesk_client_id: int | None = None
if org_id:
org = await get_organization(org_id)
intradesk_client_id = org.get("intradesk_id") if org else None
# Уникальный токен для этого запроса
token = str(time.monotonic_ns())
mid = event.message.body.mid if (event.message and event.message.body) else None
chat_id = event.message.recipient.chat_id if event.message else None
# Записываем токен в контекст и сразу показываем загрузку
await context.set_data({"load_token": token})
await bot_reply(
event,
text="⏳ Загружаем список заявок...",
attachments=None,
user_id=bot_user["max_user_id"],
chat_id=chat_id,
)
if not intradesk_client_id:
# Проверяем токен перед рендером
data = await context.get_data()
if data.get("load_token") != token:
return
await _edit_or_send(event.bot, mid, chat_id,
text="⚠️ Ваша организация не привязана к Intradesk. Обратитесь к администратору.",
attachments=[menu_only_kb()])
return
# Запрос к API
result = await get_client_open_tasks(intradesk_client_id, page=page, page_size=_PAGE_SIZE)
tasks = result["tasks"]
total = result["total"]
# Проверяем токен — не ушёл ли пользователь пока грузили
data = await context.get_data()
if data.get("load_token") != token:
return # Пользователь уже в другом флоу — не трогаем его экран
# Сохраняем задачи в контексте для детального просмотра
await context.set_data({"tasks": tasks, "page": page, "total": total})
if not tasks:
await _edit_or_send(event.bot, mid, chat_id,
text="📭 Активных заявок нет.",
attachments=[menu_only_kb()])
return
from bot.keyboards import intradesk_tasks_list_kb
text = _build_tasks_text(tasks, page, total)
kb = intradesk_tasks_list_kb(tasks, page=page, total=total, page_size=_PAGE_SIZE)
await _edit_or_send(event.bot, mid, chat_id, text=text, attachments=[kb])
async def _edit_or_send(bot, mid: str | None, chat_id: int | None, *, text: str, attachments) -> None:
"""Редактировать сообщение по mid или отправить новое если mid недоступен."""
if mid:
try:
await bot.edit_message(
message_id=mid,
text=text,
attachments=attachments if attachments is not None else [],
)
return
except Exception:
pass
if chat_id:
await bot.send_message(chat_id=chat_id, text=text, attachments=attachments or [])
@router.message_callback(F.callback.payload == "view_tickets")
async def cb_view_tickets(
event: MessageCallback, context: BaseContext, bot_user: dict
) -> None:
from bot.handlers.create_ticket import cleanup_ticket_flow
await cleanup_ticket_flow(event.bot, context)
await event.answer(notification="")
await _load_and_show(event, bot_user, page=0, context=context)
@router.message_callback(PayloadStartsWith("itickets_page:"))
async def cb_itickets_page(
event: MessageCallback, context: BaseContext, bot_user: dict
) -> None:
await event.answer(notification="")
try:
page = int(event.callback.payload.split(":")[1])
except (IndexError, ValueError):
page = 0
await _load_and_show(event, bot_user, page=page, context=context)
@router.message_callback(PayloadStartsWith("iticket_detail:"))
async def cb_iticket_detail(
event: MessageCallback, context: BaseContext, bot_user: dict
) -> None:
try:
task_id = int(event.callback.payload.split(":")[1])
except (IndexError, ValueError):
await event.answer(notification="Ошибка")
return
await event.answer(notification="")
# Ищем задачу в кэше контекста (загружена при показе списка)
data = await context.get_data()
task = next((t for t in (data.get("tasks") or []) if t.get("id") == task_id), None)
if task is None:
await bot_reply(
event,
text="⚠️ Заявка не найдена. Вернитесь к списку.",
attachments=[iticket_detail_kb()],
user_id=bot_user["max_user_id"],
chat_id=event.message.recipient.chat_id if event.message else None,
)
return
icon = _status_icon(task.get("status_name") or "")
num = task.get("task_number") or task["id"]
lines = [
f"📌 Заявка #{num}",
f"Статус: {icon} {task.get('status_name') or ''}",
f"Создана: {_fmt_date(task.get('created_at') or '')}",
f"Обновлена: {_fmt_date(task.get('updated_at') or '')}",
]
if task.get("name"):
lines += ["", f"📄 {task['name']}"]
# Последний комментарий — отдельный запрос к history API
comment = await get_task_last_comment(task["id"])
if comment and comment.get("text"):
ctext = comment["text"]
if len(ctext) > 500:
ctext = ctext[:500] + ""
lines += [
"",
f"💭 Последний комментарий ({comment['date']}):",
f" {ctext}",
f"{comment['author']}",
]
await bot_reply(
event,
text="\n".join(lines),
attachments=[iticket_detail_kb()],
user_id=bot_user["max_user_id"],
chat_id=event.message.recipient.chat_id if event.message else None,
)