/* ============================================================
Подбор техники — render, state, navigation, submit
============================================================ */
const Podbor = (function () {
const STORAGE_KEY = "zov-podbor-v1";
const STEPS = ["intro", "categories", "context", "infra", "scenario", "brands", "summary"];
let state = loadState();
let root = null;
let currentStep = "intro";
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: "",
address: "",
budget_total: "",
categories: [], // ['fridge','hob',...]
niches: {}, // { fridge:{w,h,d}, hob:{w,d}, oven:{w,h,d}, dw:{w,h,d} }
budget_by_cat: {},// { fridge:80000, hob:50000, ... }
infra: { stove: "", vent: "" },
scenario: { family: "", cooking: "", techniques: [], guests: "" },
brands: {}, // { fridge: {Bosch:'preferred', Liebherr:'preferred'}, ... }
notes: "",
};
}
function saveState() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
}
function update(patch) {
state = { ...state, ...patch };
saveState();
}
/* ===================== Render entry ===================== */
function mount(container) {
root = container;
document.body.classList.remove("has-bottom-nav");
const oldNav = document.getElementById("bottom-nav");
if (oldNav) oldNav.remove();
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 "intro": screen.appendChild(renderIntro()); break;
case "categories": screen.appendChild(renderCategories()); break;
case "context": screen.appendChild(renderContext()); break;
case "infra": screen.appendChild(renderInfra()); break;
case "scenario": screen.appendChild(renderScenario()); break;
case "brands": screen.appendChild(renderBrands()); break;
case "summary": screen.appendChild(renderSummary()); break;
}
}
/* ===================== Header & progress ===================== */
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 total = STEPS.length;
const pct = Math.round(((idx + 1) / total) * 100);
const labels = ["Старт", "Категории", "Контекст", "Инфра", "Сценарий", "Бренды", "Подбор"];
return el(`
${labels[idx]}${idx + 1}/${total}
`);
}
/* ===================== Step: intro ===================== */
function renderIntro() {
const node = el(`
`);
bindInputs(node);
bindNav(node);
return node;
}
/* ===================== Step: categories ===================== */
function renderCategories() {
const grid = PODBOR_CATEGORIES.map(c => `
`).join("");
const node = el(`
Какую технику
подбираем?
Выберите все категории, что нужно подобрать клиенту.
${grid}
`);
node.querySelectorAll(".cat-card").forEach(card => {
card.addEventListener("click", () => {
const cat = card.dataset.cat;
const next = state.categories.includes(cat)
? state.categories.filter(x => x !== cat)
: [...state.categories, cat];
update({ categories: next });
render();
});
});
bindNav(node);
return node;
}
/* ===================== Step: context (ниши + бюджет по категориям) ===================== */
function renderContext() {
const builtinCats = ["fridge", "hob", "oven", "dw"]; // встройка
const niches = builtinCats.filter(c => state.categories.includes(c)).map(c => {
const cat = PODBOR_CATEGORIES.find(x => x.key === c);
const n = state.niches[c] || {};
return `
`;
}).join("");
const budgets = state.categories.map(c => {
const cat = PODBOR_CATEGORIES.find(x => x.key === c);
const v = state.budget_by_cat[c] || "";
return `
`;
}).join("");
const node = el(`
Размеры
и бюджет
Если планируется встройка — укажите размеры ниш. Бюджет по категориям помогает AI распределить деньги.
${niches ? `
Ниши под встройку, мм
${niches}
` : ""}
${budgets ? `
Бюджет по категориям, ₽
${budgets}
` : ""}
`);
node.querySelectorAll("[data-niche]").forEach(inp => {
inp.addEventListener("input", e => {
const [cat, dim] = e.target.dataset.niche.split(".");
const next = { ...state.niches, [cat]: { ...(state.niches[cat] || {}), [dim]: e.target.value } };
update({ niches: next });
});
});
node.querySelectorAll("[data-budget]").forEach(inp => {
inp.addEventListener("input", e => {
const cat = e.target.dataset.budget;
const next = { ...state.budget_by_cat, [cat]: e.target.value };
update({ budget_by_cat: next });
});
});
bindNav(node);
return node;
}
/* ===================== Step: infra ===================== */
function renderInfra() {
const node = el(`
Инфраструктура
кухни
Газ или электрика — главный вопрос для варочной. Шахта вентиляции — для вытяжки.
Подключение варочной
${PODBOR_INFRA.stove.map(o => `
`).join("")}
Вентиляция для вытяжки
${PODBOR_INFRA.vent.map(o => `
`).join("")}
`);
node.querySelectorAll("[data-infra]").forEach(b => {
b.addEventListener("click", () => {
update({ infra: { ...state.infra, [b.dataset.infra]: b.dataset.val } });
render();
});
});
bindNav(node);
return node;
}
/* ===================== Step: scenario ===================== */
function renderScenario() {
const node = el(`
Сценарий
использования
Семья с детьми готовит иначе, чем пара. AI учтёт это в подборе.
Состав семьи
${PODBOR_FAMILY.map(o => `
`).join("")}
Частота готовки
${PODBOR_COOKING.map(o => `
`).join("")}
Любимые техники приготовления
${PODBOR_TECHNIQUES.map(o => `
`).join("")}
Можно несколько
`);
node.querySelectorAll("[data-scenario]").forEach(b => {
b.addEventListener("click", () => {
update({ scenario: { ...state.scenario, [b.dataset.scenario]: b.dataset.val } });
render();
});
});
node.querySelectorAll("[data-tech]").forEach(b => {
b.addEventListener("click", () => {
const cur = state.scenario.techniques || [];
const key = b.dataset.tech;
const next = cur.includes(key) ? cur.filter(x => x !== key) : [...cur, key];
update({ scenario: { ...state.scenario, techniques: next } });
render();
});
});
bindNav(node);
return node;
}
/* ===================== Step: brands ===================== */
function renderBrands() {
if (!state.categories.length) {
return el(`Сначала выберите категории.
`);
}
const blocks = state.categories.map(catKey => {
const cat = PODBOR_CATEGORIES.find(x => x.key === catKey);
const brands = PODBOR_BRANDS[catKey] || { premium: [], middle: [], budget: [] };
const catState = state.brands[catKey] || {};
const tierBlock = (tier) => `
${PODBOR_BUDGET_TIERS.find(t => t.key === tier).label}
${(brands[tier] || []).map(b => {
const status = catState[b] || "none";
return ``;
}).join("")}
`;
return `
${cat.label}
★ Тап — предпочтительно · ✓ Двойной — допустимо · — Третий — снять
${tierBlock("premium")}${tierBlock("middle")}${tierBlock("budget")}
`;
}).join("");
const node = el(`
Бренды
по категориям
Какие марки уважаете, какие — допустимы. AI сначала пробует preferred.
${blocks}
`);
node.querySelectorAll(".chip[data-brand]").forEach(c => {
c.addEventListener("click", () => {
const catKey = c.dataset.cat, brand = c.dataset.brand;
const cur = (state.brands[catKey] || {})[brand] || "none";
const nextStatus = cur === "none" ? "preferred" : cur === "preferred" ? "acceptable" : "none";
const catBrands = { ...(state.brands[catKey] || {}) };
if (nextStatus === "none") delete catBrands[brand];
else catBrands[brand] = nextStatus;
update({ brands: { ...state.brands, [catKey]: catBrands } });
render();
});
});
bindNav(node);
return node;
}
/* ===================== Step: summary + submit ===================== */
function renderSummary() {
const node = el(`
Готово
к подбору
Проверьте и отправьте — AI вернёт предложение в чат с ботом.
Клиент${state.client_name || "—"}
Бюджет${state.budget_total ? state.budget_total + " ₽" : "—"}
Категорий${state.categories.length}
Семья${PODBOR_FAMILY.find(f => f.key === state.scenario.family)?.label || "—"}
Готовка${PODBOR_COOKING.find(f => f.key === state.scenario.cooking)?.label || "—"}
Подключение${PODBOR_INFRA.stove.find(f => f.key === state.infra.stove)?.label || "—"}
`);
bindInputs(node);
bindNav(node);
node.querySelector("#submitBtn").addEventListener("click", () => onSubmit(node));
return node;
}
/* ===================== Submit ===================== */
async function onSubmit(node) {
const btn = node.querySelector("#submitBtn");
const result = node.querySelector("#submitResult");
btn.disabled = true;
btn.innerHTML = ' AI думает...';
result.innerHTML = "";
if (!BACKEND_URL) {
result.innerHTML = `BACKEND_URL не настроен (dev-режим).
`;
btn.disabled = false; btn.textContent = "Отправить · AI подберёт"; return;
}
try {
const res = await fetch(`${BACKEND_URL}?path=podbor`, {
method: "POST",
body: JSON.stringify({
initData: tg?.initData || "",
checklist: state,
client_name: state.client_name,
}),
});
const data = await res.json();
if (data.error) {
result.innerHTML = `Ошибка: ${data.error}
`;
} else {
result.innerHTML = `
${ICONS.check}
Подбор отправлен в чат бота
Лид #${(data.id || "").slice(0, 6)} · откройте Telegram
`;
haptic && haptic("success");
}
} 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 bindNav(node) {
node.querySelectorAll("[data-go]").forEach(b => {
b.addEventListener("click", () => go(b.dataset.go));
});
}
return { mount, go, getState: () => state, reset: () => { state = defaultState(); saveState(); render(); } };
})();