Files
tg_zayavka/bot/keyboards.py
tikeev.k 4548a57f83 .
2026-06-01 17:38:44 +03:00

362 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)