mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 17:04:48 +00:00
bot: persistent reply keyboard with WebApp buttons + info actions
Now after /start, manager sees a bottom keyboard (4 rows) for fast access: Row 1: 🤖 Подбор техники | 📐 Новый замер ← WebApp Row 2: 👥 Мои клиенты | 🏠 Кабинет ← WebApp Row 3: ℹ️ Что умеет бот? | 📞 Куратор ← text Row 4: 📋 Чек-лист встречи ← text WebApp buttons jump straight to a MiniApp screen via ?go=<podbor|measure|clients>; app.js parses ?go on load and pre-sets location.hash so the right module mounts. Added /menu (re-show keyboard) and /hide (remove). Text buttons trigger in-chat info responses (bot description, contact, meeting checklist). Cache bust v=20260513b.
This commit is contained in:
parent
a084542bbf
commit
b2438507c3
@ -1,11 +1,14 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from aiogram import Router
|
from aiogram import F, Router
|
||||||
from aiogram.filters import CommandStart
|
from aiogram.filters import Command, CommandStart
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
Message,
|
|
||||||
InlineKeyboardMarkup,
|
|
||||||
InlineKeyboardButton,
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
KeyboardButton,
|
||||||
|
Message,
|
||||||
|
ReplyKeyboardMarkup,
|
||||||
|
ReplyKeyboardRemove,
|
||||||
WebAppInfo,
|
WebAppInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,6 +23,13 @@ def _bust_cache(url: str) -> str:
|
|||||||
return f"{url}{sep}t={int(time.time())}"
|
return f"{url}{sep}t={int(time.time())}"
|
||||||
|
|
||||||
|
|
||||||
|
def _with_query(url: str, **params: str) -> str:
|
||||||
|
"""Append query params (e.g. role=manager, go=podbor) preserving existing ones."""
|
||||||
|
sep = "&" if "?" in url else "?"
|
||||||
|
pairs = "&".join(f"{k}={v}" for k, v in params.items() if v)
|
||||||
|
return f"{url}{sep}{pairs}" if pairs else url
|
||||||
|
|
||||||
|
|
||||||
def role_choice_kb(miniapp_url: str) -> InlineKeyboardMarkup:
|
def role_choice_kb(miniapp_url: str) -> InlineKeyboardMarkup:
|
||||||
"""Two WebApp buttons — one tap opens the cabinet directly, no intermediate step."""
|
"""Two WebApp buttons — one tap opens the cabinet directly, no intermediate step."""
|
||||||
return InlineKeyboardMarkup(
|
return InlineKeyboardMarkup(
|
||||||
@ -27,21 +37,131 @@ def role_choice_kb(miniapp_url: str) -> InlineKeyboardMarkup:
|
|||||||
[
|
[
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="👤 Менеджер",
|
text="👤 Менеджер",
|
||||||
web_app=WebAppInfo(url=_bust_cache(f"{miniapp_url}?role=manager")),
|
web_app=WebAppInfo(url=_bust_cache(_with_query(miniapp_url, role="manager"))),
|
||||||
),
|
),
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text="🏠 Клиент",
|
text="🏠 Клиент",
|
||||||
web_app=WebAppInfo(url=_bust_cache(f"{miniapp_url}?role=client")),
|
web_app=WebAppInfo(url=_bust_cache(_with_query(miniapp_url, role="client"))),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def manager_reply_kb(miniapp_url: str) -> ReplyKeyboardMarkup:
|
||||||
|
"""Persistent bottom keyboard — fast access to key MiniApp screens + info text actions.
|
||||||
|
Reply-keyboard `web_app` buttons открывают MiniApp с указанным URL/query."""
|
||||||
|
def wapp(go: str) -> WebAppInfo:
|
||||||
|
return WebAppInfo(url=_bust_cache(_with_query(miniapp_url, role="manager", go=go)))
|
||||||
|
|
||||||
|
return ReplyKeyboardMarkup(
|
||||||
|
keyboard=[
|
||||||
|
[
|
||||||
|
KeyboardButton(text="🤖 Подбор техники", web_app=wapp("podbor")),
|
||||||
|
KeyboardButton(text="📐 Новый замер", web_app=wapp("measure")),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="👥 Мои клиенты", web_app=wapp("clients")),
|
||||||
|
KeyboardButton(
|
||||||
|
text="🏠 Кабинет",
|
||||||
|
web_app=WebAppInfo(url=_bust_cache(_with_query(miniapp_url, role="manager"))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="ℹ️ Что умеет бот?"),
|
||||||
|
KeyboardButton(text="📞 Связь с куратором"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="📋 Чек-лист встречи"),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
resize_keyboard=True,
|
||||||
|
is_persistent=True,
|
||||||
|
input_field_placeholder="Выберите действие…",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- /start ----------
|
||||||
|
|
||||||
@router.message(CommandStart())
|
@router.message(CommandStart())
|
||||||
async def cmd_start(message: Message, config: Config) -> None:
|
async def cmd_start(message: Message, config: Config) -> None:
|
||||||
|
# Сразу даём постоянную клавиатуру + inline-выбор роли. Менеджер будет
|
||||||
|
# видеть нижнюю клавиатуру после первого тапа на роль.
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"👋 Здравствуйте, я бот-помощник от Руслана ВАСИЛЬЕВА.\n\n"
|
"👋 Здравствуйте, я бот-помощник от Руслана ВАСИЛЬЕВА.\n\n"
|
||||||
"Кто вы?",
|
"Кто вы?",
|
||||||
reply_markup=role_choice_kb(config.miniapp_url),
|
reply_markup=role_choice_kb(config.miniapp_url),
|
||||||
)
|
)
|
||||||
|
# Постоянная клавиатура снизу — для быстрого доступа из любого экрана чата
|
||||||
|
await message.answer(
|
||||||
|
"📲 Внизу появилась панель быстрого доступа — открывайте кабинет или нужный экран одним тапом.",
|
||||||
|
reply_markup=manager_reply_kb(config.miniapp_url),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- /menu (вернуть клавиатуру если она была скрыта) ----------
|
||||||
|
|
||||||
|
@router.message(Command("menu"))
|
||||||
|
async def cmd_menu(message: Message, config: Config) -> None:
|
||||||
|
await message.answer(
|
||||||
|
"📲 Панель быстрого доступа:",
|
||||||
|
reply_markup=manager_reply_kb(config.miniapp_url),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- /hide (убрать клавиатуру) ----------
|
||||||
|
|
||||||
|
@router.message(Command("hide"))
|
||||||
|
async def cmd_hide(message: Message) -> None:
|
||||||
|
await message.answer("Клавиатура скрыта. Вернуть — /menu", reply_markup=ReplyKeyboardRemove())
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Текстовые кнопки нижней клавиатуры ----------
|
||||||
|
|
||||||
|
@router.message(F.text == "ℹ️ Что умеет бот?")
|
||||||
|
async def kb_about(message: Message) -> None:
|
||||||
|
await message.answer(
|
||||||
|
"<b>ZOV Tech Picker — что умеет бот:</b>\n\n"
|
||||||
|
"🤖 <b>Подбор техники</b> — AI собирает 3-7 моделей под клиента, "
|
||||||
|
"со сравнением цен на 4 маркетплейсах, плюсами/минусами и ссылками\n\n"
|
||||||
|
"📐 <b>Замеры кухни</b> — мастер из 6 шагов: форма, размеры, окна/двери, фото, "
|
||||||
|
"сохраняется в карточку клиента\n\n"
|
||||||
|
"👥 <b>Клиенты</b> — история подборов и замеров по каждому клиенту, "
|
||||||
|
"с возможностью переоткрыть отчёт или скачать PDF\n\n"
|
||||||
|
"🏠 <b>Кабинет</b> — главный экран менеджера с задачами на сегодня\n\n"
|
||||||
|
"<i>Подсказка: внизу постоянная панель — открывает нужный экран одним тапом.</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "📞 Связь с куратором")
|
||||||
|
async def kb_contact(message: Message) -> None:
|
||||||
|
await message.answer(
|
||||||
|
"<b>Куратор сети:</b>\n\n"
|
||||||
|
"👤 Руслан Васильев\n"
|
||||||
|
"Telegram: @wasrusgen\n"
|
||||||
|
"Канал партнёрской сети: @wasrusgen1\n\n"
|
||||||
|
"Пишите по любым вопросам — от подключения к боту до сложных подборов техники."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text == "📋 Чек-лист встречи")
|
||||||
|
async def kb_checklist(message: Message) -> None:
|
||||||
|
await message.answer(
|
||||||
|
"<b>📋 Чек-лист встречи с клиентом</b>\n\n"
|
||||||
|
"<b>До встречи:</b>\n"
|
||||||
|
"• Получить контакт и согласовать время\n"
|
||||||
|
"• Уточнить — новая кухня или замена техники\n"
|
||||||
|
"• Понять бюджет (премиум / средний / эконом)\n\n"
|
||||||
|
"<b>На встрече:</b>\n"
|
||||||
|
"1. Замер кухни (📐 Новый замер в боте)\n"
|
||||||
|
" — стены, потолок, площадь\n"
|
||||||
|
" — окна, двери, вытяжка, газ/электро\n"
|
||||||
|
" — 5-10 фото со всех углов\n\n"
|
||||||
|
"2. Образ жизни клиента (что готовит, как часто)\n"
|
||||||
|
"3. Категории техники нужны (холодильник / варочная / духовка / посудомойка / вытяжка / СВЧ / кофемашина)\n"
|
||||||
|
"4. Запустить 🤖 Подбор техники — получить 3-7 моделей за 30 сек\n\n"
|
||||||
|
"<b>После встречи:</b>\n"
|
||||||
|
"• Скачать PDF подбора → отправить клиенту\n"
|
||||||
|
"• Поставить замер и подбор в карточку клиента\n"
|
||||||
|
"• Следующий шаг: дизайн-проект кухни ЗОВ"
|
||||||
|
)
|
||||||
|
|||||||
@ -359,6 +359,18 @@ async function init() {
|
|||||||
// Hash-роутер: позволяет открывать подэкраны (например подбор) напрямую
|
// Hash-роутер: позволяет открывать подэкраны (например подбор) напрямую
|
||||||
window.addEventListener("hashchange", routeByHash);
|
window.addEventListener("hashchange", routeByHash);
|
||||||
|
|
||||||
|
// ?go=podbor|clients|measure — бот может задать стартовый экран через query,
|
||||||
|
// потому что Telegram WebApp не передаёт hash через KeyboardButton.web_app.
|
||||||
|
const qp = new URLSearchParams(window.location.search);
|
||||||
|
const goScreen = qp.get("go");
|
||||||
|
if (goScreen && !location.hash) {
|
||||||
|
const map = { podbor: "#/podbor", clients: "#/clients", measure: "#/measure" };
|
||||||
|
if (map[goScreen]) {
|
||||||
|
// Меняем hash без триггера hashchange (init сам отрендерит правильный экран)
|
||||||
|
history.replaceState(null, "", location.pathname + location.search + map[goScreen]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const me = await fetchMe();
|
const me = await fetchMe();
|
||||||
window.__zovMe = me; // кешируем профиль для подэкранов
|
window.__zovMe = me; // кешируем профиль для подэкранов
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
<link rel="stylesheet" href="assets/styles.css?v=20260513a">
|
<link rel="stylesheet" href="assets/styles.css?v=20260513b">
|
||||||
<link rel="stylesheet" href="assets/podbor.css?v=20260513a">
|
<link rel="stylesheet" href="assets/podbor.css?v=20260513b">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="app">
|
<main id="app">
|
||||||
@ -21,12 +21,12 @@
|
|||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="assets/icons.js?v=20260513a"></script>
|
<script src="assets/icons.js?v=20260513b"></script>
|
||||||
<script src="assets/podbor.config.js?v=20260513a"></script>
|
<script src="assets/podbor.config.js?v=20260513b"></script>
|
||||||
<script src="assets/podbor.picts.js?v=20260513a"></script>
|
<script src="assets/podbor.picts.js?v=20260513b"></script>
|
||||||
<script src="assets/podbor.js?v=20260513a"></script>
|
<script src="assets/podbor.js?v=20260513b"></script>
|
||||||
<script src="assets/clients.js?v=20260513a"></script>
|
<script src="assets/clients.js?v=20260513b"></script>
|
||||||
<script src="assets/measurements.js?v=20260513a"></script>
|
<script src="assets/measurements.js?v=20260513b"></script>
|
||||||
<script src="assets/app.js?v=20260513a"></script>
|
<script src="assets/app.js?v=20260513b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user