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

378 lines
14 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, main_menu_kb, menu_only_kb
from bot.send_helper import bot_reply, bot_send
from bot.states import CommentStates
from database.connection import get_db
from database.queries.intradesk import get_intradesk_contact_id_for_phone
from database.queries.organizations import get_organization
from external.intradesk_api import add_task_comment, 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 09:57'"""
if not iso or len(iso) < 10:
return iso or ""
y, m, d = iso[:10].split("-")
# iso может быть "2026-03-26T09:57:11Z" или "2026-03-26 09:57"
time_part = iso[11:16] if len(iso) >= 16 else ""
return f"{d}.{m}.{y}" + (f" {time_part}" if time_part else "")
def _fmt_date_short(iso: str) -> str:
"""'2026-03-26 11:54''26.03 11:54'"""
if not iso or len(iso) < 10:
return iso or ""
y, m, d = iso[:10].split("-")
time_part = iso[11:16] if len(iso) >= 16 else ""
return f"{d}.{m}" + (f" {time_part}" if time_part else "")
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})')
else:
lines.append("💬 Нет комментариев")
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(task_id=task["id"])],
user_id=bot_user["max_user_id"],
chat_id=event.message.recipient.chat_id if event.message else None,
)
# ── Оставить комментарий ──────────────────────────────────────────────────────
@router.message_callback(PayloadStartsWith("iticket_comment:"))
async def cb_add_comment(
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="")
# Ищем task_number в кэше контекста
data = await context.get_data()
task = next((t for t in (data.get("tasks") or []) if t.get("id") == task_id), None)
task_number = task.get("task_number") if task else None
await context.set_state(CommentStates.waiting_text)
await context.set_data({
"comment_task_id": task_id,
"comment_task_number": task_number,
"flow_message_ids": [],
})
chat_id = event.message.recipient.chat_id if event.message else None
if chat_id is None:
return
num_str = f" к заявке #{task_number}" if task_number else ""
mid = await bot_send(
event.bot,
chat_id=chat_id,
user_id=bot_user["max_user_id"],
text=(
f"💬 Напишите комментарий{num_str}:\n\n"
"Для отмены нажмите «🏠 Главное меню»."
),
attachments=[menu_only_kb()],
)
# Сохраняем mid для возможной очистки
if mid:
d = await context.get_data()
d["flow_message_ids"] = [mid]
await context.set_data(d)
@router.message_created(states=CommentStates.waiting_text)
async def on_comment_text(
event: MessageCreated, context: BaseContext, bot_user: dict
) -> None:
chat_id, user_id = event.get_ids()
if chat_id is None:
return
body = event.message.body
text = (body.text or "").strip() if body else ""
if not text:
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text="❗ Комментарий не может быть пустым. Напишите текст:",
attachments=[menu_only_kb()],
)
return
data = await context.get_data()
task_number = data.get("comment_task_number")
# Сбрасываем состояние до отправки API — защита от повторов
await context.clear()
# Получаем contact_person_id
contact_person_id: int | None = None
if bot_user.get("phone") and bot_user.get("org_id"):
org = await get_organization(bot_user["org_id"])
intradesk_id = org.get("intradesk_id") if org else None
if intradesk_id:
db = get_db()
contact_person_id = await get_intradesk_contact_id_for_phone(
db, intradesk_id, bot_user["phone"]
)
ok = await add_task_comment(
task_number=task_number,
text=text,
contact_person_id=contact_person_id,
)
from bot.handlers.menu import build_main_menu_text
if ok:
num_str = f" к заявке #{task_number}" if task_number else ""
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text=f"✅ Комментарий{num_str} успешно отправлен!",
)
else:
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text="⚠️ Не удалось отправить комментарий. Попробуйте позже.",
)
await bot_send(
event.bot,
chat_id=chat_id,
user_id=user_id,
text=await build_main_menu_text(bot_user),
attachments=[main_menu_kb()],
)