/* ============================================================
Самозамер кухни — #/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 }),
});
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 ["А"];
}
/* ---- Step 1: Kitchen type ---- */
function renderStep1(state) {
const wrap = document.createElement("div");
wrap.innerHTML = `
`;
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 = `
`;
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 = `
`;
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 };
})();