mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 18:04:47 +00:00
roles: multi-role foundation (manager / client / measurer / assembler)
Users.role теперь хранит CSV-список ролей: 'manager,measurer'.
Парсим, добавляем, отзываем — все через sheets.parse_roles / grant_role /
revoke_role / list_users_with_role. Старые однострочные значения работают
как раньше (legacy compat).
Backend:
- /api/me возвращает roles[] (массив), role (главная для legacy-UI),
plus capabilities {measurer, assembler} для staff
- /api/grant_role (admin-only) — добавить/отозвать роль
- /api/staff_list (manager-only) — список сотрудников по роли
(будет использоваться в dropdown «выбрать замерщика»)
- При role=staff отдаём отдельный кабинет; если у юзера нет measurer/
assembler — возвращаем error=no_staff_role
Bot:
- /start — 3-я reply-кнопка [🔧 Я сотрудник]. При тапе MiniApp получает
?role=staff и решает кабинет по capabilities.
- /whoami — сотрудник присылает свой Telegram ID, пересылает куратору
чтобы тот выдал роль через /api/grant_role.
MiniApp:
- renderStaff() — заглушка кабинета сотрудника с шапкой (имя, аватар,
список ролей) и пустым inbox («Пока пусто»). Если есть measurer —
быстрая кнопка «Сделать новый замер».
- При error=no_staff_role — экран с инструкцией как получить роль.
- CSS .staff-head / .staff-no-role.
Cache bust v=20260513f.
This commit is contained in:
parent
6d57372b0b
commit
d859e9791c
@ -105,6 +105,8 @@ async def _dispatch_post(request: Request):
|
|||||||
"podbor": _handle_podbor,
|
"podbor": _handle_podbor,
|
||||||
"clients": _handle_clients,
|
"clients": _handle_clients,
|
||||||
"lead": _handle_lead,
|
"lead": _handle_lead,
|
||||||
|
"grant_role": _handle_grant_role,
|
||||||
|
"staff_list": _handle_staff_list,
|
||||||
"ping": lambda b: {"pong": True, "time": _now_iso()},
|
"ping": lambda b: {"pong": True, "time": _now_iso()},
|
||||||
"seed_admin": lambda b: _handle_seed_admin(),
|
"seed_admin": lambda b: _handle_seed_admin(),
|
||||||
"test_ai": lambda b: _handle_test_ai(),
|
"test_ai": lambda b: _handle_test_ai(),
|
||||||
@ -177,6 +179,20 @@ async def api_measurement_detail(request: Request):
|
|||||||
return _handle_measurement_detail(body)
|
return _handle_measurement_detail(body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/grant_role")
|
||||||
|
async def api_grant_role(request: Request):
|
||||||
|
"""Админ выдаёт роль другому пользователю.
|
||||||
|
body: {initData, target_tg_id, role: 'measurer'|'assembler'|'manager'|'client', action: 'grant'|'revoke'}"""
|
||||||
|
body = await _safe_json(request)
|
||||||
|
return _handle_grant_role(body)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/staff_list")
|
||||||
|
async def api_staff_list(request: Request):
|
||||||
|
body = await _safe_json(request)
|
||||||
|
return _handle_staff_list(body)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/photo/{measurement_id}/{filename}")
|
@app.get("/api/photo/{measurement_id}/{filename}")
|
||||||
async def api_photo(measurement_id: str, filename: str):
|
async def api_photo(measurement_id: str, filename: str):
|
||||||
"""Отдаёт фото замера. Защита от path traversal — только разрешённые id и имена."""
|
"""Отдаёт фото замера. Защита от path traversal — только разрешённые id и имена."""
|
||||||
@ -393,16 +409,48 @@ def _handle_me(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
tg_user = auth["user"]
|
tg_user = auth["user"]
|
||||||
tg_id = tg_user["id"]
|
tg_id = tg_user["id"]
|
||||||
start_param = body.get("startParam") or auth.get("start_param")
|
start_param = body.get("startParam") or auth.get("start_param")
|
||||||
explicit_role = body.get("role") if body.get("role") in ("manager", "client") else None
|
explicit_role = body.get("role") if body.get("role") in ("manager", "client", "staff") else None
|
||||||
user = sheets.get_or_create_user(tg_user, start_param, explicit_role)
|
user = sheets.get_or_create_user(tg_user, start_param, explicit_role)
|
||||||
|
roles = sheets.parse_roles(user.get("role", ""))
|
||||||
|
|
||||||
if user.get("role") == "manager":
|
# Staff (замерщик / сборщик) — отдельный кабинет, доступен только тем у кого роль выдана
|
||||||
|
if explicit_role == "staff":
|
||||||
|
has_measurer = "measurer" in roles
|
||||||
|
has_assembler = "assembler" in roles
|
||||||
|
if not (has_measurer or has_assembler):
|
||||||
|
return {
|
||||||
|
"role": "staff",
|
||||||
|
"roles": roles,
|
||||||
|
"error": "no_staff_role",
|
||||||
|
"user": {
|
||||||
|
"tg_id": tg_id,
|
||||||
|
"full_name": user.get("full_name", ""),
|
||||||
|
"avatar_initial": _initial(user.get("full_name") or tg_user.get("first_name", "")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
full_name = user.get("full_name", "") or tg_user.get("first_name", "")
|
||||||
|
return {
|
||||||
|
"role": "staff",
|
||||||
|
"roles": roles,
|
||||||
|
"user": {
|
||||||
|
"tg_id": tg_id,
|
||||||
|
"full_name": full_name,
|
||||||
|
"avatar_initial": _initial(full_name),
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"measurer": has_measurer,
|
||||||
|
"assembler": has_assembler,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if "manager" in roles:
|
||||||
m = sheets.get_manager_profile(tg_id) or {
|
m = sheets.get_manager_profile(tg_id) or {
|
||||||
"full_name": user.get("full_name", ""), "salon": "",
|
"full_name": user.get("full_name", ""), "salon": "",
|
||||||
"is_zov_employee": False, "status": "lapsed", "active_until": None,
|
"is_zov_employee": False, "status": "lapsed", "active_until": None,
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"role": "manager",
|
"role": "manager",
|
||||||
|
"roles": roles,
|
||||||
"user": {
|
"user": {
|
||||||
"tg_id": tg_id,
|
"tg_id": tg_id,
|
||||||
"full_name": m.get("full_name") or user.get("full_name", ""),
|
"full_name": m.get("full_name") or user.get("full_name", ""),
|
||||||
@ -429,6 +477,7 @@ def _handle_me(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
full_name = c.get("full_name") or user.get("full_name", "")
|
full_name = c.get("full_name") or user.get("full_name", "")
|
||||||
return {
|
return {
|
||||||
"role": "client",
|
"role": "client",
|
||||||
|
"roles": roles,
|
||||||
"user": {
|
"user": {
|
||||||
"tg_id": tg_id,
|
"tg_id": tg_id,
|
||||||
"full_name": full_name,
|
"full_name": full_name,
|
||||||
@ -764,6 +813,63 @@ def _handle_lead(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_grant_role(body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Только админ может выдавать/отзывать роли."""
|
||||||
|
cfg = get_config()
|
||||||
|
auth = verify_init_data(body.get("initData") or "", cfg.bot_token)
|
||||||
|
if not auth or not auth.get("user"):
|
||||||
|
return {"error": "invalid_init_data"}
|
||||||
|
tg_id = auth["user"]["id"]
|
||||||
|
if int(tg_id) != int(cfg.admin_tg_id):
|
||||||
|
return {"error": "admin_only"}
|
||||||
|
|
||||||
|
target = body.get("target_tg_id")
|
||||||
|
role = body.get("role", "").strip()
|
||||||
|
action = body.get("action", "grant")
|
||||||
|
if not target or not role:
|
||||||
|
return {"error": "missing_fields"}
|
||||||
|
try:
|
||||||
|
target_int = int(target)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return {"error": "bad_target"}
|
||||||
|
|
||||||
|
if role not in sheets.VALID_ROLES:
|
||||||
|
return {"error": "unknown_role", "valid": sorted(sheets.VALID_ROLES)}
|
||||||
|
|
||||||
|
if action == "revoke":
|
||||||
|
changed = sheets.revoke_role(target_int, role)
|
||||||
|
else:
|
||||||
|
changed = sheets.grant_role(target_int, role)
|
||||||
|
|
||||||
|
sheets.log_event("role_changed", tg_id, {"target": target_int, "role": role, "action": action, "changed": changed})
|
||||||
|
|
||||||
|
updated_user = sheets.find_user(target_int) or {}
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"target_tg_id": target_int,
|
||||||
|
"changed": changed,
|
||||||
|
"roles": sheets.parse_roles(updated_user.get("role", "")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_staff_list(body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Список сотрудников с указанной ролью — для dropdown «выбрать замерщика»."""
|
||||||
|
cfg = get_config()
|
||||||
|
auth = verify_init_data(body.get("initData") or "", cfg.bot_token)
|
||||||
|
if not auth or not auth.get("user"):
|
||||||
|
return {"error": "invalid_init_data"}
|
||||||
|
tg_id = auth["user"]["id"]
|
||||||
|
user = sheets.find_user(tg_id)
|
||||||
|
if not user or not sheets.has_role(user, "manager"):
|
||||||
|
return {"error": "only_manager"}
|
||||||
|
|
||||||
|
role = (body.get("role") or "").strip()
|
||||||
|
if role not in sheets.VALID_ROLES:
|
||||||
|
return {"error": "unknown_role"}
|
||||||
|
|
||||||
|
return {"ok": True, "role": role, "staff": sheets.list_users_with_role(role)}
|
||||||
|
|
||||||
|
|
||||||
def _handle_measurement_detail(body: dict[str, Any]) -> dict[str, Any]:
|
def _handle_measurement_detail(body: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Возвращает один замер целиком — для детальной страницы и печати."""
|
"""Возвращает один замер целиком — для детальной страницы и печати."""
|
||||||
cfg = get_config()
|
cfg = get_config()
|
||||||
|
|||||||
@ -108,7 +108,87 @@ def find_user(tg_id: int) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
full_name = (f"{row.get('first_name', '')} {row.get('last_name', '')}".strip()
|
full_name = (f"{row.get('first_name', '')} {row.get('last_name', '')}".strip()
|
||||||
or row.get("tg_username", ""))
|
or row.get("tg_username", ""))
|
||||||
return {**row, "full_name": full_name}
|
roles = parse_roles(row.get("role", ""))
|
||||||
|
return {**row, "full_name": full_name, "roles": roles}
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Multi-role helpers ----
|
||||||
|
|
||||||
|
VALID_ROLES = {"manager", "client", "measurer", "assembler"}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_roles(role_str: str) -> list[str]:
|
||||||
|
"""Парсит CSV-роли: 'manager,measurer' → ['manager', 'measurer'].
|
||||||
|
Старые однострочные значения тоже работают: 'manager' → ['manager']."""
|
||||||
|
if not role_str:
|
||||||
|
return []
|
||||||
|
parts = [p.strip() for p in str(role_str).split(",") if p.strip()]
|
||||||
|
return [p for p in parts if p in VALID_ROLES]
|
||||||
|
|
||||||
|
|
||||||
|
def has_role(user: dict[str, Any] | None, role: str) -> bool:
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
return role in parse_roles(user.get("role", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def primary_role(user: dict[str, Any] | None) -> str:
|
||||||
|
"""Первая (главная) роль для legacy-кода: manager > measurer > assembler > client."""
|
||||||
|
if not user:
|
||||||
|
return ""
|
||||||
|
roles = parse_roles(user.get("role", ""))
|
||||||
|
for r in ("manager", "measurer", "assembler", "client"):
|
||||||
|
if r in roles:
|
||||||
|
return r
|
||||||
|
return roles[0] if roles else ""
|
||||||
|
|
||||||
|
|
||||||
|
def grant_role(tg_id: int, role: str) -> bool:
|
||||||
|
"""Добавляет роль пользователю (если её ещё нет). Возвращает True если что-то изменилось."""
|
||||||
|
if role not in VALID_ROLES:
|
||||||
|
return False
|
||||||
|
user = find_user(tg_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
current = parse_roles(user.get("role", ""))
|
||||||
|
if role in current:
|
||||||
|
return False
|
||||||
|
current.append(role)
|
||||||
|
return update_cell_by_key("Users", "tg_id", tg_id, "role", ",".join(current))
|
||||||
|
|
||||||
|
|
||||||
|
def revoke_role(tg_id: int, role: str) -> bool:
|
||||||
|
user = find_user(tg_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
current = parse_roles(user.get("role", ""))
|
||||||
|
if role not in current:
|
||||||
|
return False
|
||||||
|
current.remove(role)
|
||||||
|
new_val = ",".join(current) if current else "client" # fallback роль
|
||||||
|
return update_cell_by_key("Users", "tg_id", tg_id, "role", new_val)
|
||||||
|
|
||||||
|
|
||||||
|
def list_users_with_role(role: str) -> list[dict[str, Any]]:
|
||||||
|
"""Все пользователи, у которых есть указанная роль (для dropdown «выбрать замерщика»)."""
|
||||||
|
s = sheet("Users")
|
||||||
|
rows = s.get_all_values()
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
headers = rows[0]
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for r in rows[1:]:
|
||||||
|
row = dict(zip(headers, r + [""] * (len(headers) - len(r))))
|
||||||
|
if role in parse_roles(row.get("role", "")):
|
||||||
|
full_name = (f"{row.get('first_name', '')} {row.get('last_name', '')}".strip()
|
||||||
|
or row.get("tg_username", ""))
|
||||||
|
out.append({
|
||||||
|
"tg_id": row.get("tg_id"),
|
||||||
|
"full_name": full_name,
|
||||||
|
"tg_username": row.get("tg_username", ""),
|
||||||
|
"roles": parse_roles(row.get("role", "")),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_user(tg_user: dict[str, Any], start_param: str | None,
|
def get_or_create_user(tg_user: dict[str, Any], start_param: str | None,
|
||||||
@ -122,14 +202,18 @@ def get_or_create_user(tg_user: dict[str, Any], start_param: str | None,
|
|||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
update_cell_by_key("Users", "tg_id", tg_id, "last_seen_at", now_str)
|
update_cell_by_key("Users", "tg_id", tg_id, "last_seen_at", now_str)
|
||||||
# Админ всегда manager
|
# Админ всегда имеет роль manager (могут быть и другие)
|
||||||
if tg_id == admin_id and existing.get("role") != "manager":
|
if tg_id == admin_id and not has_role(existing, "manager"):
|
||||||
update_cell_by_key("Users", "tg_id", tg_id, "role", "manager")
|
grant_role(tg_id, "manager")
|
||||||
ensure_admin_manager(tg_user)
|
ensure_admin_manager(tg_user)
|
||||||
existing["role"] = "manager"
|
existing["roles"] = parse_roles((find_user(tg_id) or {}).get("role", ""))
|
||||||
elif explicit_role and tg_id != admin_id and existing.get("role") != explicit_role:
|
# explicit_role из query (?role=manager|client|staff) — не перетираем уже выданные роли,
|
||||||
update_cell_by_key("Users", "tg_id", tg_id, "role", explicit_role)
|
# только добавляем если человек впервые открыл эту секцию
|
||||||
existing["role"] = explicit_role
|
elif explicit_role and explicit_role in VALID_ROLES and not has_role(existing, explicit_role):
|
||||||
|
# client/manager — стандартные роли любой может получить через выбор в боте
|
||||||
|
if explicit_role in ("manager", "client"):
|
||||||
|
grant_role(tg_id, explicit_role)
|
||||||
|
existing["roles"] = parse_roles((find_user(tg_id) or {}).get("role", ""))
|
||||||
return existing
|
return existing
|
||||||
|
|
||||||
# Новый пользователь
|
# Новый пользователь
|
||||||
@ -148,7 +232,7 @@ def get_or_create_user(tg_user: dict[str, Any], start_param: str | None,
|
|||||||
tg_user.get("username", ""),
|
tg_user.get("username", ""),
|
||||||
tg_user.get("first_name", ""),
|
tg_user.get("first_name", ""),
|
||||||
tg_user.get("last_name", ""),
|
tg_user.get("last_name", ""),
|
||||||
role,
|
role, # хранится как CSV; для новых = одна роль
|
||||||
now_str,
|
now_str,
|
||||||
now_str,
|
now_str,
|
||||||
invite_code,
|
invite_code,
|
||||||
|
|||||||
@ -36,7 +36,7 @@ def _wapp(miniapp_url: str, role: str) -> WebAppInfo:
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Reply keyboard — выбор роли. Оба кнопки сразу открывают MiniApp.
|
# Reply keyboard — выбор роли. Три кнопки, все WebApp.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def role_choice_kb(miniapp_url: str) -> ReplyKeyboardMarkup:
|
def role_choice_kb(miniapp_url: str) -> ReplyKeyboardMarkup:
|
||||||
@ -46,6 +46,9 @@ def role_choice_kb(miniapp_url: str) -> ReplyKeyboardMarkup:
|
|||||||
KeyboardButton(text="👤 Я менеджер", web_app=_wapp(miniapp_url, "manager")),
|
KeyboardButton(text="👤 Я менеджер", web_app=_wapp(miniapp_url, "manager")),
|
||||||
KeyboardButton(text="🏠 Я клиент", web_app=_wapp(miniapp_url, "client")),
|
KeyboardButton(text="🏠 Я клиент", web_app=_wapp(miniapp_url, "client")),
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
KeyboardButton(text="🔧 Я сотрудник", web_app=_wapp(miniapp_url, "staff")),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
resize_keyboard=True,
|
resize_keyboard=True,
|
||||||
is_persistent=True,
|
is_persistent=True,
|
||||||
@ -61,7 +64,8 @@ def role_choice_kb(miniapp_url: str) -> ReplyKeyboardMarkup:
|
|||||||
async def cmd_start(message: Message, config: Config) -> None:
|
async def cmd_start(message: Message, config: Config) -> None:
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"👋 Здравствуйте, я бот-помощник от Руслана ВАСИЛЬЕВА.\n\n"
|
"👋 Здравствуйте, я бот-помощник от Руслана ВАСИЛЬЕВА.\n\n"
|
||||||
"Выберите, кто вы — кабинет откроется одним тапом.",
|
"Выберите, кто вы — кабинет откроется одним тапом.\n\n"
|
||||||
|
"<i>«Сотрудник» — для замерщиков и сборщиков ЗОВ. Если вы менеджер или клиент — выбирайте свою роль.</i>",
|
||||||
reply_markup=role_choice_kb(config.miniapp_url),
|
reply_markup=role_choice_kb(config.miniapp_url),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -74,3 +78,21 @@ async def cmd_menu(message: Message, config: Config) -> None:
|
|||||||
@router.message(Command("hide"))
|
@router.message(Command("hide"))
|
||||||
async def cmd_hide(message: Message) -> None:
|
async def cmd_hide(message: Message) -> None:
|
||||||
await message.answer("Клавиатура скрыта. Вернуть — /menu", reply_markup=ReplyKeyboardRemove())
|
await message.answer("Клавиатура скрыта. Вернуть — /menu", reply_markup=ReplyKeyboardRemove())
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# /whoami — сотрудник присылает свой ID куратору, чтобы тот выдал роль
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@router.message(Command("whoami"))
|
||||||
|
async def cmd_whoami(message: Message) -> None:
|
||||||
|
user = message.from_user
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
await message.answer(
|
||||||
|
f"<b>Ваш Telegram ID:</b> <code>{user.id}</code>\n"
|
||||||
|
f"Username: @{user.username or '—'}\n"
|
||||||
|
f"Имя: {user.first_name or ''} {user.last_name or ''}".strip()
|
||||||
|
+ "\n\n"
|
||||||
|
"<i>Перешлите это сообщение куратору @wasrusgen чтобы вам выдали роль замерщика/сборщика.</i>"
|
||||||
|
)
|
||||||
|
|||||||
@ -343,6 +343,72 @@ function buildMenu(items) {
|
|||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------- Staff (замерщик / сборщик) ----------------- */
|
||||||
|
function renderStaff(me) {
|
||||||
|
app.innerHTML = "";
|
||||||
|
|
||||||
|
if (me.error === "no_staff_role") {
|
||||||
|
app.appendChild(el(`
|
||||||
|
<div class="staff-no-role">
|
||||||
|
<div class="staff-no-role-ico">🔒</div>
|
||||||
|
<h2 class="display-title">У вас нет<br><span class="accent">прав сотрудника</span></h2>
|
||||||
|
<p class="lede">Чтобы получить роль замерщика или сборщика — отправьте куратору ваш Telegram ID.</p>
|
||||||
|
<div class="block">
|
||||||
|
<div class="kv"><span>Ваш ID</span><strong><code>${me.user?.tg_id || "—"}</code></strong></div>
|
||||||
|
<div class="kv"><span>Имя</span><strong>${me.user?.full_name || "—"}</strong></div>
|
||||||
|
</div>
|
||||||
|
<p class="muted" style="text-align:center;margin-top:16px;">
|
||||||
|
В боте отправьте <code>/whoami</code> и перешлите ответ
|
||||||
|
<a href="https://t.me/wasrusgen" target="_blank">@wasrusgen</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const caps = me.capabilities || {};
|
||||||
|
const labels = [];
|
||||||
|
if (caps.measurer) labels.push("замерщик");
|
||||||
|
if (caps.assembler) labels.push("сборщик");
|
||||||
|
const subtitle = labels.length ? labels.join(" · ") : "сотрудник";
|
||||||
|
|
||||||
|
app.appendChild(el(`
|
||||||
|
<div class="staff-head">
|
||||||
|
<div class="staff-avatar">${me.user?.avatar_initial || "?"}</div>
|
||||||
|
<div>
|
||||||
|
<div class="kicker">${subtitle}</div>
|
||||||
|
<h2 class="display-title">${me.user?.full_name || "Сотрудник"}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`));
|
||||||
|
|
||||||
|
// Заглушка — реальный инбокс заявок будет в следующем коммите
|
||||||
|
const inbox = el(`
|
||||||
|
<section class="block">
|
||||||
|
<div class="block-head">📥 Входящие заявки</div>
|
||||||
|
<div class="empty" style="padding:24px 12px;text-align:center;color:var(--muted);">
|
||||||
|
Пока пусто — менеджеры ещё не назначили вам заявки.<br>
|
||||||
|
Здесь появятся ${caps.measurer ? "замеры" : ""}${caps.measurer && caps.assembler ? " и " : ""}${caps.assembler ? "сборки" : ""}.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`);
|
||||||
|
app.appendChild(inbox);
|
||||||
|
|
||||||
|
// Если у сотрудника также есть роль measurer — показываем быструю кнопку «Сделать замер»
|
||||||
|
if (caps.measurer) {
|
||||||
|
const quick = el(`
|
||||||
|
<div class="podbor-cta-row" style="margin-top:16px;">
|
||||||
|
<button class="btn-primary" id="newMeasure">📐 Сделать новый замер</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
quick.querySelector("#newMeasure").addEventListener("click", () => {
|
||||||
|
haptic && haptic("impact");
|
||||||
|
location.hash = "#/measure";
|
||||||
|
});
|
||||||
|
app.appendChild(quick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderError() {
|
function renderError() {
|
||||||
app.innerHTML = "";
|
app.innerHTML = "";
|
||||||
app.appendChild(el(`
|
app.appendChild(el(`
|
||||||
@ -403,8 +469,13 @@ async function init() {
|
|||||||
hideSplash();
|
hideSplash();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (me.role === "manager") renderManager(me);
|
if (me.role === "staff") {
|
||||||
else renderClient(me);
|
renderStaff(me);
|
||||||
|
} else if (me.role === "manager") {
|
||||||
|
renderManager(me);
|
||||||
|
} else {
|
||||||
|
renderClient(me);
|
||||||
|
}
|
||||||
hideSplash();
|
hideSplash();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -424,7 +495,8 @@ function routeByHash() {
|
|||||||
// Главный экран по роли
|
// Главный экран по роли
|
||||||
const me = window.__zovMe;
|
const me = window.__zovMe;
|
||||||
if (!me) { init(); return; }
|
if (!me) { init(); return; }
|
||||||
if (me.role === "manager") renderManager(me);
|
if (me.role === "staff") renderStaff(me);
|
||||||
|
else if (me.role === "manager") renderManager(me);
|
||||||
else renderClient(me);
|
else renderClient(me);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1989,6 +1989,46 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Кабинет сотрудника (замерщик/сборщик) ===== */
|
||||||
|
.staff-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin: 16px 4px 22px;
|
||||||
|
}
|
||||||
|
.staff-avatar {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--walnut, #6B4A2B);
|
||||||
|
color: var(--paper, #FBF7F0);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 22px;
|
||||||
|
font-family: var(--font-display, "Newsreader", serif);
|
||||||
|
}
|
||||||
|
.staff-no-role {
|
||||||
|
padding: 32px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.staff-no-role-ico {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.staff-no-role .block {
|
||||||
|
margin-top: 18px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.staff-no-role code {
|
||||||
|
background: var(--warm, rgba(107, 74, 43, 0.08));
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font-mono, "JetBrains Mono", monospace);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Фото замера ===== */
|
/* ===== Фото замера ===== */
|
||||||
.photo-uploader { margin: 12px 0 14px; }
|
.photo-uploader { margin: 12px 0 14px; }
|
||||||
|
|
||||||
|
|||||||
@ -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=20260513e">
|
<link rel="stylesheet" href="assets/styles.css?v=20260513f">
|
||||||
<link rel="stylesheet" href="assets/podbor.css?v=20260513e">
|
<link rel="stylesheet" href="assets/podbor.css?v=20260513f">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Splash — за пределами #app, render-функции его не смывают -->
|
<!-- Splash — за пределами #app, render-функции его не смывают -->
|
||||||
@ -34,12 +34,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main id="app"></main>
|
<main id="app"></main>
|
||||||
<script src="assets/icons.js?v=20260513e"></script>
|
<script src="assets/icons.js?v=20260513f"></script>
|
||||||
<script src="assets/podbor.config.js?v=20260513e"></script>
|
<script src="assets/podbor.config.js?v=20260513f"></script>
|
||||||
<script src="assets/podbor.picts.js?v=20260513e"></script>
|
<script src="assets/podbor.picts.js?v=20260513f"></script>
|
||||||
<script src="assets/podbor.js?v=20260513e"></script>
|
<script src="assets/podbor.js?v=20260513f"></script>
|
||||||
<script src="assets/clients.js?v=20260513e"></script>
|
<script src="assets/clients.js?v=20260513f"></script>
|
||||||
<script src="assets/measurements.js?v=20260513e"></script>
|
<script src="assets/measurements.js?v=20260513f"></script>
|
||||||
<script src="assets/app.js?v=20260513e"></script>
|
<script src="assets/app.js?v=20260513f"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user