diff --git a/bot/main.py b/bot/main.py index cba9e1c..540530e 100644 --- a/bot/main.py +++ b/bot/main.py @@ -4,6 +4,7 @@ import logging from aiogram import Bot, Dispatcher from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode +from aiogram.types import MenuButtonWebApp, WebAppInfo from config import load_config from handlers import start @@ -28,6 +29,19 @@ async def main() -> None: if config.use_webhook: raise NotImplementedError("Webhook mode будет добавлен после MVP") + # Универсальная меню-кнопка — открывает MiniApp одним тапом. + # Внутри MiniApp пользователь выбирает роль (менеджер/клиент/сотрудник). + try: + await bot.set_chat_menu_button( + menu_button=MenuButtonWebApp( + text="ЗОВ", + web_app=WebAppInfo(url=config.miniapp_url), + ), + ) + logging.info("Установлена меню-кнопка MiniApp: %s", config.miniapp_url) + except Exception as e: + logging.warning("Не удалось установить меню-кнопку: %s", e) + logging.info("Запуск в режиме polling") await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js index 2c595b3..d9c82b6 100644 --- a/miniapp/assets/app.js +++ b/miniapp/assets/app.js @@ -346,6 +346,70 @@ function buildMenu(items) { return menu; } +/* ----------------- Role chooser — первый экран MiniApp ----------------- */ +function renderRoleChooser() { + app.innerHTML = ""; + document.body.classList.remove("has-bottom-nav"); + const oldNav = document.getElementById("bottom-nav"); + if (oldNav) oldNav.remove(); + + app.appendChild(el(` +
+
+
ЗОВ — кухня и техника
+

Кто вы?

+

Выберите роль — кабинет откроется одним тапом.

+
+
+ + + +
+

+ Свой выбор можно изменить позже в профиле. +

+
+ `)); + app.querySelectorAll(".role-card").forEach(card => { + card.addEventListener("click", () => { + const role = card.dataset.role; + haptic && haptic("impact"); + // Меняем URL и перезапускаем init() — fetchMe пойдёт с правильной ролью + const qp = new URLSearchParams(window.location.search); + qp.set("role", role); + history.replaceState(null, "", `?${qp.toString()}${location.hash || ""}`); + // Показываем splash снова — на время загрузки + const splashEl = document.createElement("div"); + splashEl.id = "splash"; + splashEl.className = "loader splash"; + splashEl.innerHTML = `
Открываем кабинет
`; + document.body.appendChild(splashEl); + init(); + }); + }); +} + /* ----------------- Staff (замерщик / сборщик) ----------------- */ async function renderStaff(me) { app.innerHTML = ""; @@ -658,11 +722,8 @@ function hideSplash() { async function init() { setupTelegram(); - // Hash-роутер: позволяет открывать подэкраны (например подбор) напрямую window.addEventListener("hashchange", routeByHash); - // ?go=podbor|clients|measure|request — бот может задать стартовый экран через query, - // потому что Telegram WebApp не передаёт hash через KeyboardButton.web_app. const qp = new URLSearchParams(window.location.search); const goScreen = qp.get("go"); if (goScreen && !location.hash) { @@ -673,11 +734,18 @@ async function init() { request: "#/request", }; if (map[goScreen]) { - // Меняем hash без триггера hashchange (init сам отрендерит правильный экран) history.replaceState(null, "", location.pathname + location.search + map[goScreen]); } } + // Если нет ?role= в URL — показываем выбор роли (универсально для всех клиентов) + const explicitRole = qp.get("role"); + if (!explicitRole && !location.hash) { + renderRoleChooser(); + hideSplash(); + return; + } + try { const me = await fetchMe(); window.__zovMe = me; // кешируем профиль для подэкранов diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index 87276d6..a814928 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -1989,6 +1989,91 @@ overflow-x: auto; } +/* ===== Role chooser — первый экран ===== */ +.role-chooser { + padding: 32px 18px; + max-width: 480px; + margin: 0 auto; + min-height: calc(100vh - 60px); + display: flex; + flex-direction: column; + justify-content: center; +} +.role-chooser-head { + text-align: center; + margin-bottom: 28px; +} +.role-chooser-head .kicker { + font-family: var(--font-mono, "JetBrains Mono", monospace); + font-size: 10.5px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--muted, #998877); + margin-bottom: 12px; +} +.role-chooser-head .display-title { + font-size: 36px; + margin: 0 0 12px; +} +.role-chooser-head .lede { + font-size: 14px; + color: var(--ink-2, #6B5C4A); + margin: 0; +} +.role-cards { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; +} +.role-card { + display: flex; + align-items: center; + gap: 14px; + padding: 18px 18px; + background: var(--card, #fff); + border: 1px solid var(--line-strong, rgba(15, 15, 14, 0.16)); + border-radius: 16px; + cursor: pointer; + transition: background 0.15s, transform 0.1s; + text-align: left; + width: 100%; + font-family: inherit; +} +.role-card:active { + background: var(--paper-2, #F5EDDC); + transform: scale(0.985); +} +.role-card .role-icon { + font-size: 32px; + width: 52px; + height: 52px; + display: grid; + place-items: center; + background: var(--warm, rgba(107, 74, 43, 0.08)); + border-radius: 14px; + flex-shrink: 0; +} +.role-card .role-text { flex: 1; min-width: 0; } +.role-card .role-title { + font-family: var(--font-display, "Newsreader", serif); + font-style: italic; + font-size: 20px; + color: var(--ink, #1F1A14); + margin-bottom: 2px; +} +.role-card .role-sub { + font-size: 12.5px; + color: var(--muted, #998877); + font-family: var(--font-mono, "JetBrains Mono", monospace); + letter-spacing: 0.02em; +} +.role-card .role-arrow { + color: var(--muted, #998877); + font-size: 22px; + flex-shrink: 0; +} + /* ===== Замер: фото с тегами ===== */ .podbor-header .podbor-help { background: transparent; diff --git a/miniapp/index.html b/miniapp/index.html index a56f9f8..ae264c3 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + + @@ -34,13 +34,13 @@
- - - - - - - - + + + + + + + +