import decimal
import html
from typing import Any
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from .domain import (
MAX_DEAL_MESSAGE_LENGTH,
DealStageFilter,
)
class DealFormatter:
"""Тексты карточек и списков Telegram."""
@staticmethod
def truncate(text: str, limit: int) -> str:
if len(text) <= limit:
return text
return text[: limit - 3].rstrip() + "..."
@staticmethod
def list_title(
stage_filter: DealStageFilter,
page: int,
total_deals: int,
total_pages: int
) -> str:
title = html.escape(stage_filter.title)
return (
f"Сделки: {title}\n"
f"Всего сделок: {total_deals}\n"
f"Страница {page + 1} из {total_pages}"
)
@staticmethod
def money(value: Any, currency: Any) -> str:
try:
amount = decimal.Decimal(str(value or "0"))
formatted = f"{amount:,.2f}".replace(",", " ")
except decimal.InvalidOperation:
formatted = html.escape(str(value or "0"))
return f"{formatted} {html.escape(str(currency or ''))}".strip()
@classmethod
def deal_summary(cls, deal: dict) -> str:
summary = " · ".join(
(
f"#{deal.get('ID', '-')}",
str(deal.get("TITLE") or "Без названия"),
cls.money(deal.get("OPPORTUNITY"), deal.get("CURRENCY_ID"))
)
)
return cls.truncate(summary, 60)
@classmethod
def deal_details(cls, deal: dict) -> str:
deal_id = html.escape(str(deal.get("ID", "-")))
title = html.escape(str(deal.get("TITLE") or "Без названия"))
stage = html.escape(
str(deal.get("STAGE_NAME") or deal.get("STAGE_ID") or "-")
)
source = html.escape(
str(deal.get("SOURCE_NAME") or deal.get("SOURCE_ID") or "-")
)
assigned = html.escape(str(deal.get("ASSIGNED_BY_ID") or "не назначен"))
date = html.escape(str(deal.get("DATE_CREATE") or "-"))
client = html.escape(str(deal.get("CLIENT_NAME") or "не указан"))
company = html.escape(str(deal.get("CLIENT_COMPANY") or ""))
phone = html.escape(str(deal.get("CLIENT_PHONE") or "не найден"))
comments = html.escape(str(deal.get("COMMENTS") or "")).strip()
lines = [
f"Сделка #{deal_id}",
f"{title}",
"",
f"Клиент: {client}",
*([f"Компания: {company}"] if company else []),
f"Телефон клиента: {phone}",
f"Стадия: {stage}",
f"Источник сделки: {source}",
f"Сумма: {cls.money(deal.get('OPPORTUNITY'), deal.get('CURRENCY_ID'))}",
f"Ответственный: {assigned}",
f"Дата создания: {date}"
]
if comments:
lines.extend(["", f"Комментарий:\n{comments}"])
return cls.truncate("\n".join(lines), MAX_DEAL_MESSAGE_LENGTH)
@classmethod
def deal_history(cls, deal_id: str, events: list[dict]) -> str:
lines = [f"История сделки #{html.escape(deal_id)}"]
if not events:
lines.extend(["", "Изменения стадий пока не найдены."])
return "\n".join(lines)
event_names = {
"1": "Сделка создана",
"2": "Переход на стадию",
"3": "Переход на финальную стадию",
"5": "Изменение воронки"
}
for event in events:
date = html.escape(
str(event.get("CREATED_TIME") or "дата не указана"))
event_type = str(event.get("TYPE_ID") or "")
name = event_names.get(event_type, "Изменение стадии")
stage = html.escape(str(
event.get("STAGE_NAME")
or event.get("STAGE_ID")
or "не указана"
))
lines.extend(
[
"",
f"• {name}",
f" Стадия: {stage}",
f" {date}"
]
)
return cls.truncate("\n".join(lines), MAX_DEAL_MESSAGE_LENGTH)
class DealKeyboards:
@staticmethod
def deals_page(
deals: list[dict],
stage_filters: tuple[DealStageFilter, ...],
stage_key: str,
page: int,
has_next: bool
) -> InlineKeyboardMarkup:
filter_buttons = [
InlineKeyboardButton(
text=("✓ " if stage.key == stage_key else "") + stage.title,
callback_data=f"deals:page:{stage.key}:0"
)
for stage in stage_filters
]
rows = [
[
InlineKeyboardButton(
text=DealFormatter.deal_summary(deal),
callback_data=f"deal:view:{deal['ID']}"
)
]
for deal in deals
]
navigation = []
if page > 0:
navigation.append(
InlineKeyboardButton(
text="Назад",
callback_data=f"deals:page:{stage_key}:{page - 1}"
)
)
if has_next:
navigation.append(
InlineKeyboardButton(
text="Вперед",
callback_data=f"deals:page:{stage_key}:{page + 1}"
)
)
if navigation:
rows.append(navigation)
rows.append(
[
InlineKeyboardButton(
text="Обновить",
callback_data=f"deals:page:{stage_key}:{page}"
)
]
)
rows.extend(
filter_buttons[index: index + 2]
for index in range(0, len(filter_buttons), 2)
)
return InlineKeyboardMarkup(inline_keyboard=rows)
@staticmethod
def deal_card(
deal: dict,
viewer_bitrix_user_id: int
) -> InlineKeyboardMarkup:
deal_id = str(deal["ID"])
responsible_id = str(deal.get("ASSIGNED_BY_ID") or "")
rows = []
if (
str(deal.get("IS_NEW") or "").upper() == "Y"
and responsible_id != str(viewer_bitrix_user_id)
):
rows.append(
[
InlineKeyboardButton(
text="Стать ответственным и взять в работу",
callback_data=(
f"deal:assign:{deal_id}:{responsible_id}")
)
]
)
if (
responsible_id == str(viewer_bitrix_user_id)
and deal.get("NEXT_STAGE_ID")
):
next_stage_name = DealFormatter.truncate(
str(deal.get("NEXT_STAGE_NAME") or "следующая стадия"),
42,
)
if deal.get("NEXT_STAGE_IS_FINAL"):
button_text = f"Завершить: {next_stage_name}"
else:
button_text = f"Следующая стадия: {next_stage_name}"
rows.append(
[
InlineKeyboardButton(
text=button_text,
callback_data=f"deal:advance:{deal_id}",
)
]
)
rows.append(
[
InlineKeyboardButton(
text="Позвонить позже",
callback_data=f"deal:remind:{deal_id}"
)
]
)
rows.append(
[
InlineKeyboardButton(
text="История изменений",
callback_data=f"deal:history:{deal_id}"
)
]
)
rows.append(
[
InlineKeyboardButton(
text="К списку сделок",
callback_data="deals:page:new:0"
)
]
)
return InlineKeyboardMarkup(inline_keyboard=rows)
@staticmethod
def deal_history(deal_id: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="К сделке",
callback_data=f"deal:view:{deal_id}"
)
],
[
InlineKeyboardButton(
text="К списку сделок",
callback_data="deals:page:new:0"
)
],
]
)