From e934576e5ce9b992ac10aef02b2118a31eeb0d2b Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Mon, 18 May 2026 13:25:49 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9=20=D0=BA=D0=B0=D0=B1=D0=B8=D0=BD=D0=B5?= =?UTF-8?q?=D1=82=20#/c/cabinet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cabinet.js — дашборд клиента: профиль, менеджер, подборы, сборки. Параллельная загрузка: /api/me + /api/proposal_list + /api/assembly_list. Backend: assembly_list теперь поддерживает роль client (client_tg_id фильтр). me.js: кнопка «Мой кабинет» → #/c/cabinet для роли client. Co-Authored-By: Claude Sonnet 4.6 --- miniapp/assets/app.js | 5 +- miniapp/assets/cabinet.js | 204 ++++++++++++++++++++++++++++++++++++++ miniapp/assets/me.js | 4 +- miniapp/index.html | 3 +- 4 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 miniapp/assets/cabinet.js diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js index 468115a..128e06c 100644 --- a/miniapp/assets/app.js +++ b/miniapp/assets/app.js @@ -1,4 +1,4 @@ -// ЗОВ MiniApp — главный скрипт. v20260518i +// ЗОВ MiniApp — главный скрипт. v20260518j // На входе: подписанный initData от Telegram. // Ходим на backend → получаем профиль (роль, статус) → рендерим меню. @@ -1757,6 +1757,9 @@ function routeByHash() { } else if (location.hash.startsWith("#/me")) { if (typeof MeScreen !== "undefined") MeScreen.mount(app); else init(); + } else if (location.hash === "#/c/cabinet") { + if (typeof CabinetScreen !== "undefined") CabinetScreen.mount(app); + else init(); } else if (location.hash.startsWith("#/c/proposal")) { app.innerHTML = ""; document.body.classList.remove("has-bottom-nav"); diff --git a/miniapp/assets/cabinet.js b/miniapp/assets/cabinet.js new file mode 100644 index 0000000..625b78b --- /dev/null +++ b/miniapp/assets/cabinet.js @@ -0,0 +1,204 @@ +/* ============================================================ + Клиентский кабинет — #/c/cabinet + Доступен только роли client. + ============================================================ */ + +const CabinetScreen = (function () { + + function escHtml(s) { + return String(s == null ? "" : s) + .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + function fmtDate(iso) { + if (!iso) return "—"; + try { + return new Date(iso).toLocaleDateString("ru-RU", { day: "numeric", month: "short" }); + } catch { return iso.slice(0, 10); } + } + + async function _api(path, body = {}) { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 15000); + try { + const res = await fetch(`${BACKEND_URL}/api/${path}`, { + method: "POST", signal: ctrl.signal, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ initData: tg?.initData || "", initDataUnsafe: tg?.initDataUnsafe || null, ...body }), + }); + return await res.json(); + } catch (e) { + if (e.name === "AbortError") throw new Error("Сервер не отвечает"); + throw e; + } finally { clearTimeout(t); } + } + + const STATUS_LABELS = { + draft: "📝 Черновик", + sent: "📨 Отправлен", + reviewed: "✅ Просмотрен", + approved: "🎉 Принят", + rejected: "❌ Отклонён", + created: "🆕 Создана", + scheduled: "📅 Запланирована", + in_progress: "🔨 В работе", + done: "✅ Завершена", + cancelled: "❌ Отменена", + }; + + function statusChip(status) { + const label = STATUS_LABELS[status] || status || "—"; + return `${escHtml(label)}`; + } + + // ── Блок «Менеджер» ────────────────────────────────────────────────────── + function renderManagerBlock(mgr) { + if (!mgr?.full_name) return ""; + const tgLink = mgr.tg_id ? `📩 Написать` : ""; + return ` +
+
Мой менеджер
+
+
+
${escHtml(mgr.full_name)}
+ ${mgr.salon ? `
${escHtml(mgr.salon)}
` : ""} +
+ ${tgLink} +
+
`; + } + + // ── Блок «Подборы» ─────────────────────────────────────────────────────── + function renderProposalsBlock(proposals) { + if (!proposals?.length) { + return ` +
+
Мои подборы
+
Подборов пока нет
+ +
`; + } + const items = proposals.slice(0, 3).map(p => ` +
+
+
+
Подбор от ${escHtml(fmtDate(p.created_at))}
+
${p.n_categories || 0} категор. · ${p.n_variants || 0} вар.
+
+ ${statusChip(p.status)} +
+
`).join(""); + return ` +
+
+ Мои подборы + ${proposals.length > 3 ? `Все ${proposals.length}` : ""} +
+
${items}
+ +
`; + } + + // ── Блок «Сборки» ──────────────────────────────────────────────────────── + function renderAssembliesBlock(assemblies) { + if (!assemblies?.length) { + return ` +
+
Мои сборки
+
Сборок пока нет
+
`; + } + const items = assemblies.slice(0, 3).map(a => ` +
+
+
${escHtml(a.address || "Адрес не указан")}
+
${escHtml(fmtDate(a.scheduled_at || a.ts))}
+
+ ${statusChip(a.status)} +
`).join(""); + return ` +
+
Мои сборки
+
${items}
+ ${assemblies.length > 3 ? `
+${assemblies.length - 3} ещё
` : ""} +
`; + } + + async function mount(container) { + container.innerHTML = ""; + document.body.classList.remove("has-bottom-nav"); + const oldNav = document.getElementById("bottom-nav"); + if (oldNav) oldNav.remove(); + + // Header + const h = document.createElement("header"); + h.className = "podbor-header"; + h.innerHTML = ` + +
Мой кабинет
+
+ `; + h.querySelector(".podbor-back").addEventListener("click", () => { + haptic && haptic("impact"); + history.back(); + }); + container.appendChild(h); + + const screen = document.createElement("div"); + screen.className = "podbor-screen"; + screen.innerHTML = `
`; + container.appendChild(screen); + + try { + // Параллельно грузим профиль + подборы + сборки + const [me, proposalsData, assembliesData] = await Promise.all([ + _api("me"), + _api("proposal_list").catch(() => ({ proposals: [] })), + _api("assembly_list").catch(() => ({ assemblies: [] })), + ]); + + screen.innerHTML = ""; + + if (me.error) { + screen.innerHTML = `
${escHtml(me.error)}
`; + return; + } + + const u = me.user || {}; + const initial = u.avatar_initial || (u.full_name || "К")[0].toUpperCase(); + + // Аватар + имя + screen.innerHTML = ` +
+
+ ${escHtml(initial)} +
+
+
${escHtml(u.full_name || "Клиент")}
+
Личный кабинет
+
+
+ ${renderManagerBlock(me.manager)} + ${renderProposalsBlock(proposalsData.proposals || [])} + ${renderAssembliesBlock(assembliesData.assemblies || [])} +
+ `; + + // Навигация по data-href + screen.querySelectorAll("[data-href]").forEach(el => { + el.addEventListener("click", () => { + haptic && haptic("impact"); + location.hash = el.dataset.href; + }); + }); + + } catch (e) { + screen.innerHTML = `
Ошибка: ${escHtml(e.message)}
`; + } + } + + return { mount }; +})(); diff --git a/miniapp/assets/me.js b/miniapp/assets/me.js index e49a8c0..a6303e7 100644 --- a/miniapp/assets/me.js +++ b/miniapp/assets/me.js @@ -157,8 +157,8 @@ const MeScreen = (function () {
- - + +
`; diff --git a/miniapp/index.html b/miniapp/index.html index 40250c4..b46e50f 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -47,6 +47,7 @@ - + +