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" ) ], ] )