from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup from bot.db import is_overdue from bot.taxonomy import SECTIONS, SUBTOPICS, TOPICS STATUS_LABELS = { "new": "🆕 Новая", "in_progress": "🛠 В работе", "waiting": "⏳ Ожидание ответа", "deferred": "💤 Отложена", "closed": "✅ Закрыта", "rejected": "🚫 Отклонена", } PRIORITY_LABELS = { "low": "🟢 Низкий", "medium": "🟡 Средний", "high": "🟠 Высокий", "critical": "🔴 Критичный", } PRIORITY_EMOJI = { "low": "🟢", "medium": "🟡", "high": "🟠", "critical": "🔴", } def status_label(status: str) -> str: return STATUS_LABELS.get(status, status) def priority_label(priority: str) -> str: return PRIORITY_LABELS.get(priority, priority) def priority_emoji(priority: str) -> str: return PRIORITY_EMOJI.get(priority, "⚪️") def main_menu() -> ReplyKeyboardMarkup: return ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="Создать заявку")], [KeyboardButton(text="Мои заявки"), KeyboardButton(text="FAQ")], ], resize_keyboard=True, ) def sections() -> InlineKeyboardMarkup: """Корневое меню: 11 разделов согласно схеме.""" return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text=label, callback_data=f"sec:{code}")] for code, label in SECTIONS ] ) def topics(section_code: str) -> InlineKeyboardMarkup: """Подтемы выбранного раздела + кнопка «Назад к разделам».""" rows: list[list[InlineKeyboardButton]] = [ [InlineKeyboardButton(text=label, callback_data=f"tp:{section_code}:{code}")] for code, label in TOPICS.get(section_code, []) ] rows.append([InlineKeyboardButton(text="⬅️ К разделам", callback_data="sec:back")]) return InlineKeyboardMarkup(inline_keyboard=rows) def subtopics(section_code: str, topic_code: str) -> InlineKeyboardMarkup: """Уточнение для подтемы, у которой есть 3-й уровень (например, тип CRM).""" rows: list[list[InlineKeyboardButton]] = [ [InlineKeyboardButton(text=label, callback_data=f"sb:{section_code}:{topic_code}:{code}")] for code, label in SUBTOPICS.get((section_code, topic_code), []) ] rows.append([InlineKeyboardButton(text="⬅️ К темам", callback_data=f"sec:{section_code}")]) return InlineKeyboardMarkup(inline_keyboard=rows) def instruction_result() -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="Не помогло, создать заявку", callback_data="instruction:create")], [InlineKeyboardButton(text="Отменить создание", callback_data="ticket:cancel")], ] ) def cancel_creation() -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="Отменить создание", callback_data="ticket:cancel")] ] ) def attachment_actions(count: int = 0) -> InlineKeyboardMarkup: """Кнопки на шаге прикрепления фото: завершить (с любым числом фото) или отменить.""" done_text = f"✅ Готово ({count})" if count else "✅ Готово (без фото)" return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text=done_text, callback_data="att:done")], [InlineKeyboardButton(text="Отменить создание", callback_data="ticket:cancel")], ] ) def duplicate_options(existing_id: int) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text=f"➕ Дополнить заявку #{existing_id}", callback_data=f"dup:append:{existing_id}")], [InlineKeyboardButton(text="🆕 Создать новую заявку", callback_data="dup:new")], [InlineKeyboardButton(text="Отменить создание", callback_data="ticket:cancel")], ] ) def user_tickets(tickets: list, status_filter: str = "active") -> InlineKeyboardMarkup: archive_label = "Архив закрытых" if status_filter == "active" else "Активные заявки" archive_filter = "closed" if status_filter == "active" else "active" rows = [ [ InlineKeyboardButton( text=f"#{ticket['id']} · {status_label(ticket['status'])} · {ticket['created_at']}", callback_data=f"user_ticket:{ticket['id']}:{status_filter}", ) ] for ticket in tickets ] rows.append([InlineKeyboardButton(text=archive_label, callback_data=f"user_tickets:{archive_filter}")]) return InlineKeyboardMarkup(inline_keyboard=rows) def back_to_user_tickets(status_filter: str = "active") -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="Назад к моим заявкам", callback_data=f"user_tickets:{status_filter}")] ] ) def user_ticket_actions(ticket_id: int, status: str, status_filter: str = "active") -> InlineKeyboardMarkup: rows: list[list[InlineKeyboardButton]] = [] if status not in ("closed", "rejected"): rows.append( [InlineKeyboardButton(text="💬 Добавить комментарий", callback_data=f"user_comment:{ticket_id}:{status_filter}")] ) rows.append( [InlineKeyboardButton(text="💬 Чат с поддержкой", callback_data=f"user_chat:{ticket_id}")] ) rows.append( [InlineKeyboardButton(text="✅ Вопрос решён, закрыть", callback_data=f"user_close:{ticket_id}:{status_filter}")] ) rows.append([InlineKeyboardButton(text="Назад к моим заявкам", callback_data=f"user_tickets:{status_filter}")]) return InlineKeyboardMarkup(inline_keyboard=rows) def user_comment_cancel(ticket_id: int, status_filter: str = "active") -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="Отмена", callback_data=f"user_comment_cancel:{ticket_id}:{status_filter}")] ] ) def user_close_confirm(ticket_id: int, status_filter: str = "active") -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="✅ Да, закрыть заявку", callback_data=f"user_close_yes:{ticket_id}:{status_filter}")], [InlineKeyboardButton(text="⬅️ Нет, оставить", callback_data=f"user_ticket:{ticket_id}:{status_filter}")], ] ) def user_reply_kb(ticket_id: int) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="💬 Ответить", callback_data=f"user_chat:{ticket_id}")] ] ) def user_chat_controls() -> ReplyKeyboardMarkup: return ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="❌ Завершить чат")]], resize_keyboard=True, ) def admin_reply_kb(ticket_id: int) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="💬 Ответить", callback_data=f"admin:chat:{ticket_id}:open")] ] ) # ----------------------------------------------------------------- admin side FILTER_TITLES = { "open": "Открытые заявки", "new": "Новые заявки", "in_progress": "Заявки в работе", "waiting": "Ожидают ответа", "deferred": "Отложенные заявки", "overdue": "Просроченные по SLA", "mine": "Мои заявки", "closed": "Архив (закрытые/отклонённые)", } def filter_title(list_filter: str) -> str: return FILTER_TITLES.get(list_filter, "Заявки") def admin_dashboard(stats: dict[str, int]) -> InlineKeyboardMarkup: def btn(text: str, key: str) -> InlineKeyboardButton: return InlineKeyboardButton(text=text, callback_data=f"adm:list:{key}") return InlineKeyboardMarkup( inline_keyboard=[ [btn(f"📋 Все открытые · {stats['open']}", "open")], [btn(f"🆕 Новые · {stats['new']}", "new"), btn(f"🛠 В работе · {stats['in_progress']}", "in_progress")], [btn(f"⏳ Ожидание · {stats['waiting']}", "waiting"), btn(f"💤 Отложены · {stats['deferred']}", "deferred")], [btn(f"🔴 Просрочены · {stats['overdue']}", "overdue"), btn("🙋 Мои", "mine")], [ InlineKeyboardButton(text="🔎 Поиск по №", callback_data="adm:find"), InlineKeyboardButton(text=f"🗄 Архив · {stats['closed']}", callback_data="adm:list:closed"), ], ] ) def open_tickets(tickets: list, list_filter: str, sla_minutes: int) -> InlineKeyboardMarkup: rows = [] for ticket in tickets: mark = " 🔴" if is_overdue(ticket, sla_minutes) else "" rows.append( [ InlineKeyboardButton( text=f"{priority_emoji(ticket['priority'])} #{ticket['id']} · {ticket['topic']} · {status_label(ticket['status'])}{mark}", callback_data=f"admin_ticket:{ticket['id']}:{list_filter}", ) ] ) rows.append([InlineKeyboardButton(text="⬅️ Дашборд", callback_data="adm:dash")]) return InlineKeyboardMarkup(inline_keyboard=rows) def priority_menu(ticket_id: int, list_filter: str) -> InlineKeyboardMarkup: rows = [ [InlineKeyboardButton(text=label, callback_data=f"setprio:{ticket_id}:{level}")] for level, label in PRIORITY_LABELS.items() ] rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_ticket:{ticket_id}:{list_filter}")]) return InlineKeyboardMarkup(inline_keyboard=rows) def assignee_menu(ticket_id: int, admins: list[tuple[int, str]], list_filter: str) -> InlineKeyboardMarkup: rows = [ [InlineKeyboardButton(text=f"👤 {name}", callback_data=f"assignto:{ticket_id}:{admin_id}")] for admin_id, name in admins ] rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_ticket:{ticket_id}:{list_filter}")]) return InlineKeyboardMarkup(inline_keyboard=rows) def back_to_ticket(ticket_id: int, list_filter: str) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="⬅️ К заявке", callback_data=f"admin_ticket:{ticket_id}:{list_filter}")] ] ) # Быстрые причины отложения заявки («быстрые ответы»). DEFER_REASONS = [ "Ожидаем запчасти / поставку", "Согласуем время визита", "Ждём ответ от поставщика / вендора", "Ожидаем информацию от заявителя", "Передано смежному отделу", ] def defer_reasons_menu(ticket_id: int, list_filter: str) -> InlineKeyboardMarkup: rows = [ [InlineKeyboardButton(text=reason, callback_data=f"defrsn:{ticket_id}:{index}:{list_filter}")] for index, reason in enumerate(DEFER_REASONS) ] rows.append([InlineKeyboardButton(text="✏️ Своя причина", callback_data=f"defrsn:{ticket_id}:custom:{list_filter}")]) rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data=f"admin_ticket:{ticket_id}:{list_filter}")]) return InlineKeyboardMarkup(inline_keyboard=rows) def chat_session_kb(ticket_id: int, list_filter: str) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="❌ Закрыть чат", callback_data=f"admin:chatend:{ticket_id}:{list_filter}")] ] ) def admin_cancel(ticket_id: int, list_filter: str) -> InlineKeyboardMarkup: return InlineKeyboardMarkup( inline_keyboard=[ [InlineKeyboardButton(text="Отмена", callback_data=f"adm:back:{ticket_id}:{list_filter}")] ] ) def ticket_actions(ticket, viewer_id: int, list_filter: str = "open", with_back: bool = False) -> InlineKeyboardMarkup: ticket_id = ticket["id"] status = ticket["status"] is_holder = ticket["assignee_id"] == viewer_id def cb(action: str) -> str: return f"admin:{action}:{ticket_id}:{list_filter}" rows: list[list[InlineKeyboardButton]] = [] if status in ("closed", "rejected"): rows.append([InlineKeyboardButton(text="♻️ Переоткрыть", callback_data=cb("reopen"))]) else: if status == "new": rows.append([InlineKeyboardButton(text="▶️ Взять в работу", callback_data=cb("assign"))]) elif status in ("waiting", "deferred"): rows.append([InlineKeyboardButton(text="▶️ Вернуть в работу", callback_data=cb("assign"))]) elif status == "in_progress" and not is_holder: rows.append([InlineKeyboardButton(text="✋ Перехватить", callback_data=cb("assign"))]) action_row: list[InlineKeyboardButton] = [] if status != "new" and is_holder: action_row.append(InlineKeyboardButton(text="✅ Закрыть", callback_data=cb("close"))) if status == "in_progress": action_row.append(InlineKeyboardButton(text="⏳ В ожидание", callback_data=cb("wait"))) if status in ("new", "in_progress"): action_row.append(InlineKeyboardButton(text="💤 Отложить", callback_data=cb("defer"))) for i in range(0, len(action_row), 2): rows.append(action_row[i : i + 2]) rows.append( [ InlineKeyboardButton(text="🎚 Приоритет", callback_data=cb("prio")), InlineKeyboardButton(text="👥 Назначить", callback_data=cb("assignto")), ] ) rows.append( [ InlineKeyboardButton(text="📝 Заметка", callback_data=cb("note")), InlineKeyboardButton(text="💬 Чат", callback_data=cb("chat")), ] ) rows.append([InlineKeyboardButton(text="🚫 Отклонить", callback_data=cb("reject"))]) rows.append([InlineKeyboardButton(text="🕓 История", callback_data=cb("history"))]) if with_back: rows.append([InlineKeyboardButton(text="⬅️ К списку", callback_data=f"adm:list:{list_filter}")]) return InlineKeyboardMarkup(inline_keyboard=rows)