/* ============================================================
Замеры кухни — 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 `
${pict}
${o.label}
${o.hint ? `${o.hint}
` : ""}
${isOn ? `${ICONS.check}
` : ""}
`;
}).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(`
${label}
`);
}
const node = el(`
`);
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(`
Фотокухни
Сними помещение со всех углов. Минимум: общий вид, окно/дверь, ниши и коммуникации.
+
Добавить фото
камера или галерея · до 12 шт
Назад
Дальше
`);
const list = node.querySelector("#photoList");
const input = node.querySelector("#photoInput");
function refreshList() {
list.innerHTML = "";
photos.forEach((ph, idx) => {
const tile = el(`
×
`);
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 };
})();