diff --git a/miniapp/assets/app.js b/miniapp/assets/app.js
index 847cbfa..783676d 100644
--- a/miniapp/assets/app.js
+++ b/miniapp/assets/app.js
@@ -1722,6 +1722,10 @@ function routeByHash() {
} else if (location.hash === "#/c/orders") {
if (typeof OrdersScreen !== "undefined") OrdersScreen.mount(app);
else init();
+ } else if (location.hash.startsWith("#/c/assembly/")) {
+ const assemblyId = decodeURIComponent(location.hash.replace("#/c/assembly/", ""));
+ if (typeof AssemblyDetailScreen !== "undefined") AssemblyDetailScreen.mount(app, assemblyId);
+ else init();
} else if (location.hash === "#/c/selfmeasure") {
if (typeof SelfMeasureScreen !== "undefined") SelfMeasureScreen.mount(app);
else init();
diff --git a/miniapp/assets/assembly_detail.js b/miniapp/assets/assembly_detail.js
new file mode 100644
index 0000000..4abeaef
--- /dev/null
+++ b/miniapp/assets/assembly_detail.js
@@ -0,0 +1,174 @@
+/* ============================================================
+ Детальная карточка сборки — #/c/assembly/:id
+ Доступна клиенту, менеджеру, мастеру.
+ ============================================================ */
+
+const AssemblyDetailScreen = (function () {
+
+ function escHtml(s) {
+ return String(s == null ? "" : s)
+ .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+ }
+
+ function fmtDate(iso) {
+ if (!iso) return null;
+ try {
+ return new Date(iso).toLocaleDateString("ru-RU", {
+ day: "numeric", month: "long", year: "numeric",
+ hour: "2-digit", minute: "2-digit"
+ });
+ } catch { return iso.slice(0, 16).replace("T", " "); }
+ }
+
+ 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 }),
+ });
+ if (!res.ok) throw new Error(`Ошибка сервера (${res.status})`);
+ return await res.json();
+ } catch (e) {
+ if (e.name === "AbortError") throw new Error("Сервер не отвечает");
+ throw e;
+ } finally { clearTimeout(t); }
+ }
+
+ const STATUS = {
+ created: { icon: "🆕", text: "Создана", color: "#8e8e8e" },
+ scheduled: { icon: "📅", text: "Запланирована", color: "#2980B9" },
+ in_progress: { icon: "🔨", text: "В процессе", color: "#F39C12" },
+ done: { icon: "✅", text: "Завершена", color: "#27AE60" },
+ cancelled: { icon: "❌", text: "Отменена", color: "#C0392B" },
+ };
+
+ function row(label, value, opts = {}) {
+ if (!value) return "";
+ return `
+
+
${escHtml(label)}
+
${opts.html ? value : escHtml(value)}
+
`;
+ }
+
+ async function mount(container, assemblyId) {
+ 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 data = await _api("assembly_detail", { assembly_id: assemblyId });
+
+ if (data.error) {
+ screen.innerHTML = `${escHtml(data.error)}
`;
+ return;
+ }
+
+ const sl = STATUS[data.status] || { icon: "🔧", text: data.status, color: "#8e8e8e" };
+
+ // Статус-баннер
+ const statusBanner = `
+
+
${sl.icon}
+
+
${escHtml(sl.text)}
+
ID: ${escHtml(data.id)}
+
+
`;
+
+ // Основные данные
+ const mainBlock = `
+
+ ${row("Адрес", data.address)}
+ ${row("Объём работ", data.scope_of_work)}
+ ${row("Дата сборки", fmtDate(data.scheduled_at))}
+ ${row("Начало", fmtDate(data.started_at))}
+ ${row("Завершение", fmtDate(data.completed_at))}
+
`;
+
+ // Заметка менеджера
+ const noteBlock = data.manager_note ? `
+
+
Заметка
+
${escHtml(data.manager_note)}
+
` : "";
+
+ // Фото результата
+ const photosAfter = (data.photos_after || []).filter(Boolean);
+ const photosBlock = photosAfter.length ? `
+
+
Фото результата
+
+ ${photosAfter.map(u => `
+
+
+ `).join("")}
+
+
` : "";
+
+ // Подпись
+ const signBlock = data.signed_by_name ? `
+
+
+
Принято клиентом
+
${escHtml(data.signed_by_name)}
+
+
${escHtml(fmtDate(data.signed_at) || "")}
+
` : "";
+
+ // Кнопка Google Calendar
+ const calBtn = data.gcal_event_url ? `
+ ` : "";
+
+ screen.innerHTML = statusBanner + mainBlock + noteBlock + photosBlock + signBlock + calBtn +
+ ``;
+
+ } catch (e) {
+ screen.innerHTML = `Ошибка: ${escHtml(e.message)}
`;
+ }
+ }
+
+ return { mount };
+})();
diff --git a/miniapp/assets/cabinet.js b/miniapp/assets/cabinet.js
index ab04400..9373752 100644
--- a/miniapp/assets/cabinet.js
+++ b/miniapp/assets/cabinet.js
@@ -187,6 +187,10 @@ const CabinetScreen = (function () {
+
+ Выезд специалиста: 2 500 ₽
+ · за КАД СПб +40 ₽/км
+
`;
diff --git a/miniapp/assets/orders.js b/miniapp/assets/orders.js
index 1b4420c..fcdbed5 100644
--- a/miniapp/assets/orders.js
+++ b/miniapp/assets/orders.js
@@ -119,7 +119,7 @@ const OrdersScreen = (function () {
subtitle: a.scope_of_work || null,
statusText: sl.text,
statusColor: sl.color,
- href: null,
+ href: a.id ? `#/c/assembly/${encodeURIComponent(a.id)}` : null,
calUrl: a.gcal_event_url || null,
};
}
diff --git a/miniapp/assets/selfmeasure.js b/miniapp/assets/selfmeasure.js
index 11ba3b5..948d917 100644
--- a/miniapp/assets/selfmeasure.js
+++ b/miniapp/assets/selfmeasure.js
@@ -134,11 +134,23 @@ const SelfMeasureScreen = (function () {
return ["А"];
}
+ /* ---- Price info block ---- */
+ const PRICE_INFO_HTML = `
+
+
💰 Стоимость выезда специалиста
+
+ В черте КАД Санкт-Петербурга — 2 500 ₽
+ За пределами КАД — 2 500 ₽ + 40 ₽/км от кольцевой до адреса
+
+
`;
+
/* ---- Step 1: Kitchen type ---- */
function renderStep1(state) {
const wrap = document.createElement("div");
wrap.innerHTML = `
-
+ ${PRICE_INFO_HTML}
+
`;
@@ -526,7 +538,8 @@ const SelfMeasureScreen = (function () {
const wrap = document.createElement("div");
wrap.innerHTML = `
-
+ ${PRICE_INFO_HTML}
+
Контактные данные
diff --git a/miniapp/index.html b/miniapp/index.html
index f17c5aa..fe7a1fe 100644
--- a/miniapp/index.html
+++ b/miniapp/index.html
@@ -50,6 +50,7 @@
+