/* ============================================================ Самозамер кухни — #/c/selfmeasure 5-шаговый мастер: тип кухни → стены → коммуникации → фото → контакт ============================================================ */ const SelfMeasureScreen = (function () { function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } 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); } } /* ---- SVG schematics for kitchen types ---- */ const KITCHEN_SVGS = { straight: ` А `, l_shape: ` А Б `, u_shape: ` А Б В `, island: ` Остров А Б `, }; const KITCHEN_LABELS = { straight: "Прямая", l_shape: "Угловая Г", u_shape: "Угловая П", island: "Островная", }; /* ---- Walls by kitchen type ---- */ function getWalls(type) { if (type === "straight") return ["А"]; if (type === "island") return ["А", "Б"]; if (type === "l_shape") return ["А", "Б"]; if (type === "u_shape") return ["А", "Б", "В"]; 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}
Выберите тип кухни
`; const grid = document.createElement("div"); grid.style.cssText = "display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px 16px;"; ["straight", "l_shape", "u_shape", "island"].forEach(type => { const card = document.createElement("button"); card.style.cssText = ` display:flex;flex-direction:column;align-items:center;gap:6px; padding:12px 8px;border-radius:12px;border:2px solid var(--border); background:var(--surface);cursor:pointer;transition:border-color 0.2s,background 0.2s; `; if (state.kitchenType === type) { card.style.borderColor = "var(--accent)"; card.style.background = "var(--accent-faint, rgba(61,122,181,0.08))"; } card.innerHTML = ` ${KITCHEN_SVGS[type]}
${escHtml(KITCHEN_LABELS[type])}
`; card.addEventListener("click", () => { haptic && haptic("impact"); state.kitchenType = type; // Re-render step const parent = wrap.parentNode; const newStep = renderStep1(state); parent.replaceChild(newStep, wrap); }); grid.appendChild(card); }); wrap.appendChild(grid); return wrap; } /* ---- Step 2: Wall dimensions ---- */ function renderStep2(state) { const walls = getWalls(state.kitchenType); const wrap = document.createElement("div"); // SVG diagram const svgDiagram = buildWallDiagramSVG(state.kitchenType, walls); wrap.innerHTML = `
Размеры стен
${svgDiagram}
Измеряйте каждую стену от угла до угла (в сантиметрах).
`; const fieldsWrap = document.createElement("div"); fieldsWrap.style.cssText = "padding:0 16px;display:flex;flex-direction:column;gap:10px;"; if (!state.walls) state.walls = {}; walls.forEach(w => { const row = document.createElement("div"); row.innerHTML = ` `; const inp = row.querySelector("input"); inp.addEventListener("input", () => { state.walls[w] = inp.value.trim(); }); fieldsWrap.appendChild(row); }); if (state.kitchenType === "island") { const note = document.createElement("div"); note.style.cssText = "font-size:12px;color:var(--muted);padding:4px 0;"; note.textContent = "Для островной кухни укажите длину основной рабочей зоны (стены А и Б). Размеры острова уточним отдельно."; fieldsWrap.appendChild(note); } wrap.appendChild(fieldsWrap); return wrap; } function buildWallDiagramSVG(type, walls) { const w = 200, h = 140; const common = `viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" fill="none" xmlns="http://www.w3.org/2000/svg"`; if (type === "straight") { return ` А `; } if (type === "l_shape") { return ` А Б `; } if (type === "u_shape") { return ` А Б В `; } // island return ` Остров А Б `; } /* ---- Step 3: Communications ---- */ function renderStep3(state) { const walls = getWalls(state.kitchenType); if (!state.comms) state.comms = { water: {}, gas: {}, electric: {} }; if (state.commsSkipped === undefined) state.commsSkipped = false; const wrap = document.createElement("div"); // Skip button const skipBtn = document.createElement("button"); skipBtn.className = "btn-secondary"; skipBtn.style.cssText = "margin:16px 16px 0;width:calc(100% - 32px);display:block;"; skipBtn.textContent = "⏭ Пропустить — только предварительный расчёт"; skipBtn.addEventListener("click", () => { haptic && haptic("impact"); state.commsSkipped = true; refreshStep3(wrap, state, walls); }); wrap.appendChild(skipBtn); const contentDiv = document.createElement("div"); contentDiv.id = "step3-content"; wrap.appendChild(contentDiv); refreshStep3(wrap, state, walls); return wrap; } function refreshStep3(wrap, state, walls) { const contentDiv = wrap.querySelector("#step3-content"); contentDiv.innerHTML = ""; if (state.commsSkipped) { // Disclaimer const disc = document.createElement("div"); disc.className = "block"; disc.style.cssText = "margin:12px 16px 0;"; disc.innerHTML = `
Пропустить коммуникации
`; disc.querySelector("#commsSkipCheck").addEventListener("change", e => { state.commsSkipConfirmed = e.target.checked; }); disc.querySelector("#commsUnskipBtn").addEventListener("click", () => { haptic && haptic("impact"); state.commsSkipped = false; state.commsSkipConfirmed = false; refreshStep3(wrap, state, walls); }); contentDiv.appendChild(disc); } else { // Full comms form const block = document.createElement("div"); block.className = "block"; block.style.cssText = "margin:12px 16px 0;"; block.innerHTML = `
Коммуникации
Укажите расположение коммуникаций. Измеряйте расстояние от левого угла стены (смотря на стену лицом).
${buildCommsHintSVG()}
`; contentDiv.appendChild(block); // Water (always shown) contentDiv.appendChild(buildCommsSection("Вода 🚿", "water", state, walls, true)); // Gas contentDiv.appendChild(buildCommsSectionGas(state, walls)); // Electric contentDiv.appendChild(buildCommsSection("Электрика ⚡", "electric", state, walls, true)); } } function buildCommsHintSVG() { return ` от левого угла → `; } function buildCommsSection(title, key, state, walls, alwaysShow) { if (!state.comms[key]) state.comms[key] = {}; const section = document.createElement("div"); section.className = "block"; section.style.cssText = "margin:8px 16px 0;"; section.innerHTML = `
${escHtml(title)}
`; const wallOpts = walls.map(w => ``).join(""); const posOpts = ["Левый угол", "Центр", "Правый угол"].map(p => `` ).join(""); const form = document.createElement("div"); form.style.cssText = "display:flex;flex-direction:column;gap:8px;"; form.innerHTML = `
`; form.querySelectorAll("[data-field]").forEach(inp => { const ev = inp.tagName === "SELECT" ? "change" : "input"; inp.addEventListener(ev, () => { state.comms[key][inp.dataset.field] = inp.value; }); // Init state if (!state.comms[key][inp.dataset.field] && inp.tagName === "SELECT") { state.comms[key][inp.dataset.field] = inp.value; } }); section.appendChild(form); return section; } function buildCommsSectionGas(state, walls) { if (!state.comms.gas) state.comms.gas = {}; const section = document.createElement("div"); section.className = "block"; section.style.cssText = "margin:8px 16px 0;"; const hasGas = !!state.comms.gas.enabled; section.innerHTML = ` `; const gasFields = document.createElement("div"); gasFields.id = "gas-fields"; gasFields.style.display = hasGas ? "block" : "none"; section.appendChild(gasFields); if (hasGas) { const inner = buildCommsSection("", "gas", state, walls, false); inner.style.margin = "0"; inner.querySelector(".block-head") && (inner.querySelector(".block-head").style.display = "none"); gasFields.appendChild(inner); } section.querySelector("#gasCheck").addEventListener("change", e => { state.comms.gas.enabled = e.target.checked; if (e.target.checked) { gasFields.style.display = "block"; gasFields.innerHTML = ""; const inner = buildCommsSection("", "gas", state, walls, false); inner.style.margin = "0"; const bh = inner.querySelector(".block-head"); if (bh) bh.style.display = "none"; gasFields.appendChild(inner); } else { gasFields.style.display = "none"; } }); return section; } /* ---- Step 4: Photos ---- */ function renderStep4() { const wrap = document.createElement("div"); wrap.innerHTML = `
Фотографии
📸
Сфотографируйте каждую стену с рулеткой.
📤 Загрузка фото — только через Telegram. После отправки замера прикрепите фото ответным сообщением в чате с менеджером.
`; return wrap; } /* ---- Step 5: Contact + submit ---- */ function renderStep5(state, onSubmit) { if (!state.contact) state.contact = {}; const wrap = document.createElement("div"); wrap.innerHTML = ` ${PRICE_INFO_HTML}
Контактные данные
`; wrap.querySelector("#sm-name").addEventListener("input", e => state.contact.name = e.target.value.trim()); wrap.querySelector("#sm-phone").addEventListener("input", e => state.contact.phone = e.target.value.trim()); wrap.querySelector("#sm-address").addEventListener("input", e => state.contact.address = e.target.value.trim()); return wrap; } /* ---- Validation ---- */ function validateStep(step, state) { if (step === 1) { return !!state.kitchenType; } if (step === 2) { const walls = getWalls(state.kitchenType); return walls.every(w => state.walls && parseInt(state.walls[w]) > 0); } if (step === 3) { if (state.commsSkipped) return !!state.commsSkipConfirmed; return true; // comms are optional when not skipped } if (step === 4) { return true; // photos always optional } if (step === 5) { const c = state.contact || {}; return !!(c.name && c.phone && c.address); } return true; } function getValidationMessage(step, state) { if (step === 1) return "Выберите тип кухни"; if (step === 2) { const walls = getWalls(state.kitchenType); const missing = walls.filter(w => !state.walls || !parseInt(state.walls[w])); return `Введите длину стены: ${missing.join(", ")}`; } if (step === 3 && state.commsSkipped && !state.commsSkipConfirmed) { return "Подтвердите понимание или введите коммуникации"; } if (step === 5) { const c = state.contact || {}; if (!c.name) return "Введите имя"; if (!c.phone) return "Введите телефон"; if (!c.address) return "Введите адрес"; } return ""; } const STEP_TITLES = [ "Тип кухни", "Размеры стен", "Коммуникации", "Фотографии", "Контакт", ]; /* ---- Main mount ---- */ async function mount(container) { container.innerHTML = ""; document.body.classList.remove("has-bottom-nav"); const oldNav = document.getElementById("bottom-nav"); if (oldNav) oldNav.remove(); // State const state = { kitchenType: null, walls: {}, commsSkipped: false, commsSkipConfirmed: false, comms: { water: {}, gas: {}, electric: {} }, contact: {}, step: 1, }; // Try pre-fill contact from /api/me try { const me = await _api("me"); if (me && !me.error && me.user) { state.contact.name = me.user.full_name || ""; state.contact.phone = me.user.phone || ""; } } catch (e) { /* ignore */ } // Header const header = document.createElement("header"); header.className = "podbor-header"; header.innerHTML = `
Шаг 1 / 5 — ${escHtml(STEP_TITLES[0])}
`; header.querySelector(".podbor-back").addEventListener("click", () => { haptic && haptic("impact"); if (state.step > 1) { state.step--; renderCurrentStep(); } else { history.back(); } }); container.appendChild(header); // Progress bar const progressBar = document.createElement("div"); progressBar.style.cssText = "height:3px;background:var(--border);margin:0;"; const progressFill = document.createElement("div"); progressFill.style.cssText = "height:3px;background:var(--accent);transition:width 0.3s;"; progressBar.appendChild(progressFill); container.appendChild(progressBar); // Screen const screen = document.createElement("div"); screen.className = "podbor-screen"; screen.style.cssText = "padding-bottom:80px;"; container.appendChild(screen); // Bottom navigation const bottomNav = document.createElement("div"); bottomNav.style.cssText = ` position:fixed;bottom:0;left:0;right:0;z-index:100; padding:12px 16px;padding-bottom:calc(12px + env(safe-area-inset-bottom)); background:var(--bg);border-top:1px solid var(--border); display:flex;gap:8px; `; bottomNav.innerHTML = ` `; container.appendChild(bottomNav); const errorDiv = document.createElement("div"); errorDiv.style.cssText = "color:#C0392B;font-size:13px;text-align:center;padding:4px 16px;min-height:20px;"; container.insertBefore(errorDiv, bottomNav); function renderCurrentStep() { screen.innerHTML = ""; errorDiv.textContent = ""; // Update header const titleEl = container.querySelector("#sm-header-title"); if (titleEl) titleEl.textContent = `Шаг ${state.step} / 5 — ${STEP_TITLES[state.step - 1]}`; // Progress progressFill.style.width = `${(state.step / 5) * 100}%`; // Back button const backBtn = container.querySelector("#sm-back-btn"); const nextBtn = container.querySelector("#sm-next-btn"); if (state.step === 5) { nextBtn.textContent = "Отправить замер"; } else { nextBtn.textContent = "Далее →"; } backBtn.style.display = state.step === 1 ? "none" : ""; // Render step content let stepEl; if (state.step === 1) stepEl = renderStep1(state); else if (state.step === 2) stepEl = renderStep2(state); else if (state.step === 3) stepEl = renderStep3(state); else if (state.step === 4) stepEl = renderStep4(); else if (state.step === 5) stepEl = renderStep5(state); if (stepEl) screen.appendChild(stepEl); } // Next button handler container.querySelector("#sm-next-btn").addEventListener("click", async () => { haptic && haptic("impact"); errorDiv.textContent = ""; if (!validateStep(state.step, state)) { errorDiv.textContent = getValidationMessage(state.step, state); return; } if (state.step === 5) { await doSubmit(); return; } state.step++; renderCurrentStep(); }); // Back button handler container.querySelector("#sm-back-btn").addEventListener("click", () => { haptic && haptic("impact"); if (state.step > 1) { state.step--; renderCurrentStep(); } }); async function doSubmit() { const nextBtn = container.querySelector("#sm-next-btn"); nextBtn.disabled = true; nextBtn.textContent = "Отправляем…"; errorDiv.textContent = ""; const payload = { kitchen_type: state.kitchenType, walls: state.walls, comms_skipped: state.commsSkipped, comms_skip_confirmed: state.commsSkipConfirmed, communications: state.commsSkipped ? null : state.comms, client_name: state.contact.name, client_phone: state.contact.phone, address: state.contact.address, }; try { const res = await _api("self_measure_submit", payload); if (res.error) throw new Error(res.error); // Success screen screen.innerHTML = ""; container.querySelector("#sm-back-btn").style.display = "none"; nextBtn.style.display = "none"; errorDiv.textContent = ""; const success = document.createElement("div"); success.style.cssText = "text-align:center;padding:40px 24px;"; success.innerHTML = `
Замер отправлен!
Менеджер свяжется с вами в ближайшее время.
`; success.querySelector("#sm-done-btn").addEventListener("click", () => { haptic && haptic("success"); location.hash = "#/c/cabinet"; }); screen.appendChild(success); haptic && haptic("success"); } catch (e) { errorDiv.textContent = "Ошибка: " + e.message; nextBtn.disabled = false; nextBtn.textContent = "Отправить замер"; } } renderCurrentStep(); } return { mount }; })();