273 lines
9.1 KiB
Python
273 lines
9.1 KiB
Python
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"<b>Сделки: {title}</b>\n"
|
||
f"Всего сделок: <b>{total_deals}</b>\n"
|
||
f"Страница <b>{page + 1}</b> из <b>{total_pages}</b>"
|
||
)
|
||
|
||
@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"<b>Сделка #{deal_id}</b>",
|
||
f"<b>{title}</b>",
|
||
"",
|
||
f"Клиент: <code>{client}</code>",
|
||
*([f"Компания: <code>{company}</code>"] if company else []),
|
||
f"Телефон клиента: <code>{phone}</code>",
|
||
f"Стадия: <code>{stage}</code>",
|
||
f"Источник сделки: <code>{source}</code>",
|
||
f"Сумма: {cls.money(deal.get('OPPORTUNITY'), deal.get('CURRENCY_ID'))}",
|
||
f"Ответственный: <code>{assigned}</code>",
|
||
f"Дата создания: <code>{date}</code>"
|
||
]
|
||
if comments:
|
||
lines.extend(["", f"<b>Комментарий:</b>\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"<b>История сделки #{html.escape(deal_id)}</b>"]
|
||
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"• <b>{name}</b>",
|
||
f" Стадия: <code>{stage}</code>",
|
||
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"
|
||
)
|
||
],
|
||
]
|
||
)
|