""" Обработчики просмотра заявок (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, )