/* ============================================================ Замеры кухни — wizard для менеджера ============================================================ */ const Measurements = (function () { const STORAGE_KEY = "zov-measurement-draft"; const STEPS = ["client", "layout", "size", "openings", "photos", "summary"]; const STEP_LABELS = ["Клиент", "Форма", "Размеры", "Окна/двери", "Фото", "Готово"]; // Фото держим только в памяти (data-URL'ы тяжёлые, localStorage не годится) let photos = []; // Array<{ name: string, dataUrl: string, size: number }> const LAYOUTS = [ { key: "linear", label: "Прямая", hint: "одна стена", pict: "layout_linear" }, { key: "l_shape", label: "Угловая Г", hint: "две стены, угол", pict: "layout_l_shape" }, { key: "u_shape", label: "П-образная", hint: "три стены", pict: "layout_u_shape" }, { key: "island", label: "С островом", hint: "линейная + блок", pict: "layout_island" }, { key: "peninsula", label: "Полуостров", hint: "Г + барная", pict: "layout_peninsula" }, ]; let state = loadState(); let root = null; let currentStep = "client"; function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return JSON.parse(raw); } catch (e) {} return defaultState(); } function defaultState() { return { client_name: "", client_phone: "", client_tg_id: "", layout: "", area_m2: "", ceiling_mm: "", walls: {}, // { wall1: 3200, wall2: 4100, ... } — мм openings: { window: "", // расположение окна door: "", // расположение двери }, notes: "", }; } function saveState() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } function update(patch) { state = { ...state, ...patch }; saveState(); } function reset() { state = defaultState(); saveState(); photos = []; } /* ===================== Mount + Render ===================== */ function mount(container) { root = container; document.body.classList.remove("has-bottom-nav"); const oldNav = document.getElementById("bottom-nav"); if (oldNav) oldNav.remove(); currentStep = "client"; photos = []; // на старте нового замера — чистый список render(); } function go(step) { if (!STEPS.includes(step)) return; currentStep = step; render(); window.scrollTo({ top: 0, behavior: "smooth" }); haptic && haptic("impact"); } function render() { if (!root) return; root.innerHTML = ""; root.appendChild(renderHeader()); root.appendChild(renderProgress()); const screen = el(`
`); root.appendChild(screen); switch (currentStep) { case "client": screen.appendChild(renderClient()); break; case "layout": screen.appendChild(renderLayout()); break; case "size": screen.appendChild(renderSize()); break; case "openings": screen.appendChild(renderOpenings()); break; case "photos": screen.appendChild(renderPhotos()); break; case "summary": screen.appendChild(renderSummary()); break; } } function renderHeader() { const h = el(`
Новый замер
`); h.querySelector(".podbor-back").addEventListener("click", () => { const idx = STEPS.indexOf(currentStep); if (idx <= 0) { location.hash = ""; location.reload(); } else { go(STEPS[idx - 1]); } }); return h; } function renderProgress() { const idx = STEPS.indexOf(currentStep); const pct = Math.round(((idx + 1) / STEPS.length) * 100); return el(`
${STEP_LABELS[idx]}${idx + 1}/${STEPS.length}
`); } /* ===================== Шаг 1: Клиент ===================== */ function renderClient() { const node = el(`

Для какого
клиента?

Имя и телефон клиента, для которого делается замер.

`); bindInputs(node); node.querySelector("#next").addEventListener("click", () => { const name = (state.client_name || "").trim(); const phone = (state.client_phone || "").trim(); if (!name) { node.querySelector("#nameError").textContent = "Укажите имя"; return; } // Используем нормализацию из podbor if (phone && window.Podbor && typeof normalizePhoneShared === "function") { // not exposed — поэтому минимальная локальная проверка } if (phone && phone.replace(/\D/g, "").length < 10) { node.querySelector("#phoneError").textContent = "Слишком короткий номер"; return; } go("layout"); }); return node; } /* ===================== Шаг 2: Форма ===================== */ function renderLayout() { const cur = state.layout || ""; const cards = LAYOUTS.map(o => { const isOn = cur === o.key; const pict = PODBOR_PICTS[o.pict] || ""; return ` `; }).join(""); const node = el(`

Форма кухни

Как расположены гарнитуры?

${cards}
`); node.querySelectorAll(".wiz-card").forEach(card => { card.addEventListener("click", () => { update({ layout: card.dataset.val }); haptic && haptic("impact"); go("size"); }); }); node.querySelector("#back").addEventListener("click", () => go("client")); return node; } /* ===================== Шаг 3: Размеры ===================== */ function renderSize() { // По выбранной планировке — определяем сколько стен const wallsCount = { linear: 1, l_shape: 2, u_shape: 3, island: 1, peninsula: 2, }[state.layout] || 1; const wallInputs = []; for (let i = 1; i <= wallsCount; i++) { const v = (state.walls && state.walls[`wall${i}`]) || ""; const label = wallsCount === 1 ? "Длина стены, мм" : `Стена ${i} (${i === 1 ? "основная" : "доп."}), мм`; wallInputs.push(`
`); } const node = el(`

Размеры
кухни

Длины стен в миллиметрах + высота потолка.

${wallInputs.join("")}
`); bindInputs(node); // Wall inputs — пишем в state.walls node.querySelectorAll("[data-wall]").forEach(inp => { inp.addEventListener("input", e => { const w = { ...(state.walls || {}), [e.target.dataset.wall]: e.target.value }; update({ walls: w }); }); }); node.querySelector("#back").addEventListener("click", () => go("layout")); node.querySelector("#next").addEventListener("click", () => go("openings")); return node; } /* ===================== Шаг 4: Окна и двери ===================== */ function renderOpenings() { const o = state.openings || {}; const node = el(`

Окна
и двери

Опиши расположение — где окно, откуда вход, есть ли коммуникации.

`); bindInputs(node); node.querySelectorAll("[data-open]").forEach(inp => { inp.addEventListener("input", e => { update({ openings: { ...(state.openings || {}), [e.target.dataset.open]: e.target.value } }); }); }); node.querySelector("#back").addEventListener("click", () => go("size")); node.querySelector("#next").addEventListener("click", () => go("photos")); return node; } /* ===================== Шаг 5: Фото замера ===================== */ function renderPhotos() { const node = el(`

Фото
кухни

Сними помещение со всех углов. Минимум: общий вид, окно/дверь, ниши и коммуникации.

`); const list = node.querySelector("#photoList"); const input = node.querySelector("#photoInput"); function refreshList() { list.innerHTML = ""; photos.forEach((ph, idx) => { const tile = el(`
фото ${idx + 1}
`); tile.querySelector(".photo-rm").addEventListener("click", e => { const i = +e.currentTarget.dataset.idx; photos.splice(i, 1); haptic && haptic("impact"); refreshList(); }); list.appendChild(tile); }); } input.addEventListener("change", async (e) => { const files = Array.from(e.target.files || []); input.value = ""; // позволяем выбрать тот же файл снова if (!files.length) return; for (const f of files) { if (photos.length >= 12) break; if (!f.type || !f.type.startsWith("image/")) continue; try { const dataUrl = await compressImage(f, 1600, 0.78); photos.push({ name: f.name || `photo_${photos.length + 1}`, dataUrl, size: dataUrl.length }); } catch (err) { console.warn("Не удалось сжать фото", err); } } refreshList(); haptic && haptic("success"); }); refreshList(); node.querySelector("#back").addEventListener("click", () => go("openings")); node.querySelector("#next").addEventListener("click", () => go("summary")); return node; } /* Жмём картинку через canvas, возвращаем data-URL jpeg ~75% */ function compressImage(file, maxSide = 1600, quality = 0.78) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = reject; reader.onload = e => { const img = new Image(); img.onerror = reject; img.onload = () => { let { width, height } = img; if (width > maxSide || height > maxSide) { if (width >= height) { height = Math.round(height * maxSide / width); width = maxSide; } else { width = Math.round(width * maxSide / height); height = maxSide; } } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); try { resolve(canvas.toDataURL("image/jpeg", quality)); } catch (err) { reject(err); } }; img.src = e.target.result; }; reader.readAsDataURL(file); }); } /* ===================== Шаг 6: Готово + Submit ===================== */ function renderSummary() { const layout = LAYOUTS.find(l => l.key === state.layout); const wallsText = Object.entries(state.walls || {}) .map(([k, v]) => v ? `${k.replace("wall", "стена ")}: ${v} мм` : "") .filter(Boolean).join(" · "); const node = el(`

Готово
к сохранению

Проверьте и сохраните замер.

Клиент${escHtml(state.client_name)}
${state.client_phone ? `
Телефон${escHtml(state.client_phone)}
` : ""}
Форма${layout?.label || "—"}
${wallsText ? `
Стены${escHtml(wallsText)}
` : ""} ${state.area_m2 ? `
Площадь${escHtml(state.area_m2)} м²
` : ""} ${state.ceiling_mm ? `
Потолок${escHtml(state.ceiling_mm)} мм
` : ""} ${(state.openings || {}).window ? `
Окно${escHtml(state.openings.window)}
` : ""} ${(state.openings || {}).door ? `
Дверь${escHtml(state.openings.door)}
` : ""} ${state.notes ? `
Заметки${escHtml(state.notes)}
` : ""} ${photos.length ? `
Фото${photos.length} шт
` : ""}
${photos.length ? `
${photos.map(p => `
`).join("")}
` : ""}
`); node.querySelector("#back").addEventListener("click", () => go("photos")); node.querySelector("#submitBtn").addEventListener("click", () => onSubmit(node)); return node; } async function onSubmit(node) { const btn = node.querySelector("#submitBtn"); const result = node.querySelector("#submitResult"); btn.disabled = true; btn.innerHTML = ' сохраняем...'; result.innerHTML = ""; if (!BACKEND_URL) { result.innerHTML = `
BACKEND_URL не настроен.
`; btn.disabled = false; btn.textContent = "Сохранить замер"; return; } const measurement = { layout: state.layout, area_m2: state.area_m2, ceiling_mm: state.ceiling_mm, walls: state.walls, openings: state.openings, infra: {}, niches: {}, // Бэкенд раскодирует data-URL → файл и сохранит имена в Sheets photos: photos.map(p => p.dataUrl), notes: state.notes, // Контакт клиента — заносим в заметки если он не зарегистрирован в системе client_name: state.client_name, client_phone: state.client_phone, }; try { const res = await fetch(`${BACKEND_URL}/api/measurement`, { method: "POST", body: JSON.stringify({ initData: tg?.initData || "", measurement, }), }); const data = await res.json(); if (data.error) { result.innerHTML = `
Ошибка: ${data.error}
`; } else { result.innerHTML = `
${ICONS.check}
Замер сохранён
ID #${(data.id || "").slice(0, 6)}
`; haptic && haptic("success"); reset(); // сбрасываем форму для следующего замера node.querySelector("#newOne")?.addEventListener("click", () => { mount(root); }); node.querySelector("#toHome")?.addEventListener("click", () => { location.hash = ""; location.reload(); }); } } catch (e) { result.innerHTML = `
Сеть: ${e.message}
`; } btn.disabled = false; btn.textContent = "Сохранить ещё раз"; } /* ===================== Helpers ===================== */ function bindInputs(node) { node.querySelectorAll("[data-bind]").forEach(inp => { inp.addEventListener("input", e => { update({ [e.target.dataset.bind]: e.target.value }); }); }); } function escHtml(s) { return String(s == null ? "" : s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function escAttr(s) { return escHtml(s); } return { mount, reset }; })();