feat: экран #/me — Мой профиль для всех ролей

manager: статус доступа, салон, быстрые переходы
staff: роли-чипы, кнопки на замеры/сборки
client: менеджер, кнопки подбора и сборок

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-05-18 12:17:52 +03:00
parent be79f5447b
commit bdbdf4b259
3 changed files with 219 additions and 1 deletions

View File

@ -1746,6 +1746,9 @@ function routeByHash() {
} else if (location.hash.startsWith("#/master")) {
const me = window.__zovMe;
if (me) renderStaff(me); else init();
} else if (location.hash.startsWith("#/me")) {
if (typeof MeScreen !== "undefined") MeScreen.mount(app);
else init();
} else if (location.hash.startsWith("#/c/proposal")) {
app.innerHTML = "";
document.body.classList.remove("has-bottom-nav");

214
miniapp/assets/me.js Normal file
View File

@ -0,0 +1,214 @@
/* ============================================================
Экран «Мой профиль» #/me
Работает для всех ролей: manager, staff, client
============================================================ */
const MeScreen = (function () {
function escHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
async function _fetchWithTimeout(url, body, ms = 15000) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try {
const res = await fetch(url, { method: "POST", signal: ctrl.signal, body: JSON.stringify(body) });
return await res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Сервер не отвечает");
throw e;
} finally { clearTimeout(t); }
}
function header(container) {
const h = document.createElement("header");
h.className = "podbor-header";
h.innerHTML = `
<button class="podbor-back" aria-label="Назад">${(window.ICONS || {}).arrow_left || ""}</button>
<div class="podbor-title">Мой профиль</div>
<div style="width:36px"></div>
`;
h.querySelector(".podbor-back").addEventListener("click", () => {
haptic && haptic("impact");
history.back();
});
container.appendChild(h);
}
function avatarBlock(initial, name, subtitle) {
return `
<div style="display:flex;align-items:center;gap:14px;padding:20px 16px 8px">
<div style="width:52px;height:52px;border-radius:50%;background:var(--accent);
display:flex;align-items:center;justify-content:center;
font-size:22px;font-weight:700;color:#fff;flex-shrink:0;">
${escHtml(initial)}
</div>
<div>
<div style="font-weight:600;font-size:16px;color:var(--ink);">${escHtml(name)}</div>
${subtitle ? `<div style="font-size:12px;color:var(--muted);margin-top:2px;">${escHtml(subtitle)}</div>` : ""}
</div>
</div>
`;
}
function roleChip(label, color) {
const colors = { gold: "#C5A55E", blue: "#3D7AB5", green: "#4A9E6A", muted: "var(--muted)" };
const c = colors[color] || colors.gold;
return `<span style="display:inline-block;padding:3px 10px;border-radius:20px;
font-size:11px;font-weight:600;letter-spacing:.04em;
background:${c}22;color:${c};margin-right:6px;margin-bottom:4px;">
${escHtml(label)}
</span>`;
}
function renderManager(container, me) {
const u = me.user || {};
const statusLabel = me.status === "active" ? "✅ Активен" :
me.status === "trial" ? "🟡 Пробный" : "🔴 Неактивен";
const statusColor = me.status === "active" ? "green" :
me.status === "trial" ? "gold" : "muted";
const screen = document.createElement("div");
screen.className = "podbor-screen";
screen.innerHTML = `
${avatarBlock(u.avatar_initial || "?", u.full_name || "Менеджер", u.salon || "")}
<div class="block" style="margin:12px 16px 0;">
<div class="block-head">Статус доступа</div>
<div class="kv">
<span>Статус</span>
<strong>${roleChip(statusLabel, statusColor)}</strong>
</div>
${me.status_until ? `<div class="kv"><span>Активен до</span><strong>${escHtml(me.status_until)}</strong></div>` : ""}
${u.salon ? `<div class="kv"><span>Салон</span><strong>${escHtml(u.salon)}</strong></div>` : ""}
</div>
<div class="block" style="margin:12px 16px 0;">
<div class="block-head">Быстрый переход</div>
<div class="podbor-cta-row" style="flex-wrap:wrap;gap:8px;padding-top:4px;">
<button class="btn-secondary" data-href="#/clients">👥 Клиенты</button>
<button class="btn-secondary" data-href="#/measurements">📐 Замеры</button>
<button class="btn-secondary" data-href="#/assembly">🔨 Сборки</button>
<button class="btn-secondary" data-href="#/request">📋 Заявка</button>
</div>
</div>
`;
screen.querySelectorAll("[data-href]").forEach(btn => {
btn.addEventListener("click", () => {
haptic && haptic("impact");
location.hash = btn.dataset.href;
});
});
container.appendChild(screen);
}
function renderStaffMe(container, me) {
const u = me.user || {};
const caps = me.capabilities || {};
const chips = [
caps.measurer && roleChip("замерщик", "blue"),
caps.assembler && roleChip("сборщик", "green"),
].filter(Boolean).join("");
const screen = document.createElement("div");
screen.className = "podbor-screen";
screen.innerHTML = `
${avatarBlock(u.avatar_initial || "?", u.full_name || "Сотрудник", "")}
<div style="padding:4px 16px 12px;">${chips}</div>
<div class="block" style="margin:0 16px;">
<div class="block-head">Мои задачи</div>
<div class="podbor-cta-row" style="flex-wrap:wrap;gap:8px;padding-top:4px;">
${caps.measurer ? `<button class="btn-secondary" data-href="#/master">📥 Входящие заявки</button>` : ""}
${caps.measurer ? `<button class="btn-secondary" data-href="#/measure">📐 Новый замер</button>` : ""}
${caps.assembler ? `<button class="btn-secondary" data-href="#/assembly">🔨 Мои сборки</button>` : ""}
</div>
</div>
`;
screen.querySelectorAll("[data-href]").forEach(btn => {
btn.addEventListener("click", () => {
haptic && haptic("impact");
location.hash = btn.dataset.href;
});
});
container.appendChild(screen);
}
function renderClientMe(container, me) {
const u = me.user || {};
const mgr = me.manager || {};
const screen = document.createElement("div");
screen.className = "podbor-screen";
screen.innerHTML = `
${avatarBlock(u.avatar_initial || "?", u.full_name || "Клиент", "Личный кабинет")}
${mgr.full_name ? `
<div class="block" style="margin:12px 16px 0;">
<div class="block-head">Мой менеджер</div>
<div class="kv"><span>Имя</span><strong>${escHtml(mgr.full_name)}</strong></div>
${mgr.salon ? `<div class="kv"><span>Салон</span><strong>${escHtml(mgr.salon)}</strong></div>` : ""}
</div>
` : ""}
<div class="block" style="margin:12px 16px 0;">
<div class="podbor-cta-row" style="flex-wrap:wrap;gap:8px;padding-top:4px;">
<button class="btn-primary" data-href="#/picker">🛒 Подбор техники</button>
<button class="btn-secondary" data-href="#/assembly">📦 Мои сборки</button>
</div>
</div>
`;
screen.querySelectorAll("[data-href]").forEach(btn => {
btn.addEventListener("click", () => {
haptic && haptic("impact");
location.hash = btn.dataset.href;
});
});
container.appendChild(screen);
}
async function mount(container) {
container.innerHTML = "";
document.body.classList.remove("has-bottom-nav");
const oldNav = document.getElementById("bottom-nav");
if (oldNav) oldNav.remove();
header(container);
const loading = document.createElement("div");
loading.className = "loader-inline";
loading.innerHTML = `<div class="spinner"></div>`;
container.appendChild(loading);
try {
const me = await _fetchWithTimeout(`${BACKEND_URL}/api/me`, {
initData: tg?.initData || "",
initDataUnsafe: tg?.initDataUnsafe || null,
});
loading.remove();
if (me.error) {
container.appendChild(el(`<div class="error" style="margin:16px;">${escHtml(me.error)}</div>`));
return;
}
const role = me.role;
if (role === "manager" || me.roles?.includes("manager")) {
renderManager(container, me);
} else if (role === "staff") {
renderStaffMe(container, me);
} else {
renderClientMe(container, me);
}
} catch (e) {
loading.remove();
container.appendChild(el(`<div class="error" style="margin:16px;">Ошибка: ${escHtml(e.message)}</div>`));
}
}
return { mount };
})();

View File

@ -45,6 +45,7 @@
<script src="assets/request.js?v=20260518f"></script>
<script src="assets/assembly.js?v=20260518f"></script>
<script src="assets/proposals.js?v=20260518f"></script>
<script src="assets/app.js?v=20260518g"></script>
<script src="assets/me.js?v=20260518h"></script>
<script src="assets/app.js?v=20260518h"></script>
</body>
</html>