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 @@
-
+
+