норм
This commit is contained in:
parent
547a4a1dcb
commit
7fab711886
8 changed files with 322 additions and 84 deletions
|
|
@ -6,7 +6,9 @@
|
|||
"Bash(python -c \"import maxapi; import inspect; members = [m for m in dir\\(maxapi\\) if not m.startswith\\(''_''\\)]; print\\(members\\)\")",
|
||||
"Bash(python -c \"from maxapi import Bot; methods = [m for m in dir\\(Bot\\) if not m.startswith\\(''_''\\)]; print\\(methods\\)\")",
|
||||
"Bash(python -c \"from maxapi import Bot; import inspect; print\\(inspect.signature\\(Bot.edit_message\\)\\)\")",
|
||||
"Bash(python -c \"from maxapi.types.updates.message_callback import MessageCallback; print\\([m for m in dir\\(MessageCallback\\) if not m.startswith\\(''_''\\)]\\)\")"
|
||||
"Bash(python -c \"from maxapi.types.updates.message_callback import MessageCallback; print\\([m for m in dir\\(MessageCallback\\) if not m.startswith\\(''_''\\)]\\)\")",
|
||||
"Bash(python -c \"from maxapi.types import SendedMessage; print\\(SendedMessage.__fields__ if hasattr\\(SendedMessage, ''__fields__''\\) else dir\\(SendedMessage\\)\\)\")",
|
||||
"Bash(python -c \"from maxapi.context import State; s = State\\(\\); print\\(type\\(s\\), dir\\(s\\)\\)\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,54 @@ logger = get_logger("bot.handlers.create_ticket")
|
|||
|
||||
router = Router(router_id="create_ticket")
|
||||
|
||||
|
||||
async def cleanup_ticket_flow(bot, context: BaseContext) -> None:
|
||||
"""
|
||||
Удалить висящие сообщения незавершённого потока создания заявки.
|
||||
Вызывается из любого обработчика, который прерывает этот поток
|
||||
(главное меню, список заявок, повторное нажатие «Создать заявку»).
|
||||
"""
|
||||
from bot.states import TicketStates # avoid circular at module level
|
||||
state = await context.get_state()
|
||||
creation_states = [
|
||||
TicketStates.waiting_description,
|
||||
TicketStates.collecting_attachments,
|
||||
TicketStates.confirming,
|
||||
]
|
||||
if state not in creation_states:
|
||||
return
|
||||
data = await context.get_data()
|
||||
for mid in (data.get("flow_message_ids") or []):
|
||||
try:
|
||||
await bot.delete_message(message_id=mid)
|
||||
except Exception:
|
||||
pass
|
||||
await context.clear()
|
||||
|
||||
|
||||
async def _track_mid(context: BaseContext, mid: str | None) -> None:
|
||||
"""Добавить message_id в список сообщений потока создания."""
|
||||
if not mid:
|
||||
return
|
||||
data = await context.get_data()
|
||||
ids: list[str] = data.get("flow_message_ids") or []
|
||||
ids.append(mid)
|
||||
data["flow_message_ids"] = ids
|
||||
await context.set_data(data)
|
||||
|
||||
|
||||
async def _delete_flow_messages(bot, context: BaseContext, extra_mid: str | None = None) -> None:
|
||||
"""Удалить все сообщения, накопленные за время потока создания."""
|
||||
data = await context.get_data()
|
||||
ids: list[str] = list(data.get("flow_message_ids") or [])
|
||||
if extra_mid:
|
||||
ids.append(extra_mid)
|
||||
for mid in ids:
|
||||
try:
|
||||
await bot.delete_message(message_id=mid)
|
||||
except Exception:
|
||||
pass # сообщение уже удалено или слишком старое
|
||||
|
||||
_MEDIA_TYPES = {
|
||||
AttachmentType.IMAGE,
|
||||
AttachmentType.VIDEO,
|
||||
|
|
@ -120,8 +168,11 @@ def _preview_text(org_name: str | None, description: str, pending_atts: list[dic
|
|||
async def cb_create_ticket(
|
||||
event: MessageCallback, context: BaseContext, bot_user: dict
|
||||
) -> None:
|
||||
# Зачищаем предыдущий незавершённый поток, если был
|
||||
await cleanup_ticket_flow(event.bot, context)
|
||||
|
||||
await context.set_state(TicketStates.waiting_description)
|
||||
await context.set_data({"pending_attachments": []})
|
||||
await context.set_data({"pending_attachments": [], "flow_message_ids": []})
|
||||
logger.info("📝 create_ticket started | user_id=%s", bot_user["max_user_id"])
|
||||
await event.answer(notification="")
|
||||
|
||||
|
|
@ -129,7 +180,7 @@ async def cb_create_ticket(
|
|||
if chat_id is None:
|
||||
return
|
||||
|
||||
await bot_send(
|
||||
mid = await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=bot_user["max_user_id"],
|
||||
|
|
@ -140,6 +191,7 @@ async def cb_create_ticket(
|
|||
),
|
||||
attachments=[done_ticket_kb()],
|
||||
)
|
||||
await _track_mid(context, mid)
|
||||
|
||||
|
||||
# ── Шаг 2: первое сообщение с описанием ──────────────────────────────────────
|
||||
|
|
@ -159,7 +211,7 @@ async def on_ticket_description(
|
|||
await context.update_data(description=text, pending_attachments=attachments)
|
||||
await context.set_state(TicketStates.collecting_attachments)
|
||||
|
||||
await bot_send(
|
||||
mid = await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
|
|
@ -169,6 +221,7 @@ async def on_ticket_description(
|
|||
),
|
||||
attachments=[done_ticket_kb()],
|
||||
)
|
||||
await _track_mid(context, mid)
|
||||
|
||||
|
||||
# ── Шаг 3: дополнительные вложения/текст ─────────────────────────────────────
|
||||
|
|
@ -227,13 +280,14 @@ async def cb_done_ticket(
|
|||
|
||||
await context.set_state(TicketStates.confirming)
|
||||
|
||||
await bot_send(
|
||||
mid = await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=bot_user["max_user_id"],
|
||||
text=_preview_text(org_name, description, pending_atts),
|
||||
attachments=[confirm_ticket_kb()],
|
||||
)
|
||||
await _track_mid(context, mid)
|
||||
|
||||
|
||||
# ── Шаг 5б: "Продолжить редактирование" ──────────────────────────────────────
|
||||
|
|
@ -252,7 +306,7 @@ async def cb_edit_ticket(
|
|||
if chat_id is None:
|
||||
return
|
||||
|
||||
await bot_send(
|
||||
mid = await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=bot_user["max_user_id"],
|
||||
|
|
@ -262,6 +316,7 @@ async def cb_edit_ticket(
|
|||
),
|
||||
attachments=[done_ticket_kb()],
|
||||
)
|
||||
await _track_mid(context, mid)
|
||||
|
||||
|
||||
# ── Шаг 5в: "Отменить создание" ──────────────────────────────────────────────
|
||||
|
|
@ -274,6 +329,10 @@ async def cb_cancel_ticket(
|
|||
event: MessageCallback, context: BaseContext, bot_user: dict
|
||||
) -> None:
|
||||
await event.answer(notification="Создание отменено")
|
||||
|
||||
# Удаляем все сообщения потока + текущее сообщение с кнопками
|
||||
callback_mid = event.message.body.mid if (event.message and event.message.body) else None
|
||||
await _delete_flow_messages(event.bot, context, extra_mid=callback_mid)
|
||||
await context.clear()
|
||||
|
||||
chat_id = event.message.recipient.chat_id if event.message else None
|
||||
|
|
@ -301,6 +360,9 @@ async def cb_confirm_ticket(
|
|||
# Защита от двойного нажатия: читаем данные и сбрасываем состояние атомарно.
|
||||
# Повторный tap уже не пройдёт фильтр states=confirming.
|
||||
data = await context.get_data()
|
||||
# Удаляем все сообщения потока + текущее (предпросмотр с кнопками)
|
||||
callback_mid = event.message.body.mid if (event.message and event.message.body) else None
|
||||
await _delete_flow_messages(event.bot, context, extra_mid=callback_mid)
|
||||
await context.clear()
|
||||
|
||||
description = (data.get("description") or "").strip()
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ async def build_main_menu_text(bot_user: dict) -> str:
|
|||
async def cb_main_menu(
|
||||
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 context.set_state(None)
|
||||
await event.answer(notification="")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
"""
|
||||
Обработчики просмотра заявок (Intradesk).
|
||||
Обработчики просмотра заявок (Intradesk API).
|
||||
|
||||
Навигация через callback payload — все переходы редактируют текущее сообщение:
|
||||
Заявки загружаются из Intradesk API по нажатию кнопки — только незакрытые.
|
||||
Навигация через callback payload:
|
||||
"view_tickets" → список, страница 0
|
||||
"itickets_page:{n}" → список, страница n
|
||||
"iticket_detail:{id}" → карточка задачи
|
||||
"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
|
||||
|
|
@ -15,24 +19,20 @@ from maxapi.filters import F
|
|||
from maxapi.types.updates.message_callback import MessageCallback
|
||||
|
||||
from bot.filters import PayloadStartsWith
|
||||
from bot.keyboards import intradesk_tasks_list_kb, iticket_detail_kb, menu_only_kb
|
||||
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.intradesk import (
|
||||
count_tasks_for_local_org,
|
||||
get_task_for_local_org,
|
||||
get_tasks_for_local_org,
|
||||
)
|
||||
from external.intradesk_api import get_task_last_comment
|
||||
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 = 8
|
||||
_PAGE_SIZE = 5
|
||||
|
||||
|
||||
def _status_icon(status_name: str) -> str:
|
||||
sn = status_name.lower()
|
||||
if "выполнен" in sn:
|
||||
if "выполнен" in sn or "закрыт" in sn:
|
||||
return "🟢"
|
||||
if "работ" in sn:
|
||||
return "🟡"
|
||||
|
|
@ -41,43 +41,144 @@ def _status_icon(status_name: str) -> str:
|
|||
return "⚪"
|
||||
|
||||
|
||||
def _priority_icon(priority_name: str) -> str:
|
||||
pn = priority_name.lower()
|
||||
if "критич" in pn:
|
||||
return "🔴"
|
||||
if "высок" in pn:
|
||||
return "🟠"
|
||||
if "средн" in pn:
|
||||
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}"
|
||||
|
||||
|
||||
async def _tasks_page_text(org_id: int, page: int) -> tuple[str, list[dict], int]:
|
||||
"""Возвращает (текст, задачи, всего)."""
|
||||
db = get_db()
|
||||
tasks = await get_tasks_for_local_org(db, org_id, page=page, page_size=_PAGE_SIZE)
|
||||
total = await count_tasks_for_local_org(db, org_id)
|
||||
if not tasks:
|
||||
return "📭 У вашей организации пока нет заявок.", [], 0
|
||||
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))
|
||||
text = f"📋 Заявки — стр. {page + 1} из {pages} (всего: {total}):"
|
||||
return text, tasks, total
|
||||
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="")
|
||||
text, tasks, total = await _tasks_page_text(bot_user["org_id"], 0)
|
||||
kb = intradesk_tasks_list_kb(tasks, page=0, total=total) if tasks else menu_only_kb()
|
||||
await bot_reply(
|
||||
event,
|
||||
text=text,
|
||||
attachments=[kb],
|
||||
user_id=bot_user["max_user_id"],
|
||||
chat_id=event.message.recipient.chat_id if event.message else None,
|
||||
)
|
||||
await _load_and_show(event, bot_user, page=0, context=context)
|
||||
|
||||
|
||||
@router.message_callback(PayloadStartsWith("itickets_page:"))
|
||||
|
|
@ -89,15 +190,7 @@ async def cb_itickets_page(
|
|||
page = int(event.callback.payload.split(":")[1])
|
||||
except (IndexError, ValueError):
|
||||
page = 0
|
||||
text, tasks, total = await _tasks_page_text(bot_user["org_id"], page)
|
||||
kb = intradesk_tasks_list_kb(tasks, page=page, total=total) if tasks else menu_only_kb()
|
||||
await bot_reply(
|
||||
event,
|
||||
text=text,
|
||||
attachments=[kb],
|
||||
user_id=bot_user["max_user_id"],
|
||||
chat_id=event.message.recipient.chat_id if event.message else None,
|
||||
)
|
||||
await _load_and_show(event, bot_user, page=page, context=context)
|
||||
|
||||
|
||||
@router.message_callback(PayloadStartsWith("iticket_detail:"))
|
||||
|
|
@ -110,42 +203,35 @@ async def cb_iticket_detail(
|
|||
await event.answer(notification="Ошибка")
|
||||
return
|
||||
|
||||
db = get_db()
|
||||
task = await get_task_for_local_org(db, task_id, bot_user["org_id"])
|
||||
|
||||
if task is None:
|
||||
await event.answer(notification="Нет доступа")
|
||||
return
|
||||
|
||||
await event.answer(notification="")
|
||||
|
||||
sicon = _status_icon(task.get("status_name") or "")
|
||||
picon = _priority_icon(task.get("priority_name") or "")
|
||||
# Ищем задачу в кэше контекста (загружена при показе списка)
|
||||
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"Статус: {sicon} {task.get('status_name') or '—'}",
|
||||
f"Приоритет: {picon} {task.get('priority_name') or '—'}",
|
||||
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("service_name"):
|
||||
lines.append(f"Сервис: {task['service_name']}")
|
||||
if task.get("executor_name"):
|
||||
lines.append(f"Исполнитель: {task['executor_name']}")
|
||||
lines.append(f"Создана: {(task.get('created_at') or '')[:10] or '—'}")
|
||||
lines.append(f"Обновлена: {(task.get('updated_at') or '')[:10] or '—'}")
|
||||
|
||||
if task.get("name"):
|
||||
lines += ["", f"📄 {task['name']}"]
|
||||
|
||||
if task.get("description"):
|
||||
desc = re.sub(r"<[^>]+>", "", task["description"]).strip()
|
||||
if desc:
|
||||
if len(desc) > 800:
|
||||
desc = desc[:800] + "…"
|
||||
lines += ["", f"💬 {desc}"]
|
||||
|
||||
# Последний комментарий — запрашиваем из Intradesk API
|
||||
# Последний комментарий — отдельный запрос к history API
|
||||
comment = await get_task_last_comment(task["id"])
|
||||
if comment and comment.get("text"):
|
||||
ctext = comment["text"]
|
||||
|
|
|
|||
|
|
@ -9,19 +9,23 @@ async def bot_send(
|
|||
user_id: int | None,
|
||||
text: str,
|
||||
attachments=None,
|
||||
) -> None:
|
||||
"""Отправить новое сообщение."""
|
||||
) -> str | None:
|
||||
"""Отправить новое сообщение. Возвращает message_id (mid) или None."""
|
||||
kw: dict = {"chat_id": chat_id, "text": text}
|
||||
if attachments is not None:
|
||||
kw["attachments"] = attachments
|
||||
await bot.send_message(**kw)
|
||||
result = await bot.send_message(**kw)
|
||||
mid: str | None = None
|
||||
if result and result.message and result.message.body:
|
||||
mid = result.message.body.mid
|
||||
await log_message(
|
||||
direction="out",
|
||||
max_user_id=user_id,
|
||||
max_chat_id=chat_id,
|
||||
message_id=None,
|
||||
message_id=mid,
|
||||
text=text,
|
||||
)
|
||||
return mid
|
||||
|
||||
|
||||
async def bot_edit(
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
82
external/intradesk_api.py
vendored
82
external/intradesk_api.py
vendored
|
|
@ -60,6 +60,88 @@ async def get_task_last_comment(task_id: int) -> dict | None:
|
|||
return None
|
||||
|
||||
|
||||
async def get_client_open_tasks(
|
||||
client_id: int,
|
||||
page: int = 0,
|
||||
page_size: int = 5,
|
||||
) -> dict:
|
||||
"""
|
||||
Получить незакрытые заявки клиента из Intradesk API.
|
||||
|
||||
Возвращает {"tasks": [...], "total": int}.
|
||||
Каждая задача содержит поля из OData + "last_comment" (dict или None).
|
||||
"""
|
||||
key = config.INTRADESK_TASKS_API_KEY
|
||||
if not key:
|
||||
return {"tasks": [], "total": 0}
|
||||
|
||||
skip = page * page_size
|
||||
url = (
|
||||
f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks"
|
||||
f"?ApiKey={key}"
|
||||
f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)"
|
||||
f"&$orderby=updatedat desc"
|
||||
f"&$top={page_size}&$skip={skip}"
|
||||
f"&$count=true"
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, timeout=20.0)
|
||||
if resp.status_code != 200:
|
||||
logger.warning("get_client_open_tasks client=%s → HTTP %s", client_id, resp.status_code)
|
||||
return {"tasks": [], "total": 0}
|
||||
data = resp.json()
|
||||
except Exception as exc:
|
||||
logger.warning("get_client_open_tasks error: %s", exc)
|
||||
return {"tasks": [], "total": 0}
|
||||
|
||||
total = data.get("@odata.count") or 0
|
||||
raw_tasks = data.get("value") or []
|
||||
|
||||
# Словари для разрешения имён
|
||||
dicts_map: dict[int, str] = {}
|
||||
for d in data.get("dictionaries") or []:
|
||||
did = d.get("id")
|
||||
dname = d.get("name") or ""
|
||||
if isinstance(dname, dict):
|
||||
dname = dname.get("ru") or dname.get("en") or ""
|
||||
if did:
|
||||
dicts_map[did] = dname
|
||||
|
||||
tasks = []
|
||||
for t in raw_tasks:
|
||||
status_id = t.get("status")
|
||||
last_comment = _extract_last_comment_from_lifetime(t.get("lifetime"))
|
||||
tasks.append({
|
||||
"id": t.get("id"),
|
||||
"task_number": t.get("tasknumber"),
|
||||
"name": t.get("name") or "",
|
||||
"status_name": dicts_map.get(status_id, "") if status_id else "",
|
||||
"created_at": t.get("createdat") or "",
|
||||
"updated_at": t.get("updatedat") or "",
|
||||
"last_comment": last_comment,
|
||||
})
|
||||
|
||||
return {"tasks": tasks, "total": total}
|
||||
|
||||
|
||||
def _extract_last_comment_from_lifetime(lifetime: dict | None) -> dict | None:
|
||||
"""Извлечь последний комментарий из поля lifetime задачи (если последнее событие — комментарий)."""
|
||||
if not lifetime:
|
||||
return None
|
||||
for entry in (lifetime.get("data") or []):
|
||||
for ev in (entry.get("events") or {}).get("data") or []:
|
||||
if ev.get("blockname") == "comment" and ev.get("type") == 50:
|
||||
raw_date = entry.get("eventat") or ""
|
||||
short_date = raw_date[:10] # "2026-03-26"
|
||||
return {
|
||||
"text": ev.get("stringvalue") or "",
|
||||
"author": entry.get("username") or "—",
|
||||
"date": short_date,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
async def create_intradesk_task(
|
||||
name: str,
|
||||
description: str,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue