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 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+