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 // Регистрируем пользователя если первый раз, обновляем last_seen_at
const startParam = body.startParam || auth.start_param; 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") { if (user.role === "manager") {
const m = getManagerProfile(tgId) || synthesizeManagerFromUser(user); const m = getManagerProfile(tgId) || synthesizeManagerFromUser(user);
@ -301,7 +302,7 @@ function findUser(tgId) {
return null; return null;
} }
function getOrCreateUser(tgUser, startParam) { function getOrCreateUser(tgUser, startParam, explicitRole) {
const tgId = tgUser.id; const tgId = tgUser.id;
const props = PropertiesService.getScriptProperties(); const props = PropertiesService.getScriptProperties();
const adminId = parseInt(props.getProperty("ADMIN_TG_ID") || "0", 10); const adminId = parseInt(props.getProperty("ADMIN_TG_ID") || "0", 10);
@ -309,22 +310,26 @@ function getOrCreateUser(tgUser, startParam) {
const existing = findUser(tgId); const existing = findUser(tgId);
if (existing) { if (existing) {
updateColumnByKey("Users", "tg_id", tgId, "last_seen_at", new Date()); updateColumnByKey("Users", "tg_id", tgId, "last_seen_at", new Date());
// Если это админ и роль ещё не manager — повышаем + автозавод в Managers // Админ всегда manager
if (tgId === adminId && existing.role !== "manager") { if (tgId === adminId && existing.role !== "manager") {
updateColumnByKey("Users", "tg_id", tgId, "role", "manager"); updateColumnByKey("Users", "tg_id", tgId, "role", "manager");
ensureAdminManager(tgUser); ensureAdminManager(tgUser);
existing.role = "manager"; 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; return existing;
} }
// Определяем роль: // Новый пользователь — определяем роль
// - админ → manager (автозавод в Managers как ZOV-employee)
// - invite-код менеджера → клиент с привязкой
// - иначе → client
let role = "client"; let role = "client";
let inviteCode = ""; let inviteCode = "";
if (tgId === adminId) { if (tgId === adminId) {
role = "manager"; role = "manager";
} else if (explicitRole) {
role = explicitRole;
} else if (startParam && startParam.indexOf("client_inv_") === 0) { } else if (startParam && startParam.indexOf("client_inv_") === 0) {
role = "client"; role = "client";
inviteCode = startParam; inviteCode = startParam;
@ -337,7 +342,7 @@ function getOrCreateUser(tgUser, startParam) {
if (tgId === adminId) { if (tgId === adminId) {
ensureAdminManager(tgUser); ensureAdminManager(tgUser);
} }
log("user_registered", tgId, { role, startParam }); log("user_registered", tgId, { role, startParam, explicitRole });
return findUser(tgId); 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.filters import CommandStart
from aiogram.types import ( from aiogram.types import (
Message, Message,
InlineKeyboardMarkup, InlineKeyboardMarkup,
InlineKeyboardButton, InlineKeyboardButton,
WebAppInfo, WebAppInfo,
CallbackQuery,
) )
from config import Config from config import Config
@ -13,25 +14,25 @@ from config import Config
router = Router(name="start") router = Router(name="start")
def role_choice_kb() -> InlineKeyboardMarkup: def _bust_cache(url: str) -> str:
return InlineKeyboardMarkup( """Append unique timestamp to MiniApp URL so Telegram WebView can't cache between sessions."""
inline_keyboard=[ sep = "&" if "?" in url else "?"
[ return f"{url}{sep}t={int(time.time())}"
InlineKeyboardButton(text="👤 Менеджер", callback_data="role:manager"),
InlineKeyboardButton(text="🏠 Клиент", callback_data="role:client"),
]
]
)
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( return InlineKeyboardMarkup(
inline_keyboard=[ inline_keyboard=[
[ [
InlineKeyboardButton( InlineKeyboardButton(
text="🚀 Открыть кабинет", text="👤 Менеджер",
web_app=WebAppInfo(url=miniapp_url), 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()) @router.message(CommandStart())
async def cmd_start(message: Message, config: Config) -> None: async def cmd_start(message: Message, config: Config) -> None:
# TODO: проверить, есть ли пользователь в БД (Google Sheet → users).
# Если есть → сразу показывать "Открыть кабинет".
# Если нет → спрашивать роль.
await message.answer( await message.answer(
"👋 Здравствуйте, я бот-помощник от Руслана ВАСИЛЬЕВА.\n\n" "👋 Здравствуйте, я бот-помощник от Руслана ВАСИЛЬЕВА.\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, // Заголовок Content-Type НЕ ставим — иначе браузер шлёт CORS preflight,
// который Apps Script не обрабатывает. Без заголовка fetch использует // который Apps Script не обрабатывает. Без заголовка fetch использует
// text/plain — Apps Script всё равно парсит body как JSON. // 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`, { const res = await fetch(`${BACKEND_URL}?path=me`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
initData: tg?.initData || "", initData: tg?.initData || "",
startParam: tg?.initDataUnsafe?.start_param || null, startParam: tg?.initDataUnsafe?.start_param || null,
role: explicitRole,
}), }),
}); });
if (!res.ok) throw new Error("backend HTTP " + res.status); 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="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=20260509h"> <link rel="stylesheet" href="assets/styles.css?v=20260509i">
</head> </head>
<body> <body>
<main id="app"> <main id="app">
@ -20,7 +20,7 @@
<div class="spinner"></div> <div class="spinner"></div>
</div> </div>
</main> </main>
<script src="assets/icons.js?v=20260509h"></script> <script src="assets/icons.js?v=20260509i"></script>
<script src="assets/app.js?v=20260509h"></script> <script src="assets/app.js?v=20260509i"></script>
</body> </body>
</html> </html>