feat: one-tap role buttons (WebApp directly, no intermediate step) + role param in URL/backend

This commit is contained in:
wasrusgen 2026-05-09 13:05:20 +03:00
parent 017d179746
commit af7dc07720
4 changed files with 38 additions and 44 deletions

View File

@ -147,7 +147,8 @@ function handleMe(body) {
// Регистрируем пользователя если первый раз, обновляем last_seen_at
const startParam = body.startParam || auth.start_param;
const user = getOrCreateUser(auth.user, startParam);
const explicitRole = (body.role === "manager" || body.role === "client") ? body.role : null;
const user = getOrCreateUser(auth.user, startParam, explicitRole);
if (user.role === "manager") {
const m = getManagerProfile(tgId) || synthesizeManagerFromUser(user);
@ -301,7 +302,7 @@ function findUser(tgId) {
return null;
}
function getOrCreateUser(tgUser, startParam) {
function getOrCreateUser(tgUser, startParam, explicitRole) {
const tgId = tgUser.id;
const props = PropertiesService.getScriptProperties();
const adminId = parseInt(props.getProperty("ADMIN_TG_ID") || "0", 10);
@ -309,22 +310,26 @@ function getOrCreateUser(tgUser, startParam) {
const existing = findUser(tgId);
if (existing) {
updateColumnByKey("Users", "tg_id", tgId, "last_seen_at", new Date());
// Если это админ и роль ещё не manager — повышаем + автозавод в Managers
// Админ всегда manager
if (tgId === adminId && existing.role !== "manager") {
updateColumnByKey("Users", "tg_id", tgId, "role", "manager");
ensureAdminManager(tgUser);
existing.role = "manager";
}
// Не-админ может явно сменить роль через URL ?role=
else if (explicitRole && tgId !== adminId && existing.role !== explicitRole) {
updateColumnByKey("Users", "tg_id", tgId, "role", explicitRole);
existing.role = explicitRole;
}
return existing;
}
// Определяем роль:
// - админ → manager (автозавод в Managers как ZOV-employee)
// - invite-код менеджера → клиент с привязкой
// - иначе → client
// Новый пользователь — определяем роль
let role = "client";
let inviteCode = "";
if (tgId === adminId) {
role = "manager";
} else if (explicitRole) {
role = explicitRole;
} else if (startParam && startParam.indexOf("client_inv_") === 0) {
role = "client";
inviteCode = startParam;
@ -337,7 +342,7 @@ function getOrCreateUser(tgUser, startParam) {
if (tgId === adminId) {
ensureAdminManager(tgUser);
}
log("user_registered", tgId, { role, startParam });
log("user_registered", tgId, { role, startParam, explicitRole });
return findUser(tgId);
}

View File

@ -1,11 +1,12 @@
from aiogram import Router, F
import time
from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import (
Message,
InlineKeyboardMarkup,
InlineKeyboardButton,
WebAppInfo,
CallbackQuery,
)
from config import Config
@ -13,25 +14,25 @@ from config import Config
router = Router(name="start")
def role_choice_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="👤 Менеджер", callback_data="role:manager"),
InlineKeyboardButton(text="🏠 Клиент", callback_data="role:client"),
]
]
)
def _bust_cache(url: str) -> str:
"""Append unique timestamp to MiniApp URL so Telegram WebView can't cache between sessions."""
sep = "&" if "?" in url else "?"
return f"{url}{sep}t={int(time.time())}"
def open_app_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."""
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="🚀 Открыть кабинет",
web_app=WebAppInfo(url=miniapp_url),
)
text="👤 Менеджер",
web_app=WebAppInfo(url=_bust_cache(f"{miniapp_url}?role=manager")),
),
InlineKeyboardButton(
text="🏠 Клиент",
web_app=WebAppInfo(url=_bust_cache(f"{miniapp_url}?role=client")),
),
]
]
)
@ -39,25 +40,8 @@ def open_app_kb(miniapp_url: str) -> InlineKeyboardMarkup:
@router.message(CommandStart())
async def cmd_start(message: Message, config: Config) -> None:
# TODO: проверить, есть ли пользователь в БД (Google Sheet → users).
# Если есть → сразу показывать "Открыть кабинет".
# Если нет → спрашивать роль.
await message.answer(
"👋 Здравствуйте, я бот-помощник от Руслана ВАСИЛЬЕВА.\n\n"
"Кто вы?",
reply_markup=role_choice_kb(),
reply_markup=role_choice_kb(config.miniapp_url),
)
@router.callback_query(F.data.startswith("role:"))
async def on_role_chosen(callback: CallbackQuery, config: Config) -> None:
role = callback.data.split(":", 1)[1]
# TODO: сохранить роль в БД (Google Sheet → users)
text = {
"manager": "Отлично, открываю кабинет менеджера 👇",
"client": "Спасибо! Открываю ваш кабинет 👇",
}.get(role, "Открываю кабинет 👇")
await callback.message.edit_text(text, reply_markup=open_app_kb(config.miniapp_url))
await callback.answer()

View File

@ -53,11 +53,16 @@ async function fetchMe() {
// Заголовок Content-Type НЕ ставим — иначе браузер шлёт CORS preflight,
// который Apps Script не обрабатывает. Без заголовка fetch использует
// text/plain — Apps Script всё равно парсит body как JSON.
// Роль приходит в URL (?role=manager|client) — её бот подставляет в WebApp-кнопку
const urlParams = new URLSearchParams(window.location.search);
const explicitRole = urlParams.get("role");
const res = await fetch(`${BACKEND_URL}?path=me`, {
method: "POST",
body: JSON.stringify({
initData: tg?.initData || "",
startParam: tg?.initDataUnsafe?.start_param || null,
role: explicitRole,
}),
});
if (!res.ok) throw new Error("backend HTTP " + res.status);

View File

@ -12,7 +12,7 @@
<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">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css?v=20260509h">
<link rel="stylesheet" href="assets/styles.css?v=20260509i">
</head>
<body>
<main id="app">
@ -20,7 +20,7 @@
<div class="spinner"></div>
</div>
</main>
<script src="assets/icons.js?v=20260509h"></script>
<script src="assets/app.js?v=20260509h"></script>
<script src="assets/icons.js?v=20260509i"></script>
<script src="assets/app.js?v=20260509i"></script>
</body>
</html>